- 1 :
/**
- 2 :
* RezoViz represents connections between people, places and organizations that co-occur in multiple documents.
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "docId": null,
- 8 :
* "limit": null,
- 9 :
* "minEdgeCount": null,
- 10 :
* "nerService": null,
- 11 :
* "query": null,
- 12 :
* "stopList": null,
- 13 :
* "type": null,
- 14 :
* };
- 15 :
*
- 16 :
* loadCorpus("austen").tool("rezoviz", config);
- 17 :
*
- 18 :
* @class RezoViz
- 19 :
* @tutorial rezoviz
- 20 :
* @memberof Tools
- 21 :
*/
- 22 :
Ext.define('Voyant.panel.RezoViz', {
- 23 :
extend: 'Ext.panel.Panel',
- 24 :
mixins: ['Voyant.panel.Panel'],
- 25 :
alias: 'widget.rezoviz',
- 26 :
statics: {
- 27 :
i18n: {
- 28 :
timedOut: 'The entities call took too long and has timed out. Retry?',
- 29 :
maxLinks: 'Max. Links',
- 30 :
nerService: 'Entity Identification Service'
- 31 :
},
- 32 :
api: {
- 33 :
/**
- 34 :
* @memberof Tools.RezoViz
- 35 :
* @instance
- 36 :
* @property {query}
- 37 :
*/
- 38 :
query: undefined,
- 39 :
- 40 :
/**
- 41 :
* @memberof Tools.RezoViz
- 42 :
* @instance
- 43 :
* @property {limit}
- 44 :
* @default
- 45 :
*/
- 46 :
limit: 50,
- 47 :
- 48 :
/**
- 49 :
* @memberof Tools.RezoViz
- 50 :
* @instance
- 51 :
* @property {String[]} type The entity types to include in the results. One or more of: 'location', 'organization', 'person'.
- 52 :
*/
- 53 :
type: ['organization','location','person'],
- 54 :
- 55 :
/**
- 56 :
* @memberof Tools.RezoViz
- 57 :
* @instance
- 58 :
* @property {Number} minEdgeCount
- 59 :
*/
- 60 :
minEdgeCount: 2,
- 61 :
- 62 :
/**
- 63 :
* @memberof Tools.RezoViz
- 64 :
* @instance
- 65 :
* @property {stopList}
- 66 :
* @default
- 67 :
*/
- 68 :
stopList: 'auto',
- 69 :
- 70 :
/**
- 71 :
* @memberof Tools.RezoViz
- 72 :
* @instance
- 73 :
* @property {docId}
- 74 :
*/
- 75 :
docId: undefined,
- 76 :
- 77 :
/**
- 78 :
* @memberof Tools.RezoViz
- 79 :
* @instance
- 80 :
* @property {String} nerService Which NER service to use: 'spacy', 'nssi', or 'voyant'.
- 81 :
* @default
- 82 :
*/
- 83 :
nerService: 'spacy'
- 84 :
},
- 85 :
glyph: 'xf1e0@FontAwesome'
- 86 :
},
- 87 :
- 88 :
config: {
- 89 :
graphStyle: {
- 90 :
link: {
- 91 :
normal: {
- 92 :
stroke: '#000000',
- 93 :
strokeOpacity: 0.1
- 94 :
},
- 95 :
highlight: {
- 96 :
stroke: '#000000',
- 97 :
strokeOpacity: 0.5
- 98 :
}
- 99 :
}
- 100 :
},
- 101 :
- 102 :
options: [{xtype: 'stoplistoption'}]
- 103 :
},
- 104 :
- 105 :
constructor: function(config) {
- 106 :
this.callParent(arguments);
- 107 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 108 :
},
- 109 :
- 110 :
initComponent: function() {
- 111 :
var me = this;
- 112 :
- 113 :
var graphStyle = {};
- 114 :
var entityTypes = ['person', 'location', 'organization'];
- 115 :
entityTypes.forEach(function(entityType) {
- 116 :
var baseColor = me.getApplication().getColorForEntityType(entityType, true);
- 117 :
var nFill = d3.hsl(baseColor);
- 118 :
nFill.s *= .85;
- 119 :
nFill.l *= 1.15;
- 120 :
var nStroke = d3.hsl(baseColor);
- 121 :
nStroke.s *= .85;
- 122 :
var hFill = d3.hsl(baseColor);
- 123 :
var hStroke = d3.hsl(baseColor);
- 124 :
hStroke.l *= .75;
- 125 :
graphStyle[entityType+'Node'] = {
- 126 :
normal: {
- 127 :
fill: nFill.toString(),
- 128 :
stroke: nStroke.toString()
- 129 :
},
- 130 :
highlight: {
- 131 :
fill: hFill.toString(),
- 132 :
stroke: hStroke.toString()
- 133 :
}
- 134 :
}
- 135 :
});
- 136 :
this.setGraphStyle(Ext.apply(this.getGraphStyle(), graphStyle));
- 137 :
- 138 :
Ext.apply(me, {
- 139 :
title: this.localize('title'),
- 140 :
layout: 'fit',
- 141 :
items: {
- 142 :
xtype: 'voyantnetworkgraph',
- 143 :
applyNodeStyle: function(sel, nodeState) {
- 144 :
var state = nodeState === undefined ? 'normal' : nodeState;
- 145 :
var style = this.getGraphStyle().node[state];
- 146 :
sel.selectAll('rect')
- 147 :
.style('fill', function(d) { var type = d.type+'Node'; return me.getGraphStyle()[type][state].fill; })
- 148 :
.style('stroke', function(d) { var type = d.type+'Node'; return me.getGraphStyle()[type][state].stroke; });
- 149 :
},
- 150 :
listeners: {
- 151 :
nodeclicked: function(graph, node) {
- 152 :
me.dispatchEvent('termsClicked', me, [node.term]);
- 153 :
},
- 154 :
edgeclicked: function(graph, edge) {
- 155 :
me.dispatchEvent('termsClicked', me, ['"'+edge.source.term+' '+edge.target.term+'"~'+me.getApiParam('context')]);
- 156 :
}
- 157 :
}
- 158 :
},
- 159 :
dockedItems: [{
- 160 :
dock: 'bottom',
- 161 :
xtype: 'toolbar',
- 162 :
overflowHandler: 'scroller',
- 163 :
items: [{
- 164 :
xtype: 'corpusdocumentselector'
- 165 :
},{
- 166 :
xtype: 'button',
- 167 :
text: this.localize('categories'),
- 168 :
menu: {
- 169 :
items: [{
- 170 :
xtype: 'menucheckitem',
- 171 :
text: this.localize('people'),
- 172 :
itemId: 'person',
- 173 :
checked: true
- 174 :
},{
- 175 :
xtype: 'menucheckitem',
- 176 :
text: this.localize('locations'),
- 177 :
itemId: 'location',
- 178 :
checked: true
- 179 :
},{
- 180 :
xtype: 'menucheckitem',
- 181 :
text: this.localize('organizations'),
- 182 :
itemId: 'organization',
- 183 :
checked: true
- 184 :
},{
- 185 :
xtype: 'button',
- 186 :
text: this.localize('reload'),
- 187 :
style: 'margin: 5px;',
- 188 :
handler: this.categoriesHandler,
- 189 :
scope: this
- 190 :
}]
- 191 :
}
- 192 :
},{
- 193 :
xtype: 'button',
- 194 :
text: this.localize('nerService'),
- 195 :
menu: {
- 196 :
items: [{
- 197 :
xtype: 'menucheckitem',
- 198 :
group: 'nerService',
- 199 :
text: 'SpaCy',
- 200 :
itemId: 'spacy',
- 201 :
checked: true,
- 202 :
handler: this.serviceHandler,
- 203 :
scope: this
- 204 :
},{
- 205 :
xtype: 'menucheckitem',
- 206 :
group: 'nerService',
- 207 :
text: 'NSSI',
- 208 :
itemId: 'nssi',
- 209 :
checked: true,
- 210 :
handler: this.serviceHandler,
- 211 :
scope: this
- 212 :
},{
- 213 :
xtype: 'menucheckitem',
- 214 :
group: 'nerService',
- 215 :
text: 'Voyant',
- 216 :
itemId: 'voyant',
- 217 :
checked: false,
- 218 :
handler: this.serviceHandler,
- 219 :
scope: this
- 220 :
}]
- 221 :
}
- 222 :
},{
- 223 :
xtype: 'numberfield',
- 224 :
itemId: 'minEdgeCount',
- 225 :
fieldLabel: this.localize('minEdgeCount'),
- 226 :
labelAlign: 'right',
- 227 :
labelWidth: 120,
- 228 :
width: 170,
- 229 :
maxValue: 10,
- 230 :
minValue: 1,
- 231 :
allowDecimals: false,
- 232 :
allowExponential: false,
- 233 :
allowOnlyWhitespace: false,
- 234 :
listeners: {
- 235 :
render: function(field) {
- 236 :
field.setRawValue(this.getApiParam('minEdgeCount'));
- 237 :
},
- 238 :
change: function(field, newVal) {
- 239 :
if (field.isValid()) {
- 240 :
this.setApiParam('minEdgeCount', newVal);
- 241 :
this.preloadEntities();
- 242 :
}
- 243 :
},
- 244 :
scope: this
- 245 :
}
- 246 :
},{
- 247 :
xtype: 'slider',
- 248 :
fieldLabel: this.localize('maxLinks'),
- 249 :
labelAlign: 'right',
- 250 :
labelWidth: 100,
- 251 :
width: 170,
- 252 :
minValue: 10,
- 253 :
maxValue: 1000,
- 254 :
increment: 10,
- 255 :
listeners: {
- 256 :
render: function(field) {
- 257 :
field.setValue(this.getApiParam('limit'));
- 258 :
},
- 259 :
changecomplete: function(field, newVal) {
- 260 :
this.setApiParam('limit', newVal);
- 261 :
this.preloadEntities();
- 262 :
},
- 263 :
scope: this
- 264 :
}
- 265 :
}]
- 266 :
}],
- 267 :
listeners: {
- 268 :
entityResults: function(src, entities) {
- 269 :
this.getEntities();
- 270 :
},
- 271 :
scope: this
- 272 :
}
- 273 :
});
- 274 :
- 275 :
this.on('loadedCorpus', function(src, corpus) {
- 276 :
if (this.isVisible()) {
- 277 :
this.preloadEntities();
- 278 :
}
- 279 :
}, this);
- 280 :
- 281 :
this.on('corpusSelected', function(src, corpus) {
- 282 :
this.setApiParam('docId', undefined);
- 283 :
this.preloadEntities();
- 284 :
}, this);
- 285 :
this.on('documentsSelected', function(src, docIds) {
- 286 :
this.setApiParam('docId', docIds);
- 287 :
this.preloadEntities();
- 288 :
}, this);
- 289 :
- 290 :
this.on('activate', function() { // load after tab activate (if we're in a tab panel)
- 291 :
if (this.getCorpus()) {
- 292 :
// only preloadEntities if there isn't already data
- 293 :
if (this.down('voyantnetworkgraph').getNodeData().length === 0) {
- 294 :
Ext.Function.defer(this.preloadEntities, 100, this);
- 295 :
}
- 296 :
}
- 297 :
}, this);
- 298 :
- 299 :
this.on('query', function(src, query) {this.loadFromQuery(query);}, this);
- 300 :
- 301 :
me.callParent(arguments);
- 302 :
- 303 :
},
- 304 :
- 305 :
categoriesHandler: function(item) {
- 306 :
var categories = [];
- 307 :
item.up('menu').items.each(function(checkitem) {
- 308 :
if (checkitem.checked) {
- 309 :
categories.push(checkitem.itemId);
- 310 :
}
- 311 :
});
- 312 :
- 313 :
this.setApiParam('type', categories);
- 314 :
this.preloadEntities();
- 315 :
},
- 316 :
- 317 :
serviceHandler: function(menuitem) {
- 318 :
this.setApiParam('nerService', menuitem.itemId);
- 319 :
this.preloadEntities();
- 320 :
},
- 321 :
- 322 :
preloadEntities: function() {
- 323 :
new Voyant.data.util.DocumentEntities({annotator: this.getApiParam('nerService')});
- 324 :
},
- 325 :
- 326 :
getEntities: function() {
- 327 :
this.down('voyantnetworkgraph').resetGraph();
- 328 :
- 329 :
var corpusId = this.getCorpus().getId();
- 330 :
var el = this.getLayout().getRenderTarget();
- 331 :
el.mask(this.localize('loadingEntities'));
- 332 :
- 333 :
Ext.Ajax.request({
- 334 :
url: this.getApplication().getTromboneUrl(),
- 335 :
method: 'POST',
- 336 :
params: {
- 337 :
tool: 'corpus.EntityCollocationsGraph',
- 338 :
annotator: this.getApiParam('nerService'),
- 339 :
type: this.getApiParam('type'),
- 340 :
limit: this.getApiParam('limit'),
- 341 :
minEdgeCount: this.getApiParam('minEdgeCount'),
- 342 :
corpus: this.getCorpus().getId(),
- 343 :
docId: this.getApiParam('docId'),
- 344 :
stopList: this.getApiParam('stopList'),
- 345 :
noCache: true
- 346 :
},
- 347 :
timeout: 120000,
- 348 :
success: function(response) {
- 349 :
el.unmask();
- 350 :
var obj = Ext.decode(response.responseText);
- 351 :
if (obj.entityCollocationsGraph.edges.length==0) {
- 352 :
this.showError({msg: this.localize('noEntities')});
- 353 :
var currMinEdgeCount = this.getApiParam('minEdgeCount');
- 354 :
if (currMinEdgeCount > 1) {
- 355 :
Ext.Msg.confirm(this.localize('error'), this.localize('noEntitiesForEdgeCount'), function(button) {
- 356 :
if (button === 'yes') {
- 357 :
var newEdgeCount = Math.max(1, currMinEdgeCount-1);
- 358 :
this.queryById('minEdgeCount').setRawValue(newEdgeCount);
- 359 :
this.setApiParam('minEdgeCount', newEdgeCount);
- 360 :
this.preloadEntities();
- 361 :
}
- 362 :
}, this);
- 363 :
}
- 364 :
}
- 365 :
else {
- 366 :
this.processEntities(obj.entityCollocationsGraph);
- 367 :
}
- 368 :
},
- 369 :
failure: function(response) {
- 370 :
el.unmask();
- 371 :
Ext.Msg.confirm(this.localize('error'), this.localize('timedOut'), function(button) {
- 372 :
if (button === 'yes') {
- 373 :
this.preloadEntities();
- 374 :
}
- 375 :
}, this);
- 376 :
},
- 377 :
scope: this
- 378 :
});
- 379 :
},
- 380 :
- 381 :
processEntities: function(entityParent) {
- 382 :
var nodes = entityParent.nodes;
- 383 :
var edges = entityParent.edges;
- 384 :
- 385 :
var el = this.getLayout().getRenderTarget();
- 386 :
var cX = el.getWidth()/2;
- 387 :
var cY = el.getHeight()/2;
- 388 :
- 389 :
var visNodes = [];
- 390 :
for (var i = 0; i < nodes.length; i++) {
- 391 :
var n = nodes[i];
- 392 :
- 393 :
visNodes.push({
- 394 :
term: n.term,
- 395 :
title: n.term + ' ('+n.rawFreq+')',
- 396 :
type: n.type,
- 397 :
value: n.rawFreq,
- 398 :
fixed: false,
- 399 :
x: cX,
- 400 :
y: cY
- 401 :
});
- 402 :
}
- 403 :
- 404 :
var visEdges = [];
- 405 :
for (var i = 0; i < edges.length; i++) {
- 406 :
var link = edges[i].nodes;
- 407 :
- 408 :
var sourceId = nodes[link[0]].term;
- 409 :
var targetId = nodes[link[1]].term;
- 410 :
visEdges.push({
- 411 :
source: sourceId,
- 412 :
target: targetId,
- 413 :
rawFreq: nodes[link[1]].rawFreq // TODO
- 414 :
});
- 415 :
}
- 416 :
- 417 :
this.down('voyantnetworkgraph').loadJson({nodes: visNodes, edges: visEdges});
- 418 :
}
- 419 :
- 420 :
});