1. 1 : // assuming Bubblelines library is loaded by containing page (via voyant.jsp)
  2. 2 : /**
  3. 3 : * Bubblelines visualizes the frequency and distribution of terms in a corpus.
  4. 4 : *
  5. 5 : * @example
  6. 6 : *
  7. 7 : * let config = {
  8. 8 : * bins: 5, // number of bins to separate a document into
  9. 9 : * docIndex: 1, //document index to restrict to (can be comma-separated list)
  10. 10 : * maxDocs: 5, // maximum number of documents to show
  11. 11 : * query: "love", // a query to search for in the corpus
  12. 12 : * stopList: null, // a named stopword list or comma-separated list of words
  13. 13 : * };
  14. 14 : *
  15. 15 : * loadCorpus("austen").tool("bubblelines", config);
  16. 16 : *
  17. 17 : * @class Bubblelines
  18. 18 : * @tutorial bubblelines
  19. 19 : * @memberof Tools
  20. 20 : */
  21. 21 : Ext.define('Voyant.panel.Bubblelines', {
  22. 22 : extend: 'Ext.panel.Panel',
  23. 23 : mixins: ['Voyant.panel.Panel'],
  24. 24 : alias: 'widget.bubblelines',
  25. 25 : statics: {
  26. 26 : i18n: {
  27. 27 : },
  28. 28 : api: {
  29. 29 : /**
  30. 30 : * @memberof Tools.Bubblelines
  31. 31 : * @instance
  32. 32 : * @property {bins}
  33. 33 : * @default
  34. 34 : */
  35. 35 : bins: 50,
  36. 36 :
  37. 37 : /**
  38. 38 : * @memberof Tools.Bubblelines
  39. 39 : * @instance
  40. 40 : * @property {query}
  41. 41 : */
  42. 42 : query: null,
  43. 43 :
  44. 44 : /**
  45. 45 : * @memberof Tools.Bubblelines
  46. 46 : * @instance
  47. 47 : * @property {stopList}
  48. 48 : * @default
  49. 49 : */
  50. 50 : stopList: 'auto',
  51. 51 :
  52. 52 : /**
  53. 53 : * @memberof Tools.Bubblelines
  54. 54 : * @instance
  55. 55 : * @property {docId}
  56. 56 : */
  57. 57 : docId: undefined,
  58. 58 :
  59. 59 : /**
  60. 60 : * @memberof Tools.Bubblelines
  61. 61 : * @instance
  62. 62 : * @property {docIndex}
  63. 63 : */
  64. 64 : docIndex: undefined,
  65. 65 :
  66. 66 : /**
  67. 67 : * @memberof Tools.Bubblelines
  68. 68 : * @instance
  69. 69 : * @property {Number} maxDocs The maximum number of documents to show.
  70. 70 : * @default
  71. 71 : */
  72. 72 : maxDocs: 50
  73. 73 : },
  74. 74 : glyph: 'xf06e@FontAwesome'
  75. 75 : },
  76. 76 : config: {
  77. 77 : bubblelines: undefined,
  78. 78 : termStore: undefined,
  79. 79 : docTermStore: undefined,
  80. 80 : selectedDocs: undefined,
  81. 81 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'},{xtype: 'colorpaletteoption'}]
  82. 82 : },
  83. 83 :
  84. 84 : termTpl: new Ext.XTemplate(
  85. 85 : '<tpl for=".">',
  86. 86 : '<div class="term" style="color: rgb({color});float: left;padding: 3px;margin: 2px;">{term}</div>',
  87. 87 : '</tpl>'
  88. 88 : ),
  89. 89 :
  90. 90 : constructor: function() {
  91. 91 : this.callParent(arguments);
  92. 92 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  93. 93 :
  94. 94 : this.on('loadedCorpus', function(src, corpus) {
  95. 95 : this.setDocTermStore(corpus.getDocumentTerms({
  96. 96 : proxy: {
  97. 97 : extraParams: {
  98. 98 : withDistributions: 'raw',
  99. 99 : withPositions: true
  100. 100 : }
  101. 101 : },
  102. 102 : listeners: {
  103. 103 : load: function(store, records, successful, options) {
  104. 104 : records.forEach(function(record) {
  105. 105 : var termData = this.processTerms(record);
  106. 106 : var docId = record.get('docId');
  107. 107 : var term = record.get('term');
  108. 108 : var termObj = {};
  109. 109 : termObj[term] = termData;
  110. 110 : this.getBubblelines().addTermsToDoc(termObj, docId);
  111. 111 : }, this);
  112. 112 : this.getBubblelines().doBubblelinesLayout();
  113. 113 : },
  114. 114 : scope: this
  115. 115 : }
  116. 116 : }));
  117. 117 :
  118. 118 : if (this.isVisible() && this.getBubblelines()) {
  119. 119 : this.initLoad();
  120. 120 : }
  121. 121 : }, this);
  122. 122 :
  123. 123 : this.on('activate', function() { // load after tab activate (if we're in a tab panel)
  124. 124 : if (this.getCorpus()) {
  125. 125 : Ext.Function.defer(this.initLoad, 100, this);
  126. 126 : }
  127. 127 : }, this);
  128. 128 :
  129. 129 : this.on('query', function(src, query) {
  130. 130 : if (query !== undefined && query != '') {
  131. 131 : this.getDocTermsFromQuery(query);
  132. 132 : }
  133. 133 : }, this);
  134. 134 :
  135. 135 : this.on('documentsSelected', function(src, docIds) {
  136. 136 : this.setApiParam('docId', docIds);
  137. 137 : this.getBubblelines().cache.each(function(d) {
  138. 138 : d.hidden = docIds.indexOf(d.id) === -1;
  139. 139 : });
  140. 140 : this.getBubblelines().drawGraph();
  141. 141 : }, this);
  142. 142 :
  143. 143 : this.on('termsClicked', function(src, terms) {
  144. 144 : if (src !== this) {
  145. 145 : var queryTerms = [];
  146. 146 : terms.forEach(function(term) {
  147. 147 : if (Ext.isString(term)) {queryTerms.push(term);}
  148. 148 : else if (term.term) {queryTerms.push(term.term);}
  149. 149 : else if (term.getTerm) {queryTerms.push(term.getTerm());}
  150. 150 : });
  151. 151 : this.getDocTermsFromQuery(queryTerms);
  152. 152 : }
  153. 153 : }, this);
  154. 154 :
  155. 155 : this.on('documentTermsClicked', function(src, terms) {
  156. 156 : var queryTerms = [];
  157. 157 : terms.forEach(function(term) {
  158. 158 : if (term.getTerm()) {queryTerms.push(term.getTerm());}
  159. 159 : });
  160. 160 : this.getDocTermsFromQuery(queryTerms);
  161. 161 : }, this);
  162. 162 :
  163. 163 : this.down('#granularity').setValue(parseInt(this.getApiParam('bins')));
  164. 164 : },
  165. 165 :
  166. 166 : initComponent: function() {
  167. 167 : this.setTermStore(Ext.create('Ext.data.ArrayStore', {
  168. 168 : fields: ['term', 'color'],
  169. 169 : listeners: {
  170. 170 : load: function(store, records, successful, options) {
  171. 171 : var termsView = this.down('#termsView');
  172. 172 : for (var i = 0; i < records.length; i++) {
  173. 173 : var r = records[i];
  174. 174 : termsView.select(r, true);
  175. 175 : }
  176. 176 : },
  177. 177 : scope: this
  178. 178 : }
  179. 179 : }));
  180. 180 :
  181. 181 : Ext.apply(this, {
  182. 182 : title: this.localize('title'),
  183. 183 : dockedItems: [{
  184. 184 : dock: 'bottom',
  185. 185 : xtype: 'toolbar',
  186. 186 : overflowHandler: 'scroller',
  187. 187 : items: [{
  188. 188 : xtype: 'querysearchfield'
  189. 189 : },{
  190. 190 : text: this.localize('clearTerms'),
  191. 191 : glyph: 'xf014@FontAwesome',
  192. 192 : handler: function() {
  193. 193 : this.down('#termsView').getSelectionModel().deselectAll(true);
  194. 194 : this.getTermStore().removeAll();
  195. 195 : this.setApiParams({query: null});
  196. 196 : this.getBubblelines().removeAllTerms();
  197. 197 : this.getBubblelines().drawGraph();
  198. 198 : },
  199. 199 : scope: this
  200. 200 : },{
  201. 201 : xtype: 'documentselectorbutton'
  202. 202 : },{
  203. 203 : xtype: 'slider',
  204. 204 : itemId: 'granularity',
  205. 205 : fieldLabel: this.localize('granularity'),
  206. 206 : labelAlign: 'right',
  207. 207 : labelWidth: 70,
  208. 208 : width: 150,
  209. 209 : increment: 10,
  210. 210 : minValue: 10,
  211. 211 : maxValue: 300,
  212. 212 : listeners: {
  213. 213 : changecomplete: function(slider, newvalue) {
  214. 214 : this.setApiParams({bins: newvalue});
  215. 215 : this.getBubblelines().bubbleSpacing = newvalue;
  216. 216 : this.reloadTermsData();
  217. 217 : },
  218. 218 : scope: this
  219. 219 : }
  220. 220 : },{
  221. 221 : xtype: 'checkbox',
  222. 222 : boxLabel: this.localize('separateLines'),
  223. 223 : boxLabelAlign: 'before',
  224. 224 : checked: false,
  225. 225 : handler: function(checkbox, checked) {
  226. 226 : this.getBubblelines().SEPARATE_LINES_FOR_TERMS = checked;
  227. 227 : this.getBubblelines().lastClickedBubbles = {};
  228. 228 : this.getBubblelines().setCanvasHeight();
  229. 229 : this.getBubblelines().drawGraph();
  230. 230 : },
  231. 231 : scope: this
  232. 232 :
  233. 233 : }]
  234. 234 : }],
  235. 235 : border: false,
  236. 236 : layout: 'fit',
  237. 237 : items: {
  238. 238 : layout: {
  239. 239 : type: 'vbox',
  240. 240 : align: 'stretch'
  241. 241 : },
  242. 242 : defaults: {border: false},
  243. 243 : items: [{
  244. 244 : height: 30,
  245. 245 : itemId: 'termsView',
  246. 246 : xtype: 'dataview',
  247. 247 : store: this.getTermStore(),
  248. 248 : tpl: this.termTpl,
  249. 249 : itemSelector: 'div.term',
  250. 250 : overItemCls: 'over',
  251. 251 : selectedItemCls: 'selected',
  252. 252 : selectionModel: {
  253. 253 : mode: 'SIMPLE'
  254. 254 : },
  255. 255 : // cls: 'selected', // default selected
  256. 256 : focusCls: '',
  257. 257 : listeners: {
  258. 258 : beforeitemclick: function(dv, record, item, index, event, opts) {
  259. 259 : event.preventDefault();
  260. 260 : event.stopPropagation();
  261. 261 : dv.fireEvent('itemcontextmenu', dv, record, item, index, event, opts);
  262. 262 : return false;
  263. 263 : },
  264. 264 : beforecontainerclick: function() {
  265. 265 : // cancel deselect all
  266. 266 : event.preventDefault();
  267. 267 : event.stopPropagation();
  268. 268 : return false;
  269. 269 : },
  270. 270 : selectionchange: function(selModel, selections) {
  271. 271 : var dv = this.down('#termsView');
  272. 272 : var terms = [];
  273. 273 :
  274. 274 : dv.getStore().each(function(r) {
  275. 275 : if (selections.indexOf(r) !== -1) {
  276. 276 : terms.push(r.get('term'));
  277. 277 : Ext.fly(dv.getNodeByRecord(r)).removeCls('unselected').addCls('selected');
  278. 278 : } else {
  279. 279 : Ext.fly(dv.getNodeByRecord(r)).removeCls('selected').addCls('unselected');
  280. 280 : }
  281. 281 : });
  282. 282 :
  283. 283 : for (var index in this.getBubblelines().lastClickedBubbles) {
  284. 284 : var lcTerms = this.getBubblelines().lastClickedBubbles[index];
  285. 285 : for (var term in lcTerms) {
  286. 286 : if (terms.indexOf(term) == -1) {
  287. 287 : delete this.getBubblelines().lastClickedBubbles[index][term];
  288. 288 : }
  289. 289 : }
  290. 290 :
  291. 291 : }
  292. 292 : this.getBubblelines().termsFilter = terms;
  293. 293 : this.getBubblelines().setCanvasHeight();
  294. 294 : this.getBubblelines().drawGraph();
  295. 295 : },
  296. 296 : itemcontextmenu: function(dv, record, el, index, event) {
  297. 297 : event.preventDefault();
  298. 298 : event.stopPropagation();
  299. 299 : var isSelected = dv.isSelected(el);
  300. 300 : var menu = new Ext.menu.Menu({
  301. 301 : floating: true,
  302. 302 : items: [{
  303. 303 : text: isSelected ? this.localize('hideTerm') : this.localize('showTerm'),
  304. 304 : handler: function() {
  305. 305 : if (isSelected) {
  306. 306 : dv.deselect(index);
  307. 307 : } else {
  308. 308 : dv.select(index, true);
  309. 309 : }
  310. 310 : },
  311. 311 : scope: this
  312. 312 : },{
  313. 313 : text: this.localize('removeTerm'),
  314. 314 : handler: function() {
  315. 315 : dv.deselect(index);
  316. 316 : var term = this.getTermStore().getAt(index).get('term');
  317. 317 : this.getTermStore().removeAt(index);
  318. 318 : dv.refresh();
  319. 319 :
  320. 320 : this.getBubblelines().removeTerm(term);
  321. 321 : this.getBubblelines().setCanvasHeight();
  322. 322 : this.getBubblelines().drawGraph();
  323. 323 : },
  324. 324 : scope: this
  325. 325 : }]
  326. 326 : });
  327. 327 : menu.showAt(event.getXY());
  328. 328 : },
  329. 329 : scope: this
  330. 330 : }
  331. 331 : },{
  332. 332 : flex: 1,
  333. 333 : xtype: 'container',
  334. 334 : autoEl: 'div',
  335. 335 : itemId: 'canvasParent',
  336. 336 : layout: 'fit',
  337. 337 : overflowY: 'auto',
  338. 338 : overflowX: 'hidden'
  339. 339 : }],
  340. 340 : listeners: {
  341. 341 : render: function(component) {
  342. 342 : var canvasParent = this.down('#canvasParent');
  343. 343 : this.setBubblelines(new Bubblelines({
  344. 344 : container: canvasParent,
  345. 345 : clickHandler: this.bubbleClickHandler.bind(this)
  346. 346 : }));
  347. 347 : this.getBubblelines().bubbleSpacing = parseInt(this.getApiParam('bins'));
  348. 348 : },
  349. 349 : afterlayout: function(container) {
  350. 350 : if (this.getBubblelines().initialized === false) {
  351. 351 : this.getBubblelines().initializeCanvas();
  352. 352 : }
  353. 353 : },
  354. 354 : resize: function(cnt, width, height) {
  355. 355 : this.getBubblelines().doBubblelinesLayout();
  356. 356 : },
  357. 357 : scope: this
  358. 358 : }
  359. 359 : }
  360. 360 : });
  361. 361 :
  362. 362 : this.callParent(arguments);
  363. 363 : },
  364. 364 :
  365. 365 : initLoad: function() {
  366. 366 : // get doc info
  367. 367 : var docIds = [];
  368. 368 : this.getCorpus().getDocuments().each(function(doc, index, total) {
  369. 369 : var inLimit = index < this.getApiParam('maxDocs');
  370. 370 : this.getBubblelines().addDocToCache({
  371. 371 : id: doc.getId(),
  372. 372 : index: doc.getIndex(),
  373. 373 : title: doc.getShortTitle(),
  374. 374 : totalTokens: doc.get('tokensCount-lexical'),
  375. 375 : terms: {},
  376. 376 : hidden: !inLimit
  377. 377 : });
  378. 378 : if (inLimit) {
  379. 379 : docIds.push(doc.getId());
  380. 380 : }
  381. 381 : }, this);
  382. 382 : this.setApiParam('docId', docIds);
  383. 383 :
  384. 384 : // get top terms in corpus
  385. 385 : this.getCorpus().getCorpusTerms({autoload: false}).load({
  386. 386 : callback: function(records, operation, success) {
  387. 387 : var query = [];
  388. 388 : records.forEach(function(record, index) {
  389. 389 : query.push(record.get('term'));
  390. 390 : }, this);
  391. 391 : this.getDocTermsFromQuery(query);
  392. 392 : },
  393. 393 : scope: this,
  394. 394 : params: {
  395. 395 : limit: this.getApiParam('query') ? undefined : 5,
  396. 396 : stopList: this.getApiParams('stopList'),
  397. 397 : query: this.getApiParam('query')
  398. 398 : }
  399. 399 : });
  400. 400 : },
  401. 401 :
  402. 402 : /**
  403. 403 : * Get the results for the query(s) for each of the corpus documents.
  404. 404 : * @param query {String|Array}
  405. 405 : * @private
  406. 406 : */
  407. 407 : getDocTermsFromQuery: function(query) {
  408. 408 : if (query) {this.setApiParam('query', query);} // make sure it's set for subsequent calls
  409. 409 : if (this.getCorpus() && this.isVisible()) {
  410. 410 : this.getDocTermStore().load({params: this.getApiParams()});
  411. 411 : }
  412. 412 : },
  413. 413 :
  414. 414 : reloadTermsData: function() {
  415. 415 : var terms = [];
  416. 416 : for (var term in this.getBubblelines().currentTerms) {
  417. 417 : terms.push(term);
  418. 418 : }
  419. 419 : this.getDocTermsFromQuery(terms);
  420. 420 : },
  421. 421 :
  422. 422 : filterDocuments: function() {
  423. 423 : var docIds = this.getApiParam('docId');
  424. 424 : if (docIds == '') {
  425. 425 : docIds = [];
  426. 426 : this.getCorpus().getDocuments().each(function(item, index) {
  427. 427 : docIds.push(item.getId());
  428. 428 : });
  429. 429 : this.setApiParams({docId: docIds});
  430. 430 : }
  431. 431 : if (typeof docIds == 'string') docIds = [docIds];
  432. 432 :
  433. 433 : if (docIds == null) {
  434. 434 : this.setSelectedDocs(this.getCorpus().getDocuments().clone());
  435. 435 : var count = this.getSelectedDocs().getCount();
  436. 436 : if (count > 10) {
  437. 437 : for (var i = 10; i < count; i++) {
  438. 438 : this.getSelectedDocs().removeAt(10);
  439. 439 : }
  440. 440 : }
  441. 441 : docIds = [];
  442. 442 : this.getSelectedDocs().eachKey(function(docId, doc) {
  443. 443 : docIds.push(docId);
  444. 444 : }, this);
  445. 445 : this.setApiParams({docId: docIds});
  446. 446 : } else {
  447. 447 : this.setSelectedDocs(this.getCorpus().getDocuments().filterBy(function(doc, docId) {
  448. 448 : return docIds.indexOf(docId) != -1;
  449. 449 : }, this));
  450. 450 : }
  451. 451 : },
  452. 452 :
  453. 453 : processTerms: function(termRecord) {
  454. 454 : var termObj;
  455. 455 : var term = termRecord.get('term');
  456. 456 : var rawFreq = termRecord.get('rawFreq');
  457. 457 : var positions = termRecord.get('positions');
  458. 458 : if (rawFreq > 0) {
  459. 459 : var color = this.getApplication().getColorForTerm(term);
  460. 460 : if (this.getTermStore().find('term', term) === -1) {
  461. 461 : this.getTermStore().loadData([[term, color]], true);
  462. 462 : var index = this.getTermStore().find('term', term);
  463. 463 : this.down('#termsView').select(index, true); // manually select since the store's load listener isn't triggered
  464. 464 : }
  465. 465 : var distributions = termRecord.get('distributions');
  466. 466 : termObj = {positions: positions, distributions: distributions, rawFreq: rawFreq, color: color};
  467. 467 : } else {
  468. 468 : termObj = false;
  469. 469 : }
  470. 470 :
  471. 471 : return termObj;
  472. 472 : },
  473. 473 :
  474. 474 : bubbleClickHandler: function(data) {
  475. 475 : this.getApplication().dispatchEvent('termsClicked', this, data);
  476. 476 : }
  477. 477 : });