/**
* 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.
*
* @example
*
* let config = {
* "iterations": null,
* "perDocLimit": null,
* "seed": null,
* "stopList": null,
* "termsPerTopic": null,
* "topics": null
* };
*
* loadCorpus("austen").tool("Topics", config);
*
* @class Topics
* @tutorial topics
* @memberof Tools
*/
Ext.define('Voyant.panel.Topics', {
extend: 'Ext.panel.Panel',
mixins: ['Voyant.panel.Panel'],
alias: 'widget.topics',
statics: {
i18n: {
topics: 'Topics',
documents: 'Documents',
topicWeight: 'Topic weight'
},
api: {
/**
* @memberof Tools.Topics
* @instance
* @property {stopList}
* @default
*/
stopList: 'auto',
/**
* @memberof Tools.Topics
* @instance
* @property {Number} topics The number of topics.
* @default
*/
topics: 10,
/**
* @memberof Tools.Topics
* @instance
* @property {Number} termsPerTopic The number of terms per topic.
* @default
*/
termsPerTopic: 10,
/**
* @memberof Tools.Topics
* @instance
* @property {Number} iterations The number of iterations.
* @default
*/
iterations: 100,
/**
* @memberof Tools.Topics
* @instance
* @property {Number} perDocLimit The number of terms to limit each document to.
* @default
*/
perDocLimit: 1000,
/**
* @memberof Tools.Topics
* @instance
* @property {Number} seed The seed to use for random number generation.
* @default
*/
seed: 0
},
glyph: 'xf1ea@FontAwesome'
},
config: {
/**
* @private
*/
options: [{xtype: 'stoplistoption'},{
xtype: 'numberfield',
name: 'perDocLimit',
fieldLabel: 'maximum words per document',
labelAlign: 'right',
value: 1000,
minValue: 1,
step: 100,
listeners: {
afterrender: function(field) {
var win = field.up("window");
if (win && win.panel) {
field.setValue(parseInt(win.panel.getApiParam('perDocLimit')))
field.setFieldLabel(win.panel.localize("perDocLimit"))
}
},
change: function(field, val) {
var win = field.up("window");
if (val>5000 && win && win.panel) {
win.panel.toastInfo({
html: win.panel.localize("perDocLimitHigh"),
anchor: win.getTargetEl(),
align: 'tr',
maxWidth: 400
})
}
}
}
},{
xtype: 'numberfield',
name: 'iterations',
fieldLabel: 'iterations per run',
labelAlign: 'right',
value: 100,
minValue: 50,
maxValue: 1000,
step: 50,
listeners: {
afterrender: function(field) {
var win = field.up("window");
if (win && win.panel) {
field.setValue(parseInt(win.panel.getApiParam('iterations')))
field.setFieldLabel(win.panel.localize("iterations"))
}
}
}
},{
xtype: 'textfield',
name: 'seed',
fieldLabel: 'Random Seed',
labelAlign: 'right',
value: 0
}],
currentTopics: [],
currentDocument: undefined,
corpus: undefined
},
constructor: function(config) {
var me = this;
Ext.apply(this, {
title: this.localize('title'),
layout: {
type: 'hbox',
pack: 'start',
align: 'begin',
padding: '10px'
},
defaultType: 'dataview',
items: [{
itemId: 'topicsView',
flex: 2,
padding: '0 5px 0 0',
margin: '0 5px 0 0',
height: '100%',
scrollable: 'y',
store: Ext.create('Ext.data.ArrayStore',{
fields: ['index', 'terms', 'weight', 'diagnostics']
}),
selectionModel: {
type: 'dataviewmodel',
mode: 'MULTI'
},
itemSelector: 'div.topicItem',
tpl: new Ext.XTemplate(
'<div style="font-weight: bold">{[this.localize("topics")]}</div><tpl for=".">',
'<div class="topicItem" style="background-color: {[this.getColor(values.index)]}">',
'<div class="data weight" data-qtip="{[this.localize("topicWeight")]}">{[fm.number(values.weight*100, "00.0")]}%</div>',
'<span class="term">{[values.terms.join("</span> <span class=\\"term\\">")]}</span>',
'<div class="data diagnostics">{[this.processDiagnostics(values.diagnostics)]}</div>',
'</div>',
'</tpl>',
{
getColor: function(index) {
var rgb = me.getColorForTopic(index);
return 'rgba('+rgb.join(',')+',.33);'
},
localize: function(key) {
return me.localize(key);
},
processDiagnostics: function(obj) {
var string = '';
for (var key in obj) {
string += '<div><div class="key">'+key+'</div><div class="value">'+obj[key]+'</div></div>';
}
return string;
}
}
),
listeners: {
selectionchange: function(sel, selected) {
sel.view.removeCls('showWeight');
me.setCurrentDocument(undefined);
me.setCurrentTopics(selected.map(function(item) { return item.get('index') }));
me.down('#docsView').getSelectionModel().deselectAll(true);
me.down('#docsView').refresh();
}
}
},{
itemId: 'docsView',
flex: 1,
height: '100%',
scrollable: 'y',
store: Ext.create('Ext.data.JsonStore',{
fields: ['docId', 'weights']
}),
selectionModel: {
type: 'dataviewmodel',
mode: 'SINGLE',
allowDeselect: true,
toggleOnClick: true
},
itemSelector: 'div.topicItem',
tpl: new Ext.XTemplate(
'<div style="font-weight: bold">{[this.localize("documents")]}</div><tpl for=".">',
'<div class="topicItem">',
'{[this.getDocTitle(values.docId)]}',
'<div class="chart">{[this.getChart(values.docId, values.weights)]}</div>',
'</div>',
'</tpl>',
{
getDocTitle: function(docId) {
return me.getCorpus().getDocument(docId).getTitle();
},
getChart: function(docId, weights) {
var chart = '';
var topicStore = me.down('#topicsView').getStore();
topicStore.each(function(item) {
var index = item.get('index');
var weight = weights[index];
var rgb = me.getColorForTopic(index);
var alpha = me.getCurrentDocument() === docId ? '1' : me.getCurrentTopics().length === 0 ? '.33' : me.getCurrentTopics().indexOf(index) !== -1 ? '1' : '.15';
var color = 'rgba('+rgb.join(',')+','+alpha+')';
chart += '<div style="width: '+(weight*100)+'%; background-color: '+color+'"> </div>';
});
return chart;
},
localize: function(key) {
return me.localize(key);
}
}
),
listeners: {
selectionchange: function(sel, selected) {
me.setCurrentTopics([]);
var docId = selected[0] ? selected[0].get('docId') : undefined;
me.setCurrentDocument(docId);
var topicStore = me.down('#topicsView').getStore();
if (docId) {
me.down('#topicsView').addCls('showWeight').getSelectionModel().deselectAll(true);
topicStore.beginUpdate();
sel.view.getStore().query('docId', docId).each(function(item) {
var weights = item.get('weights');
weights.forEach(function(weight, index) {
topicStore.findRecord('index', index).set('weight', weight);
});
});
topicStore.endUpdate();
topicStore.sort('weight', 'DESC');
} else {
me.down('#topicsView').removeCls('showWeight').getSelectionModel().deselectAll(true);
topicStore.sort('index', 'ASC');
}
sel.view.refresh();
}
}
}],
dockedItems: {
dock: 'bottom',
xtype: 'toolbar',
overflowHandler: 'scroller',
items:[
'<span class="info-tip" data-qtip="'+this.localize('searchTip')+'">'+this.localize('search')+'</span>'
,{
xtype: 'textfield',
name: 'searchField',
hideLabel: true,
width: 80,
listeners: {
change: {
fn: me.onQuery,
scope: me,
buffer: 500
}
}
},
'<span class="info-tip" data-qtip="'+this.localize('limitTermsTip')+'">'+this.localize('limitTerms')+'</span>'
,{
width: 60,
hideLabel: true,
xtype: 'numberfield',
minValue: 1,
maxValue: 100,
listeners: {
afterrender: function(slider) {
slider.setValue(parseInt(this.getApiParam("termsPerTopic")))
},
change: function(slider, newvalue) {
this.setApiParams({termsPerTopic: newvalue});
},
scope: this
}
},
'<span class="info-tip" data-qtip="'+this.localize('numTopicsTip')+'">'+this.localize('numTopics')+'</span>'
,{
width: 60,
hideLabel: true,
xtype: 'numberfield',
minValue: 1,
maxValue: 100,
listeners: {
afterrender: function(slider) {
slider.setValue(parseInt(this.getApiParam("topics")))
},
change: function(slider, newvalue) {
this.setApiParams({topics: newvalue});
},
scope: this
}
},{
text: 'Run',//new Ext.Template(this.localize('runIterations')).apply([100]),
itemId: 'iterations',
glyph: 'xf04b@FontAwesome',
tooltip: this.localize('runIterationsTip'),
handler: this.runIterations,
scope: this
},{
text: 'Toggle diagnostics',
itemId: 'diagnostics',
glyph: 'xf129@FontAwesome',
handler: function(btn) {
me.down('#topicsView').toggleCls('showDiagnostics');
}
}]
}
});
this.callParent(arguments);
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
// create a listener for corpus loading (defined here, in case we need to load it next)
this.on('loadedCorpus', function(src, corpus) {
this.setCorpus(corpus);
if (this.rendered) {
this.initialize();
}
else {
this.on("afterrender", function() {
this.initialize();
}, this)
}
});
},
runIterations: function() {
var params = this.getApiParams();
params.tool = 'analysis.TopicModeling';
params.corpus = this.getCorpus().getAliasOrId();
params.noCache = 1;
var iterations = this.getApiParam('iterations');
var msg = Ext.MessageBox.progress({
title: this.localize("runningIterations"),
message: new Ext.Template(this.localize('runningIterationsCount')).apply([iterations])
});
Ext.Ajax.request({
url: this.getTromboneUrl(),
params: params,
success: function(response, req) {
msg.close();
var data = JSON.parse(response.responseText);
var topicsStore = this.down('#topicsView').getStore();
topicsStore.loadData(data.topicModeling.topics.map(function(topic, i) {
var words = topic.words.map(function(w) {
return w.word;
});
var diagnostics = Object.assign({}, topic);
delete diagnostics.words;
return [i, words, 0, diagnostics];
}));
data.topicModeling.topicDocuments.sort(function(a, b) {
var docIndexA = this.getCorpus().getDocument(a.docId).getIndex();
var docIndexB = this.getCorpus().getDocument(b.docId).getIndex();
return docIndexA-docIndexB;
}.bind(this));
this.down('#docsView').getStore().loadData(data.topicModeling.topicDocuments);
this.down('#docsView').refresh();
},
scope: this
});
},
getColorForTopic: function(topicIndex) {
return this.getApplication().getColor(topicIndex);
},
onQuery: function(cmp, query) {
var topicsView = this.down('#topicsView');
topicsView.getEl().query('.highlighted').forEach(function(hi) {
hi.classList.remove('highlighted');
});
if (query.trim() !== '') {
var matcher = new RegExp(query, 'gi');
var topicsStore = topicsView.getStore();
var indexes = [];
var matches = [];
topicsStore.each(function(record) {
var terms = record.get('terms');
var termMatches = [];
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
if (term.search(matcher) !== -1) {
termMatches.push(i);
}
}
if (termMatches.length > 0) {
indexes.push(record.get('index'));
}
matches.push(termMatches);
});
if (indexes.length > 0) {
topicsView.setSelection(indexes.map(function(index) {return topicsStore.findRecord('index', index)}));
topicsView.getNodes().forEach(function(node, i) {
var nodeMatches = matches[i];
if (nodeMatches.length > 0) {
var terms = node.querySelectorAll('.term');
nodeMatches.forEach(function(termIndex) {
terms[termIndex].classList.add('highlighted');
})
}
});
} else {
this.setCurrentTopics([]);
topicsView.getSelectionModel().deselectAll(true);
this.down('#docsView').refresh();
}
} else {
this.setCurrentTopics([]);
topicsView.getSelectionModel().deselectAll(true);
this.down('#docsView').refresh();
}
},
initialize: function() {
this.runIterations();
},
getExtraDataExportItems: function() {
return [{
name: 'export',
inputValue: 'dataAsTsv',
boxLabel: this.localize('exportGridCurrentTsv')
}]
},
exportDataAsTsv: function(panel, form) {
var topicsValue = "Topic\t";
var docsValue = 'Document Title';
var topicOrder = [];
var includeDiagnostics = this.down('#topicsView').hasCls('showDiagnostics');
this.down('#topicsView').getStore().getData().each(function(record, i) {
if (i === 0) {
topicsValue += record.get('terms').map(function(t, i) { return 'Term '+i; }).join("\t");
if (includeDiagnostics) {
topicsValue += "\t"+Object.keys(record.get('diagnostics')).join("\t");
}
}
topicOrder.push(record.get('index'));
topicsValue += "\nTopic "+record.get('index')+"\t"+record.get('terms').join("\t");
if (includeDiagnostics) {
topicsValue += "\t"+Object.values(record.get('diagnostics')).join("\t");
}
docsValue += "\tTopic "+record.get('index')+' Weight';
});
this.down('#docsView').getStore().getData().each(function(record) {
var title = this.getCorpus().getDocument(record.get('docId')).getTitle();
var weights = topicOrder.map(function(topicIndex) {
var weight = record.get('weights')[topicIndex];
return Ext.util.Format.number(weight*100, "00.######");
}).join("\t");
docsValue += "\n"+title+"\t"+weights;
}, this);
Ext.create('Ext.window.Window', {
title: panel.localize('exportDataTitle'),
height: 290,
width: 450,
bodyPadding: 10,
layout: {
type: 'vbox',
pack: 'start',
align: 'stretch'
},
modal: true,
defaults: {
margin: '0 0 5px 0'
},
items: [{
html: panel.localize('exportDataTsvMessage')
},{
html: '<textarea class="x-form-text-default x-form-textarea" style="height: 76px; width: 100%">'+topicsValue+'</textarea>'
},{
html: '<textarea class="x-form-text-default x-form-textarea" style="height: 76px; width: 100%">'+docsValue+'</textarea>'
}],
buttonAlign: 'center',
buttons: [{
text: 'OK',
handler: function(btn) {
btn.up('window').close();
}
}]
}).show();
}
});