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