/**
* ScatterPlot is a graph visualization of how words cluster in a corpus document similarity, correspondence analysis or principal component analysis.
*
* @example
*
* let config = {
* "analysis": null,
* "bins": null,
* "clusters": null,
* "comparisonType": null,
* "dimensions": null,
* "docId": null,
* "iterations": null,
* "label": null,
* "limit": null,
* "perplexity": null,
* "query": null,
* "stopList": null,
* "storeJson": null,
* "target": null,
* "term": null,
* "whitelist": null,
* };
*
* loadCorpus("austen").tool("scatterplot", config);
*
* @class ScatterPlot
* @tutorial scatterplot
* @memberof Tools
*/
Ext.define('Voyant.panel.ScatterPlot', {
extend: 'Ext.panel.Panel',
mixins: ['Voyant.panel.Panel'],
requires: ['Ext.chart.CartesianChart'],
alias: 'widget.scatterplot',
statics: {
i18n: {
},
api: {
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {docId}
*/
docId: undefined,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String} analysis The type of analysis to perform. Options are: 'ca', 'pca', 'tsne', and 'docSim'.
*/
analysis: 'ca',
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {limit}
* @default
*/
limit: 50,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {Number} dimensions The number of dimensions to render, either 2 or 3.
* @default
*/
dimensions: 3,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {bins}
* @default
*/
bins: 10,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {Number} clusters The number of clusters within which to group words.
* @default
*/
clusters: 3,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {Number} perplexity The TSNE perplexity value.
* @default
*/
perplexity: 15,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {Number} iterations The TSNE iterations value.
* @default
*/
iterations: 1500,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String} comparisonType The value to use for comparing terms. Options are: 'raw', 'relative', and 'tfidf'.
* @default
*/
comparisonType: 'relative',
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {stopList}
* @default
*/
stopList: 'auto',
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String} target The term to set as the target. This will filter results to terms that are near the target.
*/
target: undefined,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String[]} term Used in combination with "target" as a white list of terms to keep.
*/
term: undefined,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {query}
*/
query: undefined,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String} whitelist TODO Unused or only used in CA?
*/
whitelist: undefined,
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String[]} label The label types to show. One or more of: 'summary', 'docs', and 'terms'.
*/
label: ['summary', 'docs', 'terms'],
/**
* @memberof Tools.ScatterPlot
* @instance
* @property {String} storeJson TODO used in embed
*/
storeJson: undefined
},
glyph: 'xf06e@FontAwesome'
},
config: {
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
caStore: null,
pcaStore: null,
tsneStore: null,
docSimStore: null,
termStore: null,
chartMenu: null,
newTerm: null,
termsTimeout: null,
highlightData: {x: 0, y: 0, r: 0},
highlightTask: null
},
tokenFreqTipTemplate: null,
docFreqTipTemplate: null,
constructor: function(config) {
this.mixins['Voyant.util.Api'].constructor.apply(this, arguments);
if ("storeJson" in config) {
var json = JSON.parse(config.storeJson);
Ext.apply(config, json);
}
this.callParent(arguments);
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
},
initComponent: function() {
this.setCaStore(Ext.create('Voyant.data.store.CAAnalysis', {
listeners: {load: this.maskAndBuildChart, scope: this}
}));
this.setPcaStore(Ext.create('Voyant.data.store.PCAAnalysis', {
listeners: {load: this.maskAndBuildChart, scope: this}
}));
this.setTsneStore(Ext.create('Voyant.data.store.TSNEAnalysis', {
listeners: {load: this.maskAndBuildChart, scope: this}
}));
this.setDocSimStore(Ext.create('Voyant.data.store.DocSimAnalysis', {
listeners: {load: this.maskAndBuildChart, scope: this}
}));
this.setTermStore(Ext.create('Ext.data.JsonStore', {
fields: [
{name: 'term'},
{name: 'rawFreq', type: 'int'},
{name: 'relativeFreq', type: 'number'},
{name: 'coordinates', mapping : 'vector'},
{name: 'category'}
],
sorters: [{property: 'rawFreq', direction: 'DESC'}],
groupField: 'category'
}));
this.setChartMenu(Ext.create('Ext.menu.Menu', {
items: [
{text: this.localize('remove'), itemId: 'remove', glyph: 'xf068@FontAwesome'},
{text: this.localize('nearby'), itemId: 'nearby', glyph: 'xf0b2@FontAwesome'}
],
listeners: {
hide: function() {
var series = this.down('#chart').getSeries();
series[0].enableToolTips();
series[1].enableToolTips();
},
scope: this
}
}));
this.tokenFreqTipTemplate = new Ext.Template(this.localize('tokenFreqTip'));
this.docFreqTipTemplate = new Ext.Template(this.localize('docFreqTip'));
Ext.apply(this, {
title: this.localize('title'),
layout: 'border',
autoDestroy: true,
items: [{
itemId: 'chartParent',
region: 'center',
layout: 'fit',
tbar: {
overflowHandler: 'scroller',
items: [{
xtype: 'querysearchfield',
itemId: 'filterTerms',
width: 150
},{
text: this.localize('labels'),
itemId: 'labels',
glyph: 'xf02b@FontAwesome',
menu: {
items: [
{text: this.localize("summaryLabel"), itemId: 'summary', xtype: 'menucheckitem'},
{text: this.localize("docsLabel"), itemId: 'docs', xtype: 'menucheckitem'},
{text: this.localize("termsLabel"), itemId: 'terms', xtype: 'menucheckitem'}
],
listeners: {
afterrender: function(menu) {
var labels = this.getApiParam('label');
menu.items.each(function(item) {
item.setChecked(labels.indexOf(item.getItemId())>-1)
})
},
click: function(menu, item) {
var labels = this.getApiParam("label");
var label = item.getItemId();
if (Ext.isString(labels)) {labels = [labels]}
if (item.checked && labels.indexOf(label)==-1) {
labels.push(label)
} else if (!item.checked && labels.indexOf(label)>-1) {
labels = labels.filter(function(item) {return item!=label})
}
this.setApiParam("label", labels);
this.doLabels();
this.queryById('chart').redraw();
},
scope: this
}
}
}]
},
listeners: {
query: function(component, value) {
this.getTermStore().filter([{property: 'term', value: value, anyMatch: true}]);
this.filterChart(value);
},
scope: this
}
},{
itemId: 'optionsPanel',
title: this.localize('options'),
region: 'west',
split: true,
collapsible: true,
collapseMode: 'header',
width: 135,
scrollable: 'y',
layout: {
type: 'vbox',
align: 'stretch'
},
defaults: {
xtype: 'button',
margin: '5',
labelAlign: 'top'
},
items: [{
xtype: 'label',
text: this.localize('input')
},{
xtype: 'documentselectorbutton'
},{
text: this.localize('freqsMode'),
itemId: 'comparisonType',
glyph: 'xf201@FontAwesome',
tooltip: this.localize('freqsModeTip'),
menu: {
items: [
{text: this.localize("rawFrequencies"), itemId: 'comparisonType_raw', group: 'freqsMode', xtype: 'menucheckitem'},
{text: this.localize("relativeFrequencies"), itemId: 'comparisonType_relative', group: 'freqsMode', xtype: 'menucheckitem'},
{text: this.localize("tfidf"), itemId: 'comparisonType_tfidf', group: 'freqsMode', xtype: 'menucheckitem'}
],
listeners: {
click: function(menu, item) {
if (item !== undefined) {
var type = item.getItemId().split('_')[1];
if (type !== this.getApiParam('comparisonType')) {
this.setApiParam('comparisonType', type);
this.loadFromApis(true);
}
}
},
scope: this
}
}
},{
fieldLabel: this.localize('numTerms'),
itemId: 'limit',
xtype: 'numberfield',
minValue: 5,
listeners: {
change: function(numb, newValue, oldValue) {
function doLoad() {
this.setApiParam('limit', newValue);
this.loadFromApis();
}
if (oldValue !== null) {
if (this.getTermsTimeout() !== null) {
clearTimeout(this.getTermsTimeout());
}
if (numb.isValid()) {
this.setTermsTimeout(setTimeout(doLoad.bind(this), 500));
}
}
},
scope: this
}
},{
xtype: 'container',
html: '<hr style="border: none; border-top: 1px solid #cfcfcf;"/>'
},{
xtype: 'label',
text: this.localize('output')
},{
text: this.localize('analysis'),
itemId: 'analysis',
glyph: 'xf1ec@FontAwesome',
overflowHandler: 'scroller',
menu: {
items: [
{text: this.localize('pca'), itemId: 'analysis_pca', group:'analysis', xtype: 'menucheckitem'},
{text: this.localize('ca'), itemId: 'analysis_ca', group:'analysis', xtype: 'menucheckitem'},
{text: this.localize('tsne'), itemId: 'analysis_tsne', group:'analysis', xtype: 'menucheckitem'},
{text: this.localize('docSim'), itemId: 'analysis_docSim', group:'analysis', xtype: 'menucheckitem'}
],
listeners: {
click: function(menu, item) {
if (item !== undefined) {
var analysis = item.getItemId().split('_')[1];
if (analysis !== this.getApiParam('analysis')) {
this.doAnalysisChange(analysis);
this.loadFromApis(true);
}
}
},
scope: this
}
}
},{
fieldLabel: this.localize('perplexity'),
itemId: 'perplexity',
xtype: 'slider',
minValue: 5,
maxValue: 100,
increment: 1,
listeners: {
changecomplete: function(slider, newValue) {
this.setApiParam('perplexity', newValue);
this.loadFromApis(true);
},
scope: this
}
},{
fieldLabel: this.localize('iterations'),
itemId: 'iterations',
xtype: 'slider',
minValue: 100,
maxValue: 5000,
increment: 100,
listeners: {
changecomplete: function(slider, newValue) {
this.setApiParam('iterations', newValue);
this.loadFromApis(true);
},
scope: this
}
},{
text: this.localize('clusters'),
itemId: 'clusters',
glyph: 'xf192@FontAwesome',
menu: {
items: [
{text: '1', itemId: 'clusters_1', group: 'clusters', xtype: 'menucheckitem'},
{text: '2', itemId: 'clusters_2', group: 'clusters', xtype: 'menucheckitem'},
{text: '3', itemId: 'clusters_3', group: 'clusters', xtype: 'menucheckitem'},
{text: '4', itemId: 'clusters_4', group: 'clusters', xtype: 'menucheckitem'},
{text: '5', itemId: 'clusters_5', group: 'clusters', xtype: 'menucheckitem'}
],
listeners: {
click: function(menu, item) {
if (item !== undefined) {
var clusters = parseInt(item.getItemId().split('_')[1]);
if (clusters !== this.getApiParam('clusters')) {
this.setApiParam('clusters', clusters);
this.loadFromApis(true);
}
}
},
scope: this
}
}
},{
text: this.localize('dimensions'),
itemId: 'dimensions',
glyph: 'xf1b2@FontAwesome',
menu: {
items: [
{text: '2', itemId: 'dimensions_2', group: 'dimensions', xtype: 'menucheckitem'},
{text: '3', itemId: 'dimensions_3', group: 'dimensions', xtype: 'menucheckitem'}
],
listeners: {
click: function(menu, item) {
if (item !== undefined) {
var dims = parseInt(item.getItemId().split('_')[1]);
if (dims !== this.getApiParam('dimensions')) {
if (dims == 3 && this.getApiParam('analysis') == 'ca' && this.getCorpus().getDocumentsCount() == 3) {
dims = 2;
// TODO add info message 'Because of the nature of Correspondence Analysis, you can only use 2 dimensions with 3 documents.'
return false;
}
this.setApiParam('dimensions', dims);
this.loadFromApis(true);
}
}
},
scope: this
}
}
},{
itemId: 'reloadButton',
text: this.localize('reload'),
glyph: 'xf021@FontAwesome',
handler: function() {
this.loadFromApis();
},
scope: this
}]
},{
itemId: 'termsGrid',
xtype: 'grid',
title: this.localize('terms'),
region: 'east',
width: 250,
split: true,
collapsible: true,
collapseMode: 'header',
forceFit: true,
features: [{
ftype: 'grouping',
hideGroupedHeader: true,
enableGroupingMenu: false
}],
bbar: {
overflowHandler: 'scroller',
items: [{
itemId: 'nearbyButton',
xtype: 'button',
text: this.localize('nearby'),
glyph: 'xf0b2@FontAwesome',
flex: 1,
handler: function(btn) {
var sel = btn.up('panel').getSelection()[0];
if (sel === undefined) {
this.toastError({
html: this.localize("noTermSelected"),
anchor: btn.up("panel").getTargetEl()
});
}
else {
var term = sel.get('term');
this.getNearbyForTerm(term);
}
},
scope: this
},{
itemId: 'removeButton',
xtype: 'button',
text: this.localize('remove'),
glyph: 'xf068@FontAwesome',
flex: 1,
handler: function(btn) {
var sel = btn.up('panel').getSelection()[0];
if (sel === undefined) {
this.toastError({
html: this.localize("noTermSelected"),
anchor: btn.up("panel").getTargetEl()
});
}
else {
var term = sel.get('term');
this.removeTerm(term);
}
},
scope: this
}]
},
tbar: {
overflowHandler: 'scroller',
items: [{
xtype: 'querysearchfield',
itemId: 'addTerms',
// emptyText: this.localize('addTerm'),
flex: 1
}]
},
columns: [{
text: this.localize('term'),
dataIndex: 'term',
flex: 1,
sortable: true
},{
text: this.localize('rawFreq'),
dataIndex: 'rawFreq',
flex: 0.75,
minWidth: 70,
sortable: true
},{
text: this.localize('relFreq'),
dataIndex: 'relativeFreq',
flex: 0.75,
minWidth: 70,
sortable: true,
hidden: true
}],
selModel: {
type: 'rowmodel',
mode: 'SINGLE',
allowDeselect: true,
toggleOnClick: true,
listeners: {
selectionchange: {
fn: function(sm, selections) {
// this.getApplication().dispatchEvent('corpusTermsClicked', this, selections);
var sel = selections[0];
if (sel !== undefined) {
var term = sel.get('term');
var isDoc = sel.get('category') === 'document';
this.selectTerm(term, isDoc);
if (isDoc) {
this.queryById('nearbyButton').disable();
this.queryById('removeButton').disable();
} else {
this.queryById('nearbyButton').enable();
this.queryById('removeButton').enable();
}
} else {
this.selectTerm();
}
},
scope: this
}
}
},
store: this.getTermStore(),
listeners: {
expand: function(panel) {
panel.getView().refresh();
},
query: function(component, value) {
if (value.length > 0 && this.getTermStore().findExact('term', value[0]) === -1) {
this.setNewTerm(value);
this.loadFromApis();
} else {
this.setNewTerm(null);
}
},
scope: this
}
}]
});
this.on('boxready', function(component, width, height) {
if (width < 400) {
this.queryById('optionsPanel').collapse();
this.queryById('termsGrid').collapse();
}
if (this.config.storeClass && this.config.storeData) {
this.loadStoreFromJson(this.config.storeClass, this.config.storeData);
}
}, this);
this.on('beforedestroy', function(component) {
var oldChart = this.queryById('chart');
if (oldChart !== null) {
this.queryById('chartParent').remove(oldChart);
}
}, this);
// create a listener for corpus loading (defined here, in case we need to load it next)
this.on('loadedCorpus', function(src, corpus) {
function setCheckItemFromApi(apiParamName) {
var value = this.getApiParam(apiParamName);
var menu = this.queryById(apiParamName);
var item = menu.down('#'+apiParamName+'_'+value);
item.setChecked(true);
}
var setCheckBound = setCheckItemFromApi.bind(this);
setCheckBound('analysis');
this.doAnalysisChange(this.getApiParam('analysis'));
setCheckBound('comparisonType');
setCheckBound('clusters');
this.queryById('perplexity').setValue(this.getApiParam('perplexity'));
this.queryById('iterations').setValue(this.getApiParam('iterations'));
if (corpus.getDocumentsCount() == 3) {
this.setApiParam('dimensions', 2);
}
setCheckBound('dimensions');
this.getCaStore().setCorpus(corpus);
this.getPcaStore().setCorpus(corpus);
this.getDocSimStore().setCorpus(corpus);
this.loadFromApis();
}, this);
this.on('documentsSelected', function(src, docIds) {
this.setApiParam('docId', docIds);
this.loadFromApis();
}, this);
this.callParent(arguments);
},
doAnalysisChange: function(analysis) {
this.setApiParam('analysis', analysis);
this.queryById('nearbyButton').setDisabled(analysis === 'tsne');
this.queryById('reloadButton').setVisible(analysis === 'tsne');
this.queryById('perplexity').setVisible(analysis === 'tsne');
this.queryById('iterations').setVisible(analysis === 'tsne');
if (analysis === 'ca') {
// TODO handling for when there's no corpus
if (this.getCorpus().getDocumentsCount() == 3) {
this.setApiParam('dimensions', 2);
this.queryById('dimensions').menu.items.get(0).setChecked(true); // need 1-2 docs or 4+ docs for 3 dimensions
}
}
},
loadStoreFromJson: function(storeClass, storeData) {
if (storeClass == 'Voyant.data.store.CAAnalysis') {
this.getCaStore().loadRawData(storeData);
this.doAnalysisChange('ca');
this.maskAndBuildChart.call(this, this.getCaStore());
} else if (storeClass == 'Voyant.data.store.PCAAnalysis') {
this.getPcaStore().loadRawData(storeData);
this.doAnalysisChange('pca');
this.maskAndBuildChart.call(this, this.getPcaStore());
} else if (storeClass == 'Voyant.data.store.TSNEAnalysis') {
this.getTsneStore().loadRawData(storeData);
this.doAnalysisChange('tsne');
this.maskAndBuildChart.call(this, this.getTsneStore());
} else if (storeClass == 'Voyant.data.store.DocSimAnalysis') {
this.getDocSimStore().loadRawData(storeData);
this.doAnalysisChange('docSim');
this.maskAndBuildChart.call(this, this.getDocSimStore());
}
},
maskAndBuildChart: function(store) {
this.queryById('chartParent').mask(this.localize('plotting'));
Ext.defer(this.buildChart, 50, this, [store]);
},
buildChart: function(store) {
var that = this; // needed for tooltip renderer
var oldChart = this.queryById('chart');
if (oldChart !== null) {
this.queryById('chartParent').remove(oldChart);
}
this.queryById('termsGrid').getSelectionModel().deselectAll();
var rec = store.getAt(0);
var numDims = this.getApiParam('dimensions');
var summary = '';
if (this.getApiParam('analysis') === 'pca') {
// calculate the percentage of original data represented by the dominant principal components
var pcs = rec.getPrincipalComponents();
var eigenTotal = 0;
for (var i = 0; i < pcs.length; i++) {
var pc = pcs[i];
eigenTotal += parseFloat(pc.get('eigenValue'));
}
if (eigenTotal == 0) {
// do nothing
} else {
summary = this.localize('pcTitle')+'\n';
var pcMapping = ['xAxis', 'yAxis', 'fill'];
for (var i = 0; i < pcs.length; i++) {
if (i >= numDims) break;
var eigenValue = pcs[i].get('eigenValue');
var percentage = eigenValue / eigenTotal * 100;
summary += this.localize('pc')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%\n';
}
}
} else if (this.getApiParam('analysis') === 'tsne') {
} else {
summary = this.localize('caTitle')+'\n';
var pcMapping = ['xAxis', 'yAxis', 'fill'];
var dimensions = rec.getDimensions();
for (var i = 0; i < dimensions.length; i++) {
if (i >= numDims) break;
var percentage = dimensions[i].get('percentage');
summary += this.localize('dimension')+' '+(i+1)+' ('+this.localize(pcMapping[i])+'): '+Math.round(percentage*100)/100+'%\n';
}
}
var maxFreq = 0;
var minFreq = Number.MAX_VALUE;
var maxFill = 0;
var minFill = Number.MAX_VALUE;
if (this.getApiParam('analysis') !== 'docSim') { // docSim doesn't return terms so keep the current ones
this.getTermStore().removeAll();
}
var tokens = rec.getTokens();
var termData = [];
var docData = [];
tokens.forEach(function(token) {
var freq = token.get('rawFreq');
var category = token.get('category');
if (category === undefined) {
category = 'term'; // some analyses don't define categories
token.set('category', 'term');
}
var isTerm = category === 'term';
if (isTerm) {
if (freq > maxFreq) maxFreq = freq;
if (freq < minFreq) minFreq = freq;
}
if (this.getTermStore().findExact('term', token.get('term') === -1)) {
this.getTermStore().addSorted(token);
}
if (numDims === 3) {
var z = token.get('vector')[2];
if (z !== undefined) {
if (z < minFill) minFill = z;
if (z > maxFill) maxFill = z;
}
}
var tokenData = {
x: token.get('vector')[0], y: token.get('vector')[1] || 0, z: token.get('vector')[2] || 0,
term: token.get('term'), rawFreq: freq, relativeFreq: token.get('relativeFreq'), cluster: token.get('cluster'), category: category,
disabled: false
};
if (!isTerm) {
if (token.get('category') === 'bin') {
tokenData.term = tokenData.title = "Bin "+token.get('docIndex');
} else {
tokenData.docIndex = token.get('docIndex');
var doc = this.getCorpus().getDocument(tokenData.docIndex);
if (doc !== null) {
tokenData.term = doc.getShortTitle();
tokenData.title = doc.getTitle();
}
}
docData.push(tokenData);
} else {
termData.push(tokenData);
}
}, this);
var newCount = this.getTermStore().getCount();
this.queryById('limit').setRawValue(newCount);
this.setApiParam('limit', newCount);
var termSeriesStore = Ext.create('Ext.data.JsonStore', {
fields: ['term', 'x', 'y', 'z', 'rawFreq', 'relativeFreq', 'cluster', 'category', 'docIndex', 'disabled'],
data: termData
});
var docSeriesStore = Ext.create('Ext.data.JsonStore', {
fields: ['term', 'x', 'y', 'z', 'rawFreq', 'relativeFreq', 'cluster', 'category', 'docIndex', 'disabled'],
data: docData
});
var config = {
itemId: 'chart',
xtype: 'cartesian',
interactions: ['crosszoom','panzoom','itemhighlight'],
plugins: {
ptype: 'chartitemevents'
},
axes: [{
type: 'numeric',
position: 'bottom',
fields: ['x'],
label: {
rotate:{degrees:-30}
}
},{
type: 'numeric',
position: 'left',
fields: ['y']
}],
sprites: [{
type: 'text',
text: summary,
x: 70,
y: 70
}],
innerPadding: {top: 25, right: 25, bottom: 25, left: 25},
series: [{
type: 'customScatter',
xField: 'x',
yField: 'y',
store: termSeriesStore,
label: {
font: '14px Helvetica',
field: 'term',
display: 'over'
},
tooltip: {
trackMouse: true,
style: 'background: #fff',
renderer: function (toolTip, record, ctx) {
toolTip.setHtml(that.tokenFreqTipTemplate.apply([record.get('term'),record.get('rawFreq'),record.get('relativeFreq')]));
}
},
marker: {
type: 'circle'
},
highlight: {
fillStyle: 'yellow',
strokeStyle: 'black'
},
renderer: function (sprite, config, rendererData, index) {
var store = rendererData.store;
var item = store.getAt(index);
if (item !== null) {
var clusterIndex = item.get('cluster');
var scatterplot = that;
if (clusterIndex === -1) {
// no clusters were specified in initial call
clusterIndex = 0;
}
var fillAlpha = 0.65;
var strokeAlpha = 1;
if (item.get('disabled') === true) {
fillAlpha = 0.1;
strokeAlpha = 0.1;
} else if (numDims === 3 && item.get('z')) {
fillAlpha = scatterplot.interpolate(item.get('z'), minFill, maxFill, 0, 1);
}
var color = scatterplot.getApplication().getColor(clusterIndex);
config.fillStyle = 'rgba('+color.join(',')+','+fillAlpha+')';
config.strokeStyle = 'rgba('+color.join(',')+','+strokeAlpha+')';
var freq = item.get('rawFreq');
var radius = scatterplot.interpolate(freq, minFreq, maxFreq, 2, 20);
config.radius = radius;
}
},
scope: this
},{
type: 'customScatter',
xField: 'x',
yField: 'y',
store: docSeriesStore,
label: {
font: '14px Helvetica',
field: 'term',
display: 'over',
color: this.getDefaultDocColor(true)
},
tooltip: {
trackMouse: true,
style: 'background: #fff',
renderer: function (toolTip, record, ctx) {
toolTip.setHtml(that.docFreqTipTemplate.apply([record.get('title'),record.get('rawFreq')]));
}
},
marker: {
type: 'diamond'
},
highlight: {
fillStyle: 'yellow',
strokeStyle: 'black'
},
renderer: function (sprite, config, rendererData, index) {
var store = rendererData.store;
var item = store.getAt(index);
if (item !== null) {
var clusterIndex = item.get('cluster');
var scatterplot = that;
var color;
if (clusterIndex === -1 || scatterplot.getApiParam('analysis') !== 'docSim') {
color = scatterplot.getDefaultDocColor();
} else {
color = scatterplot.getApplication().getColor(clusterIndex);
}
var a = 0.65;
if (numDims === 3 && item.get('z')) {
a = scatterplot.interpolate(item.get('z'), minFill, maxFill, 0, 1);
}
config.fillStyle = 'rgba('+color.join(',')+','+a+')';
config.strokeStyle = 'rgba('+color.join(',')+',1)';
config.radius = 5;
}
},
scope: this
}],
listeners: {
itemclick: function(chart, item, event) {
var data = item.record.data;
if (data.category === 'doc') {
var record = this.getCorpus().getDocument(data.docIndex);
this.getApplication().dispatchEvent('documentsClicked', this, [record]);
} else if (data.category === 'term') {
var record = Ext.create('Voyant.data.model.CorpusTerm', data);
this.getApplication().dispatchEvent('corpusTermsClicked', this, [record]);
}
},
render: function(chart) {
chart.body.on('contextmenu', function(event, target) {
event.preventDefault();
var xy = event.getXY();
var parentXY = Ext.fly(target).getXY();
var x = xy[0] - parentXY[0];
var y = xy[1] - parentXY[1];
var chartItem = this.down('#chart').getItemForPoint(x,y);
if (chartItem != null && chartItem.record.get('category') === 'term') {
var series = this.down('#chart').getSeries();
series[0].disableToolTips();
series[1].disableToolTips();
var term = chartItem.record.get('term');
var text = (new Ext.Template(this.localize('removeTerm'))).apply([term]);
this.getChartMenu().queryById('remove').setText(text);
text = (new Ext.Template(this.localize('nearbyTerm'))).apply([term]);
var nearby = this.getChartMenu().queryById('nearby');
nearby.setText(text);
nearby.setDisabled(this.getApiParam('analysis') === 'tsne');
this.getChartMenu().on('click', function(menu, item) {
if (item !== undefined) {
var term = chartItem.record.get('term');
if (item.getItemId() === 'nearby') {
this.getNearbyForTerm(term);
} else {
this.removeTerm(term);
}
}
}, this, {single: true});
this.getChartMenu().showAt(xy);
}
}, this);
},
scope: this
}
};
var chart = Ext.create('Ext.chart.CartesianChart', config);
this.queryById('chartParent').insert(0, chart);
this.queryById('chartParent').unmask();
this.doLabels();
if (this.getNewTerm() !== null) {
this.selectTerm(this.getNewTerm()[0]);
this.setNewTerm(null);
}
},
getDefaultDocColor: function(returnHex) {
var color = this.getApplication().getColor(6, returnHex);
return color;
},
doLabels: function() {
var chart = this.queryById('chart');
var series = chart.getSeries();
var summary = chart.getSurface('chart').getItems()[0];
var labels = this.getApiParam("label");
if (labels.indexOf("summary")>-1) {summary.show();}
else {summary.hide();}
if (labels.indexOf("terms")>-1) {series[0].getLabel().show();}
else {series[0].getLabel().hide();}
if (labels.indexOf("docs")>-1) {series[1].getLabel().show();}
else {series[1].getLabel().hide();}
},
selectTerm: function(term, isDoc) {
var chart = this.down('#chart');
if (chart !== null) {
if (term === undefined) {
chart.getSeries()[0].setHighlightItem(null);
chart.getSeries()[1].setHighlightItem(null);
} else {
var series, index;
if (isDoc === true) {
series = chart.getSeries()[1];
index = series.getStore().findExact('title', term);
} else {
series = chart.getSeries()[0];
index = series.getStore().findExact('term', term);
}
if (index !== -1) {
var record = series.getStore().getAt(index);
var sprite = series.getSprites()[0];
// constructing series item, like in the chart series source
var item = {
series: series,
category: series.getItemInstancing() ? 'items' : 'markers',
index: index,
record: record,
field: series.getYField(),
sprite: sprite
};
series.setHighlightItem(item);
if (isDoc) {
chart.getSeries()[0].setHighlightItem(null);
} else {
chart.getSeries()[1].setHighlightItem(null);
}
var point = this.getPointFromIndex(series, index);
this.setHighlightData({x: point[0], y: point[1], r: 50});
if (this.getHighlightTask() == null) {
this.setHighlightTask(Ext.TaskManager.newTask({
run: this.doHighlight,
scope: this,
interval: 25,
repeat: this.getHighlightData().r
}));
}
this.getHighlightTask().restart();
}
}
}
},
getPointFromIndex: function(series, index) {
var sprite = series.getSprites()[0];
if (sprite.surfaceMatrix !== null) {
var matrix = sprite.attr.matrix.clone().prependMatrix(sprite.surfaceMatrix);
var dataX = sprite.attr.dataX[index];
var dataY = sprite.attr.dataY[index];
return matrix.transformPoint([dataX, dataY]);
} else {
return [0,0];
}
},
doHighlight: function() {
var chart = this.down('#chart');
if (this.getHighlightData().r > 0) {
var surf = chart.getSurface();
var highlight = null;
var items = surf.getItems();
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item.id == 'customHighlight') {
highlight = item;
break;
}
}
if (highlight == null) {
surf.add({
id: 'customHighlight',
type: 'circle',
strokeStyle: 'red',
fillStyle: 'none',
radius: this.getHighlightData().r,
x: this.getHighlightData().x,
y: this.getHighlightData().y
});
} else {
highlight.setAttributes({
x: this.getHighlightData().x,
y: this.getHighlightData().y,
radius: this.getHighlightData().r
});
this.getHighlightData().r -= 1.5;
if (this.getHighlightData().r <= 0) {
this.getHighlightData().r = 0;
surf.remove(highlight, true);
}
}
chart.redraw();
}
},
filterChart: function(query) {
if (Ext.isString(query)) query = [query];
var reQueries = [];
for (var i = 0; i < query.length; i++) {
var re = new RegExp(query[i]);
reQueries.push(re);
}
// filter terms
var chart = this.queryById('chart');
var series0 = chart.getSeries()[0];
var label0 = series0.getLabel();
series0.getStore().each(function(item) {
var match = false;
if (reQueries.length == 0) match = true;
else {
for (var i = 0; i < reQueries.length; i++) {
match = match || reQueries[i].test(item.get('term'));
if (match) break;
}
}
item.set('disabled', !match);
var index = item.store.indexOf(item);
label0.setAttributesFor(index, {hidden: !match});
}, this);
chart.redraw();
},
getCurrentTerms: function() {
var terms = [];
this.getTermStore().each(function(r) {
if (r.get('category') === 'term') {
terms.push(r.get('term'));
}
});
return terms;
},
getNearbyForTerm: function(term) {
var limit = Math.max(2000, Math.round(this.getCorpus().getWordTokensCount() / 100));
this.setApiParams({limit: limit, target: term});
this.loadFromApis();
this.setApiParam('target', undefined);
},
removeTerm: function(term) {
var series = this.down('#chart').getSeries()[0];
var index = series.getStore().findExact('term', term);
series.getStore().removeAt(index);
index = this.getTermStore().findExact('term', term);
this.getTermStore().removeAt(index);
var newCount = this.getTermStore().getCount();
this.queryById('limit').setRawValue(newCount);
},
loadFromApis: function(keepCurrentTerms) {
this.queryById('chartParent').mask(this.localize('analyzing'));
var params = {};
var terms = this.getCurrentTerms();
if (this.getNewTerm() !== null) {
terms = terms.concat(this.getNewTerm());
this.setApiParam('limit', terms.length);
}
if (terms.length > 0) {
if (this.getNewTerm() !== null || keepCurrentTerms) {
params.query = terms.join(',');
}
// params.term = terms;
}
Ext.apply(params, this.getApiParams());
if (params.target != null) {
params.term = terms;
}
if (params.analysis === 'pca') {
this.getPcaStore().load({
params: params
});
} else if (params.analysis === 'tsne'){
this.getTsneStore().load({
params: params
});
} else if (params.analysis === 'docSim'){
this.getDocSimStore().load({
params: params
});
} else {
this.getCaStore().load({
params: params
});
}
},
interpolate: function(lambda, minSrc, maxSrc, minDst, maxDst) {
return minDst + (maxDst - minDst) * Math.max(0, Math.min(1, (lambda - minSrc) / (maxSrc - minSrc)));
}
});
/*
* Adds tool tip disabling.
*/
Ext.define('Ext.chart.series.CustomScatter', {
extend: 'Ext.chart.series.Scatter',
alias: 'series.customScatter',
type: 'customScatter',
seriesType: 'scatterSeries',
tipsDisabled: false,
enableToolTips: function() {
this.tipsDisabled = false;
},
disableToolTips: function() {
this.tipsDisabled = true;
},
showTip: function (item, xy) {
if (this.tipsDisabled) {
return;
}
this.callParent(arguments);
}
});