- 1 :
// assuming Bubblelines library is loaded by containing page (via voyant.jsp)
- 2 :
/**
- 3 :
* Bubblelines visualizes the frequency and distribution of terms in a corpus.
- 4 :
*
- 5 :
* @example
- 6 :
*
- 7 :
* let config = {
- 8 :
* bins: 5, // number of bins to separate a document into
- 9 :
* docIndex: 1, //document index to restrict to (can be comma-separated list)
- 10 :
* maxDocs: 5, // maximum number of documents to show
- 11 :
* query: "love", // a query to search for in the corpus
- 12 :
* stopList: null, // a named stopword list or comma-separated list of words
- 13 :
* };
- 14 :
*
- 15 :
* loadCorpus("austen").tool("bubblelines", config);
- 16 :
*
- 17 :
* @class Bubblelines
- 18 :
* @tutorial bubblelines
- 19 :
* @memberof Tools
- 20 :
*/
- 21 :
Ext.define('Voyant.panel.Bubblelines', {
- 22 :
extend: 'Ext.panel.Panel',
- 23 :
mixins: ['Voyant.panel.Panel'],
- 24 :
alias: 'widget.bubblelines',
- 25 :
statics: {
- 26 :
i18n: {
- 27 :
},
- 28 :
api: {
- 29 :
/**
- 30 :
* @memberof Tools.Bubblelines
- 31 :
* @instance
- 32 :
* @property {bins}
- 33 :
* @default
- 34 :
*/
- 35 :
bins: 50,
- 36 :
- 37 :
/**
- 38 :
* @memberof Tools.Bubblelines
- 39 :
* @instance
- 40 :
* @property {query}
- 41 :
*/
- 42 :
query: null,
- 43 :
- 44 :
/**
- 45 :
* @memberof Tools.Bubblelines
- 46 :
* @instance
- 47 :
* @property {stopList}
- 48 :
* @default
- 49 :
*/
- 50 :
stopList: 'auto',
- 51 :
- 52 :
/**
- 53 :
* @memberof Tools.Bubblelines
- 54 :
* @instance
- 55 :
* @property {docId}
- 56 :
*/
- 57 :
docId: undefined,
- 58 :
- 59 :
/**
- 60 :
* @memberof Tools.Bubblelines
- 61 :
* @instance
- 62 :
* @property {docIndex}
- 63 :
*/
- 64 :
docIndex: undefined,
- 65 :
- 66 :
/**
- 67 :
* @memberof Tools.Bubblelines
- 68 :
* @instance
- 69 :
* @property {Number} maxDocs The maximum number of documents to show.
- 70 :
* @default
- 71 :
*/
- 72 :
maxDocs: 50
- 73 :
},
- 74 :
glyph: 'xf06e@FontAwesome'
- 75 :
},
- 76 :
config: {
- 77 :
bubblelines: undefined,
- 78 :
termStore: undefined,
- 79 :
docTermStore: undefined,
- 80 :
selectedDocs: undefined,
- 81 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'},{xtype: 'colorpaletteoption'}]
- 82 :
},
- 83 :
- 84 :
termTpl: new Ext.XTemplate(
- 85 :
'<tpl for=".">',
- 86 :
'<div class="term" style="color: rgb({color});float: left;padding: 3px;margin: 2px;">{term}</div>',
- 87 :
'</tpl>'
- 88 :
),
- 89 :
- 90 :
constructor: function() {
- 91 :
this.callParent(arguments);
- 92 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 93 :
- 94 :
this.on('loadedCorpus', function(src, corpus) {
- 95 :
this.setDocTermStore(corpus.getDocumentTerms({
- 96 :
proxy: {
- 97 :
extraParams: {
- 98 :
withDistributions: 'raw',
- 99 :
withPositions: true
- 100 :
}
- 101 :
},
- 102 :
listeners: {
- 103 :
load: function(store, records, successful, options) {
- 104 :
records.forEach(function(record) {
- 105 :
var termData = this.processTerms(record);
- 106 :
var docId = record.get('docId');
- 107 :
var term = record.get('term');
- 108 :
var termObj = {};
- 109 :
termObj[term] = termData;
- 110 :
this.getBubblelines().addTermsToDoc(termObj, docId);
- 111 :
}, this);
- 112 :
this.getBubblelines().doBubblelinesLayout();
- 113 :
},
- 114 :
scope: this
- 115 :
}
- 116 :
}));
- 117 :
- 118 :
if (this.isVisible() && this.getBubblelines()) {
- 119 :
this.initLoad();
- 120 :
}
- 121 :
}, this);
- 122 :
- 123 :
this.on('activate', function() { // load after tab activate (if we're in a tab panel)
- 124 :
if (this.getCorpus()) {
- 125 :
Ext.Function.defer(this.initLoad, 100, this);
- 126 :
}
- 127 :
}, this);
- 128 :
- 129 :
this.on('query', function(src, query) {
- 130 :
if (query !== undefined && query != '') {
- 131 :
this.getDocTermsFromQuery(query);
- 132 :
}
- 133 :
}, this);
- 134 :
- 135 :
this.on('documentsSelected', function(src, docIds) {
- 136 :
this.setApiParam('docId', docIds);
- 137 :
this.getBubblelines().cache.each(function(d) {
- 138 :
d.hidden = docIds.indexOf(d.id) === -1;
- 139 :
});
- 140 :
this.getBubblelines().drawGraph();
- 141 :
}, this);
- 142 :
- 143 :
this.on('termsClicked', function(src, terms) {
- 144 :
if (src !== this) {
- 145 :
var queryTerms = [];
- 146 :
terms.forEach(function(term) {
- 147 :
if (Ext.isString(term)) {queryTerms.push(term);}
- 148 :
else if (term.term) {queryTerms.push(term.term);}
- 149 :
else if (term.getTerm) {queryTerms.push(term.getTerm());}
- 150 :
});
- 151 :
this.getDocTermsFromQuery(queryTerms);
- 152 :
}
- 153 :
}, this);
- 154 :
- 155 :
this.on('documentTermsClicked', function(src, terms) {
- 156 :
var queryTerms = [];
- 157 :
terms.forEach(function(term) {
- 158 :
if (term.getTerm()) {queryTerms.push(term.getTerm());}
- 159 :
});
- 160 :
this.getDocTermsFromQuery(queryTerms);
- 161 :
}, this);
- 162 :
- 163 :
this.down('#granularity').setValue(parseInt(this.getApiParam('bins')));
- 164 :
},
- 165 :
- 166 :
initComponent: function() {
- 167 :
this.setTermStore(Ext.create('Ext.data.ArrayStore', {
- 168 :
fields: ['term', 'color'],
- 169 :
listeners: {
- 170 :
load: function(store, records, successful, options) {
- 171 :
var termsView = this.down('#termsView');
- 172 :
for (var i = 0; i < records.length; i++) {
- 173 :
var r = records[i];
- 174 :
termsView.select(r, true);
- 175 :
}
- 176 :
},
- 177 :
scope: this
- 178 :
}
- 179 :
}));
- 180 :
- 181 :
Ext.apply(this, {
- 182 :
title: this.localize('title'),
- 183 :
dockedItems: [{
- 184 :
dock: 'bottom',
- 185 :
xtype: 'toolbar',
- 186 :
overflowHandler: 'scroller',
- 187 :
items: [{
- 188 :
xtype: 'querysearchfield'
- 189 :
},{
- 190 :
text: this.localize('clearTerms'),
- 191 :
glyph: 'xf014@FontAwesome',
- 192 :
handler: function() {
- 193 :
this.down('#termsView').getSelectionModel().deselectAll(true);
- 194 :
this.getTermStore().removeAll();
- 195 :
this.setApiParams({query: null});
- 196 :
this.getBubblelines().removeAllTerms();
- 197 :
this.getBubblelines().drawGraph();
- 198 :
},
- 199 :
scope: this
- 200 :
},{
- 201 :
xtype: 'documentselectorbutton'
- 202 :
},{
- 203 :
xtype: 'slider',
- 204 :
itemId: 'granularity',
- 205 :
fieldLabel: this.localize('granularity'),
- 206 :
labelAlign: 'right',
- 207 :
labelWidth: 70,
- 208 :
width: 150,
- 209 :
increment: 10,
- 210 :
minValue: 10,
- 211 :
maxValue: 300,
- 212 :
listeners: {
- 213 :
changecomplete: function(slider, newvalue) {
- 214 :
this.setApiParams({bins: newvalue});
- 215 :
this.getBubblelines().bubbleSpacing = newvalue;
- 216 :
this.reloadTermsData();
- 217 :
},
- 218 :
scope: this
- 219 :
}
- 220 :
},{
- 221 :
xtype: 'checkbox',
- 222 :
boxLabel: this.localize('separateLines'),
- 223 :
boxLabelAlign: 'before',
- 224 :
checked: false,
- 225 :
handler: function(checkbox, checked) {
- 226 :
this.getBubblelines().SEPARATE_LINES_FOR_TERMS = checked;
- 227 :
this.getBubblelines().lastClickedBubbles = {};
- 228 :
this.getBubblelines().setCanvasHeight();
- 229 :
this.getBubblelines().drawGraph();
- 230 :
},
- 231 :
scope: this
- 232 :
- 233 :
}]
- 234 :
}],
- 235 :
border: false,
- 236 :
layout: 'fit',
- 237 :
items: {
- 238 :
layout: {
- 239 :
type: 'vbox',
- 240 :
align: 'stretch'
- 241 :
},
- 242 :
defaults: {border: false},
- 243 :
items: [{
- 244 :
height: 30,
- 245 :
itemId: 'termsView',
- 246 :
xtype: 'dataview',
- 247 :
store: this.getTermStore(),
- 248 :
tpl: this.termTpl,
- 249 :
itemSelector: 'div.term',
- 250 :
overItemCls: 'over',
- 251 :
selectedItemCls: 'selected',
- 252 :
selectionModel: {
- 253 :
mode: 'SIMPLE'
- 254 :
},
- 255 :
// cls: 'selected', // default selected
- 256 :
focusCls: '',
- 257 :
listeners: {
- 258 :
beforeitemclick: function(dv, record, item, index, event, opts) {
- 259 :
event.preventDefault();
- 260 :
event.stopPropagation();
- 261 :
dv.fireEvent('itemcontextmenu', dv, record, item, index, event, opts);
- 262 :
return false;
- 263 :
},
- 264 :
beforecontainerclick: function() {
- 265 :
// cancel deselect all
- 266 :
event.preventDefault();
- 267 :
event.stopPropagation();
- 268 :
return false;
- 269 :
},
- 270 :
selectionchange: function(selModel, selections) {
- 271 :
var dv = this.down('#termsView');
- 272 :
var terms = [];
- 273 :
- 274 :
dv.getStore().each(function(r) {
- 275 :
if (selections.indexOf(r) !== -1) {
- 276 :
terms.push(r.get('term'));
- 277 :
Ext.fly(dv.getNodeByRecord(r)).removeCls('unselected').addCls('selected');
- 278 :
} else {
- 279 :
Ext.fly(dv.getNodeByRecord(r)).removeCls('selected').addCls('unselected');
- 280 :
}
- 281 :
});
- 282 :
- 283 :
for (var index in this.getBubblelines().lastClickedBubbles) {
- 284 :
var lcTerms = this.getBubblelines().lastClickedBubbles[index];
- 285 :
for (var term in lcTerms) {
- 286 :
if (terms.indexOf(term) == -1) {
- 287 :
delete this.getBubblelines().lastClickedBubbles[index][term];
- 288 :
}
- 289 :
}
- 290 :
- 291 :
}
- 292 :
this.getBubblelines().termsFilter = terms;
- 293 :
this.getBubblelines().setCanvasHeight();
- 294 :
this.getBubblelines().drawGraph();
- 295 :
},
- 296 :
itemcontextmenu: function(dv, record, el, index, event) {
- 297 :
event.preventDefault();
- 298 :
event.stopPropagation();
- 299 :
var isSelected = dv.isSelected(el);
- 300 :
var menu = new Ext.menu.Menu({
- 301 :
floating: true,
- 302 :
items: [{
- 303 :
text: isSelected ? this.localize('hideTerm') : this.localize('showTerm'),
- 304 :
handler: function() {
- 305 :
if (isSelected) {
- 306 :
dv.deselect(index);
- 307 :
} else {
- 308 :
dv.select(index, true);
- 309 :
}
- 310 :
},
- 311 :
scope: this
- 312 :
},{
- 313 :
text: this.localize('removeTerm'),
- 314 :
handler: function() {
- 315 :
dv.deselect(index);
- 316 :
var term = this.getTermStore().getAt(index).get('term');
- 317 :
this.getTermStore().removeAt(index);
- 318 :
dv.refresh();
- 319 :
- 320 :
this.getBubblelines().removeTerm(term);
- 321 :
this.getBubblelines().setCanvasHeight();
- 322 :
this.getBubblelines().drawGraph();
- 323 :
},
- 324 :
scope: this
- 325 :
}]
- 326 :
});
- 327 :
menu.showAt(event.getXY());
- 328 :
},
- 329 :
scope: this
- 330 :
}
- 331 :
},{
- 332 :
flex: 1,
- 333 :
xtype: 'container',
- 334 :
autoEl: 'div',
- 335 :
itemId: 'canvasParent',
- 336 :
layout: 'fit',
- 337 :
overflowY: 'auto',
- 338 :
overflowX: 'hidden'
- 339 :
}],
- 340 :
listeners: {
- 341 :
render: function(component) {
- 342 :
var canvasParent = this.down('#canvasParent');
- 343 :
this.setBubblelines(new Bubblelines({
- 344 :
container: canvasParent,
- 345 :
clickHandler: this.bubbleClickHandler.bind(this)
- 346 :
}));
- 347 :
this.getBubblelines().bubbleSpacing = parseInt(this.getApiParam('bins'));
- 348 :
},
- 349 :
afterlayout: function(container) {
- 350 :
if (this.getBubblelines().initialized === false) {
- 351 :
this.getBubblelines().initializeCanvas();
- 352 :
}
- 353 :
},
- 354 :
resize: function(cnt, width, height) {
- 355 :
this.getBubblelines().doBubblelinesLayout();
- 356 :
},
- 357 :
scope: this
- 358 :
}
- 359 :
}
- 360 :
});
- 361 :
- 362 :
this.callParent(arguments);
- 363 :
},
- 364 :
- 365 :
initLoad: function() {
- 366 :
// get doc info
- 367 :
var docIds = [];
- 368 :
this.getCorpus().getDocuments().each(function(doc, index, total) {
- 369 :
var inLimit = index < this.getApiParam('maxDocs');
- 370 :
this.getBubblelines().addDocToCache({
- 371 :
id: doc.getId(),
- 372 :
index: doc.getIndex(),
- 373 :
title: doc.getShortTitle(),
- 374 :
totalTokens: doc.get('tokensCount-lexical'),
- 375 :
terms: {},
- 376 :
hidden: !inLimit
- 377 :
});
- 378 :
if (inLimit) {
- 379 :
docIds.push(doc.getId());
- 380 :
}
- 381 :
}, this);
- 382 :
this.setApiParam('docId', docIds);
- 383 :
- 384 :
// get top terms in corpus
- 385 :
this.getCorpus().getCorpusTerms({autoload: false}).load({
- 386 :
callback: function(records, operation, success) {
- 387 :
var query = [];
- 388 :
records.forEach(function(record, index) {
- 389 :
query.push(record.get('term'));
- 390 :
}, this);
- 391 :
this.getDocTermsFromQuery(query);
- 392 :
},
- 393 :
scope: this,
- 394 :
params: {
- 395 :
limit: this.getApiParam('query') ? undefined : 5,
- 396 :
stopList: this.getApiParams('stopList'),
- 397 :
query: this.getApiParam('query')
- 398 :
}
- 399 :
});
- 400 :
},
- 401 :
- 402 :
/**
- 403 :
* Get the results for the query(s) for each of the corpus documents.
- 404 :
* @param query {String|Array}
- 405 :
* @private
- 406 :
*/
- 407 :
getDocTermsFromQuery: function(query) {
- 408 :
if (query) {this.setApiParam('query', query);} // make sure it's set for subsequent calls
- 409 :
if (this.getCorpus() && this.isVisible()) {
- 410 :
this.getDocTermStore().load({params: this.getApiParams()});
- 411 :
}
- 412 :
},
- 413 :
- 414 :
reloadTermsData: function() {
- 415 :
var terms = [];
- 416 :
for (var term in this.getBubblelines().currentTerms) {
- 417 :
terms.push(term);
- 418 :
}
- 419 :
this.getDocTermsFromQuery(terms);
- 420 :
},
- 421 :
- 422 :
filterDocuments: function() {
- 423 :
var docIds = this.getApiParam('docId');
- 424 :
if (docIds == '') {
- 425 :
docIds = [];
- 426 :
this.getCorpus().getDocuments().each(function(item, index) {
- 427 :
docIds.push(item.getId());
- 428 :
});
- 429 :
this.setApiParams({docId: docIds});
- 430 :
}
- 431 :
if (typeof docIds == 'string') docIds = [docIds];
- 432 :
- 433 :
if (docIds == null) {
- 434 :
this.setSelectedDocs(this.getCorpus().getDocuments().clone());
- 435 :
var count = this.getSelectedDocs().getCount();
- 436 :
if (count > 10) {
- 437 :
for (var i = 10; i < count; i++) {
- 438 :
this.getSelectedDocs().removeAt(10);
- 439 :
}
- 440 :
}
- 441 :
docIds = [];
- 442 :
this.getSelectedDocs().eachKey(function(docId, doc) {
- 443 :
docIds.push(docId);
- 444 :
}, this);
- 445 :
this.setApiParams({docId: docIds});
- 446 :
} else {
- 447 :
this.setSelectedDocs(this.getCorpus().getDocuments().filterBy(function(doc, docId) {
- 448 :
return docIds.indexOf(docId) != -1;
- 449 :
}, this));
- 450 :
}
- 451 :
},
- 452 :
- 453 :
processTerms: function(termRecord) {
- 454 :
var termObj;
- 455 :
var term = termRecord.get('term');
- 456 :
var rawFreq = termRecord.get('rawFreq');
- 457 :
var positions = termRecord.get('positions');
- 458 :
if (rawFreq > 0) {
- 459 :
var color = this.getApplication().getColorForTerm(term);
- 460 :
if (this.getTermStore().find('term', term) === -1) {
- 461 :
this.getTermStore().loadData([[term, color]], true);
- 462 :
var index = this.getTermStore().find('term', term);
- 463 :
this.down('#termsView').select(index, true); // manually select since the store's load listener isn't triggered
- 464 :
}
- 465 :
var distributions = termRecord.get('distributions');
- 466 :
termObj = {positions: positions, distributions: distributions, rawFreq: rawFreq, color: color};
- 467 :
} else {
- 468 :
termObj = false;
- 469 :
}
- 470 :
- 471 :
return termObj;
- 472 :
},
- 473 :
- 474 :
bubbleClickHandler: function(data) {
- 475 :
this.getApplication().dispatchEvent('termsClicked', this, data);
- 476 :
}
- 477 :
});