1. 1 : /**
  2. 2 : * The Summary panel provides an overview of a corpus, and the content will
  3. 3 : * depend on whether the corpus includes one document or many.
  4. 4 : *
  5. 5 : * @example
  6. 6 : *
  7. 7 : * let config = {
  8. 8 : * "limit": null,
  9. 9 : * "numberOfDocumentsForDistinctiveWords": null,
  10. 10 : * "start": null,
  11. 11 : * "stopList": null,
  12. 12 : * };
  13. 13 : *
  14. 14 : * loadCorpus("austen").tool("Summary", config);
  15. 15 : *
  16. 16 : * @class Summary
  17. 17 : * @tutorial summary
  18. 18 : * @memberof Tools
  19. 19 : */
  20. 20 : Ext.define('Voyant.panel.Summary', {
  21. 21 : extend: 'Ext.panel.Panel',
  22. 22 : mixins: ['Voyant.panel.Panel'],
  23. 23 : alias: 'widget.summary',
  24. 24 : statics: {
  25. 25 : i18n: {
  26. 26 : readabilityIndex: 'Readability Index:',
  27. 27 : docsDensityTip: 'ratio of unique words in this document',
  28. 28 : avgWordsPerSentenceTip: 'average words per sentence in this document',
  29. 29 : readabilityTip: 'the Coleman-Liau readability index for this document'
  30. 30 : },
  31. 31 : api: {
  32. 32 :
  33. 33 : /**
  34. 34 : * @memberof Tools.Summary
  35. 35 : * @instance
  36. 36 : * @property {stopList}
  37. 37 : * @default
  38. 38 : */
  39. 39 : stopList: 'auto',
  40. 40 :
  41. 41 : /**
  42. 42 : * @memberof Tools.Summary
  43. 43 : * @instance
  44. 44 : * @property {start}
  45. 45 : * @default
  46. 46 : */
  47. 47 : start: 0,
  48. 48 :
  49. 49 :
  50. 50 : /**
  51. 51 : * @memberof Tools.Summary
  52. 52 : * @instance
  53. 53 : * @property {limit}
  54. 54 : * @default
  55. 55 : */
  56. 56 : limit: 5,
  57. 57 :
  58. 58 : /**
  59. 59 : * @memberof Tools.Summary
  60. 60 : * @instance
  61. 61 : * @property {Number} numberOfDocumentsForDistinctiveWords The number of items to include in the list of distinctive words (similar to the limit parameter but specific to distinctive words).
  62. 62 : */
  63. 63 : numberOfDocumentsForDistinctiveWords: 10
  64. 64 : },
  65. 65 : glyph: 'xf1ea@FontAwesome'
  66. 66 : },
  67. 67 : config: {
  68. 68 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
  69. 69 : },
  70. 70 : autoScroll: true,
  71. 71 : cls: 'corpus-summary',
  72. 72 :
  73. 73 : constructor: function(config ) {
  74. 74 :
  75. 75 : Ext.apply(this, {
  76. 76 : title: this.localize('title'),
  77. 77 : items: {
  78. 78 : itemId: 'main',
  79. 79 : cls: 'main',
  80. 80 : margin: 10
  81. 81 : },
  82. 82 : dockedItems: [{
  83. 83 : dock: 'bottom',
  84. 84 : xtype: 'toolbar',
  85. 85 : overflowHandler: 'scroller',
  86. 86 : items: [{
  87. 87 : fieldLabel: this.localize('items'),
  88. 88 : labelWidth: 40,
  89. 89 : width: 120,
  90. 90 : xtype: 'slider',
  91. 91 : increment: 5,
  92. 92 : minValue: 5,
  93. 93 : maxValue: 59,
  94. 94 : listeners: {
  95. 95 : afterrender: function(slider) {
  96. 96 : slider.setValue(this.getApiParam("limit"))
  97. 97 : },
  98. 98 : changecomplete: function(slider, newvalue) {
  99. 99 : this.setApiParams({limit: newvalue});
  100. 100 : this.loadSummary();
  101. 101 : },
  102. 102 : scope: this
  103. 103 : }
  104. 104 : }]
  105. 105 : }]
  106. 106 : });
  107. 107 :
  108. 108 : this.callParent(arguments);
  109. 109 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  110. 110 :
  111. 111 : this.on("afterrender", function() {
  112. 112 : this.body.addListener('click', function(e) {
  113. 113 : var target = e.getTarget(null, null, true);
  114. 114 : if (target && target.dom.tagName == 'A') {
  115. 115 : if (target.hasCls('document-id')) {
  116. 116 : var docId = target.getAttribute('val', 'voyant');
  117. 117 : var doc = this.getCorpus().getDocuments().getById(docId);
  118. 118 : this.dispatchEvent('documentsClicked', this, [doc]);
  119. 119 : } else if (target.hasCls('corpus-type')) {
  120. 120 : this.dispatchEvent('termsClicked', this, [target.getHtml()]);
  121. 121 : } else if (target.hasCls('document-type')) {
  122. 122 : this.dispatchEvent('documentIndexTermsClicked', this, [{
  123. 123 : term: target.getHtml(),
  124. 124 : docIndex: target.getAttribute("docIndex", 'voyant')
  125. 125 : }]);
  126. 126 : }
  127. 127 : }
  128. 128 : }, this);
  129. 129 : })
  130. 130 :
  131. 131 : // create a listener for corpus loading (defined here, in case we need to load it next)
  132. 132 : this.on('loadedCorpus', function(src, corpus) {
  133. 133 : if (this.rendered) {
  134. 134 : this.loadSummary();
  135. 135 : }
  136. 136 : else {
  137. 137 : this.on("afterrender", function() {
  138. 138 : this.loadSummary();
  139. 139 : }, this)
  140. 140 : }
  141. 141 :
  142. 142 : });
  143. 143 :
  144. 144 : // if we have a corpus, load it
  145. 145 : if (config && config.corpus) {
  146. 146 : this.fireEvent('loadedCorpus', this, config.corpus);
  147. 147 : }
  148. 148 :
  149. 149 : this.on("resize", function() {
  150. 150 : var available = this.getWidth()-200;
  151. 151 : this.query("sparklineline").forEach(function(spark) {
  152. 152 : if (spark.getWidth()>available) {
  153. 153 : spark.setWidth(available);
  154. 154 : }
  155. 155 : })
  156. 156 : }, this)
  157. 157 : },
  158. 158 :
  159. 159 : loadSummary: function() {
  160. 160 :
  161. 161 : var me = this;
  162. 162 :
  163. 163 : var main = this.queryById('main');
  164. 164 :
  165. 165 : main.removeAll();
  166. 166 : main.add({
  167. 167 : cls: 'section',
  168. 168 : html: this.getCorpus().getString()
  169. 169 : });
  170. 170 :
  171. 171 : var docs = this.getCorpus().getDocuments().getRange();
  172. 172 : var limit = this.getApiParam('limit');
  173. 173 :
  174. 174 : if (docs.length>1) {
  175. 175 :
  176. 176 : var docsLengthTpl = new Ext.XTemplate('<tpl for="." between="; "><a href="#" onclick="return false" class="document-id" voyant:val="{id}" data-qtip="{title}">{shortTitle}</a><span style="font-size: smaller"> (<span class="info-tip" data-qtip="{valTip}">{val}</span>)</span></a></tpl>')
  177. 177 :
  178. 178 : var sparkWidth;
  179. 179 : if (docs.length<25) {sparkWidth=docs.length*4;}
  180. 180 : else if (docs.length<50) {sparkWidth=docs.length*2;}
  181. 181 : else if (docs.length>100) {
  182. 182 : var available = main.getWidth()-200;
  183. 183 : sparkWidth = available < docs.length ? docs.length : available;
  184. 184 : }
  185. 185 :
  186. 186 : var numberOfTerms = this.localize('numberOfTerms');
  187. 187 :
  188. 188 : // document length
  189. 189 : docs.sort(function(d1, d2) {return d2.getLexicalTokensCount()-d1.getLexicalTokensCount()});
  190. 190 : main.add(this.showSparklineSection(
  191. 191 : function(doc) { return doc.getLexicalTokensCount(); },
  192. 192 : this.localize('docsLength'), this.localize('longest'), this.localize('shortest'),
  193. 193 : docs, limit, docsLengthTpl, sparkWidth, this.localize('numberOfTerms')
  194. 194 : ));
  195. 195 :
  196. 196 : // vocabulary density
  197. 197 : docs.sort(function(d1, d2) {return d2.getLexicalTypeTokenRatio()-d1.getLexicalTypeTokenRatio()});
  198. 198 : main.add(this.showSparklineSection(
  199. 199 : function(doc) { return Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000'); },
  200. 200 : this.localize('docsDensity'), this.localize('highest'), this.localize('lowest'),
  201. 201 : docs, limit, docsLengthTpl, sparkWidth, this.localize('docsDensityTip')
  202. 202 : ));
  203. 203 :
  204. 204 : // words per sentence
  205. 205 : docs.sort(function(d1, d2) {return d2.getAverageWordsPerSentence()-d1.getAverageWordsPerSentence()});
  206. 206 : main.add(this.showSparklineSection(
  207. 207 : function(doc) { return Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0'); },
  208. 208 : this.localize('averageWordsPerSentence'), this.localize('highest'), this.localize('lowest'),
  209. 209 : docs, limit, docsLengthTpl, sparkWidth, this.localize('avgWordsPerSentenceTip')
  210. 210 : ));
  211. 211 :
  212. 212 : } else { // single document, we can still show word density and average words per sentence
  213. 213 : var doc = docs[0];
  214. 214 : if (doc) {
  215. 215 : main.add({
  216. 216 : cls: 'section',
  217. 217 : html:"<b>"+this.localize("docsDensity")+"</b> "+Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000')
  218. 218 : });
  219. 219 : main.add({
  220. 220 : cls: 'section',
  221. 221 : html: "<b>"+this.localize("averageWordsPerSentence")+"</b> "+Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0')
  222. 222 : });
  223. 223 : }
  224. 224 : }
  225. 225 :
  226. 226 : // readability
  227. 227 : this.getCorpus().getReadability().then(function(data) {
  228. 228 : docs.forEach(function(doc) {
  229. 229 : var readDoc = data.find(function(dataDoc) {
  230. 230 : return dataDoc.docId === doc.getId();
  231. 231 : });
  232. 232 : if (readDoc) {
  233. 233 : doc.set('readability', readDoc.readability);
  234. 234 : }
  235. 235 : });
  236. 236 :
  237. 237 : var sectionIndex = main.items.length-2;
  238. 238 : if (docs.length>1) {
  239. 239 : docs.sort(function(d1, d2) {return d2.get('readability')-d1.get('readability')});
  240. 240 : main.insert(sectionIndex, me.showSparklineSection(function(doc) {
  241. 241 : return Ext.util.Format.number(doc.get('readability'),'0.000');
  242. 242 : }, me.localize('readabilityIndex'), me.localize('highest'), me.localize('lowest'), docs, limit, docsLengthTpl, sparkWidth, me.localize('readabilityTip')));
  243. 243 : } else {
  244. 244 : main.insert(sectionIndex, {
  245. 245 : cls: 'section',
  246. 246 : html: '<b>'+me.localize('readabilityIndex')+'</b> '+ Ext.util.Format.number(docs[0].get('readability'),'0.000')
  247. 247 : });
  248. 248 : }
  249. 249 : })
  250. 250 :
  251. 251 :
  252. 252 : main.add({
  253. 253 : cls: 'section',
  254. 254 : items: [{
  255. 255 : html: this.localize("mostFrequentWords"),
  256. 256 : cls: 'header'
  257. 257 : },{
  258. 258 : cls: 'contents',
  259. 259 : html: '<ul><li></li></ul>'
  260. 260 : }],
  261. 261 : listeners: {
  262. 262 : afterrender: function(container) {
  263. 263 : container.mask(me.localize("loading"));
  264. 264 : me.getCorpus().getCorpusTerms().load({
  265. 265 : params: {
  266. 266 : limit: me.getApiParam('limit'),
  267. 267 : stopList: me.getApiParam('stopList'),
  268. 268 : forTool: 'summary'
  269. 269 : },
  270. 270 : callback: function(records, operation, success) {
  271. 271 : if (success && records && records.length>0) {
  272. 272 : container.unmask();
  273. 273 : var contentsEl = container.down('panel[cls~=contents]').getTargetEl().selectNode('li');
  274. 274 : Ext.dom.Helper.append(contentsEl,
  275. 275 : new Ext.XTemplate('<tpl for="." between="; "><a href="#" onclick="return false" class="corpus-type keyword" voyant:recordId="{id}">{term}</a><span style="font-size: smaller"> ({val})</span></tpl>')
  276. 276 : .apply(records.map(function(term) {
  277. 277 : return {
  278. 278 : id: term.getId(),
  279. 279 : term: term.getTerm(),
  280. 280 : val: term.getRawFreq()
  281. 281 : }
  282. 282 : }))
  283. 283 : )
  284. 284 : }
  285. 285 : }
  286. 286 : })
  287. 287 : }
  288. 288 : }
  289. 289 : })
  290. 290 :
  291. 291 : if (docs.length>1) {
  292. 292 : main.add({
  293. 293 : cls: 'section',
  294. 294 : items: [{
  295. 295 : html: this.localize("distinctiveWords"),
  296. 296 : cls: 'header'
  297. 297 : },{
  298. 298 : cls: 'contents',
  299. 299 : html: '<ol></ol>'
  300. 300 : }],
  301. 301 : itemId: 'distinctiveWords',
  302. 302 : listeners: {
  303. 303 : afterrender: function(container) {
  304. 304 : me.showMoreDistinctiveWords();
  305. 305 : }
  306. 306 : },
  307. 307 : scope: this
  308. 308 : })
  309. 309 : }
  310. 310 :
  311. 311 : },
  312. 312 :
  313. 313 : showSparklineSection: function(docDataFunc, headerText, topText, bottomText, docs, limit, docsLengthTpl, sparkWidth, valueTip) {
  314. 314 : var me = this;
  315. 315 : return {
  316. 316 : cls: 'section',
  317. 317 : items: [{
  318. 318 : layout: 'hbox',
  319. 319 : align: 'bottom',
  320. 320 : items: [{
  321. 321 : html: headerText,
  322. 322 : cls: 'header'
  323. 323 : }, {
  324. 324 : xtype: 'sparklineline',
  325. 325 : values: this.getCorpus().getDocuments().getRange().map(function(doc) {return docDataFunc.call(me, doc)}),
  326. 326 : tipTpl: new Ext.XTemplate('{[this.getDocumentTitle(values.x,values.y)]}', {
  327. 327 : getDocumentTitle: function(docIndex, len) {
  328. 328 : return '('+len+') '+this.panel.getCorpus().getDocument(docIndex).getTitle()
  329. 329 : },
  330. 330 : panel: me
  331. 331 : }),
  332. 332 : height: 16,
  333. 333 : width: sparkWidth
  334. 334 : }]
  335. 335 : },{
  336. 336 : cls: 'contents',
  337. 337 : html: '<ul><li>'+topText+" "+docsLengthTpl.apply(docs.slice(0, docs.length>limit ? limit : parseInt(docs.length/2)).map(function(doc) {return {
  338. 338 : id: doc.getId(),
  339. 339 : shortTitle: doc.getShortTitle(),
  340. 340 : title: doc.getTitle(),
  341. 341 : val: docDataFunc.call(me, doc),
  342. 342 : valTip: valueTip
  343. 343 : }}))+'</li>'+
  344. 344 : '<li>'+bottomText+" "+docsLengthTpl.apply(docs.slice(-(docs.length>limit ? limit : parseInt(docs.length/2))).reverse().map(function(doc) {return {
  345. 345 : id: doc.getId(),
  346. 346 : shortTitle: doc.getShortTitle(),
  347. 347 : title: doc.getTitle(),
  348. 348 : val: docDataFunc.call(me, doc),
  349. 349 : valTip: valueTip
  350. 350 : }}))+'</li>'
  351. 351 : }]
  352. 352 : }
  353. 353 : },
  354. 354 :
  355. 355 : showMoreDistinctiveWords: function() {
  356. 356 : var distinctiveWordsContainer = this.queryById('distinctiveWords');
  357. 357 : var list = distinctiveWordsContainer.getTargetEl().selectNode("ol");
  358. 358 : var count = Ext.dom.Query.select("li:not(.more)", list).length;
  359. 359 : var numberOfDocumentsForDistinctiveWords = parseInt(this.getApiParam('numberOfDocumentsForDistinctiveWords'));
  360. 360 : var range = this.getCorpus().getDocuments().getRange(count, count+numberOfDocumentsForDistinctiveWords-1);
  361. 361 : if (range && Ext.isArray(range)) {
  362. 362 : var docIndex = [];
  363. 363 : range.forEach(function(doc) {
  364. 364 : docIndex.push(doc.getIndex())
  365. 365 : })
  366. 366 : if (docIndex.length>0) {
  367. 367 : this.getCorpus().getDocumentTerms().load({
  368. 368 : addRecords: true,
  369. 369 : params: {
  370. 370 : docIndex: docIndex,
  371. 371 : perDocLimit: parseInt(this.getApiParam("limit")),
  372. 372 : limit: numberOfDocumentsForDistinctiveWords*parseInt(this.getApiParam("limit")),
  373. 373 : stopList: this.getApiParam('stopList'),
  374. 374 : sort: 'TFIDF',
  375. 375 : dir: 'DESC',
  376. 376 : forTool: 'summary'
  377. 377 : },
  378. 378 : scope: this,
  379. 379 : callback: function(records, operation, success) {
  380. 380 : var docs = {};
  381. 381 : if (success && records && Ext.isArray(records)) { // TODO: why wouldn't we have records here?
  382. 382 : records.forEach(function(r, index, array) {
  383. 383 : var i = r.getDocIndex();
  384. 384 : if (!(i in docs)) {docs[i]=[]};
  385. 385 : docs[i].push({
  386. 386 : id: r.getId(),
  387. 387 : docIndex: r.getDocIndex(),
  388. 388 : type: r.getTerm(),
  389. 389 : val: Ext.util.Format.number(r.get('rawFreq'),'0,000'),
  390. 390 : docId: r.get('docId')
  391. 391 : });
  392. 392 :
  393. 393 : });
  394. 394 : var len;
  395. 395 : docIndex.forEach(function(index) {
  396. 396 : if (docs[index]) {
  397. 397 : var doc = this.getCorpus().getDocument(index);
  398. 398 : len = docs[index].length; // declare for template
  399. 399 : Ext.dom.Helper.append(list, {tag: 'li', 'voyant:index': String(index), html:
  400. 400 : '<a href="#" onclick="return false" class="document-id document-id-distinctive" voyant:val="'+doc.get('id')+'">'+doc.getShortTitle()+'</a>'+
  401. 401 : this.localize('colon')+ " "+new Ext.XTemplate(this.localize('documentType')).apply({types: docs[index]})+'.'
  402. 402 : });
  403. 403 : }
  404. 404 : }, this);
  405. 405 : distinctiveWordsContainer.updateLayout()
  406. 406 : len = numberOfDocumentsForDistinctiveWords;
  407. 407 : remaining = this.getCorpus().getDocuments().getTotalCount() - count - docIndex.length;
  408. 408 : if (remaining>0) {
  409. 409 : var tpl = new Ext.Template(this.localize('moreDistinctiveWords'));
  410. 410 : var more = Ext.dom.Helper.append(list, {tag: 'li', cls: 'more', html: tpl.apply([len>remaining ? remaining : len,remaining])}, true);
  411. 411 : more.on("click", function() {
  412. 412 : more.remove();
  413. 413 : this.showMoreDistinctiveWords();
  414. 414 : }, this)
  415. 415 : }
  416. 416 : }
  417. 417 : }
  418. 418 : });
  419. 419 : }
  420. 420 : }
  421. 421 : },
  422. 422 :
  423. 423 : // override because the doc sparklines are mostly useless as exports
  424. 424 : getExportVisualization: function() {
  425. 425 : return false;
  426. 426 : },
  427. 427 :
  428. 428 : getExtraDataExportItems: function() {
  429. 429 : return [{
  430. 430 : name: 'export',
  431. 431 : inputValue: 'dataAsTsv',
  432. 432 : boxLabel: this.localize('exportGridCurrentTsv')
  433. 433 : }];
  434. 434 : },
  435. 435 :
  436. 436 : exportDataAsTsv: function(panel, form) {
  437. 437 : var value = '';
  438. 438 : var sections = panel.query('panel[cls~=section]');
  439. 439 : sections.forEach(function(sp) {
  440. 440 : var sectionData = '';
  441. 441 : var header = sp.down('panel[cls~=header]');
  442. 442 : var contents = sp.down('panel[cls~=contents]');
  443. 443 : if (header) {
  444. 444 : sectionData += header.getEl().dom.textContent + "\n";
  445. 445 : if (contents) {
  446. 446 : contents.getEl().select('li').elements.forEach(function(li) {
  447. 447 : sectionData += li.textContent.replace(/:/, ":\t").replace(/\)[,;]/g, ")\t") + "\n";
  448. 448 : });
  449. 449 : }
  450. 450 : } else {
  451. 451 : sectionData = sp.getEl().dom.textContent + "\n";
  452. 452 : }
  453. 453 : value += sectionData + "\n";
  454. 454 : });
  455. 455 : Ext.Msg.show({
  456. 456 : title: panel.localize('exportDataTitle'),
  457. 457 : message: panel.localize('exportDataTsvMessage'),
  458. 458 : buttons: Ext.Msg.OK,
  459. 459 : icon: Ext.Msg.INFO,
  460. 460 : prompt: true,
  461. 461 : multiline: true,
  462. 462 : value: value
  463. 463 : });
  464. 464 : }
  465. 465 : });