- 1 :
/**
- 2 :
* TermsRadio is a visualization that depicts the change of the frequency of words in a corpus (or within a single document).
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "bins": 5,
- 8 :
* "limit": null,
- 9 :
* "query": null,
- 10 :
* "slider": null,
- 11 :
* "speed": null,
- 12 :
* "stopList": null,
- 13 :
* "visibleBins": null,
- 14 :
* "yAxisScale": null
- 15 :
* };
- 16 :
*
- 17 :
* loadCorpus("austen").tool("termsradio", config);
- 18 :
*
- 19 :
*
- 20 :
* @class TermsRadio
- 21 :
* @tutorial termsradio
- 22 :
* @memberof Tools
- 23 :
* @author Mark Turcato
- 24 :
* @author Andrew MacDonald
- 25 :
*/
- 26 :
Ext.define('Voyant.panel.TermsRadio', {
- 27 :
extend: 'Ext.panel.Panel',
- 28 :
mixins: ['Voyant.panel.Panel'],
- 29 :
alias: 'widget.termsradio',
- 30 :
config: {
- 31 :
/**
- 32 :
* @private
- 33 :
*/
- 34 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
- 35 :
/**
- 36 :
* @private
- 37 :
*/
- 38 :
speed: 50,
- 39 :
/**
- 40 :
* @private
- 41 :
*/
- 42 :
termsRadio: undefined
- 43 :
},
- 44 :
statics: {
- 45 :
i18n: {
- 46 :
},
- 47 :
api: {
- 48 :
/**
- 49 :
* @memberof Tools.TermsRadio
- 50 :
* @instance
- 51 :
* @property {bins}
- 52 :
* @default
- 53 :
*/
- 54 :
bins: 5,
- 55 :
- 56 :
/**
- 57 :
* @memberof Tools.TermsRadio
- 58 :
* @instance
- 59 :
* @property {Number} visibleBins How many segments or documents to show at once (default is 5).
- 60 :
* Note that this often works in parallel with the {@link #bins} value.
- 61 :
* @default
- 62 :
*/
- 63 :
visibleBins: 5,
- 64 :
- 65 :
/**
- 66 :
* @memberof Tools.TermsRadio
- 67 :
* @instance
- 68 :
* @property {String[]} docIdType The document type(s) to restrict results to.
- 69 :
* @default null
- 70 :
* @private
- 71 :
*/
- 72 :
docIdType: null,
- 73 :
- 74 :
/**
- 75 :
* @memberof Tools.TermsRadio
- 76 :
* @instance
- 77 :
* @property {limit}
- 78 :
* @default
- 79 :
*/
- 80 :
limit: 50,
- 81 :
- 82 :
/**
- 83 :
* @instance
- 84 :
* @property mode What mode to operate at, either document or corpus.
- 85 :
* @choices document, corpus
- 86 :
* @private
- 87 :
*/
- 88 :
mode: null,
- 89 :
- 90 :
/**
- 91 :
* @memberof Tools.TermsRadio
- 92 :
* @instance
- 93 :
* @property {Number} position The current shifted position of the visualization.
- 94 :
* @default 0
- 95 :
* @private
- 96 :
*/
- 97 :
position: 0,
- 98 :
- 99 :
/**
- 100 :
* @memberof Tools.TermsRadio
- 101 :
* @instance
- 102 :
* @property {String[]} selectedWords The words that have been selected.
- 103 :
* @default null
- 104 :
* @private
- 105 :
*/
- 106 :
selectedWords: [],
- 107 :
- 108 :
/**
- 109 :
* @memberof Tools.TermsRadio
- 110 :
* @instance
- 111 :
* @property {stopList}
- 112 :
* @default
- 113 :
*/
- 114 :
stopList: 'auto',
- 115 :
- 116 :
/**
- 117 :
* @memberof Tools.TermsRadio
- 118 :
* @instance
- 119 :
* @property {query}
- 120 :
*/
- 121 :
query: null,
- 122 :
- 123 :
/**
- 124 :
* @memberof Tools.TermsRadio
- 125 :
* @instance
- 126 :
* @property {String} yAxisScale The scale for the y axis. Options are: 'log' or 'linear'.
- 127 :
* @default log
- 128 :
*/
- 129 :
yAxisScale: 'log',
- 130 :
- 131 :
/**
- 132 :
* @memberof Tools.TermsRadio
- 133 :
* @instance
- 134 :
* @property {Number} speed How fast to animate the visualization.
- 135 :
* @default
- 136 :
*/
- 137 :
speed: 50,
- 138 :
- 139 :
/**
- 140 :
* @memberof Tools.TermsRadio
- 141 :
* @instance
- 142 :
* @property {Boolean} slider Whether to show the slider.
- 143 :
* @default true
- 144 :
*/
- 145 :
slider: undefined
- 146 :
},
- 147 :
glyph: 'xf201@FontAwesome'
- 148 :
}
- 149 :
- 150 :
/**
- 151 :
* @private
- 152 :
*/
- 153 :
,constructor: function(config) {
- 154 :
- 155 :
var onLoadHandler = function(mode, store, records, success, operation) {
- 156 :
this.setApiParams({mode: mode});
- 157 :
- 158 :
this.getTermsRadio().loadRecords(records);
- 159 :
- 160 :
var query = this.getApiParam('query');
- 161 :
// check for no results
- 162 :
if (query) {
- 163 :
if (records.length==0 || (records.length==1 && records[0].getRawFreq()==0)) {
- 164 :
this.toastInfo({
- 165 :
html: this.localize("termNotFound"),
- 166 :
align: 'bl'
- 167 :
});
- 168 :
} else {
- 169 :
this.getTermsRadio().highlightQuery(query, true);
- 170 :
}
- 171 :
}
- 172 :
};
- 173 :
- 174 :
this.corpusStore = Ext.create("Voyant.data.store.CorpusTerms", {
- 175 :
listeners : {
- 176 :
load: {
- 177 :
fn : onLoadHandler.bind(this, 'corpus'),
- 178 :
scope : this
- 179 :
}
- 180 :
}
- 181 :
});
- 182 :
- 183 :
this.documentStore = Ext.create("Voyant.data.store.DocumentTerms", {
- 184 :
listeners : {
- 185 :
load: {
- 186 :
fn : onLoadHandler.bind(this, 'document'),
- 187 :
scope : this
- 188 :
}
- 189 :
}
- 190 :
});
- 191 :
- 192 :
Ext.apply(config, {
- 193 :
title: this.localize('title'),
- 194 :
legendMenu: Ext.create('Ext.menu.Menu', {
- 195 :
items: [
- 196 :
{text: '', itemId: 'remove', glyph: 'xf068@FontAwesome'}
- 197 :
]
- 198 :
}),
- 199 :
tbar: new Ext.Toolbar({
- 200 :
overflowHandler: 'scroller',
- 201 :
items: {
- 202 :
xtype: 'legend',
- 203 :
store: new Ext.data.JsonStore({
- 204 :
fields : ['name', 'mark', 'selector']
- 205 :
}),
- 206 :
listeners: {
- 207 :
itemclick: function(view, record, el, index) {
- 208 :
var term = record.get('name');
- 209 :
if (this.getTermsRadio().isTermSelected(term)) {
- 210 :
this.getTermsRadio().doTermDeselect(term);
- 211 :
} else {
- 212 :
this.getTermsRadio().doTermSelect(term);
- 213 :
}
- 214 :
},
- 215 :
itemcontextmenu: function(view, record, el, index, event) {
- 216 :
event.preventDefault();
- 217 :
var xy = event.getXY();
- 218 :
- 219 :
var term = record.get('name');
- 220 :
var text = (new Ext.Template(this.localize('removeTerm'))).apply([term]);
- 221 :
this.legendMenu.queryById('remove').setText(text);
- 222 :
- 223 :
this.legendMenu.on('click', function(menu, item) {
- 224 :
if (item !== undefined) {
- 225 :
this.getTermsRadio().doTermDeselect(term, true);
- 226 :
}
- 227 :
}, this, {single: true});
- 228 :
this.legendMenu.showAt(xy);
- 229 :
},
- 230 :
scope: this
- 231 :
}
- 232 :
}
- 233 :
}),
- 234 :
bbar: {
- 235 :
overflowHandler: 'scroller',
- 236 :
items: [{
- 237 :
xtype: 'querysearchfield'
- 238 :
},{
- 239 :
glyph: 'xf04b@FontAwesome', // start with play button, which means we're paused
- 240 :
itemId: 'play',
- 241 :
handler: function(btn) {
- 242 :
var playing = btn.glyph=="xf04c@FontAwesome";
- 243 :
if (playing) {
- 244 :
this.getTermsRadio().continueTransition = false;
- 245 :
this.mask(this.localize("completingTransition"))
- 246 :
btn.setPlaying(false)
- 247 :
}
- 248 :
else {
- 249 :
this.getTermsRadio().toggleRightCheck();
- 250 :
btn.setPlaying(true);
- 251 :
}
- 252 :
},
- 253 :
scope: this,
- 254 :
setPlaying: function(bool) {
- 255 :
this.setGlyph(bool ? "xf04c@FontAwesome" : "xf04b@FontAwesome")
- 256 :
}
- 257 :
},{
- 258 :
glyph: 'xf0e2@FontAwesome',
- 259 :
// text: this.localize('reset')
- 260 :
tooltip : this.localize('resetTip'),
- 261 :
listeners : {
- 262 :
click : {fn : function() {
- 263 :
this.queryById("play").setPlaying(false);
- 264 :
this.getTermsRadio().shiftCount = 0;
- 265 :
this.getTermsRadio().prepareData();
- 266 :
this.getTermsRadio().redraw();
- 267 :
}
- 268 :
,scope : this
- 269 :
}
- 270 :
}
- 271 :
},{
- 272 :
xtype: 'label',
- 273 :
forId: 'terms',
- 274 :
text: this.localize('terms')
- 275 :
},{
- 276 :
xtype: 'slider',
- 277 :
itemId: 'terms',
- 278 :
hideLabel: true,
- 279 :
width : 60,
- 280 :
increment : 5,
- 281 :
minValue : 5,
- 282 :
maxValue : 100,
- 283 :
listeners: {
- 284 :
afterrender: function(slider) {
- 285 :
slider.setValue(parseInt(this.getApiParam("limit")))
- 286 :
},
- 287 :
changecomplete: function(slider, newvalue) {
- 288 :
this.setApiParams({limit: newvalue});
- 289 :
this.loadStore();
- 290 :
},
- 291 :
scope: this
- 292 :
}
- 293 :
},{
- 294 :
xtype: 'label',
- 295 :
forId: 'speed',
- 296 :
text: this.localize('speed')
- 297 :
},{
- 298 :
xtype: 'slider',
- 299 :
itemId: 'speed',
- 300 :
hideLabel: true,
- 301 :
width : 60,
- 302 :
increment : 5,
- 303 :
minValue : 5,
- 304 :
maxValue : 100,
- 305 :
listeners: {
- 306 :
afterrender: function(slider) {
- 307 :
slider.setValue(parseInt(this.getApiParam("speed")))
- 308 :
this.setSpeed(slider.getValue())
- 309 :
},
- 310 :
changecomplete: function(slider, newvalue) {
- 311 :
this.setApiParams({speed: newvalue});
- 312 :
this.setSpeed(newvalue)
- 313 :
},
- 314 :
scope: this
- 315 :
}
- 316 :
},{
- 317 :
xtype: 'label',
- 318 :
itemId: 'visibleSegmentsLabel',
- 319 :
forId: 'visibleBins',
- 320 :
text: this.localize('visibleSegments')
- 321 :
},{
- 322 :
xtype: 'slider',
- 323 :
itemId: 'visibleBins',
- 324 :
hideLabel: true,
- 325 :
width : 60,
- 326 :
increment : 1,
- 327 :
minValue : 1,
- 328 :
maxValue : 100,
- 329 :
listeners: {
- 330 :
afterrender: function(slider) {
- 331 :
slider.setValue(parseInt(this.getApiParam("visibleBins")))
- 332 :
},
- 333 :
changecomplete: function(slider, newvalue) {
- 334 :
this.setApiParams({visibleBins: newvalue});
- 335 :
this.numVisPoints = newvalue;
- 336 :
this.loadStore();
- 337 :
- 338 :
if (this.numVisPoints == this.getCorpus().getDocumentsCount()) {
- 339 :
this.getTermsRadio().hideSlider();
- 340 :
} else if (this.getApiParam("slider") != 'false'){
- 341 :
this.getTermsRadio().showSlider();
- 342 :
}
- 343 :
},
- 344 :
scope: this
- 345 :
}
- 346 :
},{
- 347 :
xtype: 'label',
- 348 :
itemId: 'segmentsLabel',
- 349 :
forId: 'segments',
- 350 :
text: this.localize('segments')
- 351 :
},{
- 352 :
xtype: 'slider',
- 353 :
itemId: 'segments',
- 354 :
hideLabel: true,
- 355 :
width : 60,
- 356 :
increment : 1,
- 357 :
minValue : 1,
- 358 :
maxValue : 100,
- 359 :
listeners: {
- 360 :
afterrender: function(slider) {
- 361 :
slider.setValue(parseInt(this.getApiParam("bins")))
- 362 :
},
- 363 :
changecomplete: function(slider, newvalue) {
- 364 :
this.setApiParams({bins: newvalue});
- 365 :
this.numDataPoints = newvalue;
- 366 :
this.loadStore();
- 367 :
var visibleBins = this.queryById('visibleBins');
- 368 :
visibleBins.setMaxValue(newvalue) // only relevant for doc mode
- 369 :
},
- 370 :
scope: this
- 371 :
}
- 372 :
}]
- 373 :
}
- 374 :
});
- 375 :
- 376 :
// need to add option here so we have access to localize
- 377 :
this.config.options.push({
- 378 :
xtype: 'combo',
- 379 :
queryMode : 'local',
- 380 :
triggerAction : 'all',
- 381 :
forceSelection : true,
- 382 :
editable : false,
- 383 :
fieldLabel : this.localize('yScale'),
- 384 :
labelAlign : 'right',
- 385 :
name : 'yAxisScale',
- 386 :
valueField : 'value',
- 387 :
displayField : 'name',
- 388 :
store: new Ext.data.JsonStore({
- 389 :
fields : ['name', 'value'],
- 390 :
data : [{
- 391 :
name : this.localize('linear'), value: 'linear'
- 392 :
},{
- 393 :
name : this.localize('log'), value: 'log'
- 394 :
}]
- 395 :
}),
- 396 :
listeners: {
- 397 :
afterrender: function(combo) {
- 398 :
combo.setValue(this.getApiParam('yAxisScale'));
- 399 :
},
- 400 :
scope: this
- 401 :
}
- 402 :
});
- 403 :
- 404 :
this.callParent(arguments);
- 405 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 406 :
- 407 :
this.on('boxready', function(component) {
- 408 :
var sliderParam = this.getApiParam('slider');
- 409 :
var showSlider = sliderParam === undefined ? true : sliderParam === 'true';
- 410 :
var config = {
- 411 :
parent: this,
- 412 :
container: this.body,
- 413 :
showSlider: showSlider
- 414 :
};
- 415 :
this.setTermsRadio(new TermsRadio(config));
- 416 :
}, this);
- 417 :
- 418 :
/**
- 419 :
* @event corpusTypesSelected
- 420 :
* @type listener
- 421 :
* @private
- 422 :
*/
- 423 :
this.addListener('corpusTermsClicked', function(src, terms){
- 424 :
if (this.getCorpus().getDocumentsCount() > 1) {
- 425 :
terms.forEach(function(term) {
- 426 :
var t = term.getTerm();
- 427 :
this.setApiParams({query: t});
- 428 :
this.loadStore();
- 429 :
}, this);
- 430 :
}
- 431 :
});
- 432 :
- 433 :
this.addListener('documentTermsClicked', function(src, terms){
- 434 :
if(src && src.xtype==this.xtype) {return false;}
- 435 :
- 436 :
terms.forEach(function(term) {
- 437 :
var t = term.getTerm();
- 438 :
this.setApiParams({query: t});
- 439 :
this.loadStore();
- 440 :
}, this);
- 441 :
});
- 442 :
- 443 :
this.on('query', function(src, query){
- 444 :
this.fireEvent("termsClicked", src, query);
- 445 :
});
- 446 :
- 447 :
this.on("termsClicked", function(src, terms) {
- 448 :
// TODO load term distribution data
- 449 :
terms.forEach(function(term) {
- 450 :
var queryTerm;
- 451 :
if (Ext.isString(term)) {queryTerm = term;}
- 452 :
else if (term.term) {queryTerm = term.term;}
- 453 :
else if (term.getTerm) {queryTerm = term.getTerm();}
- 454 :
- 455 :
// TODO handling for multiple terms
- 456 :
this.setApiParams({query: queryTerm});
- 457 :
this.loadStore();
- 458 :
}, this);
- 459 :
});
- 460 :
- 461 :
this.on("loadedCorpus", function(src, corpus) {
- 462 :
this.documentStore.setCorpus(corpus);
- 463 :
this.corpusStore.setCorpus(corpus);
- 464 :
- 465 :
var params = this.getApiParams();
- 466 :
params.withDistributions = true;
- 467 :
if (params.type) {
- 468 :
delete params.limit;
- 469 :
}
- 470 :
var store;
- 471 :
- 472 :
var docsCount = this.getCorpus().getDocumentsCount();
- 473 :
var segments = this.queryById("segments");
- 474 :
var visibleBins = this.queryById("visibleBins");
- 475 :
if (params.mode=='document' || docsCount == 1) {
- 476 :
this.setApiParam("mode", "document");
- 477 :
store = this.documentStore;
- 478 :
visibleBins.setMaxValue(segments.getValue())
- 479 :
} else {
- 480 :
this.setApiParam("mode", "corpus");
- 481 :
delete params.bins;
- 482 :
store = this.corpusStore;
- 483 :
segments.hide();
- 484 :
this.queryById("segmentsLabel").hide();
- 485 :
var visibleBins = this.queryById("visibleBins");
- 486 :
visibleBins.setMaxValue(docsCount);
- 487 :
if (parseInt(this.getApiParam("visibleBins")>docsCount)) {
- 488 :
visibleBins.setValue(docsCount);
- 489 :
}
- 490 :
}
- 491 :
- 492 :
// select top 3 words
- 493 :
store.on('load', function(store, records) {
- 494 :
for (var i = 0; i < 3; i++) {
- 495 :
var r = records[i];
- 496 :
if (r) {
- 497 :
this.getTermsRadio().highlightRecord(r, true);
- 498 :
}
- 499 :
}
- 500 :
}, this, {single: true});
- 501 :
store.load({params: params});
- 502 :
}, this);
- 503 :
}
- 504 :
- 505 :
,loadStore: function () {
- 506 :
this.queryById('play').setPlaying(false);
- 507 :
var params = this.getApiParams();
- 508 :
params.withDistributions = true;
- 509 :
if(this.getApiParam('mode') === 'document') {
- 510 :
this.documentStore.load({params: params});
- 511 :
}
- 512 :
if(this.getApiParam('mode') === 'corpus') {
- 513 :
delete params.bins;
- 514 :
this.corpusStore.load({params: params});
- 515 :
}
- 516 :
}
- 517 :
- 518 :
});