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