/**
* The Word Tree tool allows you to explore how keywords are used in different phrases in the corpus.
*
* @example
*
* let config = {
* "context": null,
* "docId": null,
* "docIndex": null,
* "limit": null,
* "query": null,
* "stopList": null
* };
*
* loadCorpus("austen").tool("wordtree", config);
*
* @class WordTree
* @tutorial wordtree
* @memberof Tools
*/
Ext.define('Voyant.panel.WordTree', {
extend: 'Ext.panel.Panel',
mixins: ['Voyant.panel.Panel'],
alias: 'widget.wordtree',
statics: {
i18n: {
},
api: {
/**
* @memberof Tools.WordTree
* @instance
* @property {query}
*/
query: undefined,
/**
* @memberof Tools.WordTree
* @instance
* @property {docId}
*/
docId: undefined,
/**
* @memberof Tools.WordTree
* @instance
* @property {docIndex}
*/
docIndex: undefined,
/**
* @memberof Tools.WordTree
* @instance
* @property {stopList}
* @default
*/
stopList: 'auto',
/**
* @memberof Tools.WordTree
* @instance
* @property {context}
* @default
*/
context: 10,
/**
* @memberof Tools.WordTree
* @instance
* @property {limit}
* @default
*/
limit: 100
},
glyph: 'xf0e8@FontAwesome'
},
config: {
tree: undefined,
kwicStore: undefined,
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
numBranches: 5,
lastClick: 1
},
doubleClickDelay: 300,
constructor: function(config) {
this.callParent(arguments);
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
},
initComponent: function() {
Ext.apply(this, {
title: this.localize('title'),
dockedItems: [{
dock: 'bottom',
xtype: 'toolbar',
overflowHandler: 'scroller',
items: [{
xtype: 'querysearchfield'
},
'<span data-qtip="'+this.localize('poolTip')+'" class="info-tip">'+this.localize('pool')+"</span>"
, {
xtype: 'slider',
itemId: 'poolSlider',
minValue: 10,
value: 100,
maxValue: 1000,
increment: 5,
width: 50,
listeners: {
render: function(slider) {
slider.setValue(this.getApiParam('limit'));
},
changecomplete: function(slider, newValue) {
this.setApiParam('limit', slider.getValue());
this.reload();
},
scope: this
}
},
'<span data-qtip="'+this.localize('branchesTip')+'" class="info-tip">'+this.localize('branches')+"</span>"
,{
xtype: 'slider',
itemId: 'branchesSlider',
minValue: 2,
value: 5,
maxValue: 15,
increment: 1,
width: 50,
listeners: {
render: function(slider) {
slider.setValue(this.getNumBranches());
},
changecomplete: function(slider, newValue) {
this.setNumBranches(slider.getValue());
this.reload();
},
scope: this
}
},
'<span data-qtip="'+this.localize('contextTip')+'" class="info-tip">'+this.localize('context')+"</span>"
, {
xtype: 'slider',
itemId: 'contextSlider',
minValue: 3,
value: 10,
maxValue: 20,
increment: 2,
width: 50,
listeners: {
render: function(slider) {
slider.setValue(this.getApiParam('context'));
},
changecomplete: function(slider, newValue) {
this.setApiParam('context', slider.getValue());
this.reload();
},
scope: this
}
}]
}]
});
this.setKwicStore(Ext.create('Voyant.data.store.Contexts', {
parentPanel: this,
proxy: {
extraParams: {
stripTags: 'all'
}
},
listeners: {
load: function(store, records, success, operation) {
if (success) {
this.parseRecords(records);
}
},
scope: this
}
}));
this.on('loadedCorpus', function(src, corpus) {
var corpusTerms = corpus.getCorpusTerms({autoLoad: false});
corpusTerms.load({
callback: function(records, operation, success) {
if (success && records.length>0) {
var firstTerm = records[0].getTerm();
this.setRoot(firstTerm);
}
},
scope: this,
params: {
limit: 1,
query: this.getApiParam('query'),
stopList: this.getApiParam('stopList'),
categories: this.getApiParam('categories')
}
});
}, this);
this.on('query', function(src, query) {
if (query !== undefined && query != '') {
this.setRoot(query);
}
}, this);
this.on('termsClicked', function(src, terms) {
var queryTerms = [];
terms.forEach(function(term) {
if (Ext.isString(term)) {queryTerms.push(term);}
else if (term.term) {queryTerms.push(term.term);}
else if (term.getTerm) {queryTerms.push(term.getTerm());}
});
this.setRoot(queryTerms);
}, this);
this.on('documentTermsClicked', function(src, terms) {
var queryTerms = [];
terms.forEach(function(term) {
if (term.getTerm()) {queryTerms.push(term.getTerm());}
});
this.setRoot(queryTerms);
}, this);
this.on('resize', function(panel, width, height) {
var tree = this.getTree();
if (tree !== undefined) {
tree.visWidth(width).visHeight(height);
// TODO preserve expanded branches
tree.redraw();
}
}, this);
this.on('boxready', this.initGraph, this);
this.callParent(arguments);
},
parseRecords: function(records) {
var parsedRecords = [];
for (var i = 0; i < records.length; i++) {
var r = records[i];
var pr = {
id: i,
prefix: r.getLeft().trim().split(/\s+/),
hit: r.getMiddle(),
suffix: r.getRight().trim().split(/\s+/)
};
parsedRecords.push(pr);
}
// find top tokens and sort records by them
var prefixTokenCounts = {};
var suffixTokenCounts = {};
for (var i = 0; i < parsedRecords.length; i++) {
var pr = parsedRecords[i];
var prefixToken = pr.prefix[pr.prefix.length-1];
var suffixToken = pr.suffix[0];
if (prefixTokenCounts[prefixToken]) {
prefixTokenCounts[prefixToken]++;
} else {
prefixTokenCounts[prefixToken] = 1;
}
if (suffixTokenCounts[suffixToken]) {
suffixTokenCounts[suffixToken]++;
} else {
suffixTokenCounts[suffixToken] = 1;
}
}
var sortableTokens = [];
for (var i = 0; i < parsedRecords.length; i++) {
var pr = parsedRecords[i];
var prefixToken = pr.prefix[pr.prefix.length-1];
var suffixToken = pr.suffix[0];
sortableTokens.push({
suffix: suffixToken, suffixCount: suffixTokenCounts[suffixToken],
prefix: prefixToken, prefixCount: prefixTokenCounts[prefixToken]
});
}
var prioritizeSuffix = false;
// multi-sort
sortableTokens.sort(function(a, b) {
var s1 = a.suffixCount;
var s2 = b.suffixCount;
var p1 = a.prefixCount;
var p2 = b.prefixCount;
if (prioritizeSuffix) {
if (s1 > s2) return -1;
if (s1 < s2) return 1;
if (p1 > p2) return -1;
if (p1 < p2) return 1;
} else {
if (p1 > p2) return -1;
if (p1 < p2) return 1;
if (s1 > s2) return -1;
if (s1 < s2) return 1;
}
return 0;
});
var len = Math.min(this.getNumBranches(), sortableTokens.length);
var topSuffixTokens = [];
var topPrefixTokens = [];
for (var i = 0; i < len; i++) {
topSuffixTokens.push(sortableTokens[i].suffix);
topPrefixTokens.push(sortableTokens[i].prefix);
}
// use top tokens to limit results
var prefixes = [], hits = [], suffixes = [], ids = [];
for (var i = 0; i < parsedRecords.length; i++) {
var parsedRecord = parsedRecords[i];
if (topSuffixTokens.indexOf(parsedRecord.suffix[0]) != -1 || topPrefixTokens.indexOf(parsedRecord.suffix[0]) != -1) {
prefixes.push(parsedRecord.prefix);
hits.push(parsedRecord.hit);
suffixes.push(parsedRecord.suffix);
ids.push(parsedRecord.id);
}
}
var caseSensitive = false;
var fieldNames = ["token", "POS"];
var fieldDelim = "/";
var distinguishingFieldsArray = ["token", "POS"];
this.getTree().setupFromArrays(prefixes, hits, suffixes, ids, caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray);
if (!this.getTree().succeeded()) {
this.toastInfo({
html: this.localize("emptyText"),
align: 'bl'
});
}
},
initGraph: function() {
var el = this.getLayout().getRenderTarget();
var w = el.getWidth();
var h = el.getHeight();
var dt = new doubletree.DoubleTree();
dt.init('#'+el.getId())
.visWidth(w).visHeight(h)
.handlers({
click: this.clickHandler.bind(this)
});
this.setTree(dt);
// explicitly set dimensions
// el.setWidth(el.getWidth());
// el.setHeight(el.getHeight());
},
clickHandler: function(node) {
var now = new Date().getTime();
if (this.getLastClick() && now-this.getLastClick()<this.doubleClickDelay) {
this.setLastClick(1);
var terms = [], parent = node;
while (parent != null) {
terms.push(parent.name);
parent = parent.parent;
}
this.getApplication().dispatchEvent('termsClicked', this, [terms.reverse().join(" ")]);
} else {
this.setLastClick(now);
}
},
// doubleClickHandler: function(node) {
//// dispatch phrase click instead of recentering (which can be done with search)
//// this.setRoot(node.name);
// },
//
setRoot: function(query) {
this.setApiParam('query', this.stripPunctuation(query));
this.getKwicStore().load({params: this.getApiParams()});
},
reload: function() {
var query = this.getApiParam('query');
if (query !== undefined) {
this.setRoot(query);
}
},
stripPunctuation: function(value) {
if (Ext.isString(value)) return value.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');
else {
var values = [];
value.forEach(function(v) {
values.push(v.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, ''));
});
return values;
}
return '';
}
});