- 1 :
// assuming Knots library is loaded by containing page (via voyant.jsp)
- 2 :
/**
- 3 :
* Knots is a creative visualization that represents terms in a single document as a series of twisted lines.
- 4 :
*
- 5 :
* @example
- 6 :
*
- 7 :
* let config = {
- 8 :
* "audio": false,
- 9 :
* "docId": null,
- 10 :
* "query": null,
- 11 :
* "stopList": "auto"
- 12 :
* };
- 13 :
*
- 14 :
* loadCorpus("austen").tool("knots", config);
- 15 :
*
- 16 :
* @class Knots
- 17 :
* @tutorial knots
- 18 :
* @memberof Tools
- 19 :
*/
- 20 :
Ext.define('Voyant.panel.Knots', {
- 21 :
extend: 'Ext.panel.Panel',
- 22 :
mixins: ['Voyant.panel.Panel'],
- 23 :
alias: 'widget.knots',
- 24 :
statics: {
- 25 :
i18n: {
- 26 :
},
- 27 :
api: {
- 28 :
/**
- 29 :
* @memberof Tools.Knots
- 30 :
* @instance
- 31 :
* @property {query}
- 32 :
*/
- 33 :
query: null,
- 34 :
/**
- 35 :
* @memberof Tools.Knots
- 36 :
* @instance
- 37 :
* @property {stopList}
- 38 :
* @default
- 39 :
*/
- 40 :
stopList: 'auto',
- 41 :
- 42 :
/**
- 43 :
* @memberof Tools.Knots
- 44 :
* @instance
- 45 :
* @property {docId}
- 46 :
*/
- 47 :
docId: undefined,
- 48 :
- 49 :
/**
- 50 :
* @memberof Tools.Knots
- 51 :
* @instance
- 52 :
* @property {Boolean} audio Whether or not to play audio during the visualization.
- 53 :
* @default
- 54 :
*/
- 55 :
audio: false
- 56 :
},
- 57 :
glyph: 'xf06e@FontAwesome'
- 58 :
},
- 59 :
config: {
- 60 :
knots: undefined,
- 61 :
termStore: undefined,
- 62 :
docTermStore: undefined,
- 63 :
tokensStore: undefined,
- 64 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'},{xtype: 'colorpaletteoption'}],
- 65 :
refreshInterval: 100,
- 66 :
startAngle: 315,
- 67 :
angleIncrement: 15,
- 68 :
currentTerm: undefined
- 69 :
},
- 70 :
- 71 :
termTpl: new Ext.XTemplate(
- 72 :
'<tpl for=".">',
- 73 :
'<div class="term" style="color: rgb({color});float: left;padding: 3px;margin: 2px;">{term}</div>',
- 74 :
'</tpl>'
- 75 :
),
- 76 :
- 77 :
constructor: function() {
- 78 :
this.callParent(arguments);
- 79 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 80 :
- 81 :
this.on('loadedCorpus', function(src, corpus) {
- 82 :
var firstDoc = corpus.getDocument(0);
- 83 :
var pDoc = this.processDocument(firstDoc);
- 84 :
this.getKnots().setCurrentDoc(pDoc);
- 85 :
- 86 :
this.setApiParams({docId: firstDoc.getId()});
- 87 :
this.getDocTermStore().getProxy().setExtraParam('corpus', corpus.getId());
- 88 :
this.getTokensStore().setCorpus(corpus);
- 89 :
this.getDocTermStore().load({params: {
- 90 :
limit: 5,
- 91 :
stopList: this.getApiParams('stopList')
- 92 :
}});
- 93 :
}, this);
- 94 :
- 95 :
this.on('activate', function() { // load after tab activate (if we're in a tab panel)
- 96 :
if (this.getCorpus()) {
- 97 :
Ext.Function.defer(function() {
- 98 :
this.getDocTermStore().load({params: {
- 99 :
limit: 5,
- 100 :
stopList: this.getApiParams('stopList')
- 101 :
}});
- 102 :
}, 100, this);
- 103 :
}
- 104 :
}, this);
- 105 :
- 106 :
this.on('query', function(src, query) {
- 107 :
if (query !== undefined && query != '') {
- 108 :
this.getDocTermsFromQuery(query);
- 109 :
}
- 110 :
}, this);
- 111 :
- 112 :
this.on('documentSelected', function(src, doc) {
- 113 :
- 114 :
var document = this.getCorpus().getDocument(doc)
- 115 :
this.setApiParam('docId', document.getId());
- 116 :
- 117 :
var terms = this.getKnots().currentDoc.terms;
- 118 :
var termsToKeep = [];
- 119 :
for (var t in terms) {
- 120 :
termsToKeep.push(t);
- 121 :
}
- 122 :
- 123 :
// this.getTermStore().removeAll();
- 124 :
this.setApiParams({query: termsToKeep});
- 125 :
- 126 :
var limit = termsToKeep.length;
- 127 :
if (limit === 0) {
- 128 :
limit = 5;
- 129 :
}
- 130 :
- 131 :
this.getKnots().setCurrentDoc(this.processDocument(document));
- 132 :
- 133 :
this.getDocTermStore().load({params: {
- 134 :
query: termsToKeep,
- 135 :
limit: limit,
- 136 :
stopList: this.getApiParams('stopList')
- 137 :
}});
- 138 :
}, this);
- 139 :
- 140 :
this.on('termsClicked', function(src, terms) {
- 141 :
var queryTerms = [];
- 142 :
terms.forEach(function(term) {
- 143 :
if (Ext.isString(term)) {queryTerms.push(term);}
- 144 :
else if (term.term) {queryTerms.push(term.term);}
- 145 :
else if (term.getTerm) {queryTerms.push(term.getTerm());}
- 146 :
});
- 147 :
if (queryTerms.length > 0) {
- 148 :
this.getDocTermsFromQuery(queryTerms);
- 149 :
}
- 150 :
}, this);
- 151 :
- 152 :
this.on('corpusTermsClicked', function(src, terms) {
- 153 :
var queryTerms = [];
- 154 :
terms.forEach(function(term) {
- 155 :
if (term.getTerm()) {queryTerms.push(term.getTerm());}
- 156 :
});
- 157 :
this.getDocTermsFromQuery(queryTerms);
- 158 :
}, this);
- 159 :
- 160 :
this.on('documentTermsClicked', function(src, terms) {
- 161 :
var queryTerms = [];
- 162 :
terms.forEach(function(term) {
- 163 :
if (term.getTerm()) {queryTerms.push(term.getTerm());}
- 164 :
});
- 165 :
this.getDocTermsFromQuery(queryTerms);
- 166 :
}, this);
- 167 :
},
- 168 :
- 169 :
initComponent: function() {
- 170 :
this.setTermStore(Ext.create('Ext.data.ArrayStore', {
- 171 :
fields: ['term', 'color']
- 172 :
}));
- 173 :
- 174 :
this.setDocTermStore(Ext.create("Ext.data.Store", {
- 175 :
model: "Voyant.data.model.DocumentTerm",
- 176 :
autoLoad: false,
- 177 :
remoteSort: false,
- 178 :
proxy: {
- 179 :
type: 'ajax',
- 180 :
url: Voyant.application.getTromboneUrl(),
- 181 :
extraParams: {
- 182 :
tool: 'corpus.DocumentTerms',
- 183 :
withDistributions: 'raw',
- 184 :
withPositions: true
- 185 :
},
- 186 :
reader: {
- 187 :
type: 'json',
- 188 :
rootProperty: 'documentTerms.terms',
- 189 :
totalProperty: 'documentTerms.total'
- 190 :
},
- 191 :
simpleSortMode: true
- 192 :
},
- 193 :
listeners: {
- 194 :
beforeload: function(store) {
- 195 :
store.getProxy().setExtraParam('docId', this.getApiParam('docId'));
- 196 :
},
- 197 :
load: function(store, records, successful, options) {
- 198 :
var termObj = {};
- 199 :
if (records && records.length>0) {
- 200 :
records.forEach(function(record) {
- 201 :
var termData = this.processTerms(record);
- 202 :
var docId = record.get('docId');
- 203 :
var term = record.get('term');
- 204 :
termObj[term] = termData;
- 205 :
}, this);
- 206 :
this.getKnots().addTerms(termObj);
- 207 :
this.getKnots().buildGraph();
- 208 :
}
- 209 :
else {
- 210 :
this.toastInfo({
- 211 :
html: this.localize("noTermsFound"),
- 212 :
align: 'bl'
- 213 :
})
- 214 :
}
- 215 :
},
- 216 :
scope: this
- 217 :
}
- 218 :
}));
- 219 :
- 220 :
this.setTokensStore(Ext.create("Voyant.data.store.Tokens", {
- 221 :
stripTags: "all",
- 222 :
listeners: {
- 223 :
beforeload: function(store) {
- 224 :
store.getProxy().setExtraParam('docId', this.getApiParam('docId'));
- 225 :
},
- 226 :
load: function(store, records, successful, options) {
- 227 :
var context = '';
- 228 :
var currTerm = this.getCurrentTerm();
- 229 :
records.forEach(function(record) {
- 230 :
if (record.getPosition() == currTerm.tokenId) {
- 231 :
context += '<strong>'+record.getTerm()+'</strong>';
- 232 :
} else {
- 233 :
context += record.getTerm();
- 234 :
}
- 235 :
});
- 236 :
- 237 :
Ext.Msg.show({
- 238 :
title: this.localize('context'),
- 239 :
message: context,
- 240 :
buttons: Ext.Msg.OK,
- 241 :
icon: Ext.Msg.INFO
- 242 :
});
- 243 :
},
- 244 :
scope: this
- 245 :
}
- 246 :
}));
- 247 :
- 248 :
Ext.apply(this, {
- 249 :
title: this.localize('title'),
- 250 :
dockedItems: [{
- 251 :
dock: 'bottom',
- 252 :
xtype: 'toolbar',
- 253 :
overflowHandler: 'scroller',
- 254 :
items: [{
- 255 :
xtype: 'querysearchfield'
- 256 :
},{
- 257 :
text: this.localize('clearTerms'),
- 258 :
glyph: 'xf00d@FontAwesome',
- 259 :
handler: function() {
- 260 :
this.down('#termsView').getSelectionModel().deselectAll(true);
- 261 :
this.getTermStore().removeAll();
- 262 :
this.setApiParams({query: null});
- 263 :
this.getKnots().removeAllTerms();
- 264 :
this.getKnots().drawGraph();
- 265 :
},
- 266 :
scope: this
- 267 :
},{
- 268 :
xtype: 'documentselectorbutton',
- 269 :
singleSelect: true
- 270 :
},{
- 271 :
xtype: 'slider',
- 272 :
itemId: 'speed',
- 273 :
fieldLabel: this.localize("speed"),
- 274 :
labelAlign: 'right',
- 275 :
labelWidth: 50,
- 276 :
width: 100,
- 277 :
increment: 50,
- 278 :
minValue: 0,
- 279 :
maxValue: 500,
- 280 :
value: 500-this.getRefreshInterval(),
- 281 :
listeners: {
- 282 :
changecomplete: function(slider, newvalue) {
- 283 :
this.setRefreshInterval(500-newvalue);
- 284 :
if (this.getKnots()) {this.getKnots().buildGraph();}
- 285 :
},
- 286 :
scope: this
- 287 :
}
- 288 :
},{
- 289 :
xtype: 'slider',
- 290 :
itemId: 'startAngle',
- 291 :
fieldLabel: this.localize('startAngle'),
- 292 :
labelAlign: 'right',
- 293 :
labelWidth: 35,
- 294 :
width: 85,
- 295 :
increment: 15,
- 296 :
minValue: 0,
- 297 :
maxValue: 360,
- 298 :
value: this.getStartAngle(),
- 299 :
listeners: {
- 300 :
changecomplete: function(slider, newvalue) {
- 301 :
this.setStartAngle(newvalue);
- 302 :
if (this.getKnots()) {this.getKnots().buildGraph();}
- 303 :
},
- 304 :
scope: this
- 305 :
}
- 306 :
},{
- 307 :
xtype: 'slider',
- 308 :
itemId: 'tangles',
- 309 :
fieldLabel: this.localize('tangles'),
- 310 :
labelAlign: 'right',
- 311 :
labelWidth: 30,
- 312 :
width: 80,
- 313 :
increment: 5,
- 314 :
minValue: 5,
- 315 :
maxValue: 90,
- 316 :
value: this.getAngleIncrement(),
- 317 :
listeners: {
- 318 :
changecomplete: function(slider, newvalue) {
- 319 :
this.setAngleIncrement(newvalue);
- 320 :
if (this.getKnots()) {this.getKnots().buildGraph();}
- 321 :
},
- 322 :
scope: this
- 323 :
}
- 324 :
},{
- 325 :
xtype: 'checkbox',
- 326 :
boxLabel: this.localize('sound'),
- 327 :
listeners: {
- 328 :
render: function(cmp) {
- 329 :
cmp.setValue(this.getApiParam("audio")===true || this.getApiParam("audio")=="true")
- 330 :
Ext.tip.QuickTipManager.register({
- 331 :
target: cmp.getEl(),
- 332 :
text: this.localize('soundTip')
- 333 :
});
- 334 :
- 335 :
},
- 336 :
beforedestroy: function(cmp) {
- 337 :
Ext.tip.QuickTipManager.unregister(cmp.getEl());
- 338 :
},
- 339 :
change: function(cmp, val) {
- 340 :
if (this.getKnots()) {
- 341 :
this.getKnots().setAudio(val);
- 342 :
}
- 343 :
},
- 344 :
scope: this
- 345 :
}
- 346 :
}]
- 347 :
}],
- 348 :
border: false,
- 349 :
layout: 'fit',
- 350 :
items: {
- 351 :
layout: {
- 352 :
type: 'vbox',
- 353 :
align: 'stretch'
- 354 :
},
- 355 :
defaults: {border: false},
- 356 :
items: [{
- 357 :
height: 30,
- 358 :
itemId: 'termsView',
- 359 :
xtype: 'dataview',
- 360 :
store: this.getTermStore(),
- 361 :
tpl: this.termTpl,
- 362 :
itemSelector: 'div.term',
- 363 :
overItemCls: 'over',
- 364 :
selectedItemCls: 'selected',
- 365 :
selectionModel: {
- 366 :
mode: 'SIMPLE'
- 367 :
},
- 368 :
// cls: 'selected', // default selected
- 369 :
focusCls: '',
- 370 :
listeners: {
- 371 :
beforeitemclick: function(dv, record, item, index, event, opts) {
- 372 :
event.preventDefault();
- 373 :
event.stopPropagation();
- 374 :
dv.fireEvent('itemcontextmenu', dv, record, item, index, event, opts);
- 375 :
return false;
- 376 :
},
- 377 :
beforecontainerclick: function() {
- 378 :
// cancel deselect all
- 379 :
event.preventDefault();
- 380 :
event.stopPropagation();
- 381 :
return false;
- 382 :
},
- 383 :
selectionchange: function(selModel, selections) {
- 384 :
var dv = this.down('#termsView');
- 385 :
var terms = [];
- 386 :
- 387 :
dv.getStore().each(function(r) {
- 388 :
if (selections.indexOf(r) !== -1) {
- 389 :
terms.push(r.get('term'));
- 390 :
Ext.fly(dv.getNodeByRecord(r)).removeCls('unselected').addCls('selected');
- 391 :
} else {
- 392 :
Ext.fly(dv.getNodeByRecord(r)).removeCls('selected').addCls('unselected');
- 393 :
}
- 394 :
});
- 395 :
- 396 :
this.getKnots().termsFilter = terms;
- 397 :
this.getKnots().drawGraph();
- 398 :
},
- 399 :
itemcontextmenu: function(dv, record, el, index, event) {
- 400 :
event.preventDefault();
- 401 :
event.stopPropagation();
- 402 :
var isSelected = dv.isSelected(el);
- 403 :
var menu = new Ext.menu.Menu({
- 404 :
floating: true,
- 405 :
items: [{
- 406 :
text: isSelected ? this.localize('hideTerm') : this.localize('showTerm'),
- 407 :
handler: function() {
- 408 :
if (isSelected) {
- 409 :
dv.deselect(index);
- 410 :
} else {
- 411 :
dv.select(index, true);
- 412 :
}
- 413 :
},
- 414 :
scope: this
- 415 :
},{
- 416 :
text: this.localize('removeTerm'),
- 417 :
handler: function() {
- 418 :
dv.deselect(index);
- 419 :
var term = this.getTermStore().getAt(index).get('term');
- 420 :
this.getTermStore().removeAt(index);
- 421 :
dv.refresh();
- 422 :
- 423 :
this.getKnots().removeTerm(term);
- 424 :
this.getKnots().drawGraph();
- 425 :
},
- 426 :
scope: this
- 427 :
}]
- 428 :
});
- 429 :
menu.showAt(event.getXY());
- 430 :
},
- 431 :
scope: this
- 432 :
}
- 433 :
},{
- 434 :
flex: 1,
- 435 :
xtype: 'container',
- 436 :
autoEl: 'div',
- 437 :
itemId: 'canvasParent',
- 438 :
layout: 'fit',
- 439 :
overflowY: 'auto',
- 440 :
overflowX: 'hidden'
- 441 :
}],
- 442 :
listeners: {
- 443 :
render: function(component) {
- 444 :
var canvasParent = this.down('#canvasParent');
- 445 :
this.setKnots(new Knots({
- 446 :
container: canvasParent,
- 447 :
clickHandler: this.knotClickHandler.bind(this),
- 448 :
audio: this.getApiParam("audio")===true || this.getApiParam("audio")=="true"
- 449 :
}));
- 450 :
},
- 451 :
afterlayout: function(container) {
- 452 :
if (this.getKnots().initialized === false) {
- 453 :
this.getKnots().initializeCanvas();
- 454 :
}
- 455 :
},
- 456 :
resize: function(cnt, width, height) {
- 457 :
this.getKnots().doLayout();
- 458 :
},
- 459 :
scope: this
- 460 :
}
- 461 :
}
- 462 :
});
- 463 :
- 464 :
this.callParent(arguments);
- 465 :
},
- 466 :
- 467 :
updateRefreshInterval: function(value) {
- 468 :
if (this.getKnots()) {
- 469 :
if (value < 50) {
- 470 :
value = 50;
- 471 :
this.getKnots().progressiveDraw = false;
- 472 :
} else {
- 473 :
this.getKnots().progressiveDraw = true;
- 474 :
}
- 475 :
this.getKnots().refreshInterval = value;
- 476 :
this.getKnots().buildGraph(this.getKnots().drawStep);
- 477 :
}
- 478 :
},
- 479 :
- 480 :
updateStartAngle: function(value) {
- 481 :
if (this.getKnots()) {
- 482 :
this.getKnots().startAngle = value;
- 483 :
this.getKnots().recache();
- 484 :
this.getKnots().buildGraph();
- 485 :
}
- 486 :
},
- 487 :
- 488 :
updateAngleIncrement: function(value) {
- 489 :
if (this.getKnots()) {
- 490 :
this.getKnots().angleIncrement = value;
- 491 :
this.getKnots().recache();
- 492 :
this.getKnots().buildGraph();
- 493 :
}
- 494 :
},
- 495 :
- 496 :
loadFromCorpusTerms: function(corpusTerms) {
- 497 :
if (this.getKnots()) { // get rid of existing terms
- 498 :
this.getKnots().removeAllTerms();
- 499 :
this.getTermStore().removeAll(true);
- 500 :
}
- 501 :
corpusTerms.load({
- 502 :
callback: function(records, operation, success) {
- 503 :
var query = []; //this.getApiParam('query') || [];
- 504 :
if (typeof query == 'string') query = [query];
- 505 :
records.forEach(function(record, index) {
- 506 :
query.push(record.get('term'));
- 507 :
}, this);
- 508 :
this.getDocTermsFromQuery(query);
- 509 :
},
- 510 :
scope: this,
- 511 :
params: {
- 512 :
limit: 5,
- 513 :
stopList: this.getApiParams('stopList')
- 514 :
}
- 515 :
});
- 516 :
},
- 517 :
- 518 :
/**
- 519 :
* Get the results for the query(s) for each of the corpus documents.
- 520 :
* @param query {String|Array}
- 521 :
* @private
- 522 :
*/
- 523 :
getDocTermsFromQuery: function(query) {
- 524 :
if (query) {this.setApiParam("query", query);} // make sure it's set for subsequent calls
- 525 :
var corpus = this.getCorpus();
- 526 :
if (corpus && this.isVisible()) {
- 527 :
this.setApiParams({query: query}); // assumes docId already set
- 528 :
this.getDocTermStore().load({params: this.getApiParams()});
- 529 :
}
- 530 :
},
- 531 :
- 532 :
reloadTermsData: function() {
- 533 :
var terms = [];
- 534 :
for (var term in this.bubblelines.currentTerms) {
- 535 :
terms.push(term);
- 536 :
}
- 537 :
this.getDocTermsFromQuery(terms);
- 538 :
},
- 539 :
- 540 :
filterDocuments: function() {
- 541 :
var docIds = this.getApiParam('docId');
- 542 :
if (docIds == '') {
- 543 :
docIds = [];
- 544 :
this.getCorpus().getDocuments().each(function(item, index) {
- 545 :
docIds.push(item.getId());
- 546 :
});
- 547 :
this.setApiParams({docId: docIds});
- 548 :
}
- 549 :
if (typeof docIds == 'string') docIds = [docIds];
- 550 :
- 551 :
if (docIds == null) {
- 552 :
this.selectedDocs = this.getCorpus().getDocuments().clone();
- 553 :
var count = this.selectedDocs.getCount();
- 554 :
if (count > 10) {
- 555 :
for (var i = 10; i < count; i++) {
- 556 :
this.selectedDocs.removeAt(10);
- 557 :
}
- 558 :
}
- 559 :
docIds = [];
- 560 :
this.selectedDocs.eachKey(function(docId, doc) {
- 561 :
docIds.push(docId);
- 562 :
}, this);
- 563 :
this.setApiParams({docId: docIds});
- 564 :
} else {
- 565 :
this.selectedDocs = this.getCorpus().getDocuments().filterBy(function(doc, docId) {
- 566 :
return docIds.indexOf(docId) != -1;
- 567 :
}, this);
- 568 :
}
- 569 :
},
- 570 :
- 571 :
// produce format that knots can use
- 572 :
processDocument: function(doc) {
- 573 :
var title = doc.getShortTitle();
- 574 :
title = title.replace('…', '...');
- 575 :
- 576 :
return {
- 577 :
id: doc.getId(),
- 578 :
index: doc.get('index'),
- 579 :
title: title,
- 580 :
totalTokens: doc.get('tokensCount-lexical'),
- 581 :
terms: {},
- 582 :
lineLength: undefined
- 583 :
};
- 584 :
},
- 585 :
- 586 :
processTerms: function(termRecord) {
- 587 :
var termObj;
- 588 :
var term = termRecord.get('term');
- 589 :
var rawFreq = termRecord.get('rawFreq');
- 590 :
var positions = termRecord.get('positions');
- 591 :
if (rawFreq > 0) {
- 592 :
var color = this.getApplication().getColorForTerm(term);
- 593 :
if (this.getTermStore().find('term', term) === -1) {
- 594 :
this.getTermStore().loadData([[term, color]], true);
- 595 :
var index = this.getTermStore().find('term', term);
- 596 :
this.down('#termsView').select(index, true); // manually select since the store's load listener isn't triggered
- 597 :
}
- 598 :
var distributions = termRecord.get('distributions');
- 599 :
termObj = {term: term, positions: positions, distributions: distributions, rawFreq: rawFreq, color: color};
- 600 :
} else {
- 601 :
termObj = false;
- 602 :
}
- 603 :
- 604 :
return termObj;
- 605 :
},
- 606 :
- 607 :
knotClickHandler: function(data) {
- 608 :
this.setCurrentTerm(data);
- 609 :
var start = data.tokenId - 10;
- 610 :
if (start < 0) start = 0;
- 611 :
this.getTokensStore().load({
- 612 :
start: start,
- 613 :
limit: 21
- 614 :
});
- 615 :
- 616 :
data = [data].map(function(item) {return item.term}); // make an array for the event dispatch
- 617 :
this.getApplication().dispatchEvent('termsClicked', this, data);
- 618 :
}
- 619 :
});