1. 1 : /**
  2. 2 : * ScatterPlot is a graph visualization of how words cluster in a corpus document similarity, correspondence analysis or principal component analysis.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "analysis": null,
  8. 8 : * "bins": null,
  9. 9 : * "clusters": null,
  10. 10 : * "comparisonType": null,
  11. 11 : * "dimensions": null,
  12. 12 : * "docId": null,
  13. 13 : * "iterations": null,
  14. 14 : * "label": null,
  15. 15 : * "limit": null,
  16. 16 : * "perplexity": null,
  17. 17 : * "query": null,
  18. 18 : * "stopList": null,
  19. 19 : * "storeJson": null,
  20. 20 : * "target": null,
  21. 21 : * "term": null,
  22. 22 : * "whitelist": null,
  23. 23 : * };
  24. 24 : *
  25. 25 : * loadCorpus("austen").tool("scatterplot", config);
  26. 26 : *
  27. 27 : * @class ScatterPlot
  28. 28 : * @tutorial scatterplot
  29. 29 : * @memberof Tools
  30. 30 : */
  31. 31 : Ext.define('Voyant.panel.ScatterPlot', {
  32. 32 : extend: 'Ext.panel.Panel',
  33. 33 : mixins: ['Voyant.panel.Panel'],
  34. 34 : requires: ['Ext.chart.CartesianChart'],
  35. 35 : alias: 'widget.scatterplot',
  36. 36 : statics: {
  37. 37 : i18n: {
  38. 38 : },
  39. 39 : api: {
  40. 40 : /**
  41. 41 : * @memberof Tools.ScatterPlot
  42. 42 : * @instance
  43. 43 : * @property {docId}
  44. 44 : */
  45. 45 : docId: undefined,
  46. 46 :
  47. 47 : /**
  48. 48 : * @memberof Tools.ScatterPlot
  49. 49 : * @instance
  50. 50 : * @property {String} analysis The type of analysis to perform. Options are: 'ca', 'pca', 'tsne', and 'docSim'.
  51. 51 : */
  52. 52 : analysis: 'ca',
  53. 53 :
  54. 54 : /**
  55. 55 : * @memberof Tools.ScatterPlot
  56. 56 : * @instance
  57. 57 : * @property {limit}
  58. 58 : * @default
  59. 59 : */
  60. 60 : limit: 50,
  61. 61 :
  62. 62 : /**
  63. 63 : * @memberof Tools.ScatterPlot
  64. 64 : * @instance
  65. 65 : * @property {Number} dimensions The number of dimensions to render, either 2 or 3.
  66. 66 : * @default
  67. 67 : */
  68. 68 : dimensions: 3,
  69. 69 :
  70. 70 : /**
  71. 71 : * @memberof Tools.ScatterPlot
  72. 72 : * @instance
  73. 73 : * @property {bins}
  74. 74 : * @default
  75. 75 : */
  76. 76 : bins: 10,
  77. 77 :
  78. 78 : /**
  79. 79 : * @memberof Tools.ScatterPlot
  80. 80 : * @instance
  81. 81 : * @property {Number} clusters The number of clusters within which to group words.
  82. 82 : * @default
  83. 83 : */
  84. 84 : clusters: 3,
  85. 85 :
  86. 86 : /**
  87. 87 : * @memberof Tools.ScatterPlot
  88. 88 : * @instance
  89. 89 : * @property {Number} perplexity The TSNE perplexity value.
  90. 90 : * @default
  91. 91 : */
  92. 92 : perplexity: 15,
  93. 93 :
  94. 94 : /**
  95. 95 : * @memberof Tools.ScatterPlot
  96. 96 : * @instance
  97. 97 : * @property {Number} iterations The TSNE iterations value.
  98. 98 : * @default
  99. 99 : */
  100. 100 : iterations: 1500,
  101. 101 :
  102. 102 : /**
  103. 103 : * @memberof Tools.ScatterPlot
  104. 104 : * @instance
  105. 105 : * @property {String} comparisonType The value to use for comparing terms. Options are: 'raw', 'relative', and 'tfidf'.
  106. 106 : * @default
  107. 107 : */
  108. 108 : comparisonType: 'relative',
  109. 109 :
  110. 110 : /**
  111. 111 : * @memberof Tools.ScatterPlot
  112. 112 : * @instance
  113. 113 : * @property {stopList}
  114. 114 : * @default
  115. 115 : */
  116. 116 : stopList: 'auto',
  117. 117 :
  118. 118 : /**
  119. 119 : * @memberof Tools.ScatterPlot
  120. 120 : * @instance
  121. 121 : * @property {String} target The term to set as the target. This will filter results to terms that are near the target.
  122. 122 : */
  123. 123 : target: undefined,
  124. 124 :
  125. 125 : /**
  126. 126 : * @memberof Tools.ScatterPlot
  127. 127 : * @instance
  128. 128 : * @property {String[]} term Used in combination with "target" as a white list of terms to keep.
  129. 129 : */
  130. 130 : term: undefined,
  131. 131 :
  132. 132 : /**
  133. 133 : * @memberof Tools.ScatterPlot
  134. 134 : * @instance
  135. 135 : * @property {query}
  136. 136 : */
  137. 137 : query: undefined,
  138. 138 :
  139. 139 : /**
  140. 140 : * @memberof Tools.ScatterPlot
  141. 141 : * @instance
  142. 142 : * @property {String} whitelist TODO Unused or only used in CA?
  143. 143 : */
  144. 144 : whitelist: undefined,
  145. 145 :
  146. 146 : /**
  147. 147 : * @memberof Tools.ScatterPlot
  148. 148 : * @instance
  149. 149 : * @property {String[]} label The label types to show. One or more of: 'summary', 'docs', and 'terms'.
  150. 150 : */
  151. 151 : label: ['summary', 'docs', 'terms'],
  152. 152 :
  153. 153 : /**
  154. 154 : * @memberof Tools.ScatterPlot
  155. 155 : * @instance
  156. 156 : * @property {String} storeJson TODO used in embed
  157. 157 : */
  158. 158 : storeJson: undefined
  159. 159 : },
  160. 160 : glyph: 'xf06e@FontAwesome'
  161. 161 : },
  162. 162 : config: {
  163. 163 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  164. 164 : caStore: null,
  165. 165 : pcaStore: null,
  166. 166 : tsneStore: null,
  167. 167 : docSimStore: null,
  168. 168 : termStore: null,
  169. 169 : chartMenu: null,
  170. 170 : newTerm: null,
  171. 171 : termsTimeout: null,
  172. 172 : highlightData: {x: 0, y: 0, r: 0},
  173. 173 : highlightTask: null
  174. 174 : },
  175. 175 :
  176. 176 : tokenFreqTipTemplate: null,
  177. 177 : docFreqTipTemplate: null,
  178. 178 :
  179. 179 : constructor: function(config) {
  180. 180 : this.mixins['Voyant.util.Api'].constructor.apply(this, arguments);
  181. 181 : if ("storeJson" in config) {
  182. 182 : var json = JSON.parse(config.storeJson);
  183. 183 : Ext.apply(config, json);
  184. 184 : }
  185. 185 : this.callParent(arguments);
  186. 186 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  187. 187 : },
  188. 188 :
  189. 189 : initComponent: function() {
  190. 190 : this.setCaStore(Ext.create('Voyant.data.store.CAAnalysis', {
  191. 191 : listeners: {load: this.maskAndBuildChart, scope: this}
  192. 192 : }));
  193. 193 : this.setPcaStore(Ext.create('Voyant.data.store.PCAAnalysis', {
  194. 194 : listeners: {load: this.maskAndBuildChart, scope: this}
  195. 195 : }));
  196. 196 : this.setTsneStore(Ext.create('Voyant.data.store.TSNEAnalysis', {
  197. 197 : listeners: {load: this.maskAndBuildChart, scope: this}
  198. 198 : }));
  199. 199 : this.setDocSimStore(Ext.create('Voyant.data.store.DocSimAnalysis', {
  200. 200 : listeners: {load: this.maskAndBuildChart, scope: this}
  201. 201 : }));
  202. 202 :
  203. 203 : this.setTermStore(Ext.create('Ext.data.JsonStore', {
  204. 204 : fields: [
  205. 205 : {name: 'term'},
  206. 206 : {name: 'rawFreq', type: 'int'},
  207. 207 : {name: 'relativeFreq', type: 'number'},
  208. 208 : {name: 'coordinates', mapping : 'vector'},
  209. 209 : {name: 'category'}
  210. 210 : ],
  211. 211 : sorters: [{property: 'rawFreq', direction: 'DESC'}],
  212. 212 : groupField: 'category'
  213. 213 : }));
  214. 214 :
  215. 215 : this.setChartMenu(Ext.create('Ext.menu.Menu', {
  216. 216 : items: [
  217. 217 : {text: this.localize('remove'), itemId: 'remove', glyph: 'xf068@FontAwesome'},
  218. 218 : {text: this.localize('nearby'), itemId: 'nearby', glyph: 'xf0b2@FontAwesome'}
  219. 219 : ],
  220. 220 : listeners: {
  221. 221 : hide: function() {
  222. 222 : var series = this.down('#chart').getSeries();
  223. 223 : series[0].enableToolTips();
  224. 224 : series[1].enableToolTips();
  225. 225 : },
  226. 226 : scope: this
  227. 227 : }
  228. 228 : }));
  229. 229 :
  230. 230 : this.tokenFreqTipTemplate = new Ext.Template(this.localize('tokenFreqTip'));
  231. 231 : this.docFreqTipTemplate = new Ext.Template(this.localize('docFreqTip'));
  232. 232 :
  233. 233 : Ext.apply(this, {
  234. 234 : title: this.localize('title'),
  235. 235 : layout: 'border',
  236. 236 : autoDestroy: true,
  237. 237 : items: [{
  238. 238 : itemId: 'chartParent',
  239. 239 : region: 'center',
  240. 240 : layout: 'fit',
  241. 241 : tbar: {
  242. 242 : overflowHandler: 'scroller',
  243. 243 : items: [{
  244. 244 : xtype: 'querysearchfield',
  245. 245 : itemId: 'filterTerms',
  246. 246 : width: 150
  247. 247 : },{
  248. 248 : text: this.localize('labels'),
  249. 249 : itemId: 'labels',
  250. 250 : glyph: 'xf02b@FontAwesome',
  251. 251 : menu: {
  252. 252 : items: [
  253. 253 : {text: this.localize("summaryLabel"), itemId: 'summary', xtype: 'menucheckitem'},
  254. 254 : {text: this.localize("docsLabel"), itemId: 'docs', xtype: 'menucheckitem'},
  255. 255 : {text: this.localize("termsLabel"), itemId: 'terms', xtype: 'menucheckitem'}
  256. 256 : ],
  257. 257 : listeners: {
  258. 258 : afterrender: function(menu) {
  259. 259 : var labels = this.getApiParam('label');
  260. 260 : menu.items.each(function(item) {
  261. 261 : item.setChecked(labels.indexOf(item.getItemId())>-1)
  262. 262 : })
  263. 263 : },
  264. 264 : click: function(menu, item) {
  265. 265 : var labels = this.getApiParam("label");
  266. 266 : var label = item.getItemId();
  267. 267 : if (Ext.isString(labels)) {labels = [labels]}
  268. 268 : if (item.checked && labels.indexOf(label)==-1) {
  269. 269 : labels.push(label)
  270. 270 : } else if (!item.checked && labels.indexOf(label)>-1) {
  271. 271 : labels = labels.filter(function(item) {return item!=label})
  272. 272 : }
  273. 273 : this.setApiParam("label", labels);
  274. 274 : this.doLabels();
  275. 275 : this.queryById('chart').redraw();
  276. 276 : },
  277. 277 : scope: this
  278. 278 : }
  279. 279 : }
  280. 280 : }]
  281. 281 :
  282. 282 : },
  283. 283 : listeners: {
  284. 284 : query: function(component, value) {
  285. 285 : this.getTermStore().filter([{property: 'term', value: value, anyMatch: true}]);
  286. 286 : this.filterChart(value);
  287. 287 : },
  288. 288 : scope: this
  289. 289 : }
  290. 290 : },{
  291. 291 : itemId: 'optionsPanel',
  292. 292 : title: this.localize('options'),
  293. 293 : region: 'west',
  294. 294 : split: true,
  295. 295 : collapsible: true,
  296. 296 : collapseMode: 'header',
  297. 297 : width: 135,
  298. 298 : scrollable: 'y',
  299. 299 : layout: {
  300. 300 : type: 'vbox',
  301. 301 : align: 'stretch'
  302. 302 : },
  303. 303 : defaults: {
  304. 304 : xtype: 'button',
  305. 305 : margin: '5',
  306. 306 : labelAlign: 'top'
  307. 307 : },
  308. 308 : items: [{
  309. 309 : xtype: 'label',
  310. 310 : text: this.localize('input')
  311. 311 : },{
  312. 312 : xtype: 'documentselectorbutton'
  313. 313 : },{
  314. 314 : text: this.localize('freqsMode'),
  315. 315 : itemId: 'comparisonType',
  316. 316 : glyph: 'xf201@FontAwesome',
  317. 317 : tooltip: this.localize('freqsModeTip'),
  318. 318 : menu: {
  319. 319 : items: [
  320. 320 : {text: this.localize("rawFrequencies"), itemId: 'comparisonType_raw', group: 'freqsMode', xtype: 'menucheckitem'},
  321. 321 : {text: this.localize("relativeFrequencies"), itemId: 'comparisonType_relative', group: 'freqsMode', xtype: 'menucheckitem'},
  322. 322 : {text: this.localize("tfidf"), itemId: 'comparisonType_tfidf', group: 'freqsMode', xtype: 'menucheckitem'}
  323. 323 : ],
  324. 324 : listeners: {
  325. 325 : click: function(menu, item) {
  326. 326 : if (item !== undefined) {
  327. 327 : var type = item.getItemId().split('_')[1];
  328. 328 : if (type !== this.getApiParam('comparisonType')) {
  329. 329 : this.setApiParam('comparisonType', type);
  330. 330 : this.loadFromApis(true);
  331. 331 : }
  332. 332 : }
  333. 333 : },
  334. 334 : scope: this
  335. 335 : }
  336. 336 : }
  337. 337 : },{
  338. 338 : fieldLabel: this.localize('numTerms'),
  339. 339 : itemId: 'limit',
  340. 340 : xtype: 'numberfield',
  341. 341 : minValue: 5,
  342. 342 : listeners: {
  343. 343 : change: function(numb, newValue, oldValue) {
  344. 344 : function doLoad() {
  345. 345 : this.setApiParam('limit', newValue);
  346. 346 : this.loadFromApis();
  347. 347 : }
  348. 348 : if (oldValue !== null) {
  349. 349 : if (this.getTermsTimeout() !== null) {
  350. 350 : clearTimeout(this.getTermsTimeout());
  351. 351 : }
  352. 352 : if (numb.isValid()) {
  353. 353 : this.setTermsTimeout(setTimeout(doLoad.bind(this), 500));
  354. 354 : }
  355. 355 : }
  356. 356 : },
  357. 357 : scope: this
  358. 358 : }
  359. 359 : },{
  360. 360 : xtype: 'container',
  361. 361 : html: '<hr style="border: none; border-top: 1px solid #cfcfcf;"/>'
  362. 362 : },{
  363. 363 : xtype: 'label',
  364. 364 : text: this.localize('output')
  365. 365 : },{
  366. 366 : text: this.localize('analysis'),
  367. 367 : itemId: 'analysis',
  368. 368 : glyph: 'xf1ec@FontAwesome',
  369. 369 : overflowHandler: 'scroller',
  370. 370 : menu: {
  371. 371 : items: [
  372. 372 : {text: this.localize('pca'), itemId: 'analysis_pca', group:'analysis', xtype: 'menucheckitem'},
  373. 373 : {text: this.localize('ca'), itemId: 'analysis_ca', group:'analysis', xtype: 'menucheckitem'},
  374. 374 : {text: this.localize('tsne'), itemId: 'analysis_tsne', group:'analysis', xtype: 'menucheckitem'},
  375. 375 : {text: this.localize('docSim'), itemId: 'analysis_docSim', group:'analysis', xtype: 'menucheckitem'}
  376. 376 : ],
  377. 377 : listeners: {
  378. 378 : click: function(menu, item) {
  379. 379 : if (item !== undefined) {
  380. 380 : var analysis = item.getItemId().split('_')[1];
  381. 381 : if (analysis !== this.getApiParam('analysis')) {
  382. 382 : this.doAnalysisChange(analysis);
  383. 383 : this.loadFromApis(true);
  384. 384 : }
  385. 385 : }
  386. 386 : },
  387. 387 : scope: this
  388. 388 : }
  389. 389 : }
  390. 390 : },{
  391. 391 : fieldLabel: this.localize('perplexity'),
  392. 392 : itemId: 'perplexity',
  393. 393 : xtype: 'slider',
  394. 394 : minValue: 5,
  395. 395 : maxValue: 100,
  396. 396 : increment: 1,
  397. 397 : listeners: {
  398. 398 : changecomplete: function(slider, newValue) {
  399. 399 : this.setApiParam('perplexity', newValue);
  400. 400 : this.loadFromApis(true);
  401. 401 : },
  402. 402 : scope: this
  403. 403 : }
  404. 404 : },{
  405. 405 : fieldLabel: this.localize('iterations'),
  406. 406 : itemId: 'iterations',
  407. 407 : xtype: 'slider',
  408. 408 : minValue: 100,
  409. 409 : maxValue: 5000,
  410. 410 : increment: 100,
  411. 411 : listeners: {
  412. 412 : changecomplete: function(slider, newValue) {
  413. 413 : this.setApiParam('iterations', newValue);
  414. 414 : this.loadFromApis(true);
  415. 415 : },
  416. 416 : scope: this
  417. 417 : }
  418. 418 : },{
  419. 419 : text: this.localize('clusters'),
  420. 420 : itemId: 'clusters',
  421. 421 : glyph: 'xf192@FontAwesome',
  422. 422 : menu: {
  423. 423 : items: [
  424. 424 : {text: '1', itemId: 'clusters_1', group: 'clusters', xtype: 'menucheckitem'},
  425. 425 : {text: '2', itemId: 'clusters_2', group: 'clusters', xtype: 'menucheckitem'},
  426. 426 : {text: '3', itemId: 'clusters_3', group: 'clusters', xtype: 'menucheckitem'},
  427. 427 : {text: '4', itemId: 'clusters_4', group: 'clusters', xtype: 'menucheckitem'},
  428. 428 : {text: '5', itemId: 'clusters_5', group: 'clusters', xtype: 'menucheckitem'}
  429. 429 : ],
  430. 430 : listeners: {
  431. 431 : click: function(menu, item) {
  432. 432 : if (item !== undefined) {
  433. 433 : var clusters = parseInt(item.getItemId().split('_')[1]);
  434. 434 : if (clusters !== this.getApiParam('clusters')) {
  435. 435 : this.setApiParam('clusters', clusters);
  436. 436 : this.loadFromApis(true);
  437. 437 : }
  438. 438 : }
  439. 439 : },
  440. 440 : scope: this
  441. 441 : }
  442. 442 : }
  443. 443 : },{
  444. 444 : text: this.localize('dimensions'),
  445. 445 : itemId: 'dimensions',
  446. 446 : glyph: 'xf1b2@FontAwesome',
  447. 447 : menu: {
  448. 448 : items: [
  449. 449 : {text: '2', itemId: 'dimensions_2', group: 'dimensions', xtype: 'menucheckitem'},
  450. 450 : {text: '3', itemId: 'dimensions_3', group: 'dimensions', xtype: 'menucheckitem'}
  451. 451 : ],
  452. 452 : listeners: {
  453. 453 : click: function(menu, item) {
  454. 454 : if (item !== undefined) {
  455. 455 : var dims = parseInt(item.getItemId().split('_')[1]);
  456. 456 : if (dims !== this.getApiParam('dimensions')) {
  457. 457 : if (dims == 3 && this.getApiParam('analysis') == 'ca' && this.getCorpus().getDocumentsCount() == 3) {
  458. 458 : dims = 2;
  459. 459 : // TODO add info message 'Because of the nature of Correspondence Analysis, you can only use 2 dimensions with 3 documents.'
  460. 460 : return false;
  461. 461 : }
  462. 462 :
  463. 463 : this.setApiParam('dimensions', dims);
  464. 464 : this.loadFromApis(true);
  465. 465 : }
  466. 466 : }
  467. 467 : },
  468. 468 : scope: this
  469. 469 : }
  470. 470 : }
  471. 471 : },{
  472. 472 : itemId: 'reloadButton',
  473. 473 : text: this.localize('reload'),
  474. 474 : glyph: 'xf021@FontAwesome',
  475. 475 : handler: function() {
  476. 476 : this.loadFromApis();
  477. 477 : },
  478. 478 : scope: this
  479. 479 : }]
  480. 480 : },{
  481. 481 : itemId: 'termsGrid',
  482. 482 : xtype: 'grid',
  483. 483 : title: this.localize('terms'),
  484. 484 : region: 'east',
  485. 485 : width: 250,
  486. 486 : split: true,
  487. 487 : collapsible: true,
  488. 488 : collapseMode: 'header',
  489. 489 : forceFit: true,
  490. 490 : features: [{
  491. 491 : ftype: 'grouping',
  492. 492 : hideGroupedHeader: true,
  493. 493 : enableGroupingMenu: false
  494. 494 : }],
  495. 495 : bbar: {
  496. 496 : overflowHandler: 'scroller',
  497. 497 : items: [{
  498. 498 : itemId: 'nearbyButton',
  499. 499 : xtype: 'button',
  500. 500 : text: this.localize('nearby'),
  501. 501 : glyph: 'xf0b2@FontAwesome',
  502. 502 : flex: 1,
  503. 503 : handler: function(btn) {
  504. 504 : var sel = btn.up('panel').getSelection()[0];
  505. 505 : if (sel === undefined) {
  506. 506 : this.toastError({
  507. 507 : html: this.localize("noTermSelected"),
  508. 508 : anchor: btn.up("panel").getTargetEl()
  509. 509 : });
  510. 510 : }
  511. 511 : else {
  512. 512 : var term = sel.get('term');
  513. 513 : this.getNearbyForTerm(term);
  514. 514 : }
  515. 515 : },
  516. 516 : scope: this
  517. 517 : },{
  518. 518 : itemId: 'removeButton',
  519. 519 : xtype: 'button',
  520. 520 : text: this.localize('remove'),
  521. 521 : glyph: 'xf068@FontAwesome',
  522. 522 : flex: 1,
  523. 523 : handler: function(btn) {
  524. 524 : var sel = btn.up('panel').getSelection()[0];
  525. 525 : if (sel === undefined) {
  526. 526 : this.toastError({
  527. 527 : html: this.localize("noTermSelected"),
  528. 528 : anchor: btn.up("panel").getTargetEl()
  529. 529 : });
  530. 530 : }
  531. 531 : else {
  532. 532 : var term = sel.get('term');
  533. 533 : this.removeTerm(term);
  534. 534 : }
  535. 535 : },
  536. 536 : scope: this
  537. 537 : }]
  538. 538 :
  539. 539 : },
  540. 540 : tbar: {
  541. 541 : overflowHandler: 'scroller',
  542. 542 : items: [{
  543. 543 : xtype: 'querysearchfield',
  544. 544 : itemId: 'addTerms',
  545. 545 : // emptyText: this.localize('addTerm'),
  546. 546 : flex: 1
  547. 547 : }]
  548. 548 : },
  549. 549 : columns: [{
  550. 550 : text: this.localize('term'),
  551. 551 : dataIndex: 'term',
  552. 552 : flex: 1,
  553. 553 : sortable: true
  554. 554 : },{
  555. 555 : text: this.localize('rawFreq'),
  556. 556 : dataIndex: 'rawFreq',
  557. 557 : flex: 0.75,
  558. 558 : minWidth: 70,
  559. 559 : sortable: true
  560. 560 : },{
  561. 561 : text: this.localize('relFreq'),
  562. 562 : dataIndex: 'relativeFreq',
  563. 563 : flex: 0.75,
  564. 564 : minWidth: 70,
  565. 565 : sortable: true,
  566. 566 : hidden: true
  567. 567 : }],
  568. 568 : selModel: {
  569. 569 : type: 'rowmodel',
  570. 570 : mode: 'SINGLE',
  571. 571 : allowDeselect: true,
  572. 572 : toggleOnClick: true,
  573. 573 : listeners: {
  574. 574 : selectionchange: {
  575. 575 : fn: function(sm, selections) {
  576. 576 : // this.getApplication().dispatchEvent('corpusTermsClicked', this, selections);
  577. 577 : var sel = selections[0];
  578. 578 : if (sel !== undefined) {
  579. 579 : var term = sel.get('term');
  580. 580 : var isDoc = sel.get('category') === 'document';
  581. 581 : this.selectTerm(term, isDoc);
  582. 582 :
  583. 583 : if (isDoc) {
  584. 584 : this.queryById('nearbyButton').disable();
  585. 585 : this.queryById('removeButton').disable();
  586. 586 : } else {
  587. 587 : this.queryById('nearbyButton').enable();
  588. 588 : this.queryById('removeButton').enable();
  589. 589 : }
  590. 590 : } else {
  591. 591 : this.selectTerm();
  592. 592 : }
  593. 593 : },
  594. 594 : scope: this
  595. 595 : }
  596. 596 : }
  597. 597 : },
  598. 598 : store: this.getTermStore(),
  599. 599 : listeners: {
  600. 600 : expand: function(panel) {
  601. 601 : panel.getView().refresh();
  602. 602 : },
  603. 603 : query: function(component, value) {
  604. 604 : if (value.length > 0 && this.getTermStore().findExact('term', value[0]) === -1) {
  605. 605 : this.setNewTerm(value);
  606. 606 : this.loadFromApis();
  607. 607 : } else {
  608. 608 : this.setNewTerm(null);
  609. 609 : }
  610. 610 : },
  611. 611 : scope: this
  612. 612 : }
  613. 613 : }]
  614. 614 : });
  615. 615 :
  616. 616 : this.on('boxready', function(component, width, height) {
  617. 617 : if (width < 400) {
  618. 618 : this.queryById('optionsPanel').collapse();
  619. 619 : this.queryById('termsGrid').collapse();
  620. 620 : }
  621. 621 : if (this.config.storeClass && this.config.storeData) {
  622. 622 : this.loadStoreFromJson(this.config.storeClass, this.config.storeData);
  623. 623 : }
  624. 624 : }, this);
  625. 625 :
  626. 626 : this.on('beforedestroy', function(component) {
  627. 627 : var oldChart = this.queryById('chart');
  628. 628 : if (oldChart !== null) {
  629. 629 : this.queryById('chartParent').remove(oldChart);
  630. 630 : }
  631. 631 : }, this);
  632. 632 :
  633. 633 : // create a listener for corpus loading (defined here, in case we need to load it next)
  634. 634 : this.on('loadedCorpus', function(src, corpus) {
  635. 635 : function setCheckItemFromApi(apiParamName) {
  636. 636 : var value = this.getApiParam(apiParamName);
  637. 637 : var menu = this.queryById(apiParamName);
  638. 638 : var item = menu.down('#'+apiParamName+'_'+value);
  639. 639 : item.setChecked(true);
  640. 640 : }
  641. 641 : var setCheckBound = setCheckItemFromApi.bind(this);
  642. 642 :
  643. 643 : setCheckBound('analysis');
  644. 644 : this.doAnalysisChange(this.getApiParam('analysis'));
  645. 645 :
  646. 646 : setCheckBound('comparisonType');
  647. 647 : setCheckBound('clusters');
  648. 648 :
  649. 649 : this.queryById('perplexity').setValue(this.getApiParam('perplexity'));
  650. 650 : this.queryById('iterations').setValue(this.getApiParam('iterations'));
  651. 651 :
  652. 652 : if (corpus.getDocumentsCount() == 3) {
  653. 653 : this.setApiParam('dimensions', 2);
  654. 654 : }
  655. 655 : setCheckBound('dimensions');
  656. 656 :
  657. 657 : this.getCaStore().setCorpus(corpus);
  658. 658 : this.getPcaStore().setCorpus(corpus);
  659. 659 : this.getDocSimStore().setCorpus(corpus);
  660. 660 : this.loadFromApis();
  661. 661 : }, this);
  662. 662 :
  663. 663 : this.on('documentsSelected', function(src, docIds) {
  664. 664 : this.setApiParam('docId', docIds);
  665. 665 : this.loadFromApis();
  666. 666 : }, this);
  667. 667 :
  668. 668 : this.callParent(arguments);
  669. 669 : },
  670. 670 :
  671. 671 : doAnalysisChange: function(analysis) {
  672. 672 : this.setApiParam('analysis', analysis);
  673. 673 : this.queryById('nearbyButton').setDisabled(analysis === 'tsne');
  674. 674 : this.queryById('reloadButton').setVisible(analysis === 'tsne');
  675. 675 : this.queryById('perplexity').setVisible(analysis === 'tsne');
  676. 676 : this.queryById('iterations').setVisible(analysis === 'tsne');
  677. 677 : if (analysis === 'ca') {
  678. 678 : // TODO handling for when there's no corpus
  679. 679 : if (this.getCorpus().getDocumentsCount() == 3) {
  680. 680 : this.setApiParam('dimensions', 2);
  681. 681 : this.queryById('dimensions').menu.items.get(0).setChecked(true); // need 1-2 docs or 4+ docs for 3 dimensions
  682. 682 : }
  683. 683 : }
  684. 684 : },
  685. 685 :
  686. 686 : loadStoreFromJson: function(storeClass, storeData) {
  687. 687 : if (storeClass == 'Voyant.data.store.CAAnalysis') {
  688. 688 : this.getCaStore().loadRawData(storeData);
  689. 689 : this.doAnalysisChange('ca');
  690. 690 : this.maskAndBuildChart.call(this, this.getCaStore());
  691. 691 : } else if (storeClass == 'Voyant.data.store.PCAAnalysis') {
  692. 692 : this.getPcaStore().loadRawData(storeData);
  693. 693 : this.doAnalysisChange('pca');
  694. 694 : this.maskAndBuildChart.call(this, this.getPcaStore());
  695. 695 : } else if (storeClass == 'Voyant.data.store.TSNEAnalysis') {
  696. 696 : this.getTsneStore().loadRawData(storeData);
  697. 697 : this.doAnalysisChange('tsne');
  698. 698 : this.maskAndBuildChart.call(this, this.getTsneStore());
  699. 699 : } else if (storeClass == 'Voyant.data.store.DocSimAnalysis') {
  700. 700 : this.getDocSimStore().loadRawData(storeData);
  701. 701 : this.doAnalysisChange('docSim');
  702. 702 : this.maskAndBuildChart.call(this, this.getDocSimStore());
  703. 703 : }
  704. 704 : },
  705. 705 :
  706. 706 : maskAndBuildChart: function(store) {
  707. 707 : this.queryById('chartParent').mask(this.localize('plotting'));
  708. 708 : Ext.defer(this.buildChart, 50, this, [store]);
  709. 709 : },
  710. 710 :
  711. 711 : buildChart: function(store) {
  712. 712 : var that = this; // needed for tooltip renderer
  713. 713 :
  714. 714 : var oldChart = this.queryById('chart');
  715. 715 : if (oldChart !== null) {
  716. 716 : this.queryById('chartParent').remove(oldChart);
  717. 717 : }
  718. 718 :
  719. 719 : this.queryById('termsGrid').getSelectionModel().deselectAll();
  720. 720 :
  721. 721 : var rec = store.getAt(0);
  722. 722 : var numDims = this.getApiParam('dimensions');
  723. 723 :
  724. 724 : var summary = '';
  725. 725 : if (this.getApiParam('analysis') === 'pca') {
  726. 726 : // calculate the percentage of original data represented by the dominant principal components
  727. 727 : var pcs = rec.getPrincipalComponents();
  728. 728 : var eigenTotal = 0;
  729. 729 : for (var i = 0; i < pcs.length; i++) {
  730. 730 : var pc = pcs[i];
  731. 731 : eigenTotal += parseFloat(pc.get('eigenValue'));
  732. 732 : }
  733. 733 : if (eigenTotal == 0) {
  734. 734 : // do nothing
  735. 735 : } else {
  736. 736 : summary = this.localize('pcTitle')+'\n';
  737. 737 : var pcMapping = ['xAxis', 'yAxis', 'fill'];
  738. 738 : for (var i = 0; i < pcs.length; i++) {
  739. 739 : if (i >= numDims) break;
  740. 740 :
  741. 741 : var eigenValue = pcs[i].get('eigenValue');
  742. 742 : var percentage = eigenValue / eigenTotal * 100;
  743. 743 : summary += this.localize('pc')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%\n';
  744. 744 : }
  745. 745 : }
  746. 746 : } else if (this.getApiParam('analysis') === 'tsne') {
  747. 747 :
  748. 748 : } else {
  749. 749 : summary = this.localize('caTitle')+'\n';
  750. 750 : var pcMapping = ['xAxis', 'yAxis', 'fill'];
  751. 751 :
  752. 752 : var dimensions = rec.getDimensions();
  753. 753 : for (var i = 0; i < dimensions.length; i++) {
  754. 754 : if (i >= numDims) break;
  755. 755 :
  756. 756 : var percentage = dimensions[i].get('percentage');
  757. 757 : summary += this.localize('dimension')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%\n';
  758. 758 : }
  759. 759 : }
  760. 760 :
  761. 761 : var maxFreq = 0;
  762. 762 : var minFreq = Number.MAX_VALUE;
  763. 763 : var maxFill = 0;
  764. 764 : var minFill = Number.MAX_VALUE;
  765. 765 :
  766. 766 :
  767. 767 : if (this.getApiParam('analysis') !== 'docSim') { // docSim doesn't return terms so keep the current ones
  768. 768 : this.getTermStore().removeAll();
  769. 769 : }
  770. 770 :
  771. 771 : var tokens = rec.getTokens();
  772. 772 : var termData = [];
  773. 773 : var docData = [];
  774. 774 : tokens.forEach(function(token) {
  775. 775 : var freq = token.get('rawFreq');
  776. 776 : var category = token.get('category');
  777. 777 : if (category === undefined) {
  778. 778 : category = 'term'; // some analyses don't define categories
  779. 779 : token.set('category', 'term');
  780. 780 : }
  781. 781 : var isTerm = category === 'term';
  782. 782 : if (isTerm) {
  783. 783 : if (freq > maxFreq) maxFreq = freq;
  784. 784 : if (freq < minFreq) minFreq = freq;
  785. 785 : }
  786. 786 : if (this.getTermStore().findExact('term', token.get('term') === -1)) {
  787. 787 : this.getTermStore().addSorted(token);
  788. 788 : }
  789. 789 : if (numDims === 3) {
  790. 790 : var z = token.get('vector')[2];
  791. 791 : if (z !== undefined) {
  792. 792 : if (z < minFill) minFill = z;
  793. 793 : if (z > maxFill) maxFill = z;
  794. 794 : }
  795. 795 : }
  796. 796 : var tokenData = {
  797. 797 : x: token.get('vector')[0], y: token.get('vector')[1] || 0, z: token.get('vector')[2] || 0,
  798. 798 : term: token.get('term'), rawFreq: freq, relativeFreq: token.get('relativeFreq'), cluster: token.get('cluster'), category: category,
  799. 799 : disabled: false
  800. 800 : };
  801. 801 : if (!isTerm) {
  802. 802 : if (token.get('category') === 'bin') {
  803. 803 : tokenData.term = tokenData.title = "Bin "+token.get('docIndex');
  804. 804 : } else {
  805. 805 : tokenData.docIndex = token.get('docIndex');
  806. 806 : var doc = this.getCorpus().getDocument(tokenData.docIndex);
  807. 807 : if (doc !== null) {
  808. 808 : tokenData.term = doc.getShortTitle();
  809. 809 : tokenData.title = doc.getTitle();
  810. 810 : }
  811. 811 : }
  812. 812 : docData.push(tokenData);
  813. 813 : } else {
  814. 814 : termData.push(tokenData);
  815. 815 : }
  816. 816 : }, this);
  817. 817 :
  818. 818 : var newCount = this.getTermStore().getCount();
  819. 819 : this.queryById('limit').setRawValue(newCount);
  820. 820 : this.setApiParam('limit', newCount);
  821. 821 :
  822. 822 :
  823. 823 : var termSeriesStore = Ext.create('Ext.data.JsonStore', {
  824. 824 : fields: ['term', 'x', 'y', 'z', 'rawFreq', 'relativeFreq', 'cluster', 'category', 'docIndex', 'disabled'],
  825. 825 : data: termData
  826. 826 : });
  827. 827 : var docSeriesStore = Ext.create('Ext.data.JsonStore', {
  828. 828 : fields: ['term', 'x', 'y', 'z', 'rawFreq', 'relativeFreq', 'cluster', 'category', 'docIndex', 'disabled'],
  829. 829 : data: docData
  830. 830 : });
  831. 831 :
  832. 832 : var config = {
  833. 833 : itemId: 'chart',
  834. 834 : xtype: 'cartesian',
  835. 835 : interactions: ['crosszoom','panzoom','itemhighlight'],
  836. 836 : plugins: {
  837. 837 : ptype: 'chartitemevents'
  838. 838 : },
  839. 839 : axes: [{
  840. 840 : type: 'numeric',
  841. 841 : position: 'bottom',
  842. 842 : fields: ['x'],
  843. 843 : label: {
  844. 844 : rotate:{degrees:-30}
  845. 845 : }
  846. 846 : },{
  847. 847 : type: 'numeric',
  848. 848 : position: 'left',
  849. 849 : fields: ['y']
  850. 850 : }],
  851. 851 : sprites: [{
  852. 852 : type: 'text',
  853. 853 : text: summary,
  854. 854 : x: 70,
  855. 855 : y: 70
  856. 856 : }],
  857. 857 : innerPadding: {top: 25, right: 25, bottom: 25, left: 25},
  858. 858 : series: [{
  859. 859 : type: 'customScatter',
  860. 860 : xField: 'x',
  861. 861 : yField: 'y',
  862. 862 : store: termSeriesStore,
  863. 863 : label: {
  864. 864 : font: '14px Helvetica',
  865. 865 : field: 'term',
  866. 866 : display: 'over'
  867. 867 : },
  868. 868 : tooltip: {
  869. 869 : trackMouse: true,
  870. 870 : style: 'background: #fff',
  871. 871 : renderer: function (toolTip, record, ctx) {
  872. 872 : toolTip.setHtml(that.tokenFreqTipTemplate.apply([record.get('term'),record.get('rawFreq'),record.get('relativeFreq')]));
  873. 873 : }
  874. 874 : },
  875. 875 : marker: {
  876. 876 : type: 'circle'
  877. 877 : },
  878. 878 : highlight: {
  879. 879 : fillStyle: 'yellow',
  880. 880 : strokeStyle: 'black'
  881. 881 : },
  882. 882 : renderer: function (sprite, config, rendererData, index) {
  883. 883 : var store = rendererData.store;
  884. 884 : var item = store.getAt(index);
  885. 885 : if (item !== null) {
  886. 886 : var clusterIndex = item.get('cluster');
  887. 887 : var scatterplot = that;
  888. 888 :
  889. 889 : if (clusterIndex === -1) {
  890. 890 : // no clusters were specified in initial call
  891. 891 : clusterIndex = 0;
  892. 892 : }
  893. 893 :
  894. 894 : var fillAlpha = 0.65;
  895. 895 : var strokeAlpha = 1;
  896. 896 : if (item.get('disabled') === true) {
  897. 897 : fillAlpha = 0.1;
  898. 898 : strokeAlpha = 0.1;
  899. 899 : } else if (numDims === 3 && item.get('z')) {
  900. 900 : fillAlpha = scatterplot.interpolate(item.get('z'), minFill, maxFill, 0, 1);
  901. 901 : }
  902. 902 : var color = scatterplot.getApplication().getColor(clusterIndex);
  903. 903 : config.fillStyle = 'rgba('+color.join(',')+','+fillAlpha+')';
  904. 904 : config.strokeStyle = 'rgba('+color.join(',')+','+strokeAlpha+')';
  905. 905 :
  906. 906 : var freq = item.get('rawFreq');
  907. 907 : var radius = scatterplot.interpolate(freq, minFreq, maxFreq, 2, 20);
  908. 908 : config.radius = radius;
  909. 909 : }
  910. 910 : },
  911. 911 : scope: this
  912. 912 : },{
  913. 913 : type: 'customScatter',
  914. 914 : xField: 'x',
  915. 915 : yField: 'y',
  916. 916 : store: docSeriesStore,
  917. 917 : label: {
  918. 918 : font: '14px Helvetica',
  919. 919 : field: 'term',
  920. 920 : display: 'over',
  921. 921 : color: this.getDefaultDocColor(true)
  922. 922 : },
  923. 923 : tooltip: {
  924. 924 : trackMouse: true,
  925. 925 : style: 'background: #fff',
  926. 926 : renderer: function (toolTip, record, ctx) {
  927. 927 : toolTip.setHtml(that.docFreqTipTemplate.apply([record.get('title'),record.get('rawFreq')]));
  928. 928 : }
  929. 929 : },
  930. 930 : marker: {
  931. 931 : type: 'diamond'
  932. 932 : },
  933. 933 : highlight: {
  934. 934 : fillStyle: 'yellow',
  935. 935 : strokeStyle: 'black'
  936. 936 : },
  937. 937 : renderer: function (sprite, config, rendererData, index) {
  938. 938 : var store = rendererData.store;
  939. 939 : var item = store.getAt(index);
  940. 940 : if (item !== null) {
  941. 941 : var clusterIndex = item.get('cluster');
  942. 942 : var scatterplot = that;
  943. 943 :
  944. 944 : var color;
  945. 945 : if (clusterIndex === -1 || scatterplot.getApiParam('analysis') !== 'docSim') {
  946. 946 : color = scatterplot.getDefaultDocColor();
  947. 947 : } else {
  948. 948 : color = scatterplot.getApplication().getColor(clusterIndex);
  949. 949 : }
  950. 950 :
  951. 951 : var a = 0.65;
  952. 952 : if (numDims === 3 && item.get('z')) {
  953. 953 : a = scatterplot.interpolate(item.get('z'), minFill, maxFill, 0, 1);
  954. 954 : }
  955. 955 :
  956. 956 : config.fillStyle = 'rgba('+color.join(',')+','+a+')';
  957. 957 : config.strokeStyle = 'rgba('+color.join(',')+',1)';
  958. 958 : config.radius = 5;
  959. 959 : }
  960. 960 : },
  961. 961 : scope: this
  962. 962 :
  963. 963 :
  964. 964 : }],
  965. 965 : listeners: {
  966. 966 : itemclick: function(chart, item, event) {
  967. 967 : var data = item.record.data;
  968. 968 : if (data.category === 'doc') {
  969. 969 : var record = this.getCorpus().getDocument(data.docIndex);
  970. 970 : this.getApplication().dispatchEvent('documentsClicked', this, [record]);
  971. 971 : } else if (data.category === 'term') {
  972. 972 : var record = Ext.create('Voyant.data.model.CorpusTerm', data);
  973. 973 : this.getApplication().dispatchEvent('corpusTermsClicked', this, [record]);
  974. 974 : }
  975. 975 : },
  976. 976 : render: function(chart) {
  977. 977 : chart.body.on('contextmenu', function(event, target) {
  978. 978 : event.preventDefault();
  979. 979 :
  980. 980 : var xy = event.getXY();
  981. 981 : var parentXY = Ext.fly(target).getXY();
  982. 982 : var x = xy[0] - parentXY[0];
  983. 983 : var y = xy[1] - parentXY[1];
  984. 984 : var chartItem = this.down('#chart').getItemForPoint(x,y);
  985. 985 : if (chartItem != null && chartItem.record.get('category') === 'term') {
  986. 986 : var series = this.down('#chart').getSeries();
  987. 987 : series[0].disableToolTips();
  988. 988 : series[1].disableToolTips();
  989. 989 :
  990. 990 : var term = chartItem.record.get('term');
  991. 991 :
  992. 992 : var text = (new Ext.Template(this.localize('removeTerm'))).apply([term]);
  993. 993 : this.getChartMenu().queryById('remove').setText(text);
  994. 994 : text = (new Ext.Template(this.localize('nearbyTerm'))).apply([term]);
  995. 995 : var nearby = this.getChartMenu().queryById('nearby');
  996. 996 : nearby.setText(text);
  997. 997 : nearby.setDisabled(this.getApiParam('analysis') === 'tsne');
  998. 998 :
  999. 999 : this.getChartMenu().on('click', function(menu, item) {
  1000. 1000 : if (item !== undefined) {
  1001. 1001 : var term = chartItem.record.get('term');
  1002. 1002 : if (item.getItemId() === 'nearby') {
  1003. 1003 : this.getNearbyForTerm(term);
  1004. 1004 : } else {
  1005. 1005 : this.removeTerm(term);
  1006. 1006 : }
  1007. 1007 : }
  1008. 1008 : }, this, {single: true});
  1009. 1009 : this.getChartMenu().showAt(xy);
  1010. 1010 : }
  1011. 1011 : }, this);
  1012. 1012 : },
  1013. 1013 : scope: this
  1014. 1014 : }
  1015. 1015 : };
  1016. 1016 :
  1017. 1017 : var chart = Ext.create('Ext.chart.CartesianChart', config);
  1018. 1018 : this.queryById('chartParent').insert(0, chart);
  1019. 1019 :
  1020. 1020 : this.queryById('chartParent').unmask();
  1021. 1021 :
  1022. 1022 : this.doLabels();
  1023. 1023 :
  1024. 1024 : if (this.getNewTerm() !== null) {
  1025. 1025 : this.selectTerm(this.getNewTerm()[0]);
  1026. 1026 : this.setNewTerm(null);
  1027. 1027 : }
  1028. 1028 : },
  1029. 1029 :
  1030. 1030 : getDefaultDocColor: function(returnHex) {
  1031. 1031 : var color = this.getApplication().getColor(6, returnHex);
  1032. 1032 : return color;
  1033. 1033 : },
  1034. 1034 :
  1035. 1035 : doLabels: function() {
  1036. 1036 : var chart = this.queryById('chart');
  1037. 1037 : var series = chart.getSeries();
  1038. 1038 : var summary = chart.getSurface('chart').getItems()[0];
  1039. 1039 : var labels = this.getApiParam("label");
  1040. 1040 : if (labels.indexOf("summary")>-1) {summary.show();}
  1041. 1041 : else {summary.hide();}
  1042. 1042 : if (labels.indexOf("terms")>-1) {series[0].getLabel().show();}
  1043. 1043 : else {series[0].getLabel().hide();}
  1044. 1044 : if (labels.indexOf("docs")>-1) {series[1].getLabel().show();}
  1045. 1045 : else {series[1].getLabel().hide();}
  1046. 1046 : },
  1047. 1047 :
  1048. 1048 : selectTerm: function(term, isDoc) {
  1049. 1049 : var chart = this.down('#chart');
  1050. 1050 : if (chart !== null) {
  1051. 1051 : if (term === undefined) {
  1052. 1052 : chart.getSeries()[0].setHighlightItem(null);
  1053. 1053 : chart.getSeries()[1].setHighlightItem(null);
  1054. 1054 : } else {
  1055. 1055 : var series, index;
  1056. 1056 : if (isDoc === true) {
  1057. 1057 : series = chart.getSeries()[1];
  1058. 1058 : index = series.getStore().findExact('title', term);
  1059. 1059 : } else {
  1060. 1060 : series = chart.getSeries()[0];
  1061. 1061 : index = series.getStore().findExact('term', term);
  1062. 1062 : }
  1063. 1063 : if (index !== -1) {
  1064. 1064 : var record = series.getStore().getAt(index);
  1065. 1065 : var sprite = series.getSprites()[0];
  1066. 1066 : // constructing series item, like in the chart series source
  1067. 1067 : var item = {
  1068. 1068 : series: series,
  1069. 1069 : category: series.getItemInstancing() ? 'items' : 'markers',
  1070. 1070 : index: index,
  1071. 1071 : record: record,
  1072. 1072 : field: series.getYField(),
  1073. 1073 : sprite: sprite
  1074. 1074 : };
  1075. 1075 : series.setHighlightItem(item);
  1076. 1076 : if (isDoc) {
  1077. 1077 : chart.getSeries()[0].setHighlightItem(null);
  1078. 1078 : } else {
  1079. 1079 : chart.getSeries()[1].setHighlightItem(null);
  1080. 1080 : }
  1081. 1081 :
  1082. 1082 : var point = this.getPointFromIndex(series, index);
  1083. 1083 : this.setHighlightData({x: point[0], y: point[1], r: 50});
  1084. 1084 :
  1085. 1085 : if (this.getHighlightTask() == null) {
  1086. 1086 : this.setHighlightTask(Ext.TaskManager.newTask({
  1087. 1087 : run: this.doHighlight,
  1088. 1088 : scope: this,
  1089. 1089 : interval: 25,
  1090. 1090 : repeat: this.getHighlightData().r
  1091. 1091 : }));
  1092. 1092 : }
  1093. 1093 : this.getHighlightTask().restart();
  1094. 1094 : }
  1095. 1095 : }
  1096. 1096 : }
  1097. 1097 : },
  1098. 1098 :
  1099. 1099 : getPointFromIndex: function(series, index) {
  1100. 1100 : var sprite = series.getSprites()[0];
  1101. 1101 : if (sprite.surfaceMatrix !== null) {
  1102. 1102 : var matrix = sprite.attr.matrix.clone().prependMatrix(sprite.surfaceMatrix);
  1103. 1103 : var dataX = sprite.attr.dataX[index];
  1104. 1104 : var dataY = sprite.attr.dataY[index];
  1105. 1105 : return matrix.transformPoint([dataX, dataY]);
  1106. 1106 : } else {
  1107. 1107 : return [0,0];
  1108. 1108 : }
  1109. 1109 : },
  1110. 1110 :
  1111. 1111 : doHighlight: function() {
  1112. 1112 : var chart = this.down('#chart');
  1113. 1113 : if (this.getHighlightData().r > 0) {
  1114. 1114 : var surf = chart.getSurface();
  1115. 1115 : var highlight = null;
  1116. 1116 : var items = surf.getItems();
  1117. 1117 : for (var i = 0; i < items.length; i++) {
  1118. 1118 : var item = items[i];
  1119. 1119 : if (item.id == 'customHighlight') {
  1120. 1120 : highlight = item;
  1121. 1121 : break;
  1122. 1122 : }
  1123. 1123 : }
  1124. 1124 : if (highlight == null) {
  1125. 1125 : surf.add({
  1126. 1126 : id: 'customHighlight',
  1127. 1127 : type: 'circle',
  1128. 1128 : strokeStyle: 'red',
  1129. 1129 : fillStyle: 'none',
  1130. 1130 : radius: this.getHighlightData().r,
  1131. 1131 : x: this.getHighlightData().x,
  1132. 1132 : y: this.getHighlightData().y
  1133. 1133 : });
  1134. 1134 : } else {
  1135. 1135 : highlight.setAttributes({
  1136. 1136 : x: this.getHighlightData().x,
  1137. 1137 : y: this.getHighlightData().y,
  1138. 1138 : radius: this.getHighlightData().r
  1139. 1139 : });
  1140. 1140 : this.getHighlightData().r -= 1.5;
  1141. 1141 : if (this.getHighlightData().r <= 0) {
  1142. 1142 : this.getHighlightData().r = 0;
  1143. 1143 : surf.remove(highlight, true);
  1144. 1144 : }
  1145. 1145 : }
  1146. 1146 : chart.redraw();
  1147. 1147 : }
  1148. 1148 : },
  1149. 1149 :
  1150. 1150 : filterChart: function(query) {
  1151. 1151 : if (Ext.isString(query)) query = [query];
  1152. 1152 : var reQueries = [];
  1153. 1153 : for (var i = 0; i < query.length; i++) {
  1154. 1154 : var re = new RegExp(query[i]);
  1155. 1155 : reQueries.push(re);
  1156. 1156 : }
  1157. 1157 :
  1158. 1158 : // filter terms
  1159. 1159 : var chart = this.queryById('chart');
  1160. 1160 : var series0 = chart.getSeries()[0];
  1161. 1161 : var label0 = series0.getLabel();
  1162. 1162 : series0.getStore().each(function(item) {
  1163. 1163 : var match = false;
  1164. 1164 : if (reQueries.length == 0) match = true;
  1165. 1165 : else {
  1166. 1166 : for (var i = 0; i < reQueries.length; i++) {
  1167. 1167 : match = match || reQueries[i].test(item.get('term'));
  1168. 1168 : if (match) break;
  1169. 1169 : }
  1170. 1170 : }
  1171. 1171 : item.set('disabled', !match);
  1172. 1172 : var index = item.store.indexOf(item);
  1173. 1173 : label0.setAttributesFor(index, {hidden: !match});
  1174. 1174 : }, this);
  1175. 1175 :
  1176. 1176 : chart.redraw();
  1177. 1177 : },
  1178. 1178 :
  1179. 1179 : getCurrentTerms: function() {
  1180. 1180 : var terms = [];
  1181. 1181 : this.getTermStore().each(function(r) {
  1182. 1182 : if (r.get('category') === 'term') {
  1183. 1183 : terms.push(r.get('term'));
  1184. 1184 : }
  1185. 1185 : });
  1186. 1186 : return terms;
  1187. 1187 : },
  1188. 1188 :
  1189. 1189 : getNearbyForTerm: function(term) {
  1190. 1190 : var limit = Math.max(2000, Math.round(this.getCorpus().getWordTokensCount() / 100));
  1191. 1191 : this.setApiParams({limit: limit, target: term});
  1192. 1192 : this.loadFromApis();
  1193. 1193 : this.setApiParam('target', undefined);
  1194. 1194 : },
  1195. 1195 :
  1196. 1196 : removeTerm: function(term) {
  1197. 1197 : var series = this.down('#chart').getSeries()[0];
  1198. 1198 : var index = series.getStore().findExact('term', term);
  1199. 1199 : series.getStore().removeAt(index);
  1200. 1200 :
  1201. 1201 : index = this.getTermStore().findExact('term', term);
  1202. 1202 : this.getTermStore().removeAt(index);
  1203. 1203 :
  1204. 1204 : var newCount = this.getTermStore().getCount();
  1205. 1205 : this.queryById('limit').setRawValue(newCount);
  1206. 1206 : },
  1207. 1207 :
  1208. 1208 : loadFromApis: function(keepCurrentTerms) {
  1209. 1209 : this.queryById('chartParent').mask(this.localize('analyzing'));
  1210. 1210 :
  1211. 1211 : var params = {};
  1212. 1212 : var terms = this.getCurrentTerms();
  1213. 1213 : if (this.getNewTerm() !== null) {
  1214. 1214 : terms = terms.concat(this.getNewTerm());
  1215. 1215 : this.setApiParam('limit', terms.length);
  1216. 1216 : }
  1217. 1217 : if (terms.length > 0) {
  1218. 1218 : if (this.getNewTerm() !== null || keepCurrentTerms) {
  1219. 1219 : params.query = terms.join(',');
  1220. 1220 : }
  1221. 1221 : // params.term = terms;
  1222. 1222 : }
  1223. 1223 : Ext.apply(params, this.getApiParams());
  1224. 1224 : if (params.target != null) {
  1225. 1225 : params.term = terms;
  1226. 1226 : }
  1227. 1227 : if (params.analysis === 'pca') {
  1228. 1228 : this.getPcaStore().load({
  1229. 1229 : params: params
  1230. 1230 : });
  1231. 1231 : } else if (params.analysis === 'tsne'){
  1232. 1232 : this.getTsneStore().load({
  1233. 1233 : params: params
  1234. 1234 : });
  1235. 1235 : } else if (params.analysis === 'docSim'){
  1236. 1236 : this.getDocSimStore().load({
  1237. 1237 : params: params
  1238. 1238 : });
  1239. 1239 : } else {
  1240. 1240 : this.getCaStore().load({
  1241. 1241 : params: params
  1242. 1242 : });
  1243. 1243 : }
  1244. 1244 : },
  1245. 1245 :
  1246. 1246 : interpolate: function(lambda, minSrc, maxSrc, minDst, maxDst) {
  1247. 1247 : return minDst + (maxDst - minDst) * Math.max(0, Math.min(1, (lambda - minSrc) / (maxSrc - minSrc)));
  1248. 1248 : }
  1249. 1249 : });
  1250. 1250 :
  1251. 1251 : /*
  1252. 1252 : * Adds tool tip disabling.
  1253. 1253 : */
  1254. 1254 : Ext.define('Ext.chart.series.CustomScatter', {
  1255. 1255 : extend: 'Ext.chart.series.Scatter',
  1256. 1256 :
  1257. 1257 : alias: 'series.customScatter',
  1258. 1258 : type: 'customScatter',
  1259. 1259 : seriesType: 'scatterSeries',
  1260. 1260 :
  1261. 1261 : tipsDisabled: false,
  1262. 1262 :
  1263. 1263 : enableToolTips: function() {
  1264. 1264 : this.tipsDisabled = false;
  1265. 1265 : },
  1266. 1266 :
  1267. 1267 : disableToolTips: function() {
  1268. 1268 : this.tipsDisabled = true;
  1269. 1269 : },
  1270. 1270 :
  1271. 1271 : showTip: function (item, xy) {
  1272. 1272 : if (this.tipsDisabled) {
  1273. 1273 : return;
  1274. 1274 : }
  1275. 1275 :
  1276. 1276 : this.callParent(arguments);
  1277. 1277 : }
  1278. 1278 : });