- 1 :
/**
- 2 :
* The Word Tree tool allows you to explore how keywords are used in different phrases in the corpus.
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "context": null,
- 8 :
* "docId": null,
- 9 :
* "docIndex": null,
- 10 :
* "limit": null,
- 11 :
* "query": null,
- 12 :
* "stopList": null
- 13 :
* };
- 14 :
*
- 15 :
* loadCorpus("austen").tool("wordtree", config);
- 16 :
*
- 17 :
* @class WordTree
- 18 :
* @tutorial wordtree
- 19 :
* @memberof Tools
- 20 :
*/
- 21 :
Ext.define('Voyant.panel.WordTree', {
- 22 :
extend: 'Ext.panel.Panel',
- 23 :
mixins: ['Voyant.panel.Panel'],
- 24 :
alias: 'widget.wordtree',
- 25 :
statics: {
- 26 :
i18n: {
- 27 :
},
- 28 :
api: {
- 29 :
/**
- 30 :
* @memberof Tools.WordTree
- 31 :
* @instance
- 32 :
* @property {query}
- 33 :
*/
- 34 :
query: undefined,
- 35 :
- 36 :
/**
- 37 :
* @memberof Tools.WordTree
- 38 :
* @instance
- 39 :
* @property {docId}
- 40 :
*/
- 41 :
docId: undefined,
- 42 :
- 43 :
/**
- 44 :
* @memberof Tools.WordTree
- 45 :
* @instance
- 46 :
* @property {docIndex}
- 47 :
*/
- 48 :
docIndex: undefined,
- 49 :
- 50 :
/**
- 51 :
* @memberof Tools.WordTree
- 52 :
* @instance
- 53 :
* @property {stopList}
- 54 :
* @default
- 55 :
*/
- 56 :
stopList: 'auto',
- 57 :
- 58 :
/**
- 59 :
* @memberof Tools.WordTree
- 60 :
* @instance
- 61 :
* @property {context}
- 62 :
* @default
- 63 :
*/
- 64 :
context: 10,
- 65 :
- 66 :
/**
- 67 :
* @memberof Tools.WordTree
- 68 :
* @instance
- 69 :
* @property {limit}
- 70 :
* @default
- 71 :
*/
- 72 :
limit: 100
- 73 :
},
- 74 :
glyph: 'xf0e8@FontAwesome'
- 75 :
},
- 76 :
- 77 :
config: {
- 78 :
tree: undefined,
- 79 :
kwicStore: undefined,
- 80 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
- 81 :
numBranches: 5,
- 82 :
lastClick: 1
- 83 :
},
- 84 :
- 85 :
doubleClickDelay: 300,
- 86 :
- 87 :
constructor: function(config) {
- 88 :
this.callParent(arguments);
- 89 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 90 :
},
- 91 :
- 92 :
initComponent: function() {
- 93 :
Ext.apply(this, {
- 94 :
title: this.localize('title'),
- 95 :
dockedItems: [{
- 96 :
dock: 'bottom',
- 97 :
xtype: 'toolbar',
- 98 :
overflowHandler: 'scroller',
- 99 :
items: [{
- 100 :
xtype: 'querysearchfield'
- 101 :
},
- 102 :
'<span data-qtip="'+this.localize('poolTip')+'" class="info-tip">'+this.localize('pool')+"</span>"
- 103 :
, {
- 104 :
xtype: 'slider',
- 105 :
itemId: 'poolSlider',
- 106 :
minValue: 10,
- 107 :
value: 100,
- 108 :
maxValue: 1000,
- 109 :
increment: 5,
- 110 :
width: 50,
- 111 :
listeners: {
- 112 :
render: function(slider) {
- 113 :
slider.setValue(this.getApiParam('limit'));
- 114 :
},
- 115 :
changecomplete: function(slider, newValue) {
- 116 :
this.setApiParam('limit', slider.getValue());
- 117 :
this.reload();
- 118 :
},
- 119 :
scope: this
- 120 :
}
- 121 :
},
- 122 :
'<span data-qtip="'+this.localize('branchesTip')+'" class="info-tip">'+this.localize('branches')+"</span>"
- 123 :
,{
- 124 :
- 125 :
xtype: 'slider',
- 126 :
itemId: 'branchesSlider',
- 127 :
minValue: 2,
- 128 :
value: 5,
- 129 :
maxValue: 15,
- 130 :
increment: 1,
- 131 :
width: 50,
- 132 :
listeners: {
- 133 :
render: function(slider) {
- 134 :
slider.setValue(this.getNumBranches());
- 135 :
},
- 136 :
changecomplete: function(slider, newValue) {
- 137 :
this.setNumBranches(slider.getValue());
- 138 :
this.reload();
- 139 :
},
- 140 :
scope: this
- 141 :
}
- 142 :
},
- 143 :
'<span data-qtip="'+this.localize('contextTip')+'" class="info-tip">'+this.localize('context')+"</span>"
- 144 :
, {
- 145 :
xtype: 'slider',
- 146 :
itemId: 'contextSlider',
- 147 :
minValue: 3,
- 148 :
value: 10,
- 149 :
maxValue: 20,
- 150 :
increment: 2,
- 151 :
width: 50,
- 152 :
listeners: {
- 153 :
render: function(slider) {
- 154 :
slider.setValue(this.getApiParam('context'));
- 155 :
},
- 156 :
changecomplete: function(slider, newValue) {
- 157 :
this.setApiParam('context', slider.getValue());
- 158 :
this.reload();
- 159 :
},
- 160 :
scope: this
- 161 :
}
- 162 :
}]
- 163 :
}]
- 164 :
});
- 165 :
- 166 :
this.setKwicStore(Ext.create('Voyant.data.store.Contexts', {
- 167 :
parentPanel: this,
- 168 :
proxy: {
- 169 :
extraParams: {
- 170 :
stripTags: 'all'
- 171 :
}
- 172 :
},
- 173 :
listeners: {
- 174 :
load: function(store, records, success, operation) {
- 175 :
if (success) {
- 176 :
this.parseRecords(records);
- 177 :
}
- 178 :
},
- 179 :
scope: this
- 180 :
}
- 181 :
}));
- 182 :
- 183 :
this.on('loadedCorpus', function(src, corpus) {
- 184 :
var corpusTerms = corpus.getCorpusTerms({autoLoad: false});
- 185 :
corpusTerms.load({
- 186 :
callback: function(records, operation, success) {
- 187 :
if (success && records.length>0) {
- 188 :
var firstTerm = records[0].getTerm();
- 189 :
this.setRoot(firstTerm);
- 190 :
}
- 191 :
},
- 192 :
scope: this,
- 193 :
params: {
- 194 :
limit: 1,
- 195 :
query: this.getApiParam('query'),
- 196 :
stopList: this.getApiParam('stopList'),
- 197 :
categories: this.getApiParam('categories')
- 198 :
}
- 199 :
});
- 200 :
}, this);
- 201 :
- 202 :
this.on('query', function(src, query) {
- 203 :
if (query !== undefined && query != '') {
- 204 :
this.setRoot(query);
- 205 :
}
- 206 :
}, this);
- 207 :
- 208 :
this.on('termsClicked', function(src, terms) {
- 209 :
var queryTerms = [];
- 210 :
terms.forEach(function(term) {
- 211 :
if (Ext.isString(term)) {queryTerms.push(term);}
- 212 :
else if (term.term) {queryTerms.push(term.term);}
- 213 :
else if (term.getTerm) {queryTerms.push(term.getTerm());}
- 214 :
});
- 215 :
this.setRoot(queryTerms);
- 216 :
}, this);
- 217 :
- 218 :
this.on('documentTermsClicked', function(src, terms) {
- 219 :
var queryTerms = [];
- 220 :
terms.forEach(function(term) {
- 221 :
if (term.getTerm()) {queryTerms.push(term.getTerm());}
- 222 :
});
- 223 :
this.setRoot(queryTerms);
- 224 :
}, this);
- 225 :
- 226 :
this.on('resize', function(panel, width, height) {
- 227 :
var tree = this.getTree();
- 228 :
if (tree !== undefined) {
- 229 :
tree.visWidth(width).visHeight(height);
- 230 :
// TODO preserve expanded branches
- 231 :
tree.redraw();
- 232 :
}
- 233 :
}, this);
- 234 :
- 235 :
this.on('boxready', this.initGraph, this);
- 236 :
- 237 :
this.callParent(arguments);
- 238 :
},
- 239 :
- 240 :
parseRecords: function(records) {
- 241 :
var parsedRecords = [];
- 242 :
for (var i = 0; i < records.length; i++) {
- 243 :
var r = records[i];
- 244 :
var pr = {
- 245 :
id: i,
- 246 :
prefix: r.getLeft().trim().split(/\s+/),
- 247 :
hit: r.getMiddle(),
- 248 :
suffix: r.getRight().trim().split(/\s+/)
- 249 :
};
- 250 :
parsedRecords.push(pr);
- 251 :
}
- 252 :
- 253 :
// find top tokens and sort records by them
- 254 :
var prefixTokenCounts = {};
- 255 :
var suffixTokenCounts = {};
- 256 :
for (var i = 0; i < parsedRecords.length; i++) {
- 257 :
var pr = parsedRecords[i];
- 258 :
var prefixToken = pr.prefix[pr.prefix.length-1];
- 259 :
var suffixToken = pr.suffix[0];
- 260 :
if (prefixTokenCounts[prefixToken]) {
- 261 :
prefixTokenCounts[prefixToken]++;
- 262 :
} else {
- 263 :
prefixTokenCounts[prefixToken] = 1;
- 264 :
}
- 265 :
if (suffixTokenCounts[suffixToken]) {
- 266 :
suffixTokenCounts[suffixToken]++;
- 267 :
} else {
- 268 :
suffixTokenCounts[suffixToken] = 1;
- 269 :
}
- 270 :
}
- 271 :
- 272 :
var sortableTokens = [];
- 273 :
for (var i = 0; i < parsedRecords.length; i++) {
- 274 :
var pr = parsedRecords[i];
- 275 :
var prefixToken = pr.prefix[pr.prefix.length-1];
- 276 :
var suffixToken = pr.suffix[0];
- 277 :
- 278 :
sortableTokens.push({
- 279 :
suffix: suffixToken, suffixCount: suffixTokenCounts[suffixToken],
- 280 :
prefix: prefixToken, prefixCount: prefixTokenCounts[prefixToken]
- 281 :
});
- 282 :
- 283 :
}
- 284 :
- 285 :
var prioritizeSuffix = false;
- 286 :
// multi-sort
- 287 :
sortableTokens.sort(function(a, b) {
- 288 :
var s1 = a.suffixCount;
- 289 :
var s2 = b.suffixCount;
- 290 :
- 291 :
var p1 = a.prefixCount;
- 292 :
var p2 = b.prefixCount;
- 293 :
- 294 :
if (prioritizeSuffix) {
- 295 :
if (s1 > s2) return -1;
- 296 :
if (s1 < s2) return 1;
- 297 :
if (p1 > p2) return -1;
- 298 :
if (p1 < p2) return 1;
- 299 :
} else {
- 300 :
if (p1 > p2) return -1;
- 301 :
if (p1 < p2) return 1;
- 302 :
if (s1 > s2) return -1;
- 303 :
if (s1 < s2) return 1;
- 304 :
}
- 305 :
- 306 :
return 0;
- 307 :
});
- 308 :
- 309 :
var len = Math.min(this.getNumBranches(), sortableTokens.length);
- 310 :
var topSuffixTokens = [];
- 311 :
var topPrefixTokens = [];
- 312 :
for (var i = 0; i < len; i++) {
- 313 :
topSuffixTokens.push(sortableTokens[i].suffix);
- 314 :
topPrefixTokens.push(sortableTokens[i].prefix);
- 315 :
}
- 316 :
- 317 :
// use top tokens to limit results
- 318 :
var prefixes = [], hits = [], suffixes = [], ids = [];
- 319 :
for (var i = 0; i < parsedRecords.length; i++) {
- 320 :
var parsedRecord = parsedRecords[i];
- 321 :
if (topSuffixTokens.indexOf(parsedRecord.suffix[0]) != -1 || topPrefixTokens.indexOf(parsedRecord.suffix[0]) != -1) {
- 322 :
prefixes.push(parsedRecord.prefix);
- 323 :
hits.push(parsedRecord.hit);
- 324 :
suffixes.push(parsedRecord.suffix);
- 325 :
ids.push(parsedRecord.id);
- 326 :
}
- 327 :
}
- 328 :
- 329 :
var caseSensitive = false;
- 330 :
var fieldNames = ["token", "POS"];
- 331 :
var fieldDelim = "/";
- 332 :
var distinguishingFieldsArray = ["token", "POS"];
- 333 :
this.getTree().setupFromArrays(prefixes, hits, suffixes, ids, caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray);
- 334 :
- 335 :
if (!this.getTree().succeeded()) {
- 336 :
this.toastInfo({
- 337 :
html: this.localize("emptyText"),
- 338 :
align: 'bl'
- 339 :
});
- 340 :
}
- 341 :
},
- 342 :
- 343 :
initGraph: function() {
- 344 :
var el = this.getLayout().getRenderTarget();
- 345 :
var w = el.getWidth();
- 346 :
var h = el.getHeight();
- 347 :
- 348 :
var dt = new doubletree.DoubleTree();
- 349 :
dt.init('#'+el.getId())
- 350 :
.visWidth(w).visHeight(h)
- 351 :
.handlers({
- 352 :
click: this.clickHandler.bind(this)
- 353 :
});
- 354 :
- 355 :
this.setTree(dt);
- 356 :
- 357 :
// explicitly set dimensions
- 358 :
// el.setWidth(el.getWidth());
- 359 :
// el.setHeight(el.getHeight());
- 360 :
},
- 361 :
- 362 :
clickHandler: function(node) {
- 363 :
var now = new Date().getTime();
- 364 :
if (this.getLastClick() && now-this.getLastClick()<this.doubleClickDelay) {
- 365 :
this.setLastClick(1);
- 366 :
var terms = [], parent = node;
- 367 :
while (parent != null) {
- 368 :
terms.push(parent.name);
- 369 :
parent = parent.parent;
- 370 :
}
- 371 :
this.getApplication().dispatchEvent('termsClicked', this, [terms.reverse().join(" ")]);
- 372 :
} else {
- 373 :
this.setLastClick(now);
- 374 :
}
- 375 :
},
- 376 :
- 377 :
// doubleClickHandler: function(node) {
- 378 :
//// dispatch phrase click instead of recentering (which can be done with search)
- 379 :
//// this.setRoot(node.name);
- 380 :
// },
- 381 :
//
- 382 :
setRoot: function(query) {
- 383 :
this.setApiParam('query', this.stripPunctuation(query));
- 384 :
this.getKwicStore().load({params: this.getApiParams()});
- 385 :
},
- 386 :
- 387 :
reload: function() {
- 388 :
var query = this.getApiParam('query');
- 389 :
if (query !== undefined) {
- 390 :
this.setRoot(query);
- 391 :
}
- 392 :
},
- 393 :
- 394 :
stripPunctuation: function(value) {
- 395 :
if (Ext.isString(value)) return value.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');
- 396 :
else {
- 397 :
var values = [];
- 398 :
value.forEach(function(v) {
- 399 :
values.push(v.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, ''));
- 400 :
});
- 401 :
return values;
- 402 :
}
- 403 :
return '';
- 404 :
}
- 405 :
});
- 406 :