1. 1 : /**
  2. 2 : * Collocates Graph represents keywords and terms that occur in close proximity as a force directed network graph.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * centralize: null,
  8. 8 : * context: 5,
  9. 9 : * limit: 5,
  10. 10 : * query: null,
  11. 11 : * stopList: "auto",
  12. 12 : * };
  13. 13 : *
  14. 14 : * loadCorpus("austen").tool("collocatesgraph", config);
  15. 15 : *
  16. 16 : * @class CollocatesGraph
  17. 17 : * @tutorial collocatesgraph
  18. 18 : * @memberof Tools
  19. 19 : */
  20. 20 : Ext.define('Voyant.panel.CollocatesGraph', {
  21. 21 : extend: 'Ext.panel.Panel',
  22. 22 : mixins: ['Voyant.panel.Panel'],
  23. 23 : alias: 'widget.collocatesgraph',
  24. 24 : statics: {
  25. 25 : i18n: {
  26. 26 : },
  27. 27 : api: {
  28. 28 : /**
  29. 29 : * @memberof Tools.CollocatesGraph
  30. 30 : * @instance
  31. 31 : * @property {query}
  32. 32 : */
  33. 33 : query: undefined,
  34. 34 :
  35. 35 : /**
  36. 36 : * @memberof Tools.CollocatesGraph
  37. 37 : * @instance
  38. 38 : * @property {limit}
  39. 39 : * @default
  40. 40 : */
  41. 41 : limit: 5,
  42. 42 :
  43. 43 : /**
  44. 44 : * @memberof Tools.CollocatesGraph
  45. 45 : * @instance
  46. 46 : * @property {stopList}
  47. 47 : * @default
  48. 48 : */
  49. 49 : stopList: 'auto',
  50. 50 :
  51. 51 : /**
  52. 52 : * @memberof Tools.CollocatesGraph
  53. 53 : * @instance
  54. 54 : * @property {context}
  55. 55 : * @default
  56. 56 : */
  57. 57 : context: 5,
  58. 58 :
  59. 59 : /**
  60. 60 : * @memberof Tools.CollocatesGraph
  61. 61 : * @instance
  62. 62 : * @property {String} centralize If specified, will "centralize" on this keyword
  63. 63 : */
  64. 64 : centralize: undefined
  65. 65 : },
  66. 66 : glyph: 'xf1e0@FontAwesome'
  67. 67 : },
  68. 68 :
  69. 69 : config: {
  70. 70 : options: [{xtype: 'stoplistoption'},{
  71. 71 : xtype: 'categoriesoption'
  72. 72 : }],
  73. 73 :
  74. 74 : nodeData: undefined,
  75. 75 : linkData: undefined,
  76. 76 :
  77. 77 : visId: undefined,
  78. 78 : vis: undefined,
  79. 79 : visLayout: undefined,
  80. 80 : nodes: undefined,
  81. 81 : links: undefined,
  82. 82 : zoom: undefined,
  83. 83 :
  84. 84 : dragging: false,
  85. 85 :
  86. 86 : contextMenu: undefined,
  87. 87 :
  88. 88 : currentNode: undefined,
  89. 89 :
  90. 90 : networkMode: undefined,
  91. 91 :
  92. 92 : graphStyle: {
  93. 93 : keywordNode: {
  94. 94 : normal: {
  95. 95 : fill: '#c6dbef',
  96. 96 : stroke: '#6baed6'
  97. 97 : },
  98. 98 : highlight: {
  99. 99 : fill: '#9ecae1',
  100. 100 : stroke: '#3182bd'
  101. 101 : }
  102. 102 : },
  103. 103 : contextNode: {
  104. 104 : normal: {
  105. 105 : fill: '#fdd0a2',
  106. 106 : stroke: '#fdae6b'
  107. 107 : },
  108. 108 : highlight: {
  109. 109 : fill: '#fd9a53',
  110. 110 : stroke: '#e6550d'
  111. 111 : }
  112. 112 : },
  113. 113 : link: {
  114. 114 : normal: {
  115. 115 : stroke: '#000000',
  116. 116 : strokeOpacity: 0.1
  117. 117 : },
  118. 118 : highlight: {
  119. 119 : stroke: '#000000',
  120. 120 : strokeOpacity: 0.5
  121. 121 : }
  122. 122 : }
  123. 123 : },
  124. 124 :
  125. 125 : graphPhysics: {
  126. 126 : defaultMode: {
  127. 127 : damping: 0.4, // 0 = no damping, 1 = full damping
  128. 128 : centralGravity: 0.1, // 0 = no grav, 1 = high grav
  129. 129 : nodeGravity: -50, // negative = repel, positive = attract
  130. 130 : springLength: 100,
  131. 131 : springStrength: 0.25, // 0 = not strong, >1 = probably too strong
  132. 132 : collisionScale: 1.25 // 1 = default, 0 = no collision
  133. 133 : },
  134. 134 : centralizedMode: {
  135. 135 : damping: 0.4, // 0 = no damping, 1 = full damping
  136. 136 : centralGravity: 0.1, // 0 = no grav, 1 = high grav
  137. 137 : nodeGravity: -1, // negative = repel, positive = attract
  138. 138 : springLength: 200,
  139. 139 : springStrength: 1, // 0 = not strong, >1 = probably too strong
  140. 140 : collisionScale: 1 // 1 = default, 0 = no collision
  141. 141 : }
  142. 142 : }
  143. 143 : },
  144. 144 :
  145. 145 : DEFAULT_MODE: 0,
  146. 146 : CENTRALIZED_MODE: 1,
  147. 147 :
  148. 148 : constructor: function(config) {
  149. 149 : this.setNodeData([]);
  150. 150 : this.setLinkData([]);
  151. 151 :
  152. 152 : this.setVisId(Ext.id(null, 'links_'));
  153. 153 :
  154. 154 : this.callParent(arguments);
  155. 155 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  156. 156 : },
  157. 157 :
  158. 158 : initComponent: function() {
  159. 159 : var me = this;
  160. 160 : Ext.apply(me, {
  161. 161 : title: this.localize('title'),
  162. 162 : dockedItems: [{
  163. 163 : dock: 'bottom',
  164. 164 : xtype: 'toolbar',
  165. 165 : overflowHandler: 'scroller',
  166. 166 : items: [{
  167. 167 : xtype: 'querysearchfield'
  168. 168 : },{
  169. 169 : text: me.localize('clearTerms'),
  170. 170 : glyph: 'xf014@FontAwesome',
  171. 171 : handler: this.resetGraph,
  172. 172 : scope: me
  173. 173 : },this.localize('context'),{
  174. 174 : xtype: 'slider',
  175. 175 : itemId: 'contextSlider',
  176. 176 : minValue: 3,
  177. 177 : value: 5,
  178. 178 : maxValue: 30,
  179. 179 : increment: 2,
  180. 180 : width: 50,
  181. 181 : listeners: {
  182. 182 : render: function(slider) {
  183. 183 : slider.setValue(this.getApiParam('context'));
  184. 184 : },
  185. 185 : changecomplete: function(slider, newValue) {
  186. 186 : this.setApiParam('context', slider.getValue());
  187. 187 : if (this.getNetworkMode() === this.DEFAULT_MODE) {
  188. 188 : var terms = this.getNodeData().map(function(node) { return node.term; });
  189. 189 : if (terms.length > 0) {
  190. 190 : this.setNodeData([]);
  191. 191 : this.setLinkData([]);
  192. 192 : this.refresh();
  193. 193 :
  194. 194 : this.loadFromQuery(terms);
  195. 195 : }
  196. 196 : }
  197. 197 : },
  198. 198 : scope: me
  199. 199 : }
  200. 200 : }]
  201. 201 : }]
  202. 202 : });
  203. 203 :
  204. 204 : this.setContextMenu(Ext.create('Ext.menu.Menu', {
  205. 205 : renderTo: Ext.getBody(),
  206. 206 : items: [{
  207. 207 : xtype: 'box',
  208. 208 : itemId: 'label',
  209. 209 : margin: '5px 0px 5px 5px',
  210. 210 : html: ''
  211. 211 : },{
  212. 212 : xtype: 'menuseparator'
  213. 213 : },{
  214. 214 : xtype: 'menucheckitem',
  215. 215 : text: 'Fixed',
  216. 216 : itemId: 'fixed',
  217. 217 : listeners: {
  218. 218 : checkchange: function(c, checked, e) {
  219. 219 : var node = this.getCurrentNode();
  220. 220 : if (node !== undefined) {
  221. 221 : var data = {
  222. 222 : fixed: checked
  223. 223 : };
  224. 224 : if (checked) {
  225. 225 : data.fx = node.x;
  226. 226 : data.fy = node.y;
  227. 227 : } else {
  228. 228 : data.fx = null;
  229. 229 : data.fy = null;
  230. 230 : }
  231. 231 : this.updateDataForNode(node.id, data);
  232. 232 : }
  233. 233 : },
  234. 234 : scope: this
  235. 235 : }
  236. 236 : },{
  237. 237 : xtype: 'button',
  238. 238 : text: 'Fetch Collocates',
  239. 239 : style: 'margin: 5px;',
  240. 240 : handler: function(b, e) {
  241. 241 : var node = this.getCurrentNode();
  242. 242 : if (node !== undefined) {
  243. 243 : if (this.getNetworkMode() === this.CENTRALIZED_MODE) {
  244. 244 : this.resetGraph();
  245. 245 : this.setNetworkMode(this.DEFAULT_MODE);
  246. 246 : this.setApiParam('centralize', undefined);
  247. 247 : node.start = 0;
  248. 248 : node.limit = this.getApiParam('limit');
  249. 249 : }
  250. 250 : this.fetchCollocatesForNode(node);
  251. 251 : }
  252. 252 : },
  253. 253 : scope: this
  254. 254 : },{
  255. 255 : xtype: 'button',
  256. 256 : text: 'Centralize',
  257. 257 : style: 'margin: 5px;',
  258. 258 : handler: function(b, e) {
  259. 259 : var node = this.getCurrentNode();
  260. 260 : if (node !== undefined) {
  261. 261 : this.doCentralize(node.term);
  262. 262 : }
  263. 263 : this.getContextMenu().hide();
  264. 264 : },
  265. 265 : scope: this
  266. 266 : },{
  267. 267 : xtype: 'button',
  268. 268 : text: 'Remove',
  269. 269 : style: 'margin: 5px;',
  270. 270 : handler: function(b, e) {
  271. 271 : var node = this.getCurrentNode();
  272. 272 : if (node !== undefined) {
  273. 273 : this.removeNode(node.id);
  274. 274 : }
  275. 275 : b.up('menu').hide();
  276. 276 : },
  277. 277 : scope: this
  278. 278 : }]
  279. 279 : }));
  280. 280 :
  281. 281 : this.on('loadedCorpus', function(src, corpus) {
  282. 282 : if (this.isVisible()) {
  283. 283 : this.initLoad();
  284. 284 : }
  285. 285 : }, this);
  286. 286 :
  287. 287 : this.on('activate', function() { // load after tab activate (if we're in a tab panel)
  288. 288 : if (this.getCorpus()) {
  289. 289 : if (this.getNodeData().length === 0) { // only initLoad if there isn't already data
  290. 290 : Ext.Function.defer(this.initLoad, 100, this);
  291. 291 : }
  292. 292 : }
  293. 293 : }, this);
  294. 294 :
  295. 295 : this.on('query', function(src, query) {this.loadFromQuery(query);}, this);
  296. 296 :
  297. 297 : this.on('resize', function(panel, width, height) {
  298. 298 : var vis = Ext.get(this.getVisId());
  299. 299 : if (vis) {
  300. 300 : var el = this.body;//this.getLayout().getRenderTarget();
  301. 301 : var elHeight = el.getHeight();
  302. 302 : var elWidth = el.getWidth();
  303. 303 :
  304. 304 : vis.el.dom.setAttribute('width', elWidth);
  305. 305 : vis.el.dom.setAttribute('height', elHeight);
  306. 306 : this.getVisLayout()
  307. 307 : .force('x', d3.forceX(elWidth/2))
  308. 308 : .force('y', d3.forceY(elHeight/2));
  309. 309 : // .alpha(0.5).restart(); // restarting physics messes up zoomToFit
  310. 310 :
  311. 311 : Ext.Function.defer(this.zoomToFit, 100, this);
  312. 312 : // this.zoomToFit();
  313. 313 : }
  314. 314 : }, this);
  315. 315 :
  316. 316 : this.on('beforedestroy', function(panel) {
  317. 317 : if (this.getVisLayout()) {
  318. 318 : this.getVisLayout().stop(); // make sure force simulation isn't running when removed
  319. 319 : }
  320. 320 : }, this);
  321. 321 :
  322. 322 : me.callParent(arguments);
  323. 323 :
  324. 324 : },
  325. 325 :
  326. 326 : initLoad: function() {
  327. 327 : this.initGraph();
  328. 328 : this.setNetworkMode(this.DEFAULT_MODE);
  329. 329 :
  330. 330 : if (this.getApiParam('centralize')) {
  331. 331 : this.setNetworkMode(this.CENTRALIZED_MODE);
  332. 332 : var term = this.getApiParam('centralize');
  333. 333 : this.doCentralize(term);
  334. 334 : } else {
  335. 335 : var limit = 3;
  336. 336 : var query = this.getApiParam('query');
  337. 337 : if (query !== undefined) {
  338. 338 : if (query.indexOf('^@') === 0) {
  339. 339 : // it's a category so increase limit so that we get most/all of the terms
  340. 340 : limit = 20;
  341. 341 : } else {
  342. 342 : limit = Ext.isArray(query) ? query.length : query.split(',').length;
  343. 343 : }
  344. 344 : }
  345. 345 : this.getCorpus().getCorpusTerms({autoLoad: false}).load({
  346. 346 : params: {
  347. 347 : limit: limit,
  348. 348 : query: query,
  349. 349 : stopList: this.getApiParam('stopList'),
  350. 350 : categories: this.getApiParam("categories")
  351. 351 : },
  352. 352 : callback: function(records, operation, success) {
  353. 353 : if (success) {
  354. 354 : this.loadFromCorpusTermRecords(records);
  355. 355 : }
  356. 356 : },
  357. 357 : scope: this
  358. 358 : });
  359. 359 : }
  360. 360 : },
  361. 361 :
  362. 362 : loadFromQuery: function(query) {
  363. 363 : if (Ext.isArray(query) && query.length==0) {
  364. 364 : this.setApiParam("query", undefined);
  365. 365 : this.resetGraph();
  366. 366 : return;
  367. 367 : }
  368. 368 : this.setApiParams({ query: query });
  369. 369 : var params = this.getApiParams();
  370. 370 : params.noCache=true;
  371. 371 : (Ext.isString(query) ? [query] : query).forEach(function(q) {
  372. 372 : this.getCorpus().getCorpusCollocates({autoLoad: false}).load({
  373. 373 : params: Ext.apply(Ext.clone(params), {query: q}),
  374. 374 : callback: function(records, operations, success) {
  375. 375 : if (success) {
  376. 376 : this.loadFromCorpusCollocateRecords(records);
  377. 377 : }
  378. 378 : },
  379. 379 : scope: this
  380. 380 : });
  381. 381 : }, this);
  382. 382 : },
  383. 383 :
  384. 384 : loadFromCorpusTermRecords: function(corpusTerms) {
  385. 385 : if (Ext.isArray(corpusTerms) && corpusTerms.length>0) {
  386. 386 : var terms = [];
  387. 387 : corpusTerms.forEach(function(corpusTerm) {
  388. 388 : terms.push(corpusTerm.getTerm());
  389. 389 : });
  390. 390 : this.loadFromQuery(terms);
  391. 391 : }
  392. 392 : },
  393. 393 :
  394. 394 : loadFromCorpusCollocateRecords: function(records, keywordId) {
  395. 395 : if (Ext.isArray(records)) {
  396. 396 : var start = this.getApiParam('limit');
  397. 397 :
  398. 398 : var el = this.getLayout().getRenderTarget();
  399. 399 : var cX = el.getWidth()/2;
  400. 400 : var cY = el.getHeight()/2;
  401. 401 :
  402. 402 : var existingKeys = {};
  403. 403 : this.getNodeData().forEach(function(item) {
  404. 404 : existingKeys[item.id] = true;
  405. 405 : }, this);
  406. 406 :
  407. 407 : var newNodes = [];
  408. 408 : var newLinks = [];
  409. 409 :
  410. 410 : records.forEach(function(corpusCollocate, index) {
  411. 411 : var term = corpusCollocate.getTerm();
  412. 412 : var contextTerm = corpusCollocate.getContextTerm();
  413. 413 : var termFreq = corpusCollocate.getKeywordRawFreq();
  414. 414 : var contextFreq = corpusCollocate.getContextTermRawFreq();
  415. 415 :
  416. 416 : var termValue = termFreq;
  417. 417 : var contextValue = contextFreq;
  418. 418 : if (this.getNetworkMode() === this.CENTRALIZED_MODE) {
  419. 419 : termValue = 0;
  420. 420 : contextValue = Math.log(contextFreq);
  421. 421 : }
  422. 422 :
  423. 423 : var termEntry = undefined;
  424. 424 : var contextTermEntry = undefined;
  425. 425 :
  426. 426 : if (index == 0) { // only process keyword once
  427. 427 : if (keywordId === undefined) keywordId = this.idGet(term);
  428. 428 : if (existingKeys[keywordId] !== undefined) {
  429. 429 : this.updateDataForNode(keywordId, {
  430. 430 : title: term+' ('+termFreq+')',
  431. 431 : type: 'keyword',
  432. 432 : value: termValue
  433. 433 : });
  434. 434 : } else {
  435. 435 : existingKeys[keywordId] = true;
  436. 436 :
  437. 437 : termEntry = {
  438. 438 : id: keywordId,
  439. 439 : term: term,
  440. 440 : title: term+' ('+termFreq+')',
  441. 441 : type: 'keyword',
  442. 442 : value: termValue,
  443. 443 : start: start,
  444. 444 : fixed: false,
  445. 445 : x: cX,
  446. 446 : y: cY
  447. 447 : };
  448. 448 : newNodes.push(termEntry);
  449. 449 : }
  450. 450 : }
  451. 451 :
  452. 452 : if (term != contextTerm) {
  453. 453 : var contextId = this.idGet(contextTerm);
  454. 454 : if (existingKeys[contextId] !== undefined) {
  455. 455 : } else {
  456. 456 : existingKeys[contextId] = true;
  457. 457 :
  458. 458 : contextTermEntry = {
  459. 459 : id: contextId,
  460. 460 : term: contextTerm,
  461. 461 : title: contextTerm+' ('+contextFreq+')',
  462. 462 : type: 'context',
  463. 463 : value: contextValue,
  464. 464 : start: 0,
  465. 465 : fixed: false,
  466. 466 : x: cX,
  467. 467 : y: cY
  468. 468 : };
  469. 469 : newNodes.push(contextTermEntry);
  470. 470 : }
  471. 471 :
  472. 472 : var existingLink = null;
  473. 473 : var linkData = this.getLinkData();
  474. 474 : for (var i = 0; i < linkData.length; i++) {
  475. 475 : var link = linkData[i];
  476. 476 : if ((link.source.id == keywordId && link.target.id == contextId) || (link.source.id == contextId && link.target.id == keywordId)) {
  477. 477 : existingLink = link;
  478. 478 : break;
  479. 479 : }
  480. 480 : }
  481. 481 :
  482. 482 : var linkValue = corpusCollocate.getContextTermRawFreq();
  483. 483 : if (existingLink === null) {
  484. 484 : newLinks.push({source: keywordId, target: contextId, value: linkValue, id: keywordId+'-'+contextId});
  485. 485 : } else if (existingLink.value < linkValue) {
  486. 486 : // existingLink.value = linkValue;
  487. 487 : }
  488. 488 : }
  489. 489 : }, this);
  490. 490 :
  491. 491 : this.setNodeData(this.getNodeData().concat(newNodes));
  492. 492 : this.setLinkData(this.getLinkData().concat(newLinks));
  493. 493 :
  494. 494 : this.refresh();
  495. 495 : }
  496. 496 : },
  497. 497 :
  498. 498 : idGet: function(term) {
  499. 499 : return 'links_'+term.replace(/\W/g, '_');
  500. 500 : },
  501. 501 :
  502. 502 : updateDataForNode: function(nodeId, dataObj) {
  503. 503 : var data = this.getNodeData();
  504. 504 : for (var i = 0; i < data.length; i++) {
  505. 505 : if (data[i].id === nodeId) {
  506. 506 : Ext.apply(data[i], dataObj);
  507. 507 : break;
  508. 508 : }
  509. 509 : }
  510. 510 : },
  511. 511 :
  512. 512 : removeNode: function(nodeId, removeOrphans) {
  513. 513 : var data = this.getNodeData();
  514. 514 : for (var i = 0; i < data.length; i++) {
  515. 515 : if (data[i].id === nodeId) {
  516. 516 : data.splice(i, 1);
  517. 517 : break;
  518. 518 : }
  519. 519 : }
  520. 520 :
  521. 521 : data = this.getLinkData();
  522. 522 : for (var i = data.length-1; i >= 0; i--) {
  523. 523 : if (data[i].source.id === nodeId || data[i].target.id === nodeId) {
  524. 524 : data.splice(i, 1);
  525. 525 : }
  526. 526 : }
  527. 527 :
  528. 528 :
  529. 529 : this.setApiParam("query", Ext.Array.remove(Ext.Array.from(this.getApiParam("query")), nodeId));
  530. 530 :
  531. 531 : if (removeOrphans) {
  532. 532 : // TODO
  533. 533 : }
  534. 534 :
  535. 535 : this.refresh();
  536. 536 : },
  537. 537 :
  538. 538 : doCentralize: function(term) {
  539. 539 : this.setApiParam("centralize",term);
  540. 540 : this.resetGraph();
  541. 541 :
  542. 542 : this.setNetworkMode(this.CENTRALIZED_MODE);
  543. 543 :
  544. 544 : var data = {
  545. 545 : id: this.idGet(term),
  546. 546 : term: term,
  547. 547 : title: term+' ('+1+')',
  548. 548 : type: 'keyword',
  549. 549 : value: 1000,
  550. 550 : start: 0
  551. 551 : };
  552. 552 : this.setNodeData([data]);
  553. 553 : this.refresh();
  554. 554 :
  555. 555 : var centralizeLimit = 150;
  556. 556 : var limit = this.getApiParam('limit');
  557. 557 : this.setApiParam('limit', centralizeLimit);
  558. 558 : this.fetchCollocatesForNode(data);
  559. 559 : this.setApiParam('limit', limit);
  560. 560 : },
  561. 561 :
  562. 562 : // called by setNetworkMode
  563. 563 : applyNetworkMode: function(mode) {
  564. 564 : if (this.getVisLayout()) {
  565. 565 : if (mode === this.DEFAULT_MODE) {
  566. 566 : var physics = this.getGraphPhysics().defaultMode;
  567. 567 : this.getVisLayout()
  568. 568 : .velocityDecay(physics.damping)
  569. 569 : .force('link', d3.forceLink().id(function(d) { return d.id; }).distance(physics.springLength).strength(physics.springStrength))
  570. 570 : .force('charge', d3.forceManyBody().strength(physics.nodeGravity))
  571. 571 : .force('collide', d3.forceCollide(function(d) { return Math.sqrt(d.bbox.width * d.bbox.height) * physics.collisionScale; }));
  572. 572 : this.getVisLayout().force('x').strength(physics.centralGravity);
  573. 573 : this.getVisLayout().force('y').strength(physics.centralGravity);
  574. 574 : } else {
  575. 575 : var physics = this.getGraphPhysics().centralizedMode;
  576. 576 : this.getVisLayout()
  577. 577 : .velocityDecay(physics.damping)
  578. 578 : .force('link', d3.forceLink().id(function(d) { return d.id; }).distance(physics.springLength).strength(physics.springStrength))
  579. 579 : .force('charge', d3.forceManyBody().strength(function(d) {
  580. 580 : if (d.type === 'keyword') {
  581. 581 : return -10000;
  582. 582 : } else {
  583. 583 : return 0;
  584. 584 : }
  585. 585 : }))
  586. 586 : .force('collide', d3.forceCollide(function(d) {
  587. 587 : if (d.type === 'keyword') {
  588. 588 : return d.value;
  589. 589 : } else {
  590. 590 : return Math.sqrt(d.bbox.width * d.bbox.height) * physics.collisionScale;
  591. 591 : }
  592. 592 : }));
  593. 593 : this.getVisLayout().force('x').strength(physics.centralGravity);
  594. 594 : this.getVisLayout().force('y').strength(physics.centralGravity);
  595. 595 : }
  596. 596 : }
  597. 597 :
  598. 598 : return mode; // need to return mode for it to actually be set
  599. 599 : },
  600. 600 :
  601. 601 : initGraph: function() {
  602. 602 : var el = this.getLayout().getRenderTarget();
  603. 603 : el.update('');
  604. 604 : var width = el.getWidth();
  605. 605 : var height = el.getHeight();
  606. 606 :
  607. 607 : this.setVisLayout(d3.forceSimulation()
  608. 608 : .force('x', d3.forceX(width/2))
  609. 609 : .force('y', d3.forceY(height/2))
  610. 610 : .on('tick', function() {
  611. 611 : this.getLinks()
  612. 612 : .attr('x1', function(d) { return d.source.x; })
  613. 613 : .attr('y1', function(d) { return d.source.y; })
  614. 614 : .attr('x2', function(d) { return d.target.x; })
  615. 615 : .attr('y2', function(d) { return d.target.y; });
  616. 616 : // this.getLinks().attr('d', function(d) {
  617. 617 : // return 'M' + d[0].x + ',' + d[0].y
  618. 618 : // + 'S' + d[1].x + ',' + d[1].y
  619. 619 : // + ' ' + d[2].x + ',' + d[2].y;
  620. 620 : // });
  621. 621 : this.getNodes().attr('transform', function(d) {
  622. 622 : var x = d.x;
  623. 623 : var y = d.y;
  624. 624 : if (this.getNetworkMode() === this.DEFAULT_MODE || d.type !== 'keyword') {
  625. 625 : x -= d.bbox.width*0.5;
  626. 626 : y -= d.bbox.height*0.5;
  627. 627 : } else {
  628. 628 :
  629. 629 : }
  630. 630 : return 'translate('+x+','+y+')';
  631. 631 : }.bind(this));
  632. 632 :
  633. 633 : if (!this.getDragging() && this.getVisLayout().alpha() < 0.075) {
  634. 634 : this.getVisLayout().alpha(-1); // trigger end event
  635. 635 : }
  636. 636 : }.bind(this))
  637. 637 : .on('end', function() {
  638. 638 : Ext.Function.defer(this.zoomToFit, 100, this);
  639. 639 : }.bind(this))
  640. 640 : );
  641. 641 :
  642. 642 : var svg = d3.select(el.dom).append('svg').attr('id',this.getVisId()).attr('class', 'linksGraph').attr('width', width).attr('height', height);
  643. 643 : var g = svg.append('g');
  644. 644 :
  645. 645 : var zoom = d3.zoom()
  646. 646 : .scaleExtent([1/4, 4])
  647. 647 : .on('zoom', function() {
  648. 648 : g.attr('transform', d3.event.transform);
  649. 649 : });
  650. 650 : this.setZoom(zoom);
  651. 651 : svg.call(zoom);
  652. 652 :
  653. 653 : svg.on('click', function() {
  654. 654 : this.getContextMenu().hide();
  655. 655 : }.bind(this));
  656. 656 :
  657. 657 : this.setLinks(g.append('g').attr('class', 'links').selectAll('.link'));
  658. 658 : this.setNodes(g.append('g').attr('class', 'nodes').selectAll('.node'));
  659. 659 : this.setVis(g);
  660. 660 : },
  661. 661 :
  662. 662 : resetGraph: function() {
  663. 663 : this.setNodeData([]);
  664. 664 : this.setLinkData([]);
  665. 665 : this.setNetworkMode(this.DEFAULT_MODE); // ? there was another version of this function without this
  666. 666 : this.refresh();
  667. 667 : },
  668. 668 :
  669. 669 : refresh: function() {
  670. 670 : var me = this;
  671. 671 :
  672. 672 : var nodeData = this.getNodeData();
  673. 673 : var linkData = this.getLinkData();
  674. 674 :
  675. 675 : // var nodeMap = d3.map(nodeData, function(d) { return d.id; });
  676. 676 : // var bilinks = [];
  677. 677 : // linkData.forEach(function(link) {
  678. 678 : // var s = link.source = nodeMap.get(link.source);
  679. 679 : // var t = link.target = nodeMap.get(link.target);
  680. 680 : // var i = {};
  681. 681 : // nodeData.push(i);
  682. 682 : // linkData.push({source: s, target: i}, {source: i, target: t});
  683. 683 : // bilinks.push([s,i,t]);
  684. 684 : // });
  685. 685 :
  686. 686 : var link = this.getLinks().data(linkData, function(d) { return d.id; });
  687. 687 : link.exit().remove();
  688. 688 : var linkEnter = link.enter().append('line')
  689. 689 : .attr('class', 'link')
  690. 690 : .attr('id', function(d) { return d.id; })
  691. 691 : .on('mouseover', me.linkMouseOver.bind(me))
  692. 692 : .on('mouseout', me.linkMouseOut.bind(me))
  693. 693 : .on('click', function(data) {
  694. 694 : d3.event.stopImmediatePropagation();
  695. 695 : d3.event.preventDefault();
  696. 696 : this.dispatchEvent('termsClicked', this, ['"'+data.source.term+' '+data.target.term+'"~'+this.getApiParam('context')]);
  697. 697 : }.bind(me))
  698. 698 : // .style('fill', 'none')
  699. 699 : .style('cursor', 'pointer')
  700. 700 : .style('stroke-width', function(d) {
  701. 701 : if (me.getNetworkMode() === me.DEFAULT_MODE) {
  702. 702 : return Math.max(1, Math.min(15, Math.sqrt(d.value)));
  703. 703 : } else {
  704. 704 : return 1;
  705. 705 : }
  706. 706 : });
  707. 707 :
  708. 708 : this.setLinks(linkEnter.merge(link));
  709. 709 :
  710. 710 : var node = this.getNodes().data(nodeData, function(d) { return d.id; });
  711. 711 : node.exit().remove();
  712. 712 : var nodeEnter = node.enter().append('g')
  713. 713 : .attr('class', function(d) { return 'node '+d.type; })
  714. 714 : .attr('id', function(d) { return d.id; })
  715. 715 : .on('mouseover', me.nodeMouseOver.bind(me))
  716. 716 : .on('mouseout', me.nodeMouseOut.bind(me))
  717. 717 : .on('click', function(data) {
  718. 718 : d3.event.stopImmediatePropagation();
  719. 719 : d3.event.preventDefault();
  720. 720 : this.dispatchEvent('termsClicked', this, [data.term]);
  721. 721 : }.bind(me))
  722. 722 : .on('dblclick', function(data) {
  723. 723 : d3.event.stopImmediatePropagation();
  724. 724 : d3.event.preventDefault();
  725. 725 : this.fetchCollocatesForNode(data);
  726. 726 : }.bind(me))
  727. 727 : .on('contextmenu', function(d, i) {
  728. 728 : d3.event.preventDefault();
  729. 729 : // me.getTip().hide();
  730. 730 : var menu = me.getContextMenu();
  731. 731 : menu.queryById('label').setHtml(d.term);
  732. 732 : menu.queryById('fixed').setChecked(d.fixed);
  733. 733 : menu.showAt(d3.event.pageX+10, d3.event.pageY-50);
  734. 734 : })
  735. 735 : .call(d3.drag()
  736. 736 : .on('start', function(d) {
  737. 737 : me.setDragging(true);
  738. 738 : if (!d3.event.active) me.getVisLayout().alpha(0.3).restart();
  739. 739 : d.fx = d.x;
  740. 740 : d.fy = d.y;
  741. 741 : d.fixed = true;
  742. 742 : })
  743. 743 : .on('drag', function(d) {
  744. 744 : me.getVisLayout().alpha(0.3); // don't let simulation end while the user is dragging
  745. 745 : d.fx = d3.event.x;
  746. 746 : d.fy = d3.event.y;
  747. 747 : if (me.isMasked()) {
  748. 748 : if (!me.isOffCanvas(d3.event.x, d3.event.y)) {
  749. 749 : me.unmask();
  750. 750 : }
  751. 751 : } else if (me.isOffCanvas(d3.event.x, d3.event.y)) {
  752. 752 : me.mask(me.localize('releaseToRemove'));
  753. 753 : }
  754. 754 : })
  755. 755 : .on('end', function(d) {
  756. 756 : me.setDragging(false);
  757. 757 : // if (!d3.event.active) me.getVisLayout().alpha(0);
  758. 758 : if (d.fixed != true) {
  759. 759 : d.fx = null;
  760. 760 : d.fy = null;
  761. 761 : }
  762. 762 : if (me.isOffCanvas(d3.event.x, d3.event.y)) {
  763. 763 : me.unmask();
  764. 764 : me.mask(me.localize('cleaning'));
  765. 765 : me.removeNode(d.id);
  766. 766 : me.unmask();
  767. 767 : }
  768. 768 : })
  769. 769 : );
  770. 770 :
  771. 771 : nodeEnter.append('title');
  772. 772 :
  773. 773 : if (this.getNetworkMode() === this.DEFAULT_MODE) {
  774. 774 : nodeEnter.append('rect')
  775. 775 : .style('stroke-width', 1)
  776. 776 : .style('stroke-opacity', 1);
  777. 777 : } else {
  778. 778 : nodeEnter.filter(function(d) { return d.type === 'keyword'; }).append('circle')
  779. 779 : .style('stroke-width', 1)
  780. 780 : .style('stroke-opacity', 1);
  781. 781 : }
  782. 782 :
  783. 783 : nodeEnter.append('text')
  784. 784 : .attr('font-family', function(d) { return me.getApplication().getCategoriesManager().getFeatureForTerm('font', d.term); })
  785. 785 : .text(function(d) { return d.term; })
  786. 786 : .style('cursor', 'pointer')
  787. 787 : .style('user-select', 'none')
  788. 788 : .attr('dominant-baseline', 'middle');
  789. 789 :
  790. 790 : var allNodes = nodeEnter.merge(node);
  791. 791 : allNodes.selectAll('title').text(function(d) { return d.title; });
  792. 792 : allNodes.selectAll('text')
  793. 793 : .attr('font-size', function(d) { return Math.max(10, Math.sqrt(d.value)); })
  794. 794 : .each(function(d) { d.bbox = this.getBBox(); }) // set bounding box for later use
  795. 795 :
  796. 796 : this.setNodes(allNodes);
  797. 797 :
  798. 798 : if (this.getNetworkMode() === this.DEFAULT_MODE) {
  799. 799 : this.getVis().selectAll('rect')
  800. 800 : .attr('width', function(d) { return d.bbox.width+16; })
  801. 801 : .attr('height', function(d) { return d.bbox.height+8; })
  802. 802 : .attr('rx', function(d) { return Math.max(2, d.bbox.height * 0.2); })
  803. 803 : .attr('ry', function(d) { return Math.max(2, d.bbox.height * 0.2); })
  804. 804 : .call(this.applyNodeStyle.bind(this));
  805. 805 : this.getVis().selectAll('text')
  806. 806 : .attr('dx', 8)
  807. 807 : .attr('dy', function(d) { return d.bbox.height*0.5+4; });
  808. 808 : } else {
  809. 809 : this.getVis().selectAll('circle')
  810. 810 : .attr('r', function(d) { return Math.min(150, d.bbox.width); })
  811. 811 : .call(this.applyNodeStyle.bind(this));
  812. 812 : this.getVis().selectAll('text')
  813. 813 : .attr('dx', function(d) {
  814. 814 : if (d.type === 'keyword') {
  815. 815 : return -d.bbox.width*0.5;
  816. 816 : } else {
  817. 817 : return 8;
  818. 818 : }
  819. 819 : })
  820. 820 : .attr('dy', function(d) {
  821. 821 : if (d.type === 'keyword') {
  822. 822 : return 0;
  823. 823 : } else {
  824. 824 : return d.bbox.height*0.5+4;
  825. 825 : }
  826. 826 : });
  827. 827 : }
  828. 828 : this.getVis().selectAll('line').call(this.applyLinkStyle.bind(this));
  829. 829 :
  830. 830 :
  831. 831 : this.getVisLayout().nodes(nodeData);
  832. 832 : this.getVisLayout().force('link').links(linkData);
  833. 833 : this.getVisLayout().alpha(1).restart();
  834. 834 : },
  835. 835 :
  836. 836 : isOffCanvas: function(x, y) {
  837. 837 : var vis = Ext.get(this.getVisId());
  838. 838 : return x < 0 || y < 0 || x > vis.getWidth() || y > vis.getHeight();
  839. 839 : },
  840. 840 :
  841. 841 : zoomToFit: function(paddingPercent, transitionDuration) {
  842. 842 : var bounds = this.getVis().node().getBBox();
  843. 843 : var width = bounds.width;
  844. 844 : var height = bounds.height;
  845. 845 : var midX = bounds.x + width/2;
  846. 846 : var midY = bounds.y + height/2;
  847. 847 : var svg = this.getVis().node().parentElement;
  848. 848 : var svgRect = svg.getBoundingClientRect();
  849. 849 : var fullWidth = svgRect.width;
  850. 850 : var fullHeight = svgRect.height;
  851. 851 : var scale = (paddingPercent || 0.8) / Math.max(width/fullWidth, height/fullHeight);
  852. 852 : var translate = [fullWidth/2 - scale*midX, fullHeight/2 - scale*midY];
  853. 853 : if (width<1) {return} // FIXME: something strange with spyral
  854. 854 :
  855. 855 : d3.select(svg)
  856. 856 : .transition()
  857. 857 : .duration(transitionDuration || 500)
  858. 858 : .call(this.getZoom().transform, d3.zoomIdentity.translate(translate[0],translate[1]).scale(scale));
  859. 859 : },
  860. 860 :
  861. 861 : applyNodeStyle: function(sel, nodeState) {
  862. 862 : var state = nodeState === undefined ? 'normal' : nodeState;
  863. 863 : sel.style('fill', function(d) { var type = d.type+'Node'; return this.getGraphStyle()[type][state].fill; }.bind(this));
  864. 864 : sel.style('stroke', function(d) { var type = d.type+'Node'; return this.getGraphStyle()[type][state].stroke; }.bind(this));
  865. 865 : },
  866. 866 :
  867. 867 : applyLinkStyle: function(sel, linkState) {
  868. 868 : var state = linkState === undefined ? 'normal' : linkState;
  869. 869 : sel.style('stroke', function(d) { return this.getGraphStyle().link[state].stroke; }.bind(this));
  870. 870 : sel.style('stroke-opacity', function(d) { return this.getGraphStyle().link[state].strokeOpacity; }.bind(this));
  871. 871 : },
  872. 872 :
  873. 873 : linkMouseOver: function(d) {
  874. 874 : this.getVis().selectAll('line').call(this.applyLinkStyle.bind(this));
  875. 875 : this.getVis().select('#'+d.id).call(this.applyLinkStyle.bind(this), 'highlight');
  876. 876 : },
  877. 877 :
  878. 878 : linkMouseOut: function(d) {
  879. 879 : this.getVis().selectAll('line').call(this.applyLinkStyle.bind(this));
  880. 880 : },
  881. 881 :
  882. 882 : nodeMouseOver: function(d) {
  883. 883 : this.setCurrentNode(d);
  884. 884 :
  885. 885 : this.getVis().selectAll('rect').call(this.applyNodeStyle.bind(this));
  886. 886 :
  887. 887 : this.getLinks().each(function(link) {
  888. 888 : var id;
  889. 889 : if (link.source.id == d.id) {
  890. 890 : id = link.target.id;
  891. 891 : } else if (link.target.id == d.id) {
  892. 892 : id = link.source.id;
  893. 893 : }
  894. 894 : if (id !== undefined) {
  895. 895 : this.getVis().select('#'+id+' rect').call(this.applyNodeStyle.bind(this), 'highlight');
  896. 896 : this.getVis().select('#'+link.id).call(this.applyLinkStyle.bind(this), 'highlight');
  897. 897 : }
  898. 898 : }.bind(this));
  899. 899 :
  900. 900 : this.getVis().select('#'+d.id+' rect')
  901. 901 : .style('stroke-width', 3)
  902. 902 : .call(this.applyNodeStyle.bind(this), 'highlight');
  903. 903 : },
  904. 904 :
  905. 905 : nodeMouseOut: function(d) {
  906. 906 : if (!this.getContextMenu().isVisible()) {
  907. 907 : this.setCurrentNode(undefined);
  908. 908 : }
  909. 909 :
  910. 910 : this.getVis().selectAll('rect')
  911. 911 : .style('stroke-width', 1)
  912. 912 : .call(this.applyNodeStyle.bind(this));
  913. 913 :
  914. 914 : this.getVis().selectAll('line')
  915. 915 : .call(this.applyLinkStyle.bind(this));
  916. 916 : },
  917. 917 :
  918. 918 : fetchCollocatesForNode: function(d) {
  919. 919 : var limit = this.getApiParam('limit');
  920. 920 : var query = this.getApiParam("query");
  921. 921 :
  922. 922 : var query = Ext.Array.from(this.getApiParam("query"));
  923. 923 : Ext.Array.include(query, d.term)
  924. 924 : this.setApiParam("query", query);
  925. 925 :
  926. 926 : var corpusCollocates = this.getCorpus().getCorpusCollocates({autoLoad: false});
  927. 927 : corpusCollocates.load({
  928. 928 : params: Ext.apply(this.getApiParams(), {query: d.term, start: d.start, limit: limit}),
  929. 929 : callback: function(records, operation, success) {
  930. 930 : if (success) {
  931. 931 : this.updateDataForNode(d.id, {
  932. 932 : start: d.start+limit
  933. 933 : });
  934. 934 :
  935. 935 : this.loadFromCorpusCollocateRecords(records, d.id);
  936. 936 : }
  937. 937 : },
  938. 938 : scope: this
  939. 939 : });
  940. 940 : }
  941. 941 :
  942. 942 : });