1. 1 : /**
  2. 2 : * Trends shows a line graph depicting the distribution of a word's occurrence across a corpus or document.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "chartType": null,
  8. 8 : * "docId": null,
  9. 9 : * "docIndex": null,
  10. 10 : * "labels": null,
  11. 11 : * "limit": null,
  12. 12 : * "mode": null,
  13. 13 : * "query": null,
  14. 14 : * "stopList": null,
  15. 15 : * "withDistributions": null
  16. 16 : * };
  17. 17 : *
  18. 18 : * loadCorpus("austen").tool("Trends", config);
  19. 19 : *
  20. 20 : * @class Trends
  21. 21 : * @tutorial trends
  22. 22 : * @memberof Tools
  23. 23 : */
  24. 24 : Ext.define('Voyant.panel.Trends', {
  25. 25 : extend: 'Ext.panel.Panel',
  26. 26 : mixins: ['Voyant.panel.Panel'],
  27. 27 : requires: ['Voyant.data.store.Documents'],
  28. 28 : alias: 'widget.trends',
  29. 29 : config: {
  30. 30 : /**
  31. 31 : * @private
  32. 32 : */
  33. 33 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'},
  34. 34 :
  35. 35 : {
  36. 36 : name: 'bins',
  37. 37 : xtype: 'slider',
  38. 38 : labelAlign: 'right',
  39. 39 : width: 200,
  40. 40 : minValue: 2,
  41. 41 : maxValue: 100,
  42. 42 : listeners: {
  43. 43 : afterrender: function(slider) {
  44. 44 : var trends = slider.up("window").panel;
  45. 45 : slider.setFieldLabel(trends.localize("segmentsSlider"));
  46. 46 : }
  47. 47 : }
  48. 48 : },{
  49. 49 : xtype: 'radiogroup',
  50. 50 : labelAlign: 'right',
  51. 51 : columns: 3,
  52. 52 : vertical: true,
  53. 53 : name: 'withDistributions',
  54. 54 : items: [{
  55. 55 : boxLabel: 'raw',
  56. 56 : name: 'withDistributions',
  57. 57 : inputValue: 'raw'
  58. 58 : },{
  59. 59 : boxLabel: 'relative',
  60. 60 : name: 'withDistributions',
  61. 61 : inputValue: 'relative',
  62. 62 : style: 'margin-left: 1em;'
  63. 63 : }],
  64. 64 : listeners: {
  65. 65 : afterrender: function(radiogroup) {
  66. 66 : var panel = this.up("window").panel;
  67. 67 : this.setFieldLabel(panel.localize("freqsMode"));
  68. 68 : var val = panel.getApiParam("withDistributions");
  69. 69 : radiogroup.getBoxes().forEach(function(item) {
  70. 70 : item.setBoxLabel(panel.localize(item.inputValue));
  71. 71 : item.checked = item.inputValue==val;
  72. 72 : });
  73. 73 : this.setValue({withDistributions: val});
  74. 74 : }
  75. 75 : }
  76. 76 : },{xtype: 'colorpaletteoption'}]
  77. 77 : },
  78. 78 : statics: {
  79. 79 : i18n: {},
  80. 80 : api: {
  81. 81 :
  82. 82 : /**
  83. 83 : * @memberof Tools.Trends
  84. 84 : * @instance
  85. 85 : * @property {limit}
  86. 86 : * @default
  87. 87 : */
  88. 88 : limit: 5,
  89. 89 :
  90. 90 : /**
  91. 91 : * @memberof Tools.Trends
  92. 92 : * @instance
  93. 93 : * @property {stopList}
  94. 94 : * @default
  95. 95 : */
  96. 96 : stopList: 'auto',
  97. 97 :
  98. 98 : /**
  99. 99 : * @memberof Tools.Trends
  100. 100 : * @instance
  101. 101 : * @property {query}
  102. 102 : */
  103. 103 : query: undefined,
  104. 104 :
  105. 105 : /**
  106. 106 : * @memberof Tools.Trends
  107. 107 : * @instance
  108. 108 : * @property {withDistributions}
  109. 109 : * @default
  110. 110 : */
  111. 111 : withDistributions: 'relative',
  112. 112 :
  113. 113 : /**
  114. 114 : * @memberof Tools.Trends
  115. 115 : * @instance
  116. 116 : * @property {bins}
  117. 117 : *
  118. 118 : * TODO verify this:
  119. 119 : *
  120. 120 : * The default value will depend on the nature of the corpus:
  121. 121 : *
  122. 122 : * - corpus has one document: the default number of bins is 10
  123. 123 : * - corpus has multiple documents:
  124. 124 : * - corpus has up to 100 documents: the default number is the size of the corpus
  125. 125 : * - corpus has more than 1000 documents: the default number is 100
  126. 126 : */
  127. 127 : bins: 10,
  128. 128 :
  129. 129 : /**
  130. 130 : * @memberof Tools.Trends
  131. 131 : * @instance
  132. 132 : * @property {docIndex}
  133. 133 : */
  134. 134 : docIndex: undefined,
  135. 135 :
  136. 136 : /**
  137. 137 : * @memberof Tools.Trends
  138. 138 : * @instance
  139. 139 : * @property {docId}
  140. 140 : */
  141. 141 : docId: undefined,
  142. 142 :
  143. 143 : /**
  144. 144 : * @memberof Tools.Trends
  145. 145 : * @instance
  146. 146 : * @property {String} mode Force the mode to be either "corpus" (distribution of terms across documents) or "document" (distribution of terms within a document); usually this is correctly set by default according to whether the corpus has one document ("document") or more than one ("corpus").
  147. 147 : * @default
  148. 148 : */
  149. 149 : mode: "corpus",
  150. 150 :
  151. 151 : /**
  152. 152 : * @memberof Tools.Trends
  153. 153 : * @instance
  154. 154 : * @property {String} chartType The of chart to display: Options are: 'area', 'bar', 'line', 'stacked', and 'barline'.
  155. 155 : * @default
  156. 156 : */
  157. 157 : chartType: 'barline',
  158. 158 :
  159. 159 : /**
  160. 160 : * @memberof Tools.Trends
  161. 161 : * @instance
  162. 162 : * @property {Boolean} labels Whether to show term labels.
  163. 163 : * @default
  164. 164 : */
  165. 165 : labels: false
  166. 166 : },
  167. 167 : glyph: 'xf201@FontAwesome'
  168. 168 : },
  169. 169 :
  170. 170 : layout: 'fit',
  171. 171 : documentTermsStore: undefined,
  172. 172 : //segments: undefined,
  173. 173 :
  174. 174 : /**
  175. 175 : * @private
  176. 176 : */
  177. 177 : constructor: function(config) {
  178. 178 : this.callParent(arguments);
  179. 179 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  180. 180 : },
  181. 181 :
  182. 182 : initComponent: function() {
  183. 183 : this.mixins['Voyant.util.Api'].constructor.apply(this, arguments); // we need api
  184. 184 : Ext.apply(this, {
  185. 185 : title: this.localize('title'),
  186. 186 : dockedItems: [{
  187. 187 : dock: 'bottom',
  188. 188 : xtype: 'toolbar',
  189. 189 : overflowHandler: 'scroller',
  190. 190 : items: [{
  191. 191 : xtype: 'querysearchfield'
  192. 192 : },{
  193. 193 : itemId: 'reset',
  194. 194 : text: this.localize("reset"),
  195. 195 : tooltip: this.localize("resetTip"),
  196. 196 : handler: function(btn) {
  197. 197 : this.setApiParams({
  198. 198 : docIndex: undefined,
  199. 199 : mode: undefined,
  200. 200 : query: undefined
  201. 201 : });
  202. 202 : this.loadCorpusTerms();
  203. 203 : },
  204. 204 : scope: this
  205. 205 : },{
  206. 206 : text: this.localize('display'),
  207. 207 : tooltip: this.localize('displayTip'),
  208. 208 : glyph: 'xf013@FontAwesome',
  209. 209 : menu: {
  210. 210 : listeners: {
  211. 211 : afterrender: function(menu) {
  212. 212 : var val = this.getApiParam("chartType");
  213. 213 : menu.items.each(function(item) {
  214. 214 : if (item.getItemId()==val) {
  215. 215 : item.addCls(item.activeCls);
  216. 216 : }
  217. 217 : })
  218. 218 : },
  219. 219 : scope: this
  220. 220 : },
  221. 221 : defaults: {
  222. 222 : xtype: 'menuitem',
  223. 223 : handler: function(item, checked) {
  224. 224 : if (item.xtype=="menucheckitem") { // labels
  225. 225 : this.setApiParam("labels", item.checked);
  226. 226 : } else {
  227. 227 : this.setApiParam("chartType", item.getItemId());
  228. 228 : }
  229. 229 : this.loadCorpusTerms();
  230. 230 : },
  231. 231 : scope: this
  232. 232 : },
  233. 233 : items: [{
  234. 234 : xtype: 'menucheckitem',
  235. 235 : text: this.localize('labels'),
  236. 236 : tooltip: this.localize('labelsTip'),
  237. 237 : checked: this.getApiParam("labels")===true || this.getApiParam("labels")=="true"
  238. 238 : },'-',{
  239. 239 : itemId: 'area',
  240. 240 : text: this.localize('area'),
  241. 241 : tooltip: this.localize('areaTip'),
  242. 242 : glyph: 'xe76b@Sencha-Examples'
  243. 243 : },{
  244. 244 : itemId: 'bar',
  245. 245 : text: this.localize('bar'),
  246. 246 : tooltip: this.localize('barTip'),
  247. 247 : glyph: 'xe768@Sencha-Examples'
  248. 248 : },{
  249. 249 : itemId: 'line',
  250. 250 : text: this.localize('line'),
  251. 251 : tooltip: this.localize('lineTip'),
  252. 252 : glyph: 'xe773@Sencha-Examples'
  253. 253 : },{
  254. 254 : itemId: 'stacked',
  255. 255 : text: this.localize('stacked'),
  256. 256 : tooltip: this.localize('stackedTip'),
  257. 257 : glyph: 'xe6c8@Sencha-Examples'
  258. 258 : },{
  259. 259 : itemId: 'barline',
  260. 260 : text: this.localize('barline'),
  261. 261 : tooltip: this.localize('barlineTip'),
  262. 262 : glyph: 'xe779@Sencha-Examples'
  263. 263 : }]
  264. 264 : }
  265. 265 : }]
  266. 266 : }]
  267. 267 : });
  268. 268 : this.callParent(arguments);
  269. 269 : },
  270. 270 :
  271. 271 : listeners: {
  272. 272 : loadedCorpus: function(src, corpus) {
  273. 273 : this.loadCorpusTerms();
  274. 274 : },
  275. 275 : termsClicked: function(src, terms) {
  276. 276 : var queryTerms = [];
  277. 277 : terms.forEach(function(term) {
  278. 278 : if (Ext.isString(term)) {queryTerms.push(term);}
  279. 279 : else if (term.term) {queryTerms.push(term.term);}
  280. 280 : else if (term.getTerm) {queryTerms.push(term.getTerm());}
  281. 281 :
  282. 282 : });
  283. 283 : this.setApiParam("query", queryTerms && queryTerms.length>0 ? queryTerms : undefined);
  284. 284 : this.loadCorpusTerms();
  285. 285 : },
  286. 286 : corpusTermsClicked: function(src, terms) {
  287. 287 : this.setApiParam("query", terms.map(function(term) {return term.getTerm()}));
  288. 288 : this.loadCorpusTerms();
  289. 289 : },
  290. 290 : documentSelected: function(src, document) {
  291. 291 : this.setApiParam("docIndex", this.getCorpus().getDocument(document).getIndex());
  292. 292 : this.loadDocumentTerms();
  293. 293 : },
  294. 294 : documentsClicked: function(src, documents) {
  295. 295 : if (documents.length==1) {
  296. 296 : this.fireEvent("documentSelected", this, documents[0])
  297. 297 : }
  298. 298 : },
  299. 299 : query: function(src, query) {
  300. 300 : this.fireEvent("termsClicked", this, query)
  301. 301 : },
  302. 302 : entitiesClicked: function(src, ents) {
  303. 303 : this.fireEvent("termsClicked", this, ents);
  304. 304 : }
  305. 305 : },
  306. 306 :
  307. 307 : loadCorpusTerms: function(params) {
  308. 308 : // if our corpus only has one document or if we have docIndex defined
  309. 309 : if (
  310. 310 : this.getCorpus().getDocumentsCount()<2 || // only one document
  311. 311 : this.getApiParam("mode")=="document" || // in document mode
  312. 312 : Array.from(this.getApiParam("docIndex") || "").length>0 // we have a docIndex defined
  313. 313 : ) {
  314. 314 : return this.loadDocumentTerms();
  315. 315 : }
  316. 316 : if (!this.getApiParam("query")) {
  317. 317 : this.getCorpus().getCorpusTerms().load({
  318. 318 : params: {
  319. 319 : limit: this.getApiParam('limit'),
  320. 320 : stopList: this.getApiParam("stopList")
  321. 321 : },
  322. 322 : callback: function(records, operation, success) {
  323. 323 : if (records.length==0) {
  324. 324 : if (operation && operation.error) {
  325. 325 : this.showError(this.localize("noResults")+"<p style='color: red'>"+operation.error+"</p>")
  326. 326 : } else {
  327. 327 : this.showError(this.localize("noResults"))
  328. 328 : }
  329. 329 : } else {
  330. 330 : this.setApiParam("query", records.map(function(r) {return r.getTerm()}))
  331. 331 : this.loadCorpusTerms();
  332. 332 : }
  333. 333 : },
  334. 334 : scope: this
  335. 335 : })
  336. 336 : return;
  337. 337 : }
  338. 338 : params = params || {};
  339. 339 : //this.segments.hide();
  340. 340 : var withDistributions = this.getApiParam("withDistributions");
  341. 341 : Ext.applyIf(params, {
  342. 342 : bins: this.getCorpus().getDocumentsCount(),
  343. 343 : limit: 100, // should have query, so no limit
  344. 344 : stopList: "" // automatic queries should be stopped already
  345. 345 : });
  346. 346 : var docLabels = [];
  347. 347 : var docLabelsFull = [];
  348. 348 : this.getCorpus().each(function(doc) {
  349. 349 : docLabels.push(doc.getTinyTitle());
  350. 350 : docLabelsFull.push(doc.getTitle());
  351. 351 : })
  352. 352 : if (Ext.Array.unique(docLabels).length<docLabels.length) { // we have duplicates, add index
  353. 353 : docLabels = docLabels.map(function(doc,i) {return (i+1)+")"+ doc})
  354. 354 : }
  355. 355 : Ext.applyIf(params, this.getApiParams());
  356. 356 : this.getCorpus().getCorpusTerms().load({
  357. 357 : params: params,
  358. 358 : callback: function(records, operation, success) {
  359. 359 : var data = [], series = [], chartType = this.getApiParam('chartType');
  360. 360 : records.forEach(function(record, index) {
  361. 361 : var term = record.get('term');
  362. 362 : var color = this.getApplication().getColorForTerm(term, true);
  363. 363 : record.get('distributions').forEach(function(r, i) {
  364. 364 : if (!data[i]) {
  365. 365 : data[i] = {"index": docLabels[i], "docTitle": docLabelsFull[i]};
  366. 366 : }
  367. 367 : data[i]["_"+index] = withDistributions=='relative' ? r.toFixed(7) : r;
  368. 368 : data[i]["term"+index] = term;
  369. 369 : }, this);
  370. 370 :
  371. 371 :
  372. 372 : if (chartType!='bar') {
  373. 373 : var kinds = chartType=='barline' ? ["bar","line"] : [chartType];
  374. 374 : kinds.forEach(function(kind) {
  375. 375 : series.push({
  376. 376 : type: kind=='stacked' ? 'bar' : kind,
  377. 377 : title: term,
  378. 378 : xField: 'index',
  379. 379 : yField: '_'+index,
  380. 380 : term: term,
  381. 381 : colors: [color],
  382. 382 : label: chartType=='barline' && kind=='bar' ? {
  383. 383 : display: 'none'
  384. 384 : } : {
  385. 385 : field: "term"+index
  386. 386 : }
  387. 387 : })
  388. 388 : }, this);
  389. 389 : }
  390. 390 : }, this);
  391. 391 :
  392. 392 : var terms = records.map(function(r) {return r.getTerm()})
  393. 393 : var colors = terms.map(function(term) {
  394. 394 : return this.getApplication().getColorForTerm(term, true);
  395. 395 : }, this);
  396. 396 : if (chartType=='bar') {
  397. 397 : series.push({
  398. 398 : type:'bar',
  399. 399 : title: terms,
  400. 400 : colors: colors,
  401. 401 : xField: 'index',
  402. 402 : yField: data.length>0 ? Object.keys(data[0]).filter(function(field) {return field.charAt(0)=="_"}) : undefined,
  403. 403 : label: {
  404. 404 : field: records.map(function(r,i) {return "term"+i;})
  405. 405 : }
  406. 406 : })
  407. 407 : }
  408. 408 :
  409. 409 : var store = Ext.create('Ext.data.JsonStore', {
  410. 410 : fields: data.length>0 ? Object.keys(data[0]) : undefined,
  411. 411 : data: data
  412. 412 : });
  413. 413 :
  414. 414 : this.buildChart({
  415. 415 : store: store,
  416. 416 : series: series,
  417. 417 : axes: [{
  418. 418 : type: 'numeric',
  419. 419 : position: 'left',
  420. 420 : increment: 1,
  421. 421 : title: {
  422. 422 : text: this.localize(this.getApiParam("withDistributions")+"Title")
  423. 423 : }
  424. 424 : },{
  425. 425 : type: 'category',
  426. 426 : position: 'bottom',
  427. 427 : title: {
  428. 428 : text: this.localize("corpusTitle")
  429. 429 : }
  430. 430 :
  431. 431 : }]
  432. 432 : })
  433. 433 : },
  434. 434 : scope: this
  435. 435 : });
  436. 436 :
  437. 437 : },
  438. 438 :
  439. 439 : loadDocumentTerms: function(params) {
  440. 440 : if (!this.getApiParam("query")) {
  441. 441 : this.getCorpus().getCorpusTerms().load({
  442. 442 : params: {
  443. 443 : limit: this.getApiParam('limit'),
  444. 444 : stopList: this.getApiParam("stopList")
  445. 445 : },
  446. 446 : callback: function(records, operation, success) {
  447. 447 : this.setApiParam("query", records.map(function(r) {return r.getTerm()}))
  448. 448 : this.loadDocumentTerms();
  449. 449 : },
  450. 450 : scope: this
  451. 451 : })
  452. 452 : return;
  453. 453 : }
  454. 454 : //this.segments.show();
  455. 455 : this.setApiParam("mode", "document"); // just to be sure
  456. 456 : params = params || {};
  457. 457 : var withDistributions = this.getApiParam("withDistributions");
  458. 458 : Ext.applyIf(params, {
  459. 459 : limit: 0, // always have query, so no limit, no stopList
  460. 460 : sort: 'termasc',
  461. 461 : stopList: undefined
  462. 462 : });
  463. 463 : var singleDoc;
  464. 464 : if (this.getCorpus().getDocumentsCount()==1) {
  465. 465 : singleDoc=this.getCorpus().getDocument(0)
  466. 466 : }
  467. 467 : else {
  468. 468 : singleDoc=this.getCorpus().getDocument(this.getApiParam("docIndex"))
  469. 469 : }
  470. 470 : Ext.applyIf(params, this.getApiParams());
  471. 471 :
  472. 472 : this.getCorpus().getDocumentTerms().load({
  473. 473 : params: params,
  474. 474 : callback: function(records, operation, success) {
  475. 475 : var data = [], series = [], chartType = this.getApiParam('chartType');
  476. 476 : if (!singleDoc) { // legend is easier to read if sorted by term then doc
  477. 477 : records.sort(function(a,b) {
  478. 478 : if (a.getTerm()==b.getTerm()) {
  479. 479 : return a.getDocIndex() - b.getDocIndex()
  480. 480 : }
  481. 481 : return a.getTerm().localeCompare(b.getTerm())
  482. 482 : })
  483. 483 : }
  484. 484 : records.forEach(function(record, index) {
  485. 485 : var term = record.get('term');
  486. 486 : var docIndex = record.get('docIndex');
  487. 487 : var color = singleDoc ? this.getApplication().getColorForTerm(term, true) : this.getApplication().getColor(docIndex, true);
  488. 488 : record.get('distributions').forEach(function(r, i) {
  489. 489 : if (!data[i]) {
  490. 490 : data[i] = {index: (i+1)};
  491. 491 : }
  492. 492 : data[i]["_"+index+"_"+docIndex] = withDistributions=='relative' ? r.toFixed(7) : r;
  493. 493 : data[i]["term"+index] = term;
  494. 494 : }, this);
  495. 495 :
  496. 496 : if (chartType!='bar') {
  497. 497 : var kinds = chartType=='barline' ? ["bar","line"] : [chartType];
  498. 498 : kinds.forEach(function(kind) {
  499. 499 : series.push({
  500. 500 : type: kind=='stacked' ? 'bar' : kind,
  501. 501 : title: singleDoc ? term : (docIndex+1)+") "+term,
  502. 502 : xField: 'index',
  503. 503 : yField: '_'+index+"_"+docIndex,
  504. 504 : term: term,
  505. 505 : colors: [color],
  506. 506 : label: chartType=='barline' && kind=='bar' ? {
  507. 507 : display: 'none'
  508. 508 : } : {
  509. 509 : field: "term"+index
  510. 510 : }
  511. 511 : })
  512. 512 : }, this);
  513. 513 : }
  514. 514 :
  515. 515 : }, this);
  516. 516 :
  517. 517 : if (chartType=='bar') {
  518. 518 : var isOneTerm = Ext.Array.unique(records.map(function(r) {return r.getTerm()})).length === 1;
  519. 519 : var terms = records.map(function(r) {return (1+r.get("docIndex")) +") "+r.getTerm()})
  520. 520 : var colors = records.map(function(r) {
  521. 521 : return isOneTerm ? this.getApplication().getColor(r.get("docIndex"), true) : this.getApplication().getColorForTerm(r.getTerm(), true);
  522. 522 : }, this);
  523. 523 :
  524. 524 : series.push({
  525. 525 : type:'bar',
  526. 526 : title: terms.length>0 ? terms : this.localize("noResults"),
  527. 527 : colors: colors,
  528. 528 : xField: 'index',
  529. 529 : yField: data.length>0 ? Object.keys(data[0]).filter(function(field) {return field.charAt(0)=="_"}) : undefined,
  530. 530 : label: {
  531. 531 : field: terms
  532. 532 : }
  533. 533 : })
  534. 534 : }
  535. 535 :
  536. 536 :
  537. 537 : var store = Ext.create('Ext.data.JsonStore', {
  538. 538 : fields: data.length>0 ? Object.keys(data[0]) : undefined,
  539. 539 : data: data
  540. 540 : });
  541. 541 :
  542. 542 : this.buildChart({
  543. 543 : store: store,
  544. 544 : series: series,
  545. 545 : axes: [{
  546. 546 : type: 'numeric',
  547. 547 : position: 'left',
  548. 548 : title: {
  549. 549 : text: this.localize(this.getApiParam("withDistributions")+"Title")
  550. 550 : }
  551. 551 : },{
  552. 552 : type: 'category',
  553. 553 : position: 'bottom',
  554. 554 : title: {
  555. 555 : text: this.localize("segmentsTitle") + (singleDoc ? " ("+singleDoc.getTitle()+")" : "")
  556. 556 : }
  557. 557 :
  558. 558 : }]
  559. 559 : })
  560. 560 : },
  561. 561 : scope: this
  562. 562 : });
  563. 563 :
  564. 564 : },
  565. 565 :
  566. 566 : getItemToolTip: function (toolTip, record, ctx) {
  567. 567 : var parts = ctx.field.split("_"),
  568. 568 : docIndex = parts.length==2 ? ctx.index : parts[2],
  569. 569 : pos = parseInt(parts[1]),
  570. 570 : title = ctx.series.getTitle(),
  571. 571 : term = Ext.isArray(title) ? title[pos] : title,
  572. 572 : colors = ctx.series.getColors(),
  573. 573 : color = colors.length==1 ? colors[0] : colors[pos];
  574. 574 : var html = "<span class='x-legend-item-marker' style='background:"+color+
  575. 575 : "; left: 2px;'></span> <span style='padding-left: 1.2em; font-weight: bold;'>"+
  576. 576 : term+"</span>: "+record.get(ctx.field)+
  577. 577 : "<br/><i>"+this.getCorpus().getDocument(docIndex).getShortTitle()+"</i>"
  578. 578 : if (this.getApiParam("mode")=="corpus") {
  579. 579 : html+="<div style='font-size: smaller'>"+this.localize('dblClickItem')
  580. 580 : } else {
  581. 581 : html+="<br/>"+this.localize('segment')+" "+(ctx.index+1)
  582. 582 : }
  583. 583 : toolTip.setHtml(html);
  584. 584 : },
  585. 585 :
  586. 586 : buildChart: function(config) {
  587. 587 : var chartType = this.getApiParam('chartType'), labels = false;
  588. 588 : if (this.getApiParam("labels")===true || this.getApiParam("labels")=="true") {labels=true}
  589. 589 :
  590. 590 : Ext.applyIf(config, {
  591. 591 : cls: this.getApiParam("mode")
  592. 592 : });
  593. 593 :
  594. 594 : config.series.forEach(function(serie) {
  595. 595 : Ext.applyIf(serie, {
  596. 596 : stacked: serie.type=='bar' ? false : true,
  597. 597 : showInLegend: chartType=='barline' && serie.type=='line' ? false : true,
  598. 598 : smooth: true,
  599. 599 : showMarkers: serie.type=='bar' ? false : true,
  600. 600 : marker: chartType=='barline' && serie.type=='line' ? null : {
  601. 601 : type: 'circle',
  602. 602 : radius: 2
  603. 603 : },
  604. 604 : style: {
  605. 605 : lineWidth: 1,
  606. 606 : fillOpacity: chartType=='barline' && serie.type=='bar' ? .01 : 1,
  607. 607 : strokeOpacity: chartType=='barline' && serie.type=='bar' ? .1 : 1
  608. 608 : },
  609. 609 : highlight: true,
  610. 610 : highlightCfg: {
  611. 611 : scaling: serie.type=="bar" ? 1.1 : 2
  612. 612 : },
  613. 613 : label: {
  614. 614 : // display: 'none'
  615. 615 : },
  616. 616 : tooltip: {
  617. 617 : trackMouse: true,
  618. 618 : renderer: this.getItemToolTip,
  619. 619 : scope: this
  620. 620 : },
  621. 621 : listeners: {
  622. 622 : itemclick: function(chart,item,event,eOpts ) {
  623. 623 : if (this.clickTimer) {clearTimeout(this.clickTimer);}
  624. 624 : if (this.blockClick) {return;} // set by dblclick to avoid menu disappearing
  625. 625 : this.blockClick = true // block other clicks within a sec
  626. 626 : Ext.defer(function() {
  627. 627 : this.blockClick = false;
  628. 628 : }, 1000, this);
  629. 629 : if (this.getApiParam("mode")=="document") {
  630. 630 : var parts = item.field.split("_"),
  631. 631 : docIndex = parseInt(parts[2]),
  632. 632 : doc = this.getCorpus().getDocument(docIndex),
  633. 633 : tokens = doc.get('tokensCount-lexical'),
  634. 634 : position = parseInt(item.index * tokens / parseInt(this.getApiParam("bins")))
  635. 635 : this.dispatchEvent("documentIndexTermsClicked", this, [{
  636. 636 : term: item.series.term,
  637. 637 : docIndex: docIndex,
  638. 638 : position: position
  639. 639 : }]);
  640. 640 : } else {
  641. 641 : if (this.clickTimer) {clearTimeout(this.clickTimer);}
  642. 642 : var me = this;
  643. 643 : this.clickTimer = setTimeout(function() {
  644. 644 : me.dispatchEvent("documentIndexTermsClicked", me, [{
  645. 645 : term: item.series.term,
  646. 646 : docIndex: item.index
  647. 647 : }]);
  648. 648 : }, 300)
  649. 649 :
  650. 650 : }
  651. 651 : },
  652. 652 : itemdblclick: function(chart,item,event,eOpts ) {
  653. 653 : if (this.clickTimer) {clearTimeout(this.clickTimer);}
  654. 654 : // block future single clicks to allow menu to appear
  655. 655 : this.blockClick = true
  656. 656 : Ext.defer(function() {
  657. 657 : this.blockClick = false;
  658. 658 : }, 1000, this);
  659. 659 : // block future clicks
  660. 660 : if (this.getApiParam("mode")!="document") {
  661. 661 : var m = Ext.create('Ext.menu.Menu', {
  662. 662 : items: [{
  663. 663 : text: this.localize("drillTerm"),
  664. 664 : tooltip: this.localize("drillTermTip"),
  665. 665 : // glyph: 'xf02d@FontAwesome',
  666. 666 : handler: function() {
  667. 667 : this.setApiParams({
  668. 668 : mode: 'document',
  669. 669 : query: item.series.term
  670. 670 : });
  671. 671 : this.loadDocumentTerms();
  672. 672 : },
  673. 673 : scope: this
  674. 674 : },{
  675. 675 : text: this.localize("drillDocument"),
  676. 676 : tooltip: this.localize("drillDocumentTip"),
  677. 677 : // glyph: 'xf02d@FontAwesome',
  678. 678 : handler: function() {
  679. 679 : this.setApiParams({
  680. 680 : mode: 'document',
  681. 681 : docIndex: item.index
  682. 682 : });
  683. 683 : this.loadDocumentTerms();
  684. 684 : },
  685. 685 : scope: this
  686. 686 : }],
  687. 687 : listeners: {
  688. 688 : hide: function(m) {
  689. 689 : // defer hiding otherwise click handler not called
  690. 690 : Ext.defer(function() {this.destroy()}, 200, m)
  691. 691 : },
  692. 692 : scope: this
  693. 693 : }
  694. 694 : }).showAt(event.pageX, event.pageY)
  695. 695 : }
  696. 696 : },
  697. 697 : scope: this
  698. 698 : }
  699. 699 : })
  700. 700 : Ext.applyIf(serie.label, {
  701. 701 : display: 'over',
  702. 702 : field: 'index',
  703. 703 : fontSize: 11,
  704. 704 : translateY: chartType=='line' ? 9 : undefined
  705. 705 : });
  706. 706 : if (!labels) {serie.label.display="none";} // hide label
  707. 707 : }, this)
  708. 708 : Ext.applyIf(config, {
  709. 709 : animation: true,
  710. 710 : plugins: {
  711. 711 : ptype: 'chartitemevents',
  712. 712 : moveEvents: true
  713. 713 : },
  714. 714 : legend: {
  715. 715 : docked:'top',
  716. 716 : listeners: {
  717. 717 : itemclick: function(legend, record, dom, index) {
  718. 718 : // make sure to hide related series
  719. 719 : if (legend.getStore().getCount()<legend.chart.series.length && this.getApiParam("chartType")=="barline") {
  720. 720 : var term = record.get("name"), disabled = record.get("disabled");
  721. 721 : legend.chart.series.forEach(function(serie) {
  722. 722 : if (serie.getTitle()==term) {
  723. 723 : serie.setHidden(disabled);
  724. 724 :
  725. 725 : }
  726. 726 : })
  727. 727 : legend.chart.redraw(); // not working?
  728. 728 : }
  729. 729 : },
  730. 730 : scope: this
  731. 731 : }
  732. 732 : },
  733. 733 : interactions: ['itemhighlight','crosszoom'],
  734. 734 : listeners: {}
  735. 735 : });
  736. 736 : Ext.applyIf(config.listeners, {
  737. 737 : itemhighlightchange: function (chart, item) {
  738. 738 : chart.el.dom.style.cursor = item ? 'pointer' : '';
  739. 739 : },
  740. 740 : afterrender : function() {
  741. 741 : return // TODO: this seems to cause problems, perhaps not destroying properly?
  742. 742 : Ext.defer(function() { // seem to need to defer
  743. 743 : Ext.tip.QuickTipManager.register({
  744. 744 : target: this.getTargetEl().down(".x-legend-container"),
  745. 745 : text: this.localize("toggleTip")
  746. 746 : });
  747. 747 : },1, this)
  748. 748 :
  749. 749 : },
  750. 750 : scope: this
  751. 751 : })
  752. 752 : config.axes.forEach(function(axis) {
  753. 753 : Ext.applyIf(axis, {
  754. 754 : title: {},
  755. 755 : label: {}
  756. 756 : })
  757. 757 : Ext.applyIf(axis.title, {scaling: .75});
  758. 758 : Ext.applyIf(axis.label, {scaling: .75});
  759. 759 : if (axis.type=='category') {
  760. 760 : var titles = "";
  761. 761 : config.store.each(function(r) {titles+=r.get("index")})
  762. 762 : if (titles.length>this.getTargetEl().getWidth()/9) {
  763. 763 : Ext.applyIf(axis.label, {rotate: {degrees:-30}});
  764. 764 : }
  765. 765 : Ext.applyIf(axis, {
  766. 766 : labelInSpan: true
  767. 767 : })
  768. 768 : }
  769. 769 : }, this)
  770. 770 :
  771. 771 : // remove existing chart
  772. 772 : this.query('chart').forEach(function(chart) {this.remove(chart, true);}, this);
  773. 773 :
  774. 774 : // create new chart
  775. 775 : var chart = Ext.create("Ext.chart.CartesianChart", config);
  776. 776 : this.add(chart);
  777. 777 : },
  778. 778 :
  779. 779 : reloadFromChart: function() {
  780. 780 : var chart = this.down('chart');
  781. 781 : if (chart) {
  782. 782 : var terms = [];
  783. 783 : chart.series.forEach(function(serie) {
  784. 784 : terms.push(serie.getTitle());
  785. 785 : });
  786. 786 : this.fireEvent("termsClicked", this, terms);
  787. 787 : }
  788. 788 : },
  789. 789 :
  790. 790 : getExtraDataExportItems: function() {
  791. 791 : return [
  792. 792 : {
  793. 793 : name: 'export',
  794. 794 : inputValue: 'dataAsTsv',
  795. 795 : boxLabel: this.localize('exportGridCurrentTsv')
  796. 796 : }
  797. 797 : ]
  798. 798 : },
  799. 799 :
  800. 800 : exportDataAsTsv: function(panel, form) {
  801. 801 : var value = '';
  802. 802 :
  803. 803 : var chart = panel.down('chart');
  804. 804 : var store = chart.getStore();
  805. 805 : var firstModel = store.first();
  806. 806 : var data = firstModel.getData();
  807. 807 :
  808. 808 : var fields = ['Index'];
  809. 809 :
  810. 810 : var termKeys = Object.keys(data).filter(function(key) {
  811. 811 : return key.indexOf('term') === 0;
  812. 812 : }).sort();
  813. 813 : var terms = [];
  814. 814 : termKeys.forEach(function(termKey) {
  815. 815 : terms.push(data[termKey]);
  816. 816 : });
  817. 817 :
  818. 818 : fields = fields.concat(terms);
  819. 819 : value += fields.join("\t")+"\n";
  820. 820 :
  821. 821 : var valueKeys = Object.keys(data).filter(function(key) {
  822. 822 : return key.indexOf('_') === 0;
  823. 823 : }).sort();
  824. 824 :
  825. 825 : store.each(function(model) {
  826. 826 : data = model.getData();
  827. 827 : var entry = [];
  828. 828 : if (data['docTitle'] !== undefined) {
  829. 829 : entry.push(data['docTitle']);
  830. 830 : } else {
  831. 831 : entry.push(data['index']);
  832. 832 : }
  833. 833 : valueKeys.forEach(function(valueKey) {
  834. 834 : entry.push(data[valueKey]);
  835. 835 : });
  836. 836 : value += entry.join("\t")+"\n";
  837. 837 : }, panel);
  838. 838 :
  839. 839 : Ext.Msg.show({
  840. 840 : title: panel.localize('exportDataTitle'),
  841. 841 : message: panel.localize('exportDataTsvMessage'),
  842. 842 : buttons: Ext.Msg.OK,
  843. 843 : icon: Ext.Msg.INFO,
  844. 844 : prompt: true,
  845. 845 : multiline: true,
  846. 846 : value: value
  847. 847 : });
  848. 848 : }
  849. 849 : });