- 1 :
/**
- 2 :
* The Topics tool provides a rudimentary way of generating term clusters from a document or corpus and then seeing how each topic (term cluster) is distributed across the document or corpus.
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "iterations": null,
- 8 :
* "perDocLimit": null,
- 9 :
* "seed": null,
- 10 :
* "stopList": null,
- 11 :
* "termsPerTopic": null,
- 12 :
* "topics": null
- 13 :
* };
- 14 :
*
- 15 :
* loadCorpus("austen").tool("Topics", config);
- 16 :
*
- 17 :
* @class Topics
- 18 :
* @tutorial topics
- 19 :
* @memberof Tools
- 20 :
*/
- 21 :
Ext.define('Voyant.panel.Topics', {
- 22 :
extend: 'Ext.panel.Panel',
- 23 :
mixins: ['Voyant.panel.Panel'],
- 24 :
alias: 'widget.topics',
- 25 :
statics: {
- 26 :
i18n: {
- 27 :
topics: 'Topics',
- 28 :
documents: 'Documents',
- 29 :
topicWeight: 'Topic weight'
- 30 :
},
- 31 :
api: {
- 32 :
/**
- 33 :
* @memberof Tools.Topics
- 34 :
* @instance
- 35 :
* @property {stopList}
- 36 :
* @default
- 37 :
*/
- 38 :
stopList: 'auto',
- 39 :
- 40 :
/**
- 41 :
* @memberof Tools.Topics
- 42 :
* @instance
- 43 :
* @property {Number} topics The number of topics.
- 44 :
* @default
- 45 :
*/
- 46 :
topics: 10,
- 47 :
- 48 :
/**
- 49 :
* @memberof Tools.Topics
- 50 :
* @instance
- 51 :
* @property {Number} termsPerTopic The number of terms per topic.
- 52 :
* @default
- 53 :
*/
- 54 :
termsPerTopic: 10,
- 55 :
- 56 :
/**
- 57 :
* @memberof Tools.Topics
- 58 :
* @instance
- 59 :
* @property {Number} iterations The number of iterations.
- 60 :
* @default
- 61 :
*/
- 62 :
iterations: 100,
- 63 :
- 64 :
/**
- 65 :
* @memberof Tools.Topics
- 66 :
* @instance
- 67 :
* @property {Number} perDocLimit The number of terms to limit each document to.
- 68 :
* @default
- 69 :
*/
- 70 :
perDocLimit: 1000,
- 71 :
- 72 :
/**
- 73 :
* @memberof Tools.Topics
- 74 :
* @instance
- 75 :
* @property {Number} seed The seed to use for random number generation.
- 76 :
* @default
- 77 :
*/
- 78 :
seed: 0
- 79 :
},
- 80 :
glyph: 'xf1ea@FontAwesome'
- 81 :
},
- 82 :
config: {
- 83 :
/**
- 84 :
* @private
- 85 :
*/
- 86 :
options: [{xtype: 'stoplistoption'},{
- 87 :
xtype: 'numberfield',
- 88 :
name: 'perDocLimit',
- 89 :
fieldLabel: 'maximum words per document',
- 90 :
labelAlign: 'right',
- 91 :
value: 1000,
- 92 :
minValue: 1,
- 93 :
step: 100,
- 94 :
listeners: {
- 95 :
afterrender: function(field) {
- 96 :
var win = field.up("window");
- 97 :
if (win && win.panel) {
- 98 :
field.setValue(parseInt(win.panel.getApiParam('perDocLimit')))
- 99 :
field.setFieldLabel(win.panel.localize("perDocLimit"))
- 100 :
}
- 101 :
},
- 102 :
change: function(field, val) {
- 103 :
var win = field.up("window");
- 104 :
if (val>5000 && win && win.panel) {
- 105 :
win.panel.toastInfo({
- 106 :
html: win.panel.localize("perDocLimitHigh"),
- 107 :
anchor: win.getTargetEl(),
- 108 :
align: 'tr',
- 109 :
maxWidth: 400
- 110 :
})
- 111 :
}
- 112 :
}
- 113 :
}
- 114 :
},{
- 115 :
xtype: 'numberfield',
- 116 :
name: 'iterations',
- 117 :
fieldLabel: 'iterations per run',
- 118 :
labelAlign: 'right',
- 119 :
value: 100,
- 120 :
minValue: 50,
- 121 :
maxValue: 1000,
- 122 :
step: 50,
- 123 :
listeners: {
- 124 :
afterrender: function(field) {
- 125 :
var win = field.up("window");
- 126 :
if (win && win.panel) {
- 127 :
field.setValue(parseInt(win.panel.getApiParam('iterations')))
- 128 :
field.setFieldLabel(win.panel.localize("iterations"))
- 129 :
}
- 130 :
}
- 131 :
}
- 132 :
},{
- 133 :
xtype: 'textfield',
- 134 :
name: 'seed',
- 135 :
fieldLabel: 'Random Seed',
- 136 :
labelAlign: 'right',
- 137 :
value: 0
- 138 :
}],
- 139 :
- 140 :
currentTopics: [],
- 141 :
currentDocument: undefined,
- 142 :
- 143 :
corpus: undefined
- 144 :
},
- 145 :
- 146 :
constructor: function(config) {
- 147 :
var me = this;
- 148 :
Ext.apply(this, {
- 149 :
title: this.localize('title'),
- 150 :
layout: {
- 151 :
type: 'hbox',
- 152 :
pack: 'start',
- 153 :
align: 'begin',
- 154 :
padding: '10px'
- 155 :
},
- 156 :
defaultType: 'dataview',
- 157 :
items: [{
- 158 :
itemId: 'topicsView',
- 159 :
flex: 2,
- 160 :
padding: '0 5px 0 0',
- 161 :
margin: '0 5px 0 0',
- 162 :
height: '100%',
- 163 :
scrollable: 'y',
- 164 :
store: Ext.create('Ext.data.ArrayStore',{
- 165 :
fields: ['index', 'terms', 'weight', 'diagnostics']
- 166 :
}),
- 167 :
selectionModel: {
- 168 :
type: 'dataviewmodel',
- 169 :
mode: 'MULTI'
- 170 :
},
- 171 :
itemSelector: 'div.topicItem',
- 172 :
tpl: new Ext.XTemplate(
- 173 :
'<div style="font-weight: bold">{[this.localize("topics")]}</div><tpl for=".">',
- 174 :
'<div class="topicItem" style="background-color: {[this.getColor(values.index)]}">',
- 175 :
'<div class="data weight" data-qtip="{[this.localize("topicWeight")]}">{[fm.number(values.weight*100, "00.0")]}%</div>',
- 176 :
'<span class="term">{[values.terms.join("</span> <span class=\\"term\\">")]}</span>',
- 177 :
'<div class="data diagnostics">{[this.processDiagnostics(values.diagnostics)]}</div>',
- 178 :
'</div>',
- 179 :
'</tpl>',
- 180 :
{
- 181 :
getColor: function(index) {
- 182 :
var rgb = me.getColorForTopic(index);
- 183 :
return 'rgba('+rgb.join(',')+',.33);'
- 184 :
},
- 185 :
localize: function(key) {
- 186 :
return me.localize(key);
- 187 :
},
- 188 :
processDiagnostics: function(obj) {
- 189 :
var string = '';
- 190 :
for (var key in obj) {
- 191 :
string += '<div><div class="key">'+key+'</div><div class="value">'+obj[key]+'</div></div>';
- 192 :
}
- 193 :
return string;
- 194 :
}
- 195 :
}
- 196 :
),
- 197 :
listeners: {
- 198 :
selectionchange: function(sel, selected) {
- 199 :
sel.view.removeCls('showWeight');
- 200 :
me.setCurrentDocument(undefined);
- 201 :
me.setCurrentTopics(selected.map(function(item) { return item.get('index') }));
- 202 :
- 203 :
me.down('#docsView').getSelectionModel().deselectAll(true);
- 204 :
me.down('#docsView').refresh();
- 205 :
}
- 206 :
}
- 207 :
},{
- 208 :
itemId: 'docsView',
- 209 :
flex: 1,
- 210 :
height: '100%',
- 211 :
scrollable: 'y',
- 212 :
store: Ext.create('Ext.data.JsonStore',{
- 213 :
fields: ['docId', 'weights']
- 214 :
}),
- 215 :
selectionModel: {
- 216 :
type: 'dataviewmodel',
- 217 :
mode: 'SINGLE',
- 218 :
allowDeselect: true,
- 219 :
toggleOnClick: true
- 220 :
},
- 221 :
itemSelector: 'div.topicItem',
- 222 :
tpl: new Ext.XTemplate(
- 223 :
'<div style="font-weight: bold">{[this.localize("documents")]}</div><tpl for=".">',
- 224 :
'<div class="topicItem">',
- 225 :
'{[this.getDocTitle(values.docId)]}',
- 226 :
'<div class="chart">{[this.getChart(values.docId, values.weights)]}</div>',
- 227 :
'</div>',
- 228 :
'</tpl>',
- 229 :
{
- 230 :
getDocTitle: function(docId) {
- 231 :
return me.getCorpus().getDocument(docId).getTitle();
- 232 :
},
- 233 :
getChart: function(docId, weights) {
- 234 :
var chart = '';
- 235 :
var topicStore = me.down('#topicsView').getStore();
- 236 :
topicStore.each(function(item) {
- 237 :
var index = item.get('index');
- 238 :
var weight = weights[index];
- 239 :
var rgb = me.getColorForTopic(index);
- 240 :
var alpha = me.getCurrentDocument() === docId ? '1' : me.getCurrentTopics().length === 0 ? '.33' : me.getCurrentTopics().indexOf(index) !== -1 ? '1' : '.15';
- 241 :
var color = 'rgba('+rgb.join(',')+','+alpha+')';
- 242 :
chart += '<div style="width: '+(weight*100)+'%; background-color: '+color+'"> </div>';
- 243 :
});
- 244 :
return chart;
- 245 :
},
- 246 :
localize: function(key) {
- 247 :
return me.localize(key);
- 248 :
}
- 249 :
}
- 250 :
),
- 251 :
listeners: {
- 252 :
selectionchange: function(sel, selected) {
- 253 :
me.setCurrentTopics([]);
- 254 :
- 255 :
var docId = selected[0] ? selected[0].get('docId') : undefined;
- 256 :
me.setCurrentDocument(docId);
- 257 :
- 258 :
var topicStore = me.down('#topicsView').getStore();
- 259 :
if (docId) {
- 260 :
me.down('#topicsView').addCls('showWeight').getSelectionModel().deselectAll(true);
- 261 :
topicStore.beginUpdate();
- 262 :
sel.view.getStore().query('docId', docId).each(function(item) {
- 263 :
var weights = item.get('weights');
- 264 :
weights.forEach(function(weight, index) {
- 265 :
topicStore.findRecord('index', index).set('weight', weight);
- 266 :
});
- 267 :
});
- 268 :
topicStore.endUpdate();
- 269 :
topicStore.sort('weight', 'DESC');
- 270 :
} else {
- 271 :
me.down('#topicsView').removeCls('showWeight').getSelectionModel().deselectAll(true);
- 272 :
topicStore.sort('index', 'ASC');
- 273 :
}
- 274 :
- 275 :
sel.view.refresh();
- 276 :
}
- 277 :
}
- 278 :
- 279 :
}],
- 280 :
dockedItems: {
- 281 :
dock: 'bottom',
- 282 :
xtype: 'toolbar',
- 283 :
overflowHandler: 'scroller',
- 284 :
items:[
- 285 :
'<span class="info-tip" data-qtip="'+this.localize('searchTip')+'">'+this.localize('search')+'</span>'
- 286 :
,{
- 287 :
xtype: 'textfield',
- 288 :
name: 'searchField',
- 289 :
hideLabel: true,
- 290 :
width: 80,
- 291 :
listeners: {
- 292 :
change: {
- 293 :
fn: me.onQuery,
- 294 :
scope: me,
- 295 :
buffer: 500
- 296 :
}
- 297 :
}
- 298 :
},
- 299 :
'<span class="info-tip" data-qtip="'+this.localize('limitTermsTip')+'">'+this.localize('limitTerms')+'</span>'
- 300 :
,{
- 301 :
width: 60,
- 302 :
hideLabel: true,
- 303 :
xtype: 'numberfield',
- 304 :
minValue: 1,
- 305 :
maxValue: 100,
- 306 :
listeners: {
- 307 :
afterrender: function(slider) {
- 308 :
slider.setValue(parseInt(this.getApiParam("termsPerTopic")))
- 309 :
},
- 310 :
change: function(slider, newvalue) {
- 311 :
this.setApiParams({termsPerTopic: newvalue});
- 312 :
},
- 313 :
scope: this
- 314 :
}
- 315 :
},
- 316 :
'<span class="info-tip" data-qtip="'+this.localize('numTopicsTip')+'">'+this.localize('numTopics')+'</span>'
- 317 :
,{
- 318 :
width: 60,
- 319 :
hideLabel: true,
- 320 :
xtype: 'numberfield',
- 321 :
minValue: 1,
- 322 :
maxValue: 100,
- 323 :
listeners: {
- 324 :
afterrender: function(slider) {
- 325 :
slider.setValue(parseInt(this.getApiParam("topics")))
- 326 :
},
- 327 :
change: function(slider, newvalue) {
- 328 :
this.setApiParams({topics: newvalue});
- 329 :
},
- 330 :
scope: this
- 331 :
}
- 332 :
},{
- 333 :
text: 'Run',//new Ext.Template(this.localize('runIterations')).apply([100]),
- 334 :
itemId: 'iterations',
- 335 :
glyph: 'xf04b@FontAwesome',
- 336 :
tooltip: this.localize('runIterationsTip'),
- 337 :
handler: this.runIterations,
- 338 :
scope: this
- 339 :
},{
- 340 :
text: 'Toggle diagnostics',
- 341 :
itemId: 'diagnostics',
- 342 :
glyph: 'xf129@FontAwesome',
- 343 :
handler: function(btn) {
- 344 :
me.down('#topicsView').toggleCls('showDiagnostics');
- 345 :
}
- 346 :
}]
- 347 :
}
- 348 :
});
- 349 :
- 350 :
this.callParent(arguments);
- 351 :
- 352 :
- 353 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 354 :
- 355 :
// create a listener for corpus loading (defined here, in case we need to load it next)
- 356 :
this.on('loadedCorpus', function(src, corpus) {
- 357 :
this.setCorpus(corpus);
- 358 :
if (this.rendered) {
- 359 :
this.initialize();
- 360 :
}
- 361 :
else {
- 362 :
this.on("afterrender", function() {
- 363 :
this.initialize();
- 364 :
}, this)
- 365 :
}
- 366 :
- 367 :
});
- 368 :
},
- 369 :
- 370 :
runIterations: function() {
- 371 :
var params = this.getApiParams();
- 372 :
params.tool = 'analysis.TopicModeling';
- 373 :
params.corpus = this.getCorpus().getAliasOrId();
- 374 :
params.noCache = 1;
- 375 :
- 376 :
var iterations = this.getApiParam('iterations');
- 377 :
var msg = Ext.MessageBox.progress({
- 378 :
title: this.localize("runningIterations"),
- 379 :
message: new Ext.Template(this.localize('runningIterationsCount')).apply([iterations])
- 380 :
});
- 381 :
- 382 :
Ext.Ajax.request({
- 383 :
url: this.getTromboneUrl(),
- 384 :
params: params,
- 385 :
success: function(response, req) {
- 386 :
msg.close();
- 387 :
- 388 :
var data = JSON.parse(response.responseText);
- 389 :
- 390 :
var topicsStore = this.down('#topicsView').getStore();
- 391 :
topicsStore.loadData(data.topicModeling.topics.map(function(topic, i) {
- 392 :
var words = topic.words.map(function(w) {
- 393 :
return w.word;
- 394 :
});
- 395 :
var diagnostics = Object.assign({}, topic);
- 396 :
delete diagnostics.words;
- 397 :
return [i, words, 0, diagnostics];
- 398 :
}));
- 399 :
- 400 :
data.topicModeling.topicDocuments.sort(function(a, b) {
- 401 :
var docIndexA = this.getCorpus().getDocument(a.docId).getIndex();
- 402 :
var docIndexB = this.getCorpus().getDocument(b.docId).getIndex();
- 403 :
return docIndexA-docIndexB;
- 404 :
}.bind(this));
- 405 :
this.down('#docsView').getStore().loadData(data.topicModeling.topicDocuments);
- 406 :
this.down('#docsView').refresh();
- 407 :
},
- 408 :
scope: this
- 409 :
});
- 410 :
},
- 411 :
- 412 :
getColorForTopic: function(topicIndex) {
- 413 :
return this.getApplication().getColor(topicIndex);
- 414 :
},
- 415 :
- 416 :
onQuery: function(cmp, query) {
- 417 :
var topicsView = this.down('#topicsView');
- 418 :
topicsView.getEl().query('.highlighted').forEach(function(hi) {
- 419 :
hi.classList.remove('highlighted');
- 420 :
});
- 421 :
- 422 :
if (query.trim() !== '') {
- 423 :
var matcher = new RegExp(query, 'gi');
- 424 :
var topicsStore = topicsView.getStore();
- 425 :
var indexes = [];
- 426 :
var matches = [];
- 427 :
topicsStore.each(function(record) {
- 428 :
var terms = record.get('terms');
- 429 :
var termMatches = [];
- 430 :
for (var i = 0; i < terms.length; i++) {
- 431 :
var term = terms[i];
- 432 :
if (term.search(matcher) !== -1) {
- 433 :
termMatches.push(i);
- 434 :
}
- 435 :
}
- 436 :
if (termMatches.length > 0) {
- 437 :
indexes.push(record.get('index'));
- 438 :
}
- 439 :
matches.push(termMatches);
- 440 :
});
- 441 :
if (indexes.length > 0) {
- 442 :
topicsView.setSelection(indexes.map(function(index) {return topicsStore.findRecord('index', index)}));
- 443 :
topicsView.getNodes().forEach(function(node, i) {
- 444 :
var nodeMatches = matches[i];
- 445 :
if (nodeMatches.length > 0) {
- 446 :
var terms = node.querySelectorAll('.term');
- 447 :
nodeMatches.forEach(function(termIndex) {
- 448 :
terms[termIndex].classList.add('highlighted');
- 449 :
})
- 450 :
}
- 451 :
});
- 452 :
} else {
- 453 :
this.setCurrentTopics([]);
- 454 :
topicsView.getSelectionModel().deselectAll(true);
- 455 :
this.down('#docsView').refresh();
- 456 :
}
- 457 :
} else {
- 458 :
this.setCurrentTopics([]);
- 459 :
topicsView.getSelectionModel().deselectAll(true);
- 460 :
this.down('#docsView').refresh();
- 461 :
}
- 462 :
},
- 463 :
- 464 :
initialize: function() {
- 465 :
this.runIterations();
- 466 :
},
- 467 :
- 468 :
getExtraDataExportItems: function() {
- 469 :
return [{
- 470 :
name: 'export',
- 471 :
inputValue: 'dataAsTsv',
- 472 :
boxLabel: this.localize('exportGridCurrentTsv')
- 473 :
}]
- 474 :
},
- 475 :
- 476 :
exportDataAsTsv: function(panel, form) {
- 477 :
var topicsValue = "Topic\t";
- 478 :
var docsValue = 'Document Title';
- 479 :
- 480 :
var topicOrder = [];
- 481 :
- 482 :
var includeDiagnostics = this.down('#topicsView').hasCls('showDiagnostics');
- 483 :
- 484 :
this.down('#topicsView').getStore().getData().each(function(record, i) {
- 485 :
if (i === 0) {
- 486 :
topicsValue += record.get('terms').map(function(t, i) { return 'Term '+i; }).join("\t");
- 487 :
if (includeDiagnostics) {
- 488 :
topicsValue += "\t"+Object.keys(record.get('diagnostics')).join("\t");
- 489 :
}
- 490 :
}
- 491 :
- 492 :
topicOrder.push(record.get('index'));
- 493 :
- 494 :
topicsValue += "\nTopic "+record.get('index')+"\t"+record.get('terms').join("\t");
- 495 :
if (includeDiagnostics) {
- 496 :
topicsValue += "\t"+Object.values(record.get('diagnostics')).join("\t");
- 497 :
}
- 498 :
docsValue += "\tTopic "+record.get('index')+' Weight';
- 499 :
});
- 500 :
- 501 :
this.down('#docsView').getStore().getData().each(function(record) {
- 502 :
var title = this.getCorpus().getDocument(record.get('docId')).getTitle();
- 503 :
- 504 :
var weights = topicOrder.map(function(topicIndex) {
- 505 :
var weight = record.get('weights')[topicIndex];
- 506 :
return Ext.util.Format.number(weight*100, "00.######");
- 507 :
}).join("\t");
- 508 :
- 509 :
docsValue += "\n"+title+"\t"+weights;
- 510 :
}, this);
- 511 :
- 512 :
Ext.create('Ext.window.Window', {
- 513 :
title: panel.localize('exportDataTitle'),
- 514 :
height: 290,
- 515 :
width: 450,
- 516 :
bodyPadding: 10,
- 517 :
layout: {
- 518 :
type: 'vbox',
- 519 :
pack: 'start',
- 520 :
align: 'stretch'
- 521 :
},
- 522 :
modal: true,
- 523 :
defaults: {
- 524 :
margin: '0 0 5px 0'
- 525 :
},
- 526 :
items: [{
- 527 :
html: panel.localize('exportDataTsvMessage')
- 528 :
},{
- 529 :
html: '<textarea class="x-form-text-default x-form-textarea" style="height: 76px; width: 100%">'+topicsValue+'</textarea>'
- 530 :
},{
- 531 :
html: '<textarea class="x-form-text-default x-form-textarea" style="height: 76px; width: 100%">'+docsValue+'</textarea>'
- 532 :
}],
- 533 :
buttonAlign: 'center',
- 534 :
buttons: [{
- 535 :
text: 'OK',
- 536 :
handler: function(btn) {
- 537 :
btn.up('window').close();
- 538 :
}
- 539 :
}]
- 540 :
}).show();
- 541 :
}
- 542 :
- 543 :
});