1. 1 : /**
  2. 2 : * The Phrases tool shows repeating sequences of words organized by frequency of repetition or number of words in each repeated phrase.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "columns": null,
  8. 8 : * "dir": null,
  9. 9 : * "docId": null,
  10. 10 : * "docIndex": null,
  11. 11 : * "maxLength": null,
  12. 12 : * "minLength": null,
  13. 13 : * "overlapFilter": null,
  14. 14 : * "query": null,
  15. 15 : * "sort": null,
  16. 16 : * "stopList": null
  17. 17 : * };
  18. 18 : *
  19. 19 : * loadCorpus("austen").tool("phrases", config);
  20. 20 : *
  21. 21 : * @class Phrases
  22. 22 : * @tutorial phrases
  23. 23 : * @memberof Tools
  24. 24 : */
  25. 25 : Ext.define('Voyant.panel.Phrases', {
  26. 26 : extend: 'Ext.grid.Panel',
  27. 27 : mixins: ['Voyant.panel.Panel'],
  28. 28 : alias: 'widget.phrases',
  29. 29 : isConsumptive: true,
  30. 30 : statics: {
  31. 31 : i18n: {
  32. 32 : },
  33. 33 : api: {
  34. 34 : /**
  35. 35 : * @memberof Tools.Phrases
  36. 36 : * @instance
  37. 37 : * @property {stopList}
  38. 38 : * @default
  39. 39 : */
  40. 40 : stopList: 'auto',
  41. 41 :
  42. 42 : /**
  43. 43 : * @memberof Tools.Phrases
  44. 44 : * @instance
  45. 45 : * @property {query}
  46. 46 : */
  47. 47 : query: undefined,
  48. 48 :
  49. 49 : /**
  50. 50 : * @memberof Tools.Phrases
  51. 51 : * @instance
  52. 52 : * @property {docId}
  53. 53 : */
  54. 54 : docId: undefined,
  55. 55 :
  56. 56 : /**
  57. 57 : * @memberof Tools.Phrases
  58. 58 : * @instance
  59. 59 : * @property {docIndex}
  60. 60 : */
  61. 61 : docIndex: undefined,
  62. 62 :
  63. 63 : /**
  64. 64 : * @memberof Tools.Phrases
  65. 65 : * @instance
  66. 66 : * @property {Number} minLength The minimum length (number of words) of the phrase to consider.
  67. 67 : * @default
  68. 68 : */
  69. 69 : minLength: 2,
  70. 70 :
  71. 71 : /**
  72. 72 : * @memberof Tools.Phrases
  73. 73 : * @instance
  74. 74 : * @property {Number} maxLength The maximum length (number of words) of the phrase to consider.
  75. 75 : * @default
  76. 76 : */
  77. 77 : maxLength: 50,
  78. 78 :
  79. 79 : /**
  80. 80 : * @memberof Tools.Phrases
  81. 81 : * @instance
  82. 82 : * @property {String} overlapFilter Specifies the strategory for prioritizing and filtering out phrases. Options are: 'none' (no filtering), 'length' (prioritize phrase length), or 'rawFreq' (prioritize phrase frequency). See [Phrases options](tutorial-phrases.html#options) for more info.
  83. 83 : * @default
  84. 84 : */
  85. 85 : overlapFilter: 'length',
  86. 86 :
  87. 87 : /**
  88. 88 : * @memberof Tools.Phrases
  89. 89 : * @instance
  90. 90 : * @property {columns} columns 'term', 'rawFreq', 'length', 'distributions'
  91. 91 : */
  92. 92 : columns: undefined,
  93. 93 :
  94. 94 : /**
  95. 95 : * @memberof Tools.Phrases
  96. 96 : * @instance
  97. 97 : * @property {sort}
  98. 98 : * @default
  99. 99 : */
  100. 100 : sort: 'length',
  101. 101 :
  102. 102 : /**
  103. 103 : * @memberof Tools.Phrases
  104. 104 : * @instance
  105. 105 : * @property {dir}
  106. 106 : * @default
  107. 107 : */
  108. 108 : dir: 'desc'
  109. 109 : },
  110. 110 : glyph: 'xf0ce@FontAwesome'
  111. 111 : },
  112. 112 : config: {
  113. 113 : /**
  114. 114 : * @private
  115. 115 : */
  116. 116 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  117. 117 : },
  118. 118 : constructor: function(config) {
  119. 119 :
  120. 120 : this.callParent(arguments);
  121. 121 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  122. 122 :
  123. 123 : // create a listener for corpus loading (defined here, in case we need to load it next)
  124. 124 : this.on('loadedCorpus', function(src, corpus) {
  125. 125 : if (this.isVisible()) {
  126. 126 : if (this.hasCorpusAccess(corpus)==false) {
  127. 127 : this.mask(this.localize('limitedAccess'), 'mask-no-spinner');
  128. 128 : } else {
  129. 129 : this.loadFromApis();
  130. 130 : }
  131. 131 : }
  132. 132 :
  133. 133 : });
  134. 134 :
  135. 135 : if (config.embedded) {
  136. 136 : // var cls = Ext.getClass(config.embedded).getName();
  137. 137 : // if (cls=="Voyant.data.store.DocumentTerms" || cls=="Voyant.data.model.Document") {
  138. 138 : // this.fireEvent('loadedCorpus', this, config.embedded.getCorpus())
  139. 139 : // }
  140. 140 : }
  141. 141 : else if (config.corpus) {
  142. 142 : this.fireEvent('loadedCorpus', this, config.corpus)
  143. 143 : }
  144. 144 :
  145. 145 : this.on("corpusTermsClicked", function(src, terms) {
  146. 146 : if (this.getStore().getCorpus()) { // make sure we have a corpus
  147. 147 : var query = [];
  148. 148 : terms.forEach(function(term) {
  149. 149 : query.push(term.get("term"));
  150. 150 : })
  151. 151 : this.setApiParams({
  152. 152 : query: query,
  153. 153 : docId: undefined,
  154. 154 : docIndex: undefined
  155. 155 : });
  156. 156 : if (this.isVisible()) {
  157. 157 : this.getStore().load({params: this.getApiParams()});
  158. 158 : }
  159. 159 : }
  160. 160 : });
  161. 161 :
  162. 162 : this.on("activate", function() { // load after tab activate (if we're in a tab panel)
  163. 163 : if (this.getStore().getCorpus()) {this.loadFromApis()}
  164. 164 : }, this)
  165. 165 :
  166. 166 : this.on("query", function(src, query) {
  167. 167 : this.setApiParam("query", query);
  168. 168 : this.getStore().getProxy().setExtraParam("query", query);
  169. 169 : this.loadFromApis();
  170. 170 : }, this)
  171. 171 : },
  172. 172 :
  173. 173 : loadFromApis: function() {
  174. 174 : if (this.getStore().getCorpus()) {
  175. 175 : this.getStore().load({params: this.getApiParams()});
  176. 176 : }
  177. 177 : },
  178. 178 :
  179. 179 : initComponent: function() {
  180. 180 : var me = this;
  181. 181 :
  182. 182 : var store = Ext.create("Voyant.data.store.CorpusNgramsBuffered", {
  183. 183 : parentPanel: me,
  184. 184 : leadingBufferZone: 100 // since these calls are expensive reduce buffer to 1 page
  185. 185 : });
  186. 186 :
  187. 187 : store.on("beforeload", function(store) {
  188. 188 : return me.hasCorpusAccess(store.getCorpus());
  189. 189 : });
  190. 190 : me.on("sortchange", function( ct, column, direction, eOpts ) {
  191. 191 : this.setApiParam('sort', column.dataIndex);
  192. 192 : this.setApiParam('dir', direction);
  193. 193 : var api = this.getApiParams(["stopList", "query", "docId", "docIndex", "sort", "dir", "minLength", "maxLength", "overlapFilter"]);
  194. 194 : var proxy = this.getStore().getProxy();
  195. 195 : for (var key in api) {proxy.setExtraParam(key, api[key]);}
  196. 196 : }, me)
  197. 197 :
  198. 198 : Ext.apply(me, {
  199. 199 : title: this.localize('title'),
  200. 200 : emptyText: this.localize("emptyText"),
  201. 201 : store : store,
  202. 202 : selModel: Ext.create('Ext.selection.CheckboxModel', {
  203. 203 : listeners: {
  204. 204 : selectionchange: {
  205. 205 : fn: function(sm, selections) {
  206. 206 : if (selections.length > 0) {
  207. 207 : var terms = [];
  208. 208 : selections.forEach(function(selection) {
  209. 209 : terms.push('"'+selection.getTerm()+'"')
  210. 210 : })
  211. 211 : this.getApplication().dispatchEvent('termsClicked', this, terms);
  212. 212 : }
  213. 213 : },
  214. 214 : scope: this
  215. 215 : }
  216. 216 : }
  217. 217 : }),
  218. 218 : dockedItems: [{
  219. 219 : dock: 'bottom',
  220. 220 : xtype: 'toolbar',
  221. 221 : overflowHandler: 'scroller',
  222. 222 : items: [{
  223. 223 : xtype: 'querysearchfield'
  224. 224 : }, {
  225. 225 : xtype: 'totalpropertystatus'
  226. 226 : }, '-', {
  227. 227 : text: me.localize('length'),
  228. 228 : tooltip: 'test',
  229. 229 : xtype: 'label'
  230. 230 : }, {
  231. 231 : xtype: 'slider',
  232. 232 : minValue: 2,
  233. 233 : values: [2, 50],
  234. 234 : maxValue: 50,
  235. 235 : increment: 1,
  236. 236 : width: 75,
  237. 237 : tooltip: this.localize("lengthTip"),
  238. 238 : listeners: {
  239. 239 : render: {
  240. 240 : fn: function(slider) {
  241. 241 : var values = slider.getValues();
  242. 242 : slider.setValue(0, parseInt(this.getApiParam("minLength", values[0])))
  243. 243 : slider.setValue(1, parseInt(this.getApiParam("maxLength", values[1])))
  244. 244 : },
  245. 245 : scope: me
  246. 246 : },
  247. 247 : changecomplete: {
  248. 248 : fn: function(slider, newValue) {
  249. 249 : var values = slider.getValues();
  250. 250 : this.setApiParam("minLength", parseInt(values[0]));
  251. 251 : this.setApiParam("maxLength", parseInt(values[1]));
  252. 252 : this.getStore().load({params: this.getApiParams()});
  253. 253 : },
  254. 254 : scope: me
  255. 255 : }
  256. 256 : }
  257. 257 : }, {
  258. 258 : xtype: 'corpusdocumentselector'
  259. 259 : }, '-', {
  260. 260 : xtype: 'button',
  261. 261 : text: this.localize('overlap'),
  262. 262 : tooltip: this.localize('overlapTip'),
  263. 263 : menu: {
  264. 264 : items: [
  265. 265 : {
  266. 266 : xtype: 'menucheckitem',
  267. 267 : text: this.localize("overlapNone"),
  268. 268 : group: 'overlap',
  269. 269 : inputValue: 'none',
  270. 270 : checkHandler: function() {
  271. 271 : this.setApiParam('overlapFilter', 'none')
  272. 272 : this.getStore().load({params: this.getApiParams()})
  273. 273 : },
  274. 274 : scope: this
  275. 275 : }, {
  276. 276 : xtype: 'menucheckitem',
  277. 277 : text: this.localize("overlapLength"),
  278. 278 : group: 'overlap',
  279. 279 : inputValue: 'length',
  280. 280 : checkHandler: function() {
  281. 281 : this.setApiParam('overlapFilter', 'length')
  282. 282 : this.getStore().load({params: this.getApiParams()})
  283. 283 : },
  284. 284 : scope: this
  285. 285 : }, {
  286. 286 : xtype: 'menucheckitem',
  287. 287 : text: this.localize("overlapFreq"),
  288. 288 : group: 'overlap',
  289. 289 : inputValue: 'rawFreq',
  290. 290 : checkHandler: function() {
  291. 291 : this.setApiParam('overlapFilter', 'rawFreq')
  292. 292 : this.getStore().load({params: this.getApiParams()})
  293. 293 : },
  294. 294 : scope: this
  295. 295 : }
  296. 296 : ],
  297. 297 : listeners: {
  298. 298 : afterrender: {
  299. 299 : fn: function(menu) {
  300. 300 : var overlapFilter = this.getApiParam('overlapFilter');
  301. 301 : menu.items.each(function(item) {
  302. 302 : if (item.group) {
  303. 303 : item.setChecked(item.inputValue==overlapFilter);
  304. 304 : }
  305. 305 : }, this)
  306. 306 : },
  307. 307 : scope: this
  308. 308 : }
  309. 309 :
  310. 310 : }
  311. 311 : }
  312. 312 : }]
  313. 313 : }],
  314. 314 : columns: [{
  315. 315 : text: this.localize("term"),
  316. 316 : dataIndex: 'term',
  317. 317 : tooltip: this.localize("termTip"),
  318. 318 : sortable: true,
  319. 319 : flex: 1
  320. 320 : },{
  321. 321 : text: this.localize("rawFreq"),
  322. 322 : dataIndex: 'rawFreq',
  323. 323 : tooltip: this.localize("termRawFreqTip"),
  324. 324 : sortable: true,
  325. 325 : width: 'autoSize'
  326. 326 : },{
  327. 327 : text: this.localize("length"),
  328. 328 : dataIndex: 'length',
  329. 329 : tooltip: this.localize("lengthTip"),
  330. 330 : sortable: true,
  331. 331 : width: 'autoSize'
  332. 332 : },{
  333. 333 : xtype: 'widgetcolumn',
  334. 334 : text: this.localize("trend"),
  335. 335 : tooltip: this.localize('trendTip'),
  336. 336 : width: 120,
  337. 337 : dataIndex: 'distributions',
  338. 338 : widget: {
  339. 339 : xtype: 'sparklineline'
  340. 340 : }
  341. 341 : }],
  342. 342 :
  343. 343 : listeners: {
  344. 344 : corpusSelected: function() {
  345. 345 : this.setApiParams({docIndex: undefined, docId: undefined});
  346. 346 : this.loadFromApis();
  347. 347 : },
  348. 348 : documentsSelected: function(src, docs) {
  349. 349 : var docIds = [];
  350. 350 : var corpus = this.getStore().getCorpus();
  351. 351 : docs.forEach(function(doc) {
  352. 352 : docIds.push(corpus.getDocument(doc).getId())
  353. 353 : }, this);
  354. 354 : this.setApiParams({docId: docIds, docIndex: undefined})
  355. 355 : this.loadFromApis();
  356. 356 : },
  357. 357 : termsClicked: {
  358. 358 : fn: function(src, terms) {
  359. 359 : if (this.getStore().getCorpus()) { // make sure we have a corpus
  360. 360 : var queryTerms = [];
  361. 361 : terms.forEach(function(term) {
  362. 362 : if (Ext.isString(term)) {queryTerms.push(term);}
  363. 363 : else if (term.term) {queryTerms.push(term.term);}
  364. 364 : else if (term.getTerm) {queryTerms.push(term.getTerm());}
  365. 365 : });
  366. 366 : if (queryTerms.length > 0) {
  367. 367 : this.setApiParams({
  368. 368 : docIndex: undefined,
  369. 369 : docId: undefined,
  370. 370 : query: queryTerms
  371. 371 : });
  372. 372 : if (this.isVisible()) {
  373. 373 : if (this.isVisible()) {
  374. 374 : this.getStore().clearAndLoad({params: this.getApiParams()});
  375. 375 : }
  376. 376 : }
  377. 377 : }
  378. 378 : }
  379. 379 : },
  380. 380 : scope: this
  381. 381 : }
  382. 382 : }
  383. 383 : });
  384. 384 :
  385. 385 : me.callParent(arguments);
  386. 386 :
  387. 387 : me.getStore().getProxy().setExtraParam("withDistributions", true);
  388. 388 :
  389. 389 : }
  390. 390 :
  391. 391 : })