1. 1 : // assuming Cirrus library is loaded by containing page (via voyant.jsp)
  2. 2 : /**
  3. 3 : * Cirrus tool, a wordcloud-like visualization.
  4. 4 : *
  5. 5 : * @example
  6. 6 : *
  7. 7 : * let config = {
  8. 8 : * background: null,
  9. 9 : * categories: null,
  10. 10 : * docIndex: null,
  11. 11 : * fontFamily: null,
  12. 12 : * inlineData: null,
  13. 13 : * limit: null,
  14. 14 : * stopList: null,
  15. 15 : * visible: null,
  16. 16 : * whiteList: null,
  17. 17 : * };
  18. 18 : *
  19. 19 : * loadCorpus("austen").tool("cirrus", config);
  20. 20 : *
  21. 21 : * @class Cirrus
  22. 22 : * @tutorial cirrus
  23. 23 : * @memberof Tools
  24. 24 : */
  25. 25 : Ext.define('Voyant.panel.Cirrus', {
  26. 26 : extend: 'Ext.panel.Panel',
  27. 27 : mixins: ['Voyant.panel.Panel'],
  28. 28 : alias: 'widget.cirrus',
  29. 29 : statics: {
  30. 30 : i18n: {
  31. 31 : },
  32. 32 : api: {
  33. 33 : /**
  34. 34 : * @memberof Tools.Cirrus
  35. 35 : * @instance
  36. 36 : * @property {stopList}
  37. 37 : * @default
  38. 38 : */
  39. 39 : stopList: 'auto',
  40. 40 : /**
  41. 41 : * @memberof Tools.Cirrus
  42. 42 : * @instance
  43. 43 : * @property {categories}
  44. 44 : */
  45. 45 : categories: undefined,
  46. 46 :
  47. 47 : /**
  48. 48 : * @memberof Tools.Cirrus
  49. 49 : * @instance
  50. 50 : * @property {String|String[]} whiteList a list of words to always include
  51. 51 : */
  52. 52 : whiteList: undefined,
  53. 53 :
  54. 54 : /**
  55. 55 : * @memberof Tools.Cirrus
  56. 56 : * @instance
  57. 57 : * @property {Number} limit Specify the number of terms to load (which is separate from the number of {@link Cirrus.visible} terms to show) at a time).
  58. 58 : * @default 500
  59. 59 : */
  60. 60 : limit: 500,
  61. 61 :
  62. 62 : /**
  63. 63 : * @memberof Tools.Cirrus
  64. 64 : * @instance
  65. 65 : * @property {Number} visible Specify the number of terms that are visible at a time.
  66. 66 : * @default 50
  67. 67 : */
  68. 68 : visible: 50,
  69. 69 :
  70. 70 : // TODO unused??
  71. 71 : terms: undefined,
  72. 72 :
  73. 73 : /**
  74. 74 : * @memberof Tools.Cirrus
  75. 75 : * @instance
  76. 76 : * @property {docId}
  77. 77 : */
  78. 78 : docId: undefined,
  79. 79 : /**
  80. 80 : * @memberof Tools.Cirrus
  81. 81 : * @instance
  82. 82 : * @property {docIndex}
  83. 83 : */
  84. 84 : docIndex: undefined,
  85. 85 :
  86. 86 : /**
  87. 87 : * @memberof Tools.Cirrus
  88. 88 : * @instance
  89. 89 : * @property {String} inlineData Directly specify the terms and their relative sizes.
  90. 90 : * There data format is a comma-separated list of colon-separated term/size pairs.
  91. 91 : * For example: love:20,like:15,dear:10,child:6
  92. 92 : */
  93. 93 : inlineData: undefined,
  94. 94 :
  95. 95 : /**
  96. 96 : * @memberof Tools.Cirrus
  97. 97 : * @instance
  98. 98 : * @property {String} fontFamily The CSS font-family to use for the terms
  99. 99 : * @default
  100. 100 : */
  101. 101 : fontFamily: '"Palatino Linotype", "Book Antiqua", Palatino, serif',
  102. 102 :
  103. 103 : // TODO remove these flash specific params
  104. 104 : cirrusForceFlash: false,
  105. 105 : background: '0xffffff',
  106. 106 : fade: true,
  107. 107 : smoothness: 2,
  108. 108 : diagonals: 'none' // all, bigrams, none
  109. 109 : },
  110. 110 : glyph: 'xf06e@FontAwesome'
  111. 111 : },
  112. 112 :
  113. 113 : config: {
  114. 114 : /**
  115. 115 : * @private
  116. 116 : */
  117. 117 : mode: undefined,
  118. 118 : /**
  119. 119 : * @private
  120. 120 : */
  121. 121 : options: [
  122. 122 : {xtype: 'stoplistoption'},
  123. 123 : {
  124. 124 : xtype: 'listeditor',
  125. 125 : name: 'whiteList'
  126. 126 : },
  127. 127 : {xtype: 'categoriesoption'},
  128. 128 : // {
  129. 129 : // // TODO this field does nothing
  130. 130 : // xtype: 'numberfield',
  131. 131 : // name: 'label',
  132. 132 : // fieldLabel: 'Max words',
  133. 133 : // labelAlign: 'right',
  134. 134 : // value: 500,
  135. 135 : // minValue: 50,
  136. 136 : // step: 50,
  137. 137 : // listeners: {
  138. 138 : // afterrender: function(field) {
  139. 139 : // var win = field.up("window");
  140. 140 : // if (win && win.panel) {field.setFieldLabel(win.panel.localize("maxTerms"))}
  141. 141 : // }
  142. 142 : // }
  143. 143 : // },
  144. 144 : {xtype: 'fontfamilyoption'},
  145. 145 : {xtype: 'colorpaletteoption'}
  146. 146 :
  147. 147 : ],
  148. 148 : /**
  149. 149 : * @private
  150. 150 : */
  151. 151 : records: undefined,
  152. 152 : /**
  153. 153 : * @private
  154. 154 : */
  155. 155 : terms: undefined,
  156. 156 : /**
  157. 157 : * @private
  158. 158 : */
  159. 159 : cirrusId: undefined,
  160. 160 : /**
  161. 161 : * @private
  162. 162 : */
  163. 163 : visLayout: undefined, // cloud layout algorithm
  164. 164 : /**
  165. 165 : * @private
  166. 166 : */
  167. 167 : vis: undefined, // actual vis
  168. 168 : /**
  169. 169 : * @private
  170. 170 : */
  171. 171 : tip: undefined,
  172. 172 : /**
  173. 173 : * @private
  174. 174 : */
  175. 175 : sizeAdjustment: 100, // amount to multiply a word's relative size by
  176. 176 : /**
  177. 177 : * @private
  178. 178 : */
  179. 179 : minFontSize: 12,
  180. 180 : /**
  181. 181 : * @private
  182. 182 : */
  183. 183 : largestWordSize: 0,
  184. 184 : /**
  185. 185 : * @private
  186. 186 : */
  187. 187 : smallestWordSize: 1000000
  188. 188 : },
  189. 189 :
  190. 190 : MODE_CORPUS: 'corpus',
  191. 191 : MODE_DOCUMENT: 'mode_document',
  192. 192 :
  193. 193 : layout: 'fit',
  194. 194 :
  195. 195 : /**
  196. 196 : * @private
  197. 197 : */
  198. 198 : constructor: function(config) {
  199. 199 : this.callParent(arguments);
  200. 200 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  201. 201 :
  202. 202 : this.getApplication().getCategoriesManager().addFeature('orientation', function() { return ~~(Math.random() * 2) * 90; });
  203. 203 :
  204. 204 : this.setCirrusId(Ext.id(null, 'cirrus_'));
  205. 205 : },
  206. 206 :
  207. 207 : initComponent: function (config) {
  208. 208 : Ext.apply(this, {
  209. 209 : title: this.localize('title'),
  210. 210 : dockedItems: [{
  211. 211 : dock: 'bottom',
  212. 212 : xtype: 'toolbar',
  213. 213 : overflowHandler: 'scroller',
  214. 214 : items: [{
  215. 215 : xtype: 'corpusdocumentselector',
  216. 216 : singleSelect: true
  217. 217 : },{
  218. 218 : fieldLabel: this.localize('visibleTerms'),
  219. 219 : labelWidth: 40,
  220. 220 : width: 120,
  221. 221 : xtype: 'slider',
  222. 222 : increment: 25,
  223. 223 : minValue: 25,
  224. 224 : maxValue: 500,
  225. 225 : listeners: {
  226. 226 : afterrender: function(slider) {
  227. 227 : slider.maxValue = this.getApiParam("limit")
  228. 228 : slider.minValue = Math.round(Math.max(5, parseInt(slider.maxValue/20)));
  229. 229 : if (slider.maxValue % 25 === 0 && slider.minValue % 25 === 0) {
  230. 230 : slider.increment = 25; // default values handling
  231. 231 : } else {
  232. 232 : slider.increment = Math.round((slider.maxValue - slider.minValue)/10); // 10 steps across entire range
  233. 233 : }
  234. 234 : slider.setValue(this.getApiParam("visible"));
  235. 235 : },
  236. 236 : changecomplete: function(slider, newvalue) {
  237. 237 : this.setApiParams({visible: newvalue});
  238. 238 : this.loadFromTermsRecords();
  239. 239 : },
  240. 240 : scope: this
  241. 241 : }
  242. 242 : }]
  243. 243 : }]
  244. 244 : });
  245. 245 :
  246. 246 : this.callParent(arguments);
  247. 247 :
  248. 248 :
  249. 249 : },
  250. 250 :
  251. 251 : listeners: {
  252. 252 : boxready: function() {
  253. 253 : this.initVisLayout(); // force in case we've changed fontFamily from options
  254. 254 :
  255. 255 : var dataString = this.getApiParam('inlineData');
  256. 256 : if (dataString !== undefined) {
  257. 257 : if (dataString.charAt(0)=="[") {
  258. 258 : var jsonData = Ext.decode(dataString, true);
  259. 259 : } else {
  260. 260 : if (dataString.indexOf(":")>-1) {
  261. 261 : jsonData = [];
  262. 262 : dataString.split(",").forEach(function(term) {
  263. 263 : parts = term.split(":");
  264. 264 : jsonData.push({
  265. 265 : text: parts[0],
  266. 266 : rawFreq: parseInt(parts[1])
  267. 267 : })
  268. 268 : })
  269. 269 : } else {
  270. 270 : var terms = {}
  271. 271 : jsonData = [];
  272. 272 : dataString.split(",").forEach(function(term) {
  273. 273 : if (term in terms) {
  274. 274 : terms[term]++;
  275. 275 : } else {
  276. 276 : terms[term] = 1;
  277. 277 : }
  278. 278 : });
  279. 279 : for (term in terms) {
  280. 280 : jsonData.push({
  281. 281 : text: term,
  282. 282 : rawFreq: terms[term]
  283. 283 : })
  284. 284 : }
  285. 285 : }
  286. 286 : }
  287. 287 : if (jsonData !== null && jsonData.length>0) {
  288. 288 : this.setApiParam('inlineData', jsonData);
  289. 289 : this.setTerms(jsonData);
  290. 290 : this.buildFromTerms();
  291. 291 : }
  292. 292 : }
  293. 293 : },
  294. 294 : resize: function(panel, width, height) {
  295. 295 : if (this.getVisLayout() && this.getCorpus()) {
  296. 296 : this.setAdjustedSizes();
  297. 297 :
  298. 298 : var el = this.getLayout().getRenderTarget();
  299. 299 : width = el.getWidth();
  300. 300 : height = el.getHeight();
  301. 301 :
  302. 302 : el.down('svg').set({width: width, height: height});
  303. 303 : if (this.getTerms()) {
  304. 304 : this.getVisLayout().size([width, height]).stop().words(this.getTerms()).start();
  305. 305 : }
  306. 306 : }
  307. 307 : },
  308. 308 :
  309. 309 : loadedCorpus: function(src, corpus) {
  310. 310 : this.getApplication().getCategoriesManager().addFeature('font', this.getApiParam('fontFamily')); // make sure the default for font is set from the api
  311. 311 : this.initVisLayout(); // force in case we've changed fontFamily from options
  312. 312 : if (this.getApiParam("docIndex")) {
  313. 313 : this.fireEvent("documentSelected", this, corpus.getDocument(this.getApiParam("docIndex")));
  314. 314 : } else if (this.getApiParam("docId")) {
  315. 315 : this.fireEvent("documentSelected", this, corpus.getDocument(this.getApiParam("docId")));
  316. 316 : } else {
  317. 317 : this.loadFromCorpus(corpus);
  318. 318 : }
  319. 319 : },
  320. 320 :
  321. 321 : corpusSelected: function(src, corpus) {
  322. 322 : this.loadFromCorpus(corpus);
  323. 323 :
  324. 324 : },
  325. 325 :
  326. 326 : documentSelected: function(src, document) {
  327. 327 : if (document) {
  328. 328 : var corpus = this.getCorpus();
  329. 329 : var document = corpus.getDocument(document);
  330. 330 : this.setApiParam('docId', document.getId());
  331. 331 : var documentTerms = document.getDocumentTerms({autoload: false, corpus: corpus, pageSize: this.getApiParam("maxVisible"), parentPanel: this});
  332. 332 : this.loadFromDocumentTerms(documentTerms);
  333. 333 : }
  334. 334 : },
  335. 335 :
  336. 336 : ensureCorpusView: function(src, corpus) {
  337. 337 : if (this.getMode() != this.MODE_CORPUS) {this.loadFromCorpus(corpus);}
  338. 338 : }
  339. 339 : },
  340. 340 :
  341. 341 : loadFromCorpus: function(corpus) {
  342. 342 : var jsonData = this.getApiParam('inlineData');
  343. 343 : if (jsonData === undefined) {
  344. 344 : this.setApiParams({docId: undefined, docIndex: undefined});
  345. 345 : this.loadFromCorpusTerms(corpus.getCorpusTerms({autoload: false, pageSize: this.getApiParam("maxVisible"), parentPanel: this}));
  346. 346 : } else {
  347. 347 : // if (jsonData !== undefined) {
  348. 348 : // var records = [];
  349. 349 : // for (var i = 0; i < jsonData.length; i++) {
  350. 350 : // var wordData = jsonData[i];
  351. 351 : // wordData.term = wordData.text; // inlineData/CorpusTerm format mismatch
  352. 352 : // var record = Ext.create('Voyant.data.model.CorpusTerm', wordData);
  353. 353 : // records.push(record);
  354. 354 : // }
  355. 355 : // this.setRecords(records);
  356. 356 : // this.setMode(this.MODE_CORPUS);
  357. 357 : // this.loadFromTermsRecords();
  358. 358 : // }
  359. 359 : }
  360. 360 : },
  361. 361 :
  362. 362 : loadFromDocumentTerms: function(documentTerms) {
  363. 363 : documentTerms.load({
  364. 364 : callback: function(records, operation, success) {
  365. 365 : this.setMode(this.MODE_DOCUMENT);
  366. 366 : this.setRecords(operation.getRecords()); // not sure why operation.records is different from records
  367. 367 : this.loadFromTermsRecords();
  368. 368 : },
  369. 369 : scope: this,
  370. 370 : params: this.getApiParams()
  371. 371 : });
  372. 372 : },
  373. 373 :
  374. 374 : loadFromCorpusTerms: function(corpusTerms) {
  375. 375 : corpusTerms.load({
  376. 376 : callback: function(records, operation, success) {
  377. 377 : this.setMode(this.MODE_CORPUS);
  378. 378 : this.setRecords(operation.getRecords()); // not sure why operation.records is different from records
  379. 379 : this.loadFromTermsRecords();
  380. 380 : },
  381. 381 : scope: this,
  382. 382 : params: this.getApiParams()
  383. 383 : });
  384. 384 : },
  385. 385 :
  386. 386 : loadFromTermsRecords: function() {
  387. 387 : var records = this.getRecords();
  388. 388 : var visible = this.getApiParam("visible");
  389. 389 : if (visible>records.length) {visible=records.length;}
  390. 390 : var terms = [];
  391. 391 : for (var i=0; i<visible; i++) {
  392. 392 : if (records[i].get('rawFreq')>0) {
  393. 393 : terms.push({text: records[i].get('term').replace(/"/g,''), rawFreq: records[i].get('rawFreq')});
  394. 394 : }
  395. 395 : }
  396. 396 : this.setTerms(terms);
  397. 397 : this.buildFromTerms();
  398. 398 : },
  399. 399 :
  400. 400 : initVisLayout: function(forceLayout) {
  401. 401 : if (forceLayout || this.getVisLayout() == undefined) {
  402. 402 : var cirrusForceFlash = this.getApiParam('cirrusForceFlash');
  403. 403 : if (cirrusForceFlash == 'true' || cirrusForceFlash === true) {
  404. 404 : this.setApiParam('cirrusForceFlash', true);
  405. 405 : var id = this.getCirrusId();
  406. 406 : var appVars = {
  407. 407 : id: id
  408. 408 : };
  409. 409 : var keys = ['background','fade','smoothness','diagonals'];
  410. 410 : for (var i = 0; i < keys.length; i++) {
  411. 411 : appVars[keys[i]] = this.getApiParam(keys[i]);
  412. 412 : }
  413. 413 :
  414. 414 : var swfscript = '<script type="text/javascript" src="'+this.getApplication().getBaseUrl()+'resources/swfobject/swfobject.js'+'"></script>';
  415. 415 : var cirrusLinks = '<script type="text/javascript">'+
  416. 416 : 'cirrusClickHandler'+id+' = function(word, value) {\n'+
  417. 417 : '\tif (window.console && console.info) console.info(word, value);\n'+
  418. 418 : '\tvar cirrusTool = Ext.getCmp("'+this.id+'");\n'+
  419. 419 : '\tcirrusTool.dispatchEvent("termsClicked", cirrusTool, [word]);\n'+
  420. 420 : '}\n'+
  421. 421 : 'cirrusLoaded'+id+' = function() {\n'+
  422. 422 : '\tif (window.console && console.info) console.info("cirrus flash loaded");\n'+
  423. 423 : '}\n'+
  424. 424 : 'cirrusPNGHandler'+id+' = function(base64String) {\n'+
  425. 425 : '\tvar cirrusTool = Ext.getCmp("'+this.id+'");\n'+
  426. 426 : '\tcirrusTool.cirrusPNGHandler(base64String);\n'+
  427. 427 : '}'+
  428. 428 : '</script>';
  429. 429 :
  430. 430 : this.update(swfscript+cirrusLinks, true, function() {
  431. 431 : function loadFlash(component) {
  432. 432 : if (typeof swfobject !== 'undefined') {
  433. 433 : var el = component.getLayout().getRenderTarget();
  434. 434 : var width = el.getWidth();
  435. 435 : var height = el.getHeight();
  436. 436 :
  437. 437 : var cirrusFlash = component.getApplication().getBaseUrl()+'resources/cirrus/flash/Cirrus.swf';
  438. 438 : component.add({
  439. 439 : xtype: 'flash',
  440. 440 : id: appVars.id,
  441. 441 : url: cirrusFlash,
  442. 442 : width: width,
  443. 443 : height: height,
  444. 444 : flashVars: appVars,
  445. 445 : flashParams: {
  446. 446 : menu: 'false',
  447. 447 : scale: 'showall',
  448. 448 : allowScriptAccess: 'always',
  449. 449 : bgcolor: '#222222',
  450. 450 : wmode: 'opaque'
  451. 451 : }
  452. 452 : });
  453. 453 :
  454. 454 : component.cirrusFlashApp = Ext.get(appVars.id).first().dom;
  455. 455 : } else {
  456. 456 : setTimeout(loadFlash, 50, component);
  457. 457 : }
  458. 458 : }
  459. 459 : loadFlash(this);
  460. 460 :
  461. 461 : }, this);
  462. 462 : } else {
  463. 463 : var el = this.getLayout().getRenderTarget();
  464. 464 : el.update(""); // make sure to clear existing contents (especially for re-layout)
  465. 465 : var width = el.getWidth();
  466. 466 : var height = el.getHeight();
  467. 467 : this.setVisLayout(
  468. 468 : d3.layoutCloud()
  469. 469 : .size([width, height])
  470. 470 : .overflow(true)
  471. 471 : .padding(1)
  472. 472 : .rotate(function(d) {
  473. 473 : var orientation = this.getApplication().getCategoriesManager().getFeatureForTerm('orientation', d.text);
  474. 474 : if (orientation === undefined) {
  475. 475 : orientation = ~~(Math.random() * 2) * 90;
  476. 476 : }
  477. 477 : return orientation;
  478. 478 : }.bind(this))
  479. 479 : .spiral('archimedean')
  480. 480 : .font(function(d) { return this.getApplication().getCategoriesManager().getFeatureForTerm('font', d.text); }.bind(this))
  481. 481 : .fontSize(function(d) {return d.fontSize; }.bind(this))
  482. 482 : .text(function(d) { return d.text; })
  483. 483 : .on('end', this.draw.bind(this))
  484. 484 : );
  485. 485 :
  486. 486 : var svg = d3.select(el.dom).append('svg').attr('id',this.getCirrusId()).attr('class', 'cirrusGraph').attr('width', width).attr('height', height);
  487. 487 : this.setVis(svg.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'));
  488. 488 :
  489. 489 : if (this.getTip() === undefined) {
  490. 490 : this.setTip(Ext.create('Ext.tip.Tip', {}));
  491. 491 : }
  492. 492 : }
  493. 493 : }
  494. 494 : },
  495. 495 :
  496. 496 : buildFromTerms: function() {
  497. 497 : var terms = this.getTerms();
  498. 498 : if (this.rendered && terms) {
  499. 499 : if (this.getApiParam('cirrusForceFlash') === true) {
  500. 500 : if (this.cirrusFlashApp !== undefined && this.cirrusFlashApp.clearAll !== undefined) {
  501. 501 : var words = [];
  502. 502 : for (var i = 0; i < terms.length; i++) {
  503. 503 : var t = terms[i];
  504. 504 : if (!t.text && t.term) {t.text=t.term;}
  505. 505 : words.push({word: t.text, size: t.rawFreq, label: t.rawFreq});
  506. 506 : }
  507. 507 : this.cirrusFlashApp.clearAll();
  508. 508 : this.cirrusFlashApp.addWords(words);
  509. 509 : this.cirrusFlashApp.arrangeWords();
  510. 510 : } else {
  511. 511 : Ext.defer(this.buildFromTerms, 50, this);
  512. 512 : }
  513. 513 : } else {
  514. 514 : var minSize = 1000;
  515. 515 : var maxSize = -1;
  516. 516 : for (var i = 0; i < terms.length; i++) {
  517. 517 : var size = terms[i].rawFreq;
  518. 518 : if (size < minSize) minSize = size;
  519. 519 : if (size > maxSize) maxSize = size;
  520. 520 : }
  521. 521 : this.setSmallestWordSize(minSize);
  522. 522 : this.setLargestWordSize(maxSize);
  523. 523 :
  524. 524 : // set the relative sizes for each word (0.0 to 1.0), then adjust based on available area
  525. 525 : this.setRelativeSizes();
  526. 526 : this.setAdjustedSizes();
  527. 527 :
  528. 528 : // var fontSizer = d3.scalePow().range([10, 100]).domain([minSize, maxSize]);
  529. 529 :
  530. 530 : this.getVisLayout().words(terms).start();
  531. 531 : }
  532. 532 : } else {
  533. 533 : Ext.defer(this.buildFromTerms, 50, this);
  534. 534 : }
  535. 535 : },
  536. 536 :
  537. 537 : draw: function(words, bounds) {
  538. 538 : var panel = this;
  539. 539 : var el = this.getLayout().getRenderTarget();
  540. 540 : var width = this.getVisLayout().size()[0];
  541. 541 : var height = this.getVisLayout().size()[1];
  542. 542 :
  543. 543 : var scale = bounds ? Math.min(
  544. 544 : width / Math.abs(bounds[1].x - width / 2),
  545. 545 : width / Math.abs(bounds[0].x - width / 2),
  546. 546 : height / Math.abs(bounds[1].y - height / 2),
  547. 547 : height / Math.abs(bounds[0].y - height / 2)
  548. 548 : ) / 2 : 1;
  549. 549 :
  550. 550 : var t = d3.transition().duration(1000);
  551. 551 :
  552. 552 : var nodes = this.getVis().selectAll('text').data(words, function(d) {return d.text;});
  553. 553 :
  554. 554 : nodes.exit().transition(t)
  555. 555 : .style('font-size', '1px')
  556. 556 : .remove();
  557. 557 :
  558. 558 : var nodesEnter = nodes.enter().append('text')
  559. 559 : .text(function(d) { return d.text; })
  560. 560 : .attr('text-anchor', 'middle')
  561. 561 : .attr('data-freq', function(d) { return d.rawFreq; })
  562. 562 : .attr('transform', function(d) { return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')'; })
  563. 563 : .style('font-family', function(d) { return panel.getApplication().getCategoriesManager().getFeatureForTerm('font', d.text); })
  564. 564 : .style('fill', function(d) { return panel.getApplication().getColorForTerm(d.text, true); })
  565. 565 : .style('font-size', '1px')
  566. 566 : .on('click', function(obj) {panel.dispatchEvent('termsClicked', panel, [obj.text]);})
  567. 567 : .on('mouseover', function(obj) {
  568. 568 : this.getTip().show();
  569. 569 : }.bind(this))
  570. 570 : .on('mousemove', function(obj) {
  571. 571 : var tip = this.getTip();
  572. 572 : tip.update(obj.text+': '+obj.rawFreq);
  573. 573 : var container = Ext.get(this.getCirrusId()).dom;
  574. 574 : var coords = d3.mouse(container);
  575. 575 : coords[1] += 30;
  576. 576 : tip.setPosition(coords);
  577. 577 : }.bind(this))
  578. 578 : .on('mouseout', function(obj) {
  579. 579 : this.getTip().hide();
  580. 580 : }.bind(this));
  581. 581 :
  582. 582 : var nodesUpdate = nodes.merge(nodesEnter);
  583. 583 :
  584. 584 : nodesUpdate.transition(t)
  585. 585 : .style('font-family', function(d) { return panel.getApplication().getCategoriesManager().getFeatureForTerm('font', d.text); })
  586. 586 : .style('fill', function(d) { return panel.getApplication().getColorForTerm(d.text, true); })
  587. 587 : .attr('transform', function(d) { return 'translate(' + [d.x, d.y] + ')rotate(' + d.rotate + ')'; })
  588. 588 : .style('font-size', function(d) { return d.fontSize + 'px'; });
  589. 589 :
  590. 590 : this.getVis().transition(t).attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')scale(' + scale + ')');
  591. 591 : },
  592. 592 :
  593. 593 : map: function(value, istart, istop, ostart, ostop) {
  594. 594 : return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
  595. 595 : },
  596. 596 :
  597. 597 : calculateSizeAdjustment: function() {
  598. 598 : var terms = this.getTerms();
  599. 599 : if (terms !== undefined) {
  600. 600 : var el = this.getLayout().getRenderTarget();
  601. 601 :
  602. 602 : var stageArea = el.getWidth() * el.getHeight();
  603. 603 : if (stageArea < 100000) this.setMinFontSize(8);
  604. 604 : else this.setMinFontSize(12);
  605. 605 :
  606. 606 : var pixelsPerWord = stageArea / terms.length;
  607. 607 : var totalWordsSize = 0;
  608. 608 : for (var i = 0; i < terms.length; i++) {
  609. 609 : var word = terms[i];
  610. 610 : var wordArea = this.calculateWordArea(word);
  611. 611 : totalWordsSize += wordArea;
  612. 612 : }
  613. 613 :
  614. 614 : this.setSizeAdjustment(stageArea / totalWordsSize);
  615. 615 : }
  616. 616 : },
  617. 617 :
  618. 618 : calculateWordArea: function(word) {
  619. 619 : var baseSize = Math.log(word.relativeSize * 10) * Math.LOG10E; // take the relativeSize (0.1 to 1.0), multiply by 10, then get the base-10 log of it
  620. 620 : var height = (baseSize + word.relativeSize) / 2; // find the average between relativeSize and the log
  621. 621 : var width = 0; //(baseSize / 1.5) * word.text.length;
  622. 622 : for (var i = 0; i < word.text.length; i++ ) {
  623. 623 : var letter = word.text.charAt(i);
  624. 624 : if (letter == 'f' || letter == 'i' || letter == 'j' || letter == 'l' || letter == 'r' || letter == 't') width += baseSize / 3;
  625. 625 : else if (letter == 'm' || letter == 'w') width += baseSize / (4 / 3);
  626. 626 : else width += baseSize / 1.9;
  627. 627 : }
  628. 628 : var wordArea = height * width;
  629. 629 : return wordArea;
  630. 630 : },
  631. 631 :
  632. 632 : setAdjustedSizes: function() {
  633. 633 : this.calculateSizeAdjustment();
  634. 634 : var terms = this.getTerms();
  635. 635 : if (terms !== undefined) {
  636. 636 : for (var i = 0; i < terms.length; i++) {
  637. 637 : var term = terms[i];
  638. 638 : var adjustedSize = this.findNewRelativeSize(term);
  639. 639 : term.fontSize = adjustedSize > this.getMinFontSize() ? adjustedSize : this.getMinFontSize();
  640. 640 : }
  641. 641 : }
  642. 642 : },
  643. 643 :
  644. 644 : setRelativeSizes: function() {
  645. 645 : var terms = this.getTerms();
  646. 646 : if (terms !== undefined) {
  647. 647 : for (var i = 0; i < terms.length; i++) {
  648. 648 : var word = terms[i];
  649. 649 : word.relativeSize = this.map(word.rawFreq, this.getSmallestWordSize(), this.getLargestWordSize(), 0.1, 1);
  650. 650 : }
  651. 651 : }
  652. 652 : },
  653. 653 :
  654. 654 : findNewRelativeSize: function(word) {
  655. 655 : var areaMultiplier = this.getSizeAdjustment();
  656. 656 : var area = this.calculateWordArea(word) * areaMultiplier;
  657. 657 : // given the area = (x+6)*(2*x/3*y), solve for x
  658. 658 : var newRelativeSize = (Math.sqrt(6) * Math.sqrt(6 * Math.pow(word.text.length, 2) + area * word.text.length) - 6 * word.text.length) / (2 * word.text.length);
  659. 659 : return newRelativeSize;
  660. 660 : }
  661. 661 : });