1. 1 : /**
  2. 2 : * The TermsBerry tool provides a way of exploring high frequency terms and their collocates (words that occur in proximity).
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * categories: null, // a query for the keywords (can be comma-separated list)
  8. 8 : * context: null, // a named stopword list or comma-separated list of words
  9. 9 : * docId: null, // document index to restrict to (can be comma-separated list)
  10. 10 : * docIndex: null, // the size of the context (the number of words on each side of the keyword)
  11. 11 : * numInitialTerms: null, // the initial number of terms to display
  12. 12 : * query: null, // the initial number of terms to display
  13. 13 : * stopList: null, // the initial number of terms to display
  14. 14 : * };
  15. 15 : *
  16. 16 : * loadCorpus("austen").tool("termsberry", config);
  17. 17 : *
  18. 18 : * @class TermsBerry
  19. 19 : * @tutorial termsberry
  20. 20 : * @memberof Tools
  21. 21 : */
  22. 22 : Ext.define('Voyant.panel.TermsBerry', {
  23. 23 : extend: 'Ext.panel.Panel',
  24. 24 : mixins: ['Voyant.panel.Panel'],
  25. 25 : alias: 'widget.termsberry',
  26. 26 : statics: {
  27. 27 : i18n: {
  28. 28 : },
  29. 29 : api: {
  30. 30 : /**
  31. 31 : * @memberof Tools.TermsBerry
  32. 32 : * @instance
  33. 33 : * @property {stopList}
  34. 34 : * @default
  35. 35 : */
  36. 36 : stopList: 'auto',
  37. 37 :
  38. 38 : /**
  39. 39 : * @memberof Tools.TermsBerry
  40. 40 : * @instance
  41. 41 : * @property {context}
  42. 42 : * @default
  43. 43 : */
  44. 44 : context: 2,
  45. 45 :
  46. 46 : /**
  47. 47 : * @memberof Tools.TermsBerry
  48. 48 : * @instance
  49. 49 : * @property {Number} numInitialTerms The number of initial terms to display.
  50. 50 : */
  51. 51 : numInitialTerms: 75,
  52. 52 :
  53. 53 : /**
  54. 54 : * @memberof Tools.TermsBerry
  55. 55 : * @instance
  56. 56 : * @property {query}
  57. 57 : */
  58. 58 : query: undefined,
  59. 59 :
  60. 60 : /**
  61. 61 : * @memberof Tools.TermsBerry
  62. 62 : * @instance
  63. 63 : * @property {docIndex}
  64. 64 : */
  65. 65 : docIndex: undefined,
  66. 66 :
  67. 67 : /**
  68. 68 : * @memberof Tools.TermsBerry
  69. 69 : * @instance
  70. 70 : * @property {docId}
  71. 71 : */
  72. 72 : docId: undefined,
  73. 73 :
  74. 74 : /**
  75. 75 : * @memberof Tools.TermsBerry
  76. 76 : * @instance
  77. 77 : * @property {categories}
  78. 78 : */
  79. 79 : categories: undefined
  80. 80 : },
  81. 81 : glyph: 'xf1db@FontAwesome'
  82. 82 : },
  83. 83 : config: {
  84. 84 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  85. 85 :
  86. 86 : mode: undefined,
  87. 87 :
  88. 88 : scalingFactor: 3,
  89. 89 :
  90. 90 : minRawFreq: undefined,
  91. 91 : maxRawFreq: undefined,
  92. 92 : maxCollocateValue: undefined,
  93. 93 : minFillValue: undefined,
  94. 94 : maxFillValue: undefined,
  95. 95 :
  96. 96 : currentData: {},
  97. 97 : blacklist: {},
  98. 98 :
  99. 99 : visLayout: undefined,
  100. 100 : vis: undefined,
  101. 101 : visInfo: undefined,
  102. 102 : visId: undefined,
  103. 103 :
  104. 104 : currentNode: undefined,
  105. 105 :
  106. 106 : tip: undefined,
  107. 107 : contextMenu: undefined
  108. 108 : },
  109. 109 :
  110. 110 : MODE_TOP: 'top',
  111. 111 : MODE_DISTINCT: 'distinct',
  112. 112 :
  113. 113 : MIN_TERMS: 5,
  114. 114 : MAX_TERMS: 500,
  115. 115 :
  116. 116 : COLLOCATES_LIMIT: 1000000, // a very large number so we get all of them
  117. 117 :
  118. 118 : MIN_SCALING: 1,
  119. 119 : MAX_SCALING: 5,
  120. 120 :
  121. 121 : MIN_STROKE_OPACITY: 0.1,
  122. 122 : MAX_STROKE_OPACITY: 0.3,
  123. 123 :
  124. 124 : layout: 'fit',
  125. 125 :
  126. 126 : constructor: function(config) {
  127. 127 : this.setMode(this.MODE_TOP);
  128. 128 :
  129. 129 : this.callParent(arguments);
  130. 130 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  131. 131 :
  132. 132 : this.setVisId(Ext.id(null, 'termspack_'));
  133. 133 : },
  134. 134 :
  135. 135 : initComponent: function() {
  136. 136 : Ext.apply(this, {
  137. 137 : title: this.localize('title'),
  138. 138 : dockedItems: [{
  139. 139 : dock: 'bottom',
  140. 140 : xtype: 'toolbar',
  141. 141 : overflowHandler: 'scroller',
  142. 142 : items: [{
  143. 143 : xtype: 'querysearchfield',
  144. 144 : clearOnQuery: true
  145. 145 : },{
  146. 146 : xtype: 'button',
  147. 147 : text: this.localize('strategy'),
  148. 148 : menu: {
  149. 149 : items: [{
  150. 150 : xtype: 'menucheckitem',
  151. 151 : group: 'strategy',
  152. 152 : checked: this.getMode() === this.MODE_TOP,
  153. 153 : text: this.localize('topTerms'),
  154. 154 : checkHandler: function(item, checked) {
  155. 155 : if (checked) {
  156. 156 : this.setMode(this.MODE_TOP);
  157. 157 : this.doLoad();
  158. 158 : }
  159. 159 : },
  160. 160 : scope: this
  161. 161 : },{
  162. 162 : xtype: 'menucheckitem',
  163. 163 : group: 'strategy',
  164. 164 : checked: this.getMode() === this.MODE_DISTINCT,
  165. 165 : text: this.localize('distinctTerms'),
  166. 166 : checkHandler: function(item, checked) {
  167. 167 : if (checked) {
  168. 168 : this.setMode(this.MODE_DISTINCT);
  169. 169 : this.doLoad();
  170. 170 : }
  171. 171 : },
  172. 172 : scope: this
  173. 173 : }]
  174. 174 : }
  175. 175 : },{
  176. 176 : fieldLabel: this.localize('numTerms'),
  177. 177 : labelWidth: 50,
  178. 178 : labelAlign: 'right',
  179. 179 : width: 120,
  180. 180 : xtype: 'slider',
  181. 181 : increment: 1,
  182. 182 : minValue: this.MIN_TERMS,
  183. 183 : maxValue: this.MAX_TERMS,
  184. 184 : listeners: {
  185. 185 : afterrender: function(slider) {
  186. 186 : slider.setValue(parseInt(this.getApiParam('numInitialTerms')));
  187. 187 : },
  188. 188 : changecomplete: function(slider, newvalue) {
  189. 189 : this.setApiParam("numInitialTerms", newvalue);
  190. 190 : this.doLoad();
  191. 191 : },
  192. 192 : scope: this
  193. 193 : }
  194. 194 : },{
  195. 195 : fieldLabel: this.localize('context'),
  196. 196 : labelWidth: 50,
  197. 197 : labelAlign: 'right',
  198. 198 : width: 120,
  199. 199 : xtype: 'slider',
  200. 200 : increment: 1,
  201. 201 : minValue: 1,
  202. 202 : maxValue: 30,
  203. 203 : listeners: {
  204. 204 : afterrender: function(slider) {
  205. 205 : slider.setValue(this.getApiParam('context'));
  206. 206 : },
  207. 207 : changecomplete: function(slider, newvalue) {
  208. 208 : this.setApiParams({context: newvalue});
  209. 209 : this.doLoad();
  210. 210 : },
  211. 211 : scope: this
  212. 212 : }
  213. 213 : },{
  214. 214 : fieldLabel: this.localize('scaling'),
  215. 215 : labelWidth: 50,
  216. 216 : labelAlign: 'right',
  217. 217 : width: 120,
  218. 218 : xtype: 'slider',
  219. 219 : increment: 1,
  220. 220 : minValue: this.MIN_SCALING,
  221. 221 : maxValue: this.MAX_SCALING,
  222. 222 : listeners: {
  223. 223 : afterrender: function(slider) {
  224. 224 : slider.setValue(this.getScalingFactor());
  225. 225 : },
  226. 226 : changecomplete: function(slider, newvalue) {
  227. 227 : // use the inverse of the value since it'll make more sense to the user
  228. 228 : var value = Math.abs(newvalue-(this.MAX_SCALING+1));
  229. 229 : this.setScalingFactor(value);
  230. 230 : this.reload();
  231. 231 : },
  232. 232 : scope: this
  233. 233 : }
  234. 234 : }
  235. 235 : ]
  236. 236 : }]
  237. 237 : });
  238. 238 :
  239. 239 : this.setContextMenu(Ext.create('Ext.menu.Menu', {
  240. 240 : renderTo: Ext.getBody(),
  241. 241 : items: [{
  242. 242 : xtype: 'box',
  243. 243 : itemId: 'label',
  244. 244 : margin: '5px 0px 5px 5px',
  245. 245 : html: ''
  246. 246 : },{
  247. 247 : xtype: 'menuseparator'
  248. 248 : },{
  249. 249 : xtype: 'button',
  250. 250 : text: 'Remove',
  251. 251 : style: 'margin: 5px;',
  252. 252 : handler: function(b, e) {
  253. 253 : var node = this.getCurrentNode();
  254. 254 : if (node !== undefined) {
  255. 255 : delete this.getCurrentData()[node.data.term];
  256. 256 : this.getBlacklist()[node.data.term] = true;
  257. 257 : this.setCurrentNode(undefined);
  258. 258 : }
  259. 259 : this.getContextMenu().hide();
  260. 260 : this.reload();
  261. 261 : },
  262. 262 : scope: this
  263. 263 : }]
  264. 264 : }));
  265. 265 :
  266. 266 : this.on('query', function(src, query) {
  267. 267 : if (query.length > 0) {
  268. 268 : this.doLoad(query);
  269. 269 : }
  270. 270 : }, this);
  271. 271 :
  272. 272 : this.callParent(arguments);
  273. 273 : },
  274. 274 :
  275. 275 : listeners: {
  276. 276 : boxready: function() {
  277. 277 : this.initVisLayout();
  278. 278 : },
  279. 279 :
  280. 280 : resize: function(panel, width, height) {
  281. 281 : if (this.getVisLayout() && this.getCorpus()) {
  282. 282 : var el = this.getLayout().getRenderTarget();
  283. 283 : width = el.getWidth();
  284. 284 : height = el.getHeight();
  285. 285 :
  286. 286 : el.down('svg').set({width: width, height: height});
  287. 287 :
  288. 288 : this.getVisLayout().size([width, height]);
  289. 289 :
  290. 290 : this.reload();
  291. 291 : }
  292. 292 : },
  293. 293 :
  294. 294 : loadedCorpus: function(src, corpus) {
  295. 295 : if (this.isVisible()) {
  296. 296 : this.doLoad();
  297. 297 : }
  298. 298 : },
  299. 299 : activate: function() {
  300. 300 : if (this.getCorpus()) {
  301. 301 : this.doLoad();
  302. 302 : }
  303. 303 : }
  304. 304 : },
  305. 305 :
  306. 306 : doLoad: function(query) {
  307. 307 : this.resetVis();
  308. 308 : if (query === undefined) {
  309. 309 : this.resetMinMax();
  310. 310 : this.setCurrentData({});
  311. 311 : }
  312. 312 : if (this.getMode() === this.MODE_DISTINCT) {
  313. 313 : this.getDistinctTerms(query);
  314. 314 : } else {
  315. 315 : this.getTopTerms(query);
  316. 316 : }
  317. 317 : },
  318. 318 :
  319. 319 : reload: function() {
  320. 320 : var data = this.processCollocates([]);
  321. 321 : if (data.length > 0) {
  322. 322 : this.resetVis();
  323. 323 : this.buildVisFromData(data);
  324. 324 : }
  325. 325 : },
  326. 326 :
  327. 327 : resetMinMax: function() {
  328. 328 : this.setMinRawFreq(undefined);
  329. 329 : this.setMaxRawFreq(undefined);
  330. 330 : this.setMaxCollocateValue(undefined);
  331. 331 : this.setMinFillValue(undefined);
  332. 332 : this.setMaxFillValue(undefined);
  333. 333 : },
  334. 334 :
  335. 335 : resetVis: function() {
  336. 336 : var vis = this.getVis();
  337. 337 : if (vis) {
  338. 338 : vis.selectAll('.node').remove();
  339. 339 : }
  340. 340 : },
  341. 341 :
  342. 342 : getTopTerms: function(query) {
  343. 343 : var limit = parseInt(this.getApiParam('numInitialTerms'));
  344. 344 : var stopList = this.getApiParam('stopList');
  345. 345 : var categories = this.getApiParam('categories');
  346. 346 : if (query !== undefined) {
  347. 347 : limit = undefined;
  348. 348 : stopList = undefined;
  349. 349 : }
  350. 350 : this.getCorpus().getCorpusTerms().load({
  351. 351 : params: {
  352. 352 : query: query,
  353. 353 : categories: categories,
  354. 354 : limit: limit,
  355. 355 : stopList: stopList
  356. 356 : },
  357. 357 : callback: function(records, operation, success) {
  358. 358 : if (success) {
  359. 359 : this.loadFromRecords(records);
  360. 360 : }
  361. 361 : },
  362. 362 : scope: this
  363. 363 : });
  364. 364 : },
  365. 365 :
  366. 366 : getDistinctTerms: function(query) {
  367. 367 : var limit = parseInt(this.getApiParam('numInitialTerms'));
  368. 368 : var stopList = this.getApiParam('stopList');
  369. 369 : var categories = this.getApiParam('categories');
  370. 370 : if (query !== undefined) {
  371. 371 : limit = undefined;
  372. 372 : stopList = undefined;
  373. 373 : }
  374. 374 : var perDocLimit = Math.ceil(parseInt(this.getApiParam('numInitialTerms')) / this.getCorpus().getDocumentsCount()); // ceil ensures there's at least 1 per doc
  375. 375 : this.getCorpus().getDocumentTerms().load({
  376. 376 : params: {
  377. 377 : query: query,
  378. 378 : categories: categories,
  379. 379 : limit: limit,
  380. 380 : perDocLimit: perDocLimit,
  381. 381 : stopList: stopList,
  382. 382 : sort: 'TFIDF',
  383. 383 : dir: 'DESC'
  384. 384 : },
  385. 385 : callback: function(records, operation, success) {
  386. 386 : if (success) {
  387. 387 : this.loadFromRecords(records);
  388. 388 : }
  389. 389 : },
  390. 390 : scope: this
  391. 391 : });
  392. 392 : },
  393. 393 :
  394. 394 : loadFromQuery: function(query) {
  395. 395 : this.getCorpus().getCorpusTerms().load({
  396. 396 : params: {
  397. 397 : query: query
  398. 398 : },
  399. 399 : callback: function(records, operation, success) {
  400. 400 : if (success) {
  401. 401 : this.loadFromRecords(records);
  402. 402 : }
  403. 403 : },
  404. 404 : scope: this
  405. 405 : });
  406. 406 : },
  407. 407 :
  408. 408 : loadFromRecords: function(records) {
  409. 409 : if (Ext.isArray(records) && records.length>0) {
  410. 410 : var maxFreq = this.getMaxRawFreq();
  411. 411 : var minFreq = this.getMinRawFreq();
  412. 412 : var minFillVal = this.getMinFillValue();
  413. 413 : var maxFillVal = this.getMaxFillValue();
  414. 414 : var terms = [];
  415. 415 : records.forEach(function(r) {
  416. 416 : var term = r.getTerm();
  417. 417 : if (!this.getBlacklist()[term]) {
  418. 418 : var rawFreq = r.getRawFreq();
  419. 419 : var fillVal = this.getMode() === this.MODE_DISTINCT ? r.get('tfidf') : r.getInDocumentsCount();
  420. 420 :
  421. 421 : if (maxFreq === undefined || rawFreq > maxFreq) maxFreq = rawFreq;
  422. 422 : if (minFreq === undefined || rawFreq < minFreq) minFreq = rawFreq;
  423. 423 :
  424. 424 : if (maxFillVal === undefined || fillVal > maxFillVal) maxFillVal = fillVal;
  425. 425 : if (minFillVal === undefined || fillVal < minFillVal) minFillVal = fillVal;
  426. 426 :
  427. 427 : this.getCurrentData()[term] = {
  428. 428 : term: term,
  429. 429 : rawFreq: rawFreq,
  430. 430 : relativeFreq: r.get('relativeFreq'),//r.getRelativeFreq(),
  431. 431 : fillValue: fillVal,
  432. 432 : collocates: []
  433. 433 : };
  434. 434 :
  435. 435 : terms.push(term);
  436. 436 : }
  437. 437 : }, this);
  438. 438 :
  439. 439 : this.setMaxRawFreq(maxFreq);
  440. 440 : this.setMinRawFreq(minFreq);
  441. 441 : this.setMinFillValue(minFillVal);
  442. 442 : this.setMaxFillValue(maxFillVal);
  443. 443 :
  444. 444 : this.getCollocatesForQuery(terms);
  445. 445 : }
  446. 446 : },
  447. 447 :
  448. 448 : getCollocatesForQuery: function(query) {
  449. 449 : var whitelist = [];
  450. 450 : for (var term in this.getCurrentData()) {
  451. 451 : whitelist.push(term);
  452. 452 : }
  453. 453 :
  454. 454 : this.setApiParams({
  455. 455 : mode: 'corpus'
  456. 456 : });
  457. 457 : var params = this.getApiParams();
  458. 458 : this.getCorpus().getCorpusCollocates().load({
  459. 459 : params: Ext.apply(Ext.clone(params), {query: query, collocatesWhitelist: whitelist, limit: this.COLLOCATES_LIMIT}),
  460. 460 : callback: function(records, op, success) {
  461. 461 : if (success) {
  462. 462 : this.buildVisFromData(this.processCollocates(records));
  463. 463 : }
  464. 464 : },
  465. 465 : scope: this
  466. 466 : });
  467. 467 : },
  468. 468 :
  469. 469 : processCollocates: function(records) {
  470. 470 : var currentTerms = this.getCurrentData();
  471. 471 :
  472. 472 : var maxCol = this.getMaxCollocateValue();
  473. 473 :
  474. 474 : for (var i=0; i<records.length; i++) {
  475. 475 : var r = records[i];
  476. 476 : var term = r.getTerm();
  477. 477 : var contextTerm = r.getContextTerm();
  478. 478 : var contextFreq = r.getContextTermRawFreq();
  479. 479 :
  480. 480 : if (maxCol === undefined || contextFreq > maxCol) {
  481. 481 : maxCol = contextFreq;
  482. 482 : }
  483. 483 :
  484. 484 : if (currentTerms[term] === undefined) {
  485. 485 : // should not be here
  486. 486 : } else {
  487. 487 : if (term != contextTerm) {
  488. 488 : currentTerms[term].collocates.push({
  489. 489 : term: contextTerm, value: contextFreq
  490. 490 : });
  491. 491 : }
  492. 492 : }
  493. 493 : }
  494. 494 :
  495. 495 : this.setMaxCollocateValue(maxCol);
  496. 496 :
  497. 497 : var data = [];
  498. 498 : for (var term in currentTerms) {
  499. 499 : // if (currentTerms[term].collocates.length > 0) {
  500. 500 : data.push(currentTerms[term]);
  501. 501 : // }
  502. 502 : }
  503. 503 : return data;
  504. 504 : },
  505. 505 :
  506. 506 : buildVisFromData: function(data) {
  507. 507 : var me = this;
  508. 508 :
  509. 509 : if (!this.getVis()) {return;} // not initialized
  510. 510 :
  511. 511 : var rootId = '$$$root$$$';
  512. 512 : data.push({term: rootId, collocates:[], rawFreq:1});
  513. 513 : var root = d3.stratify()
  514. 514 : .id(function(d) { return d.term; })
  515. 515 : .parentId(function(d) {
  516. 516 : if (d.term !== rootId) return rootId;
  517. 517 : else return '';
  518. 518 : })(data)
  519. 519 : .sort(function(a, b) { return a.rawFreq < b.rawFreq ? 1 : a.rawFreq > b.rawFreq ? -1 : 0; })
  520. 520 : .sum(function(d) { return Math.pow(d.rawFreq, 1/me.getScalingFactor()); });
  521. 521 : this.getVisLayout()(root);
  522. 522 :
  523. 523 : // join nodes with data
  524. 524 : var nodes = this.getVis().selectAll('.node').data(root.descendants());
  525. 525 :
  526. 526 : // update
  527. 527 : nodes.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
  528. 528 : nodes.selectAll('circle').attr('r', function(d) { return d.r; });
  529. 529 :
  530. 530 : var idGet = function(term) {
  531. 531 : return term.replace(/\W/g, '_'); // remove non-word characters to create valid DOM ids
  532. 532 : };
  533. 533 :
  534. 534 : var collocateFill = d3.scalePow().exponent(1/3)
  535. 535 : .domain([0,this.getMaxCollocateValue()]).range(['#fff', '#bd3163']);
  536. 536 :
  537. 537 : var defaultFill;
  538. 538 : if (this.getMode() === this.MODE_DISTINCT) {
  539. 539 : defaultFill = d3.scalePow().exponent(1/5)
  540. 540 : .domain([this.getMinFillValue(), this.getMaxFillValue()]).range(['#dedede', '#fff']);
  541. 541 : } else {
  542. 542 : defaultFill = d3.scaleLinear()
  543. 543 : .domain([this.getMaxFillValue(), this.getMinFillValue()]).range(['#dedede', '#fff']);
  544. 544 : }
  545. 545 :
  546. 546 : // roughly calculate font size based on available area and number of terms
  547. 547 : var size = this.getVisLayout().size();
  548. 548 : var layoutRadius = Math.min(size[0], size[1]) / 2;
  549. 549 : var layoutArea = Math.PI*(layoutRadius*layoutRadius);
  550. 550 : var totalTerms = data.length;
  551. 551 : var termArea = layoutArea / totalTerms;
  552. 552 : var termRadius = Math.sqrt(termArea / Math.PI);
  553. 553 : var minFontSize = termRadius / 3;
  554. 554 : var scalingInverse = Math.abs(this.getScalingFactor()-(this.MAX_SCALING+1));
  555. 555 : scalingInverse = Math.max(1, scalingInverse-1); // substract one to avoid too large fonts
  556. 556 : var maxFontSize = minFontSize * scalingInverse;
  557. 557 :
  558. 558 : var textSizer = d3.scaleLinear()//pow().exponent(1/2)
  559. 559 : .domain([this.getMinRawFreq(),this.getMaxRawFreq()]).range([minFontSize, maxFontSize]);
  560. 560 :
  561. 561 : // enter
  562. 562 : var node = nodes.enter().append('g')
  563. 563 : .attr('class', 'node')
  564. 564 : .style('visibility', function(d) { return d.depth > 0 ? 'visible' : 'hidden'; }) // hide root
  565. 565 : .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
  566. 566 : .on('click', function(d) {
  567. 567 : me.dispatchEvent('termsClicked', me, [d.data.term]);
  568. 568 : })
  569. 569 : .on('mouseover', function(d, i) {
  570. 570 : me.setCurrentNode(d);
  571. 571 :
  572. 572 : me.getVis().selectAll('circle')
  573. 573 : .style('stroke-width', 1)
  574. 574 : .style('stroke', '#111')
  575. 575 : .style('fill', function(d) { return defaultFill(d.data.fillValue); });
  576. 576 :
  577. 577 : d3.select(this).select('circle')
  578. 578 : .style('fill', '#89e1c2')
  579. 579 : .style('stroke', '#26926c')
  580. 580 : .style('stroke-opacity', me.MAX_STROKE_OPACITY);
  581. 581 :
  582. 582 : var fillLabel;
  583. 583 : if (me.getMode() === me.MODE_DISTINCT) {
  584. 584 : fillLabel = me.localize('tfidf');
  585. 585 : } else {
  586. 586 : fillLabel = me.localize('inDocs');
  587. 587 : }
  588. 588 :
  589. 589 : if (!me.getContextMenu().isVisible()) {
  590. 590 : var info = '<b>'+d.data.term+'</b> ('+d.data.rawFreq+')<br/>'+fillLabel+': '+d.data.fillValue;
  591. 591 : var tip = me.getTip();
  592. 592 : tip.update(info);
  593. 593 : tip.show();
  594. 594 : }
  595. 595 :
  596. 596 : for (var i = 0; i < d.data.collocates.length; i++) {
  597. 597 : var collocate = d.data.collocates[i];
  598. 598 : var match = me.getVis().selectAll('.node').filter(function(d) { return d.data.term === collocate.term; });
  599. 599 : match.select('circle')
  600. 600 : .style('fill', function(d) { return collocateFill(collocate.value); })
  601. 601 : .style('stroke', '#bd3163')
  602. 602 : .style('stroke-opacity', me.MAX_STROKE_OPACITY);
  603. 603 : match.select('tspan.value').text(function(d) { return collocate.value; });
  604. 604 : }
  605. 605 : })
  606. 606 : .on('mousemove', function() {
  607. 607 : me.getTip().setPosition(d3.event.pageX+5, d3.event.pageY-50);
  608. 608 : })
  609. 609 : .on('mouseout', function() {
  610. 610 : if (!me.getContextMenu().isVisible()) {
  611. 611 : me.setCurrentNode(undefined);
  612. 612 : }
  613. 613 :
  614. 614 : me.getVis().selectAll('circle').style('stroke-opacity', me.MIN_STROKE_OPACITY).style('stroke', '#111')
  615. 615 : .style('fill', function(d) { return defaultFill(d.data.fillValue); });
  616. 616 : me.getVis().selectAll('tspan.value').text('');
  617. 617 : me.getTip().hide();
  618. 618 : // me.getVisInfo().text('');
  619. 619 : })
  620. 620 : .on('contextmenu', function(d, i) {
  621. 621 : d3.event.preventDefault();
  622. 622 : me.getTip().hide();
  623. 623 : var menu = me.getContextMenu();
  624. 624 : menu.queryById('label').setHtml(d.data.term);
  625. 625 : menu.showAt(d3.event.pageX+5, d3.event.pageY-50);
  626. 626 : });
  627. 627 :
  628. 628 : node.append('circle')
  629. 629 : .attr('id', function(d) {
  630. 630 : return idGet(d.data.term);
  631. 631 : })
  632. 632 : .attr('r', function(d) { return d.r; })
  633. 633 : .style('fill', function(d) { return defaultFill(d.data.fillValue); })
  634. 634 : .style('stroke', '#111')
  635. 635 : .style('stroke-opacity', me.MIN_STROKE_OPACITY)
  636. 636 : .style('stroke-width', 1);
  637. 637 :
  638. 638 : node.append('clipPath').attr('id', function(d) { return 'clip-' + idGet(d.data.term); })
  639. 639 : .append('use').attr('xlink:href', function(d) { return '#' + idGet(d.data.term); });
  640. 640 :
  641. 641 : var text = node.append('text')
  642. 642 : .attr('clip-path', function(d) { return 'url(#clip-' + idGet(d.data.term) + ')'; })
  643. 643 : .style('font-family', function(d) { return me.getApplication().getCategoriesManager().getFeatureForTerm('font', d.data.term); })
  644. 644 : .style('text-anchor', 'middle')
  645. 645 : .style('cursor', 'default');
  646. 646 : text.append('tspan')
  647. 647 : .attr('class', 'term')
  648. 648 : .attr('font-size', function(d) { return textSizer(d.data.rawFreq); })
  649. 649 : .attr('x', 0)
  650. 650 : .attr('y', function(d) { return textSizer(d.data.rawFreq)/4; })
  651. 651 : .text(function(d) { return d.data.term; });
  652. 652 : text.append('tspan')
  653. 653 : .attr('class', 'value')
  654. 654 : .attr('font-size', function(d) { return textSizer(d.data.rawFreq)*0.75; })
  655. 655 : .attr('x', 0)
  656. 656 : .attr('y', function(d) { return textSizer(d.data.rawFreq)+1; });
  657. 657 :
  658. 658 : // exit
  659. 659 : nodes.exit().remove();
  660. 660 :
  661. 661 : },
  662. 662 :
  663. 663 : initVisLayout: function() {
  664. 664 : var el = this.getLayout().getRenderTarget();
  665. 665 : el.update(''); // make sure to clear existing contents (especially for re-layout)
  666. 666 : var width = el.getWidth();
  667. 667 : var height = el.getHeight();
  668. 668 :
  669. 669 : var me = this;
  670. 670 : this.setVisLayout(
  671. 671 : d3.pack().size([width, height]).padding(1.5)
  672. 672 : );
  673. 673 :
  674. 674 : var svg = d3.select(el.dom).append('svg').attr('id',this.getVisId()).attr('width', width).attr('height', height);
  675. 675 : this.setVis(svg.append('g'));
  676. 676 :
  677. 677 : this.setVisInfo(svg.append('text').attr('x', 10).attr('y', 10));
  678. 678 :
  679. 679 : if (this.getTip() === undefined) {
  680. 680 : this.setTip(Ext.create('Ext.tip.Tip', {}));
  681. 681 : }
  682. 682 : }
  683. 683 : });