1. 1 : /**
  2. 2 : * TermsRadio is a visualization that depicts the change of the frequency of words in a corpus (or within a single document).
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "bins": 5,
  8. 8 : * "limit": null,
  9. 9 : * "query": null,
  10. 10 : * "slider": null,
  11. 11 : * "speed": null,
  12. 12 : * "stopList": null,
  13. 13 : * "visibleBins": null,
  14. 14 : * "yAxisScale": null
  15. 15 : * };
  16. 16 : *
  17. 17 : * loadCorpus("austen").tool("termsradio", config);
  18. 18 : *
  19. 19 : *
  20. 20 : * @class TermsRadio
  21. 21 : * @tutorial termsradio
  22. 22 : * @memberof Tools
  23. 23 : * @author Mark Turcato
  24. 24 : * @author Andrew MacDonald
  25. 25 : */
  26. 26 : Ext.define('Voyant.panel.TermsRadio', {
  27. 27 : extend: 'Ext.panel.Panel',
  28. 28 : mixins: ['Voyant.panel.Panel'],
  29. 29 : alias: 'widget.termsradio',
  30. 30 : config: {
  31. 31 : /**
  32. 32 : * @private
  33. 33 : */
  34. 34 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  35. 35 : /**
  36. 36 : * @private
  37. 37 : */
  38. 38 : speed: 50,
  39. 39 : /**
  40. 40 : * @private
  41. 41 : */
  42. 42 : termsRadio: undefined
  43. 43 : },
  44. 44 : statics: {
  45. 45 : i18n: {
  46. 46 : },
  47. 47 : api: {
  48. 48 : /**
  49. 49 : * @memberof Tools.TermsRadio
  50. 50 : * @instance
  51. 51 : * @property {bins}
  52. 52 : * @default
  53. 53 : */
  54. 54 : bins: 5,
  55. 55 :
  56. 56 : /**
  57. 57 : * @memberof Tools.TermsRadio
  58. 58 : * @instance
  59. 59 : * @property {Number} visibleBins How many segments or documents to show at once (default is 5).
  60. 60 : * Note that this often works in parallel with the {@link #bins} value.
  61. 61 : * @default
  62. 62 : */
  63. 63 : visibleBins: 5,
  64. 64 :
  65. 65 : /**
  66. 66 : * @memberof Tools.TermsRadio
  67. 67 : * @instance
  68. 68 : * @property {String[]} docIdType The document type(s) to restrict results to.
  69. 69 : * @default null
  70. 70 : * @private
  71. 71 : */
  72. 72 : docIdType: null,
  73. 73 :
  74. 74 : /**
  75. 75 : * @memberof Tools.TermsRadio
  76. 76 : * @instance
  77. 77 : * @property {limit}
  78. 78 : * @default
  79. 79 : */
  80. 80 : limit: 50,
  81. 81 :
  82. 82 : /**
  83. 83 : * @instance
  84. 84 : * @property mode What mode to operate at, either document or corpus.
  85. 85 : * @choices document, corpus
  86. 86 : * @private
  87. 87 : */
  88. 88 : mode: null,
  89. 89 :
  90. 90 : /**
  91. 91 : * @memberof Tools.TermsRadio
  92. 92 : * @instance
  93. 93 : * @property {Number} position The current shifted position of the visualization.
  94. 94 : * @default 0
  95. 95 : * @private
  96. 96 : */
  97. 97 : position: 0,
  98. 98 :
  99. 99 : /**
  100. 100 : * @memberof Tools.TermsRadio
  101. 101 : * @instance
  102. 102 : * @property {String[]} selectedWords The words that have been selected.
  103. 103 : * @default null
  104. 104 : * @private
  105. 105 : */
  106. 106 : selectedWords: [],
  107. 107 :
  108. 108 : /**
  109. 109 : * @memberof Tools.TermsRadio
  110. 110 : * @instance
  111. 111 : * @property {stopList}
  112. 112 : * @default
  113. 113 : */
  114. 114 : stopList: 'auto',
  115. 115 :
  116. 116 : /**
  117. 117 : * @memberof Tools.TermsRadio
  118. 118 : * @instance
  119. 119 : * @property {query}
  120. 120 : */
  121. 121 : query: null,
  122. 122 :
  123. 123 : /**
  124. 124 : * @memberof Tools.TermsRadio
  125. 125 : * @instance
  126. 126 : * @property {String} yAxisScale The scale for the y axis. Options are: 'log' or 'linear'.
  127. 127 : * @default log
  128. 128 : */
  129. 129 : yAxisScale: 'log',
  130. 130 :
  131. 131 : /**
  132. 132 : * @memberof Tools.TermsRadio
  133. 133 : * @instance
  134. 134 : * @property {Number} speed How fast to animate the visualization.
  135. 135 : * @default
  136. 136 : */
  137. 137 : speed: 50,
  138. 138 :
  139. 139 : /**
  140. 140 : * @memberof Tools.TermsRadio
  141. 141 : * @instance
  142. 142 : * @property {Boolean} slider Whether to show the slider.
  143. 143 : * @default true
  144. 144 : */
  145. 145 : slider: undefined
  146. 146 : },
  147. 147 : glyph: 'xf201@FontAwesome'
  148. 148 : }
  149. 149 :
  150. 150 : /**
  151. 151 : * @private
  152. 152 : */
  153. 153 : ,constructor: function(config) {
  154. 154 :
  155. 155 : var onLoadHandler = function(mode, store, records, success, operation) {
  156. 156 : this.setApiParams({mode: mode});
  157. 157 :
  158. 158 : this.getTermsRadio().loadRecords(records);
  159. 159 :
  160. 160 : var query = this.getApiParam('query');
  161. 161 : // check for no results
  162. 162 : if (query) {
  163. 163 : if (records.length==0 || (records.length==1 && records[0].getRawFreq()==0)) {
  164. 164 : this.toastInfo({
  165. 165 : html: this.localize("termNotFound"),
  166. 166 : align: 'bl'
  167. 167 : });
  168. 168 : } else {
  169. 169 : this.getTermsRadio().highlightQuery(query, true);
  170. 170 : }
  171. 171 : }
  172. 172 : };
  173. 173 :
  174. 174 : this.corpusStore = Ext.create("Voyant.data.store.CorpusTerms", {
  175. 175 : listeners : {
  176. 176 : load: {
  177. 177 : fn : onLoadHandler.bind(this, 'corpus'),
  178. 178 : scope : this
  179. 179 : }
  180. 180 : }
  181. 181 : });
  182. 182 :
  183. 183 : this.documentStore = Ext.create("Voyant.data.store.DocumentTerms", {
  184. 184 : listeners : {
  185. 185 : load: {
  186. 186 : fn : onLoadHandler.bind(this, 'document'),
  187. 187 : scope : this
  188. 188 : }
  189. 189 : }
  190. 190 : });
  191. 191 :
  192. 192 : Ext.apply(config, {
  193. 193 : title: this.localize('title'),
  194. 194 : legendMenu: Ext.create('Ext.menu.Menu', {
  195. 195 : items: [
  196. 196 : {text: '', itemId: 'remove', glyph: 'xf068@FontAwesome'}
  197. 197 : ]
  198. 198 : }),
  199. 199 : tbar: new Ext.Toolbar({
  200. 200 : overflowHandler: 'scroller',
  201. 201 : items: {
  202. 202 : xtype: 'legend',
  203. 203 : store: new Ext.data.JsonStore({
  204. 204 : fields : ['name', 'mark', 'selector']
  205. 205 : }),
  206. 206 : listeners: {
  207. 207 : itemclick: function(view, record, el, index) {
  208. 208 : var term = record.get('name');
  209. 209 : if (this.getTermsRadio().isTermSelected(term)) {
  210. 210 : this.getTermsRadio().doTermDeselect(term);
  211. 211 : } else {
  212. 212 : this.getTermsRadio().doTermSelect(term);
  213. 213 : }
  214. 214 : },
  215. 215 : itemcontextmenu: function(view, record, el, index, event) {
  216. 216 : event.preventDefault();
  217. 217 : var xy = event.getXY();
  218. 218 :
  219. 219 : var term = record.get('name');
  220. 220 : var text = (new Ext.Template(this.localize('removeTerm'))).apply([term]);
  221. 221 : this.legendMenu.queryById('remove').setText(text);
  222. 222 :
  223. 223 : this.legendMenu.on('click', function(menu, item) {
  224. 224 : if (item !== undefined) {
  225. 225 : this.getTermsRadio().doTermDeselect(term, true);
  226. 226 : }
  227. 227 : }, this, {single: true});
  228. 228 : this.legendMenu.showAt(xy);
  229. 229 : },
  230. 230 : scope: this
  231. 231 : }
  232. 232 : }
  233. 233 : }),
  234. 234 : bbar: {
  235. 235 : overflowHandler: 'scroller',
  236. 236 : items: [{
  237. 237 : xtype: 'querysearchfield'
  238. 238 : },{
  239. 239 : glyph: 'xf04b@FontAwesome', // start with play button, which means we're paused
  240. 240 : itemId: 'play',
  241. 241 : handler: function(btn) {
  242. 242 : var playing = btn.glyph=="xf04c@FontAwesome";
  243. 243 : if (playing) {
  244. 244 : this.getTermsRadio().continueTransition = false;
  245. 245 : this.mask(this.localize("completingTransition"))
  246. 246 : btn.setPlaying(false)
  247. 247 : }
  248. 248 : else {
  249. 249 : this.getTermsRadio().toggleRightCheck();
  250. 250 : btn.setPlaying(true);
  251. 251 : }
  252. 252 : },
  253. 253 : scope: this,
  254. 254 : setPlaying: function(bool) {
  255. 255 : this.setGlyph(bool ? "xf04c@FontAwesome" : "xf04b@FontAwesome")
  256. 256 : }
  257. 257 : },{
  258. 258 : glyph: 'xf0e2@FontAwesome',
  259. 259 : // text: this.localize('reset')
  260. 260 : tooltip : this.localize('resetTip'),
  261. 261 : listeners : {
  262. 262 : click : {fn : function() {
  263. 263 : this.queryById("play").setPlaying(false);
  264. 264 : this.getTermsRadio().shiftCount = 0;
  265. 265 : this.getTermsRadio().prepareData();
  266. 266 : this.getTermsRadio().redraw();
  267. 267 : }
  268. 268 : ,scope : this
  269. 269 : }
  270. 270 : }
  271. 271 : },{
  272. 272 : xtype: 'label',
  273. 273 : forId: 'terms',
  274. 274 : text: this.localize('terms')
  275. 275 : },{
  276. 276 : xtype: 'slider',
  277. 277 : itemId: 'terms',
  278. 278 : hideLabel: true,
  279. 279 : width : 60,
  280. 280 : increment : 5,
  281. 281 : minValue : 5,
  282. 282 : maxValue : 100,
  283. 283 : listeners: {
  284. 284 : afterrender: function(slider) {
  285. 285 : slider.setValue(parseInt(this.getApiParam("limit")))
  286. 286 : },
  287. 287 : changecomplete: function(slider, newvalue) {
  288. 288 : this.setApiParams({limit: newvalue});
  289. 289 : this.loadStore();
  290. 290 : },
  291. 291 : scope: this
  292. 292 : }
  293. 293 : },{
  294. 294 : xtype: 'label',
  295. 295 : forId: 'speed',
  296. 296 : text: this.localize('speed')
  297. 297 : },{
  298. 298 : xtype: 'slider',
  299. 299 : itemId: 'speed',
  300. 300 : hideLabel: true,
  301. 301 : width : 60,
  302. 302 : increment : 5,
  303. 303 : minValue : 5,
  304. 304 : maxValue : 100,
  305. 305 : listeners: {
  306. 306 : afterrender: function(slider) {
  307. 307 : slider.setValue(parseInt(this.getApiParam("speed")))
  308. 308 : this.setSpeed(slider.getValue())
  309. 309 : },
  310. 310 : changecomplete: function(slider, newvalue) {
  311. 311 : this.setApiParams({speed: newvalue});
  312. 312 : this.setSpeed(newvalue)
  313. 313 : },
  314. 314 : scope: this
  315. 315 : }
  316. 316 : },{
  317. 317 : xtype: 'label',
  318. 318 : itemId: 'visibleSegmentsLabel',
  319. 319 : forId: 'visibleBins',
  320. 320 : text: this.localize('visibleSegments')
  321. 321 : },{
  322. 322 : xtype: 'slider',
  323. 323 : itemId: 'visibleBins',
  324. 324 : hideLabel: true,
  325. 325 : width : 60,
  326. 326 : increment : 1,
  327. 327 : minValue : 1,
  328. 328 : maxValue : 100,
  329. 329 : listeners: {
  330. 330 : afterrender: function(slider) {
  331. 331 : slider.setValue(parseInt(this.getApiParam("visibleBins")))
  332. 332 : },
  333. 333 : changecomplete: function(slider, newvalue) {
  334. 334 : this.setApiParams({visibleBins: newvalue});
  335. 335 : this.numVisPoints = newvalue;
  336. 336 : this.loadStore();
  337. 337 :
  338. 338 : if (this.numVisPoints == this.getCorpus().getDocumentsCount()) {
  339. 339 : this.getTermsRadio().hideSlider();
  340. 340 : } else if (this.getApiParam("slider") != 'false'){
  341. 341 : this.getTermsRadio().showSlider();
  342. 342 : }
  343. 343 : },
  344. 344 : scope: this
  345. 345 : }
  346. 346 : },{
  347. 347 : xtype: 'label',
  348. 348 : itemId: 'segmentsLabel',
  349. 349 : forId: 'segments',
  350. 350 : text: this.localize('segments')
  351. 351 : },{
  352. 352 : xtype: 'slider',
  353. 353 : itemId: 'segments',
  354. 354 : hideLabel: true,
  355. 355 : width : 60,
  356. 356 : increment : 1,
  357. 357 : minValue : 1,
  358. 358 : maxValue : 100,
  359. 359 : listeners: {
  360. 360 : afterrender: function(slider) {
  361. 361 : slider.setValue(parseInt(this.getApiParam("bins")))
  362. 362 : },
  363. 363 : changecomplete: function(slider, newvalue) {
  364. 364 : this.setApiParams({bins: newvalue});
  365. 365 : this.numDataPoints = newvalue;
  366. 366 : this.loadStore();
  367. 367 : var visibleBins = this.queryById('visibleBins');
  368. 368 : visibleBins.setMaxValue(newvalue) // only relevant for doc mode
  369. 369 : },
  370. 370 : scope: this
  371. 371 : }
  372. 372 : }]
  373. 373 : }
  374. 374 : });
  375. 375 :
  376. 376 : // need to add option here so we have access to localize
  377. 377 : this.config.options.push({
  378. 378 : xtype: 'combo',
  379. 379 : queryMode : 'local',
  380. 380 : triggerAction : 'all',
  381. 381 : forceSelection : true,
  382. 382 : editable : false,
  383. 383 : fieldLabel : this.localize('yScale'),
  384. 384 : labelAlign : 'right',
  385. 385 : name : 'yAxisScale',
  386. 386 : valueField : 'value',
  387. 387 : displayField : 'name',
  388. 388 : store: new Ext.data.JsonStore({
  389. 389 : fields : ['name', 'value'],
  390. 390 : data : [{
  391. 391 : name : this.localize('linear'), value: 'linear'
  392. 392 : },{
  393. 393 : name : this.localize('log'), value: 'log'
  394. 394 : }]
  395. 395 : }),
  396. 396 : listeners: {
  397. 397 : afterrender: function(combo) {
  398. 398 : combo.setValue(this.getApiParam('yAxisScale'));
  399. 399 : },
  400. 400 : scope: this
  401. 401 : }
  402. 402 : });
  403. 403 :
  404. 404 : this.callParent(arguments);
  405. 405 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  406. 406 :
  407. 407 : this.on('boxready', function(component) {
  408. 408 : var sliderParam = this.getApiParam('slider');
  409. 409 : var showSlider = sliderParam === undefined ? true : sliderParam === 'true';
  410. 410 : var config = {
  411. 411 : parent: this,
  412. 412 : container: this.body,
  413. 413 : showSlider: showSlider
  414. 414 : };
  415. 415 : this.setTermsRadio(new TermsRadio(config));
  416. 416 : }, this);
  417. 417 :
  418. 418 : /**
  419. 419 : * @event corpusTypesSelected
  420. 420 : * @type listener
  421. 421 : * @private
  422. 422 : */
  423. 423 : this.addListener('corpusTermsClicked', function(src, terms){
  424. 424 : if (this.getCorpus().getDocumentsCount() > 1) {
  425. 425 : terms.forEach(function(term) {
  426. 426 : var t = term.getTerm();
  427. 427 : this.setApiParams({query: t});
  428. 428 : this.loadStore();
  429. 429 : }, this);
  430. 430 : }
  431. 431 : });
  432. 432 :
  433. 433 : this.addListener('documentTermsClicked', function(src, terms){
  434. 434 : if(src && src.xtype==this.xtype) {return false;}
  435. 435 :
  436. 436 : terms.forEach(function(term) {
  437. 437 : var t = term.getTerm();
  438. 438 : this.setApiParams({query: t});
  439. 439 : this.loadStore();
  440. 440 : }, this);
  441. 441 : });
  442. 442 :
  443. 443 : this.on('query', function(src, query){
  444. 444 : this.fireEvent("termsClicked", src, query);
  445. 445 : });
  446. 446 :
  447. 447 : this.on("termsClicked", function(src, terms) {
  448. 448 : // TODO load term distribution data
  449. 449 : terms.forEach(function(term) {
  450. 450 : var queryTerm;
  451. 451 : if (Ext.isString(term)) {queryTerm = term;}
  452. 452 : else if (term.term) {queryTerm = term.term;}
  453. 453 : else if (term.getTerm) {queryTerm = term.getTerm();}
  454. 454 :
  455. 455 : // TODO handling for multiple terms
  456. 456 : this.setApiParams({query: queryTerm});
  457. 457 : this.loadStore();
  458. 458 : }, this);
  459. 459 : });
  460. 460 :
  461. 461 : this.on("loadedCorpus", function(src, corpus) {
  462. 462 : this.documentStore.setCorpus(corpus);
  463. 463 : this.corpusStore.setCorpus(corpus);
  464. 464 :
  465. 465 : var params = this.getApiParams();
  466. 466 : params.withDistributions = true;
  467. 467 : if (params.type) {
  468. 468 : delete params.limit;
  469. 469 : }
  470. 470 : var store;
  471. 471 :
  472. 472 : var docsCount = this.getCorpus().getDocumentsCount();
  473. 473 : var segments = this.queryById("segments");
  474. 474 : var visibleBins = this.queryById("visibleBins");
  475. 475 : if (params.mode=='document' || docsCount == 1) {
  476. 476 : this.setApiParam("mode", "document");
  477. 477 : store = this.documentStore;
  478. 478 : visibleBins.setMaxValue(segments.getValue())
  479. 479 : } else {
  480. 480 : this.setApiParam("mode", "corpus");
  481. 481 : delete params.bins;
  482. 482 : store = this.corpusStore;
  483. 483 : segments.hide();
  484. 484 : this.queryById("segmentsLabel").hide();
  485. 485 : var visibleBins = this.queryById("visibleBins");
  486. 486 : visibleBins.setMaxValue(docsCount);
  487. 487 : if (parseInt(this.getApiParam("visibleBins")>docsCount)) {
  488. 488 : visibleBins.setValue(docsCount);
  489. 489 : }
  490. 490 : }
  491. 491 :
  492. 492 : // select top 3 words
  493. 493 : store.on('load', function(store, records) {
  494. 494 : for (var i = 0; i < 3; i++) {
  495. 495 : var r = records[i];
  496. 496 : if (r) {
  497. 497 : this.getTermsRadio().highlightRecord(r, true);
  498. 498 : }
  499. 499 : }
  500. 500 : }, this, {single: true});
  501. 501 : store.load({params: params});
  502. 502 : }, this);
  503. 503 : }
  504. 504 :
  505. 505 : ,loadStore: function () {
  506. 506 : this.queryById('play').setPlaying(false);
  507. 507 : var params = this.getApiParams();
  508. 508 : params.withDistributions = true;
  509. 509 : if(this.getApiParam('mode') === 'document') {
  510. 510 : this.documentStore.load({params: params});
  511. 511 : }
  512. 512 : if(this.getApiParam('mode') === 'corpus') {
  513. 513 : delete params.bins;
  514. 514 : this.corpusStore.load({params: params});
  515. 515 : }
  516. 516 : }
  517. 517 :
  518. 518 : });