1. 1 : /**
  2. 2 : * The Word Tree tool allows you to explore how keywords are used in different phrases in the corpus.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "context": null,
  8. 8 : * "docId": null,
  9. 9 : * "docIndex": null,
  10. 10 : * "limit": null,
  11. 11 : * "query": null,
  12. 12 : * "stopList": null
  13. 13 : * };
  14. 14 : *
  15. 15 : * loadCorpus("austen").tool("wordtree", config);
  16. 16 : *
  17. 17 : * @class WordTree
  18. 18 : * @tutorial wordtree
  19. 19 : * @memberof Tools
  20. 20 : */
  21. 21 : Ext.define('Voyant.panel.WordTree', {
  22. 22 : extend: 'Ext.panel.Panel',
  23. 23 : mixins: ['Voyant.panel.Panel'],
  24. 24 : alias: 'widget.wordtree',
  25. 25 : statics: {
  26. 26 : i18n: {
  27. 27 : },
  28. 28 : api: {
  29. 29 : /**
  30. 30 : * @memberof Tools.WordTree
  31. 31 : * @instance
  32. 32 : * @property {query}
  33. 33 : */
  34. 34 : query: undefined,
  35. 35 :
  36. 36 : /**
  37. 37 : * @memberof Tools.WordTree
  38. 38 : * @instance
  39. 39 : * @property {docId}
  40. 40 : */
  41. 41 : docId: undefined,
  42. 42 :
  43. 43 : /**
  44. 44 : * @memberof Tools.WordTree
  45. 45 : * @instance
  46. 46 : * @property {docIndex}
  47. 47 : */
  48. 48 : docIndex: undefined,
  49. 49 :
  50. 50 : /**
  51. 51 : * @memberof Tools.WordTree
  52. 52 : * @instance
  53. 53 : * @property {stopList}
  54. 54 : * @default
  55. 55 : */
  56. 56 : stopList: 'auto',
  57. 57 :
  58. 58 : /**
  59. 59 : * @memberof Tools.WordTree
  60. 60 : * @instance
  61. 61 : * @property {context}
  62. 62 : * @default
  63. 63 : */
  64. 64 : context: 10,
  65. 65 :
  66. 66 : /**
  67. 67 : * @memberof Tools.WordTree
  68. 68 : * @instance
  69. 69 : * @property {limit}
  70. 70 : * @default
  71. 71 : */
  72. 72 : limit: 100
  73. 73 : },
  74. 74 : glyph: 'xf0e8@FontAwesome'
  75. 75 : },
  76. 76 :
  77. 77 : config: {
  78. 78 : tree: undefined,
  79. 79 : kwicStore: undefined,
  80. 80 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  81. 81 : numBranches: 5,
  82. 82 : lastClick: 1
  83. 83 : },
  84. 84 :
  85. 85 : doubleClickDelay: 300,
  86. 86 :
  87. 87 : constructor: function(config) {
  88. 88 : this.callParent(arguments);
  89. 89 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  90. 90 : },
  91. 91 :
  92. 92 : initComponent: function() {
  93. 93 : Ext.apply(this, {
  94. 94 : title: this.localize('title'),
  95. 95 : dockedItems: [{
  96. 96 : dock: 'bottom',
  97. 97 : xtype: 'toolbar',
  98. 98 : overflowHandler: 'scroller',
  99. 99 : items: [{
  100. 100 : xtype: 'querysearchfield'
  101. 101 : },
  102. 102 : '<span data-qtip="'+this.localize('poolTip')+'" class="info-tip">'+this.localize('pool')+"</span>"
  103. 103 : , {
  104. 104 : xtype: 'slider',
  105. 105 : itemId: 'poolSlider',
  106. 106 : minValue: 10,
  107. 107 : value: 100,
  108. 108 : maxValue: 1000,
  109. 109 : increment: 5,
  110. 110 : width: 50,
  111. 111 : listeners: {
  112. 112 : render: function(slider) {
  113. 113 : slider.setValue(this.getApiParam('limit'));
  114. 114 : },
  115. 115 : changecomplete: function(slider, newValue) {
  116. 116 : this.setApiParam('limit', slider.getValue());
  117. 117 : this.reload();
  118. 118 : },
  119. 119 : scope: this
  120. 120 : }
  121. 121 : },
  122. 122 : '<span data-qtip="'+this.localize('branchesTip')+'" class="info-tip">'+this.localize('branches')+"</span>"
  123. 123 : ,{
  124. 124 :
  125. 125 : xtype: 'slider',
  126. 126 : itemId: 'branchesSlider',
  127. 127 : minValue: 2,
  128. 128 : value: 5,
  129. 129 : maxValue: 15,
  130. 130 : increment: 1,
  131. 131 : width: 50,
  132. 132 : listeners: {
  133. 133 : render: function(slider) {
  134. 134 : slider.setValue(this.getNumBranches());
  135. 135 : },
  136. 136 : changecomplete: function(slider, newValue) {
  137. 137 : this.setNumBranches(slider.getValue());
  138. 138 : this.reload();
  139. 139 : },
  140. 140 : scope: this
  141. 141 : }
  142. 142 : },
  143. 143 : '<span data-qtip="'+this.localize('contextTip')+'" class="info-tip">'+this.localize('context')+"</span>"
  144. 144 : , {
  145. 145 : xtype: 'slider',
  146. 146 : itemId: 'contextSlider',
  147. 147 : minValue: 3,
  148. 148 : value: 10,
  149. 149 : maxValue: 20,
  150. 150 : increment: 2,
  151. 151 : width: 50,
  152. 152 : listeners: {
  153. 153 : render: function(slider) {
  154. 154 : slider.setValue(this.getApiParam('context'));
  155. 155 : },
  156. 156 : changecomplete: function(slider, newValue) {
  157. 157 : this.setApiParam('context', slider.getValue());
  158. 158 : this.reload();
  159. 159 : },
  160. 160 : scope: this
  161. 161 : }
  162. 162 : }]
  163. 163 : }]
  164. 164 : });
  165. 165 :
  166. 166 : this.setKwicStore(Ext.create('Voyant.data.store.Contexts', {
  167. 167 : parentPanel: this,
  168. 168 : proxy: {
  169. 169 : extraParams: {
  170. 170 : stripTags: 'all'
  171. 171 : }
  172. 172 : },
  173. 173 : listeners: {
  174. 174 : load: function(store, records, success, operation) {
  175. 175 : if (success) {
  176. 176 : this.parseRecords(records);
  177. 177 : }
  178. 178 : },
  179. 179 : scope: this
  180. 180 : }
  181. 181 : }));
  182. 182 :
  183. 183 : this.on('loadedCorpus', function(src, corpus) {
  184. 184 : var corpusTerms = corpus.getCorpusTerms({autoLoad: false});
  185. 185 : corpusTerms.load({
  186. 186 : callback: function(records, operation, success) {
  187. 187 : if (success && records.length>0) {
  188. 188 : var firstTerm = records[0].getTerm();
  189. 189 : this.setRoot(firstTerm);
  190. 190 : }
  191. 191 : },
  192. 192 : scope: this,
  193. 193 : params: {
  194. 194 : limit: 1,
  195. 195 : query: this.getApiParam('query'),
  196. 196 : stopList: this.getApiParam('stopList'),
  197. 197 : categories: this.getApiParam('categories')
  198. 198 : }
  199. 199 : });
  200. 200 : }, this);
  201. 201 :
  202. 202 : this.on('query', function(src, query) {
  203. 203 : if (query !== undefined && query != '') {
  204. 204 : this.setRoot(query);
  205. 205 : }
  206. 206 : }, this);
  207. 207 :
  208. 208 : this.on('termsClicked', function(src, terms) {
  209. 209 : var queryTerms = [];
  210. 210 : terms.forEach(function(term) {
  211. 211 : if (Ext.isString(term)) {queryTerms.push(term);}
  212. 212 : else if (term.term) {queryTerms.push(term.term);}
  213. 213 : else if (term.getTerm) {queryTerms.push(term.getTerm());}
  214. 214 : });
  215. 215 : this.setRoot(queryTerms);
  216. 216 : }, this);
  217. 217 :
  218. 218 : this.on('documentTermsClicked', function(src, terms) {
  219. 219 : var queryTerms = [];
  220. 220 : terms.forEach(function(term) {
  221. 221 : if (term.getTerm()) {queryTerms.push(term.getTerm());}
  222. 222 : });
  223. 223 : this.setRoot(queryTerms);
  224. 224 : }, this);
  225. 225 :
  226. 226 : this.on('resize', function(panel, width, height) {
  227. 227 : var tree = this.getTree();
  228. 228 : if (tree !== undefined) {
  229. 229 : tree.visWidth(width).visHeight(height);
  230. 230 : // TODO preserve expanded branches
  231. 231 : tree.redraw();
  232. 232 : }
  233. 233 : }, this);
  234. 234 :
  235. 235 : this.on('boxready', this.initGraph, this);
  236. 236 :
  237. 237 : this.callParent(arguments);
  238. 238 : },
  239. 239 :
  240. 240 : parseRecords: function(records) {
  241. 241 : var parsedRecords = [];
  242. 242 : for (var i = 0; i < records.length; i++) {
  243. 243 : var r = records[i];
  244. 244 : var pr = {
  245. 245 : id: i,
  246. 246 : prefix: r.getLeft().trim().split(/\s+/),
  247. 247 : hit: r.getMiddle(),
  248. 248 : suffix: r.getRight().trim().split(/\s+/)
  249. 249 : };
  250. 250 : parsedRecords.push(pr);
  251. 251 : }
  252. 252 :
  253. 253 : // find top tokens and sort records by them
  254. 254 : var prefixTokenCounts = {};
  255. 255 : var suffixTokenCounts = {};
  256. 256 : for (var i = 0; i < parsedRecords.length; i++) {
  257. 257 : var pr = parsedRecords[i];
  258. 258 : var prefixToken = pr.prefix[pr.prefix.length-1];
  259. 259 : var suffixToken = pr.suffix[0];
  260. 260 : if (prefixTokenCounts[prefixToken]) {
  261. 261 : prefixTokenCounts[prefixToken]++;
  262. 262 : } else {
  263. 263 : prefixTokenCounts[prefixToken] = 1;
  264. 264 : }
  265. 265 : if (suffixTokenCounts[suffixToken]) {
  266. 266 : suffixTokenCounts[suffixToken]++;
  267. 267 : } else {
  268. 268 : suffixTokenCounts[suffixToken] = 1;
  269. 269 : }
  270. 270 : }
  271. 271 :
  272. 272 : var sortableTokens = [];
  273. 273 : for (var i = 0; i < parsedRecords.length; i++) {
  274. 274 : var pr = parsedRecords[i];
  275. 275 : var prefixToken = pr.prefix[pr.prefix.length-1];
  276. 276 : var suffixToken = pr.suffix[0];
  277. 277 :
  278. 278 : sortableTokens.push({
  279. 279 : suffix: suffixToken, suffixCount: suffixTokenCounts[suffixToken],
  280. 280 : prefix: prefixToken, prefixCount: prefixTokenCounts[prefixToken]
  281. 281 : });
  282. 282 :
  283. 283 : }
  284. 284 :
  285. 285 : var prioritizeSuffix = false;
  286. 286 : // multi-sort
  287. 287 : sortableTokens.sort(function(a, b) {
  288. 288 : var s1 = a.suffixCount;
  289. 289 : var s2 = b.suffixCount;
  290. 290 :
  291. 291 : var p1 = a.prefixCount;
  292. 292 : var p2 = b.prefixCount;
  293. 293 :
  294. 294 : if (prioritizeSuffix) {
  295. 295 : if (s1 > s2) return -1;
  296. 296 : if (s1 < s2) return 1;
  297. 297 : if (p1 > p2) return -1;
  298. 298 : if (p1 < p2) return 1;
  299. 299 : } else {
  300. 300 : if (p1 > p2) return -1;
  301. 301 : if (p1 < p2) return 1;
  302. 302 : if (s1 > s2) return -1;
  303. 303 : if (s1 < s2) return 1;
  304. 304 : }
  305. 305 :
  306. 306 : return 0;
  307. 307 : });
  308. 308 :
  309. 309 : var len = Math.min(this.getNumBranches(), sortableTokens.length);
  310. 310 : var topSuffixTokens = [];
  311. 311 : var topPrefixTokens = [];
  312. 312 : for (var i = 0; i < len; i++) {
  313. 313 : topSuffixTokens.push(sortableTokens[i].suffix);
  314. 314 : topPrefixTokens.push(sortableTokens[i].prefix);
  315. 315 : }
  316. 316 :
  317. 317 : // use top tokens to limit results
  318. 318 : var prefixes = [], hits = [], suffixes = [], ids = [];
  319. 319 : for (var i = 0; i < parsedRecords.length; i++) {
  320. 320 : var parsedRecord = parsedRecords[i];
  321. 321 : if (topSuffixTokens.indexOf(parsedRecord.suffix[0]) != -1 || topPrefixTokens.indexOf(parsedRecord.suffix[0]) != -1) {
  322. 322 : prefixes.push(parsedRecord.prefix);
  323. 323 : hits.push(parsedRecord.hit);
  324. 324 : suffixes.push(parsedRecord.suffix);
  325. 325 : ids.push(parsedRecord.id);
  326. 326 : }
  327. 327 : }
  328. 328 :
  329. 329 : var caseSensitive = false;
  330. 330 : var fieldNames = ["token", "POS"];
  331. 331 : var fieldDelim = "/";
  332. 332 : var distinguishingFieldsArray = ["token", "POS"];
  333. 333 : this.getTree().setupFromArrays(prefixes, hits, suffixes, ids, caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray);
  334. 334 :
  335. 335 : if (!this.getTree().succeeded()) {
  336. 336 : this.toastInfo({
  337. 337 : html: this.localize("emptyText"),
  338. 338 : align: 'bl'
  339. 339 : });
  340. 340 : }
  341. 341 : },
  342. 342 :
  343. 343 : initGraph: function() {
  344. 344 : var el = this.getLayout().getRenderTarget();
  345. 345 : var w = el.getWidth();
  346. 346 : var h = el.getHeight();
  347. 347 :
  348. 348 : var dt = new doubletree.DoubleTree();
  349. 349 : dt.init('#'+el.getId())
  350. 350 : .visWidth(w).visHeight(h)
  351. 351 : .handlers({
  352. 352 : click: this.clickHandler.bind(this)
  353. 353 : });
  354. 354 :
  355. 355 : this.setTree(dt);
  356. 356 :
  357. 357 : // explicitly set dimensions
  358. 358 : // el.setWidth(el.getWidth());
  359. 359 : // el.setHeight(el.getHeight());
  360. 360 : },
  361. 361 :
  362. 362 : clickHandler: function(node) {
  363. 363 : var now = new Date().getTime();
  364. 364 : if (this.getLastClick() && now-this.getLastClick()<this.doubleClickDelay) {
  365. 365 : this.setLastClick(1);
  366. 366 : var terms = [], parent = node;
  367. 367 : while (parent != null) {
  368. 368 : terms.push(parent.name);
  369. 369 : parent = parent.parent;
  370. 370 : }
  371. 371 : this.getApplication().dispatchEvent('termsClicked', this, [terms.reverse().join(" ")]);
  372. 372 : } else {
  373. 373 : this.setLastClick(now);
  374. 374 : }
  375. 375 : },
  376. 376 :
  377. 377 : // doubleClickHandler: function(node) {
  378. 378 : //// dispatch phrase click instead of recentering (which can be done with search)
  379. 379 : //// this.setRoot(node.name);
  380. 380 : // },
  381. 381 : //
  382. 382 : setRoot: function(query) {
  383. 383 : this.setApiParam('query', this.stripPunctuation(query));
  384. 384 : this.getKwicStore().load({params: this.getApiParams()});
  385. 385 : },
  386. 386 :
  387. 387 : reload: function() {
  388. 388 : var query = this.getApiParam('query');
  389. 389 : if (query !== undefined) {
  390. 390 : this.setRoot(query);
  391. 391 : }
  392. 392 : },
  393. 393 :
  394. 394 : stripPunctuation: function(value) {
  395. 395 : if (Ext.isString(value)) return value.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');
  396. 396 : else {
  397. 397 : var values = [];
  398. 398 : value.forEach(function(v) {
  399. 399 : values.push(v.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, ''));
  400. 400 : });
  401. 401 : return values;
  402. 402 : }
  403. 403 : return '';
  404. 404 : }
  405. 405 : });
  406. 406 :