- 1 :
/**
- 2 :
* The Summary panel provides an overview of a corpus, and the content will
- 3 :
* depend on whether the corpus includes one document or many.
- 4 :
*
- 5 :
* @example
- 6 :
*
- 7 :
* let config = {
- 8 :
* "limit": null,
- 9 :
* "numberOfDocumentsForDistinctiveWords": null,
- 10 :
* "start": null,
- 11 :
* "stopList": null,
- 12 :
* };
- 13 :
*
- 14 :
* loadCorpus("austen").tool("Summary", config);
- 15 :
*
- 16 :
* @class Summary
- 17 :
* @tutorial summary
- 18 :
* @memberof Tools
- 19 :
*/
- 20 :
Ext.define('Voyant.panel.Summary', {
- 21 :
extend: 'Ext.panel.Panel',
- 22 :
mixins: ['Voyant.panel.Panel'],
- 23 :
alias: 'widget.summary',
- 24 :
statics: {
- 25 :
i18n: {
- 26 :
readabilityIndex: 'Readability Index:',
- 27 :
docsDensityTip: 'ratio of unique words in this document',
- 28 :
avgWordsPerSentenceTip: 'average words per sentence in this document',
- 29 :
readabilityTip: 'the Coleman-Liau readability index for this document'
- 30 :
},
- 31 :
api: {
- 32 :
- 33 :
/**
- 34 :
* @memberof Tools.Summary
- 35 :
* @instance
- 36 :
* @property {stopList}
- 37 :
* @default
- 38 :
*/
- 39 :
stopList: 'auto',
- 40 :
- 41 :
/**
- 42 :
* @memberof Tools.Summary
- 43 :
* @instance
- 44 :
* @property {start}
- 45 :
* @default
- 46 :
*/
- 47 :
start: 0,
- 48 :
- 49 :
- 50 :
/**
- 51 :
* @memberof Tools.Summary
- 52 :
* @instance
- 53 :
* @property {limit}
- 54 :
* @default
- 55 :
*/
- 56 :
limit: 5,
- 57 :
- 58 :
/**
- 59 :
* @memberof Tools.Summary
- 60 :
* @instance
- 61 :
* @property {Number} numberOfDocumentsForDistinctiveWords The number of items to include in the list of distinctive words (similar to the limit parameter but specific to distinctive words).
- 62 :
*/
- 63 :
numberOfDocumentsForDistinctiveWords: 10
- 64 :
},
- 65 :
glyph: 'xf1ea@FontAwesome'
- 66 :
},
- 67 :
config: {
- 68 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
- 69 :
},
- 70 :
autoScroll: true,
- 71 :
cls: 'corpus-summary',
- 72 :
- 73 :
constructor: function(config ) {
- 74 :
- 75 :
Ext.apply(this, {
- 76 :
title: this.localize('title'),
- 77 :
items: {
- 78 :
itemId: 'main',
- 79 :
cls: 'main',
- 80 :
margin: 10
- 81 :
},
- 82 :
dockedItems: [{
- 83 :
dock: 'bottom',
- 84 :
xtype: 'toolbar',
- 85 :
overflowHandler: 'scroller',
- 86 :
items: [{
- 87 :
fieldLabel: this.localize('items'),
- 88 :
labelWidth: 40,
- 89 :
width: 120,
- 90 :
xtype: 'slider',
- 91 :
increment: 5,
- 92 :
minValue: 5,
- 93 :
maxValue: 59,
- 94 :
listeners: {
- 95 :
afterrender: function(slider) {
- 96 :
slider.setValue(this.getApiParam("limit"))
- 97 :
},
- 98 :
changecomplete: function(slider, newvalue) {
- 99 :
this.setApiParams({limit: newvalue});
- 100 :
this.loadSummary();
- 101 :
},
- 102 :
scope: this
- 103 :
}
- 104 :
}]
- 105 :
}]
- 106 :
});
- 107 :
- 108 :
this.callParent(arguments);
- 109 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 110 :
- 111 :
this.on("afterrender", function() {
- 112 :
this.body.addListener('click', function(e) {
- 113 :
var target = e.getTarget(null, null, true);
- 114 :
if (target && target.dom.tagName == 'A') {
- 115 :
if (target.hasCls('document-id')) {
- 116 :
var docId = target.getAttribute('val', 'voyant');
- 117 :
var doc = this.getCorpus().getDocuments().getById(docId);
- 118 :
this.dispatchEvent('documentsClicked', this, [doc]);
- 119 :
} else if (target.hasCls('corpus-type')) {
- 120 :
this.dispatchEvent('termsClicked', this, [target.getHtml()]);
- 121 :
} else if (target.hasCls('document-type')) {
- 122 :
this.dispatchEvent('documentIndexTermsClicked', this, [{
- 123 :
term: target.getHtml(),
- 124 :
docIndex: target.getAttribute("docIndex", 'voyant')
- 125 :
}]);
- 126 :
}
- 127 :
}
- 128 :
}, this);
- 129 :
})
- 130 :
- 131 :
// create a listener for corpus loading (defined here, in case we need to load it next)
- 132 :
this.on('loadedCorpus', function(src, corpus) {
- 133 :
if (this.rendered) {
- 134 :
this.loadSummary();
- 135 :
}
- 136 :
else {
- 137 :
this.on("afterrender", function() {
- 138 :
this.loadSummary();
- 139 :
}, this)
- 140 :
}
- 141 :
- 142 :
});
- 143 :
- 144 :
// if we have a corpus, load it
- 145 :
if (config && config.corpus) {
- 146 :
this.fireEvent('loadedCorpus', this, config.corpus);
- 147 :
}
- 148 :
- 149 :
this.on("resize", function() {
- 150 :
var available = this.getWidth()-200;
- 151 :
this.query("sparklineline").forEach(function(spark) {
- 152 :
if (spark.getWidth()>available) {
- 153 :
spark.setWidth(available);
- 154 :
}
- 155 :
})
- 156 :
}, this)
- 157 :
},
- 158 :
- 159 :
loadSummary: function() {
- 160 :
- 161 :
var me = this;
- 162 :
- 163 :
var main = this.queryById('main');
- 164 :
- 165 :
main.removeAll();
- 166 :
main.add({
- 167 :
cls: 'section',
- 168 :
html: this.getCorpus().getString()
- 169 :
});
- 170 :
- 171 :
var docs = this.getCorpus().getDocuments().getRange();
- 172 :
var limit = this.getApiParam('limit');
- 173 :
- 174 :
if (docs.length>1) {
- 175 :
- 176 :
var docsLengthTpl = new Ext.XTemplate('<tpl for="." between="; "><a href="#" onclick="return false" class="document-id" voyant:val="{id}" data-qtip="{title}">{shortTitle}</a><span style="font-size: smaller"> (<span class="info-tip" data-qtip="{valTip}">{val}</span>)</span></a></tpl>')
- 177 :
- 178 :
var sparkWidth;
- 179 :
if (docs.length<25) {sparkWidth=docs.length*4;}
- 180 :
else if (docs.length<50) {sparkWidth=docs.length*2;}
- 181 :
else if (docs.length>100) {
- 182 :
var available = main.getWidth()-200;
- 183 :
sparkWidth = available < docs.length ? docs.length : available;
- 184 :
}
- 185 :
- 186 :
var numberOfTerms = this.localize('numberOfTerms');
- 187 :
- 188 :
// document length
- 189 :
docs.sort(function(d1, d2) {return d2.getLexicalTokensCount()-d1.getLexicalTokensCount()});
- 190 :
main.add(this.showSparklineSection(
- 191 :
function(doc) { return doc.getLexicalTokensCount(); },
- 192 :
this.localize('docsLength'), this.localize('longest'), this.localize('shortest'),
- 193 :
docs, limit, docsLengthTpl, sparkWidth, this.localize('numberOfTerms')
- 194 :
));
- 195 :
- 196 :
// vocabulary density
- 197 :
docs.sort(function(d1, d2) {return d2.getLexicalTypeTokenRatio()-d1.getLexicalTypeTokenRatio()});
- 198 :
main.add(this.showSparklineSection(
- 199 :
function(doc) { return Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000'); },
- 200 :
this.localize('docsDensity'), this.localize('highest'), this.localize('lowest'),
- 201 :
docs, limit, docsLengthTpl, sparkWidth, this.localize('docsDensityTip')
- 202 :
));
- 203 :
- 204 :
// words per sentence
- 205 :
docs.sort(function(d1, d2) {return d2.getAverageWordsPerSentence()-d1.getAverageWordsPerSentence()});
- 206 :
main.add(this.showSparklineSection(
- 207 :
function(doc) { return Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0'); },
- 208 :
this.localize('averageWordsPerSentence'), this.localize('highest'), this.localize('lowest'),
- 209 :
docs, limit, docsLengthTpl, sparkWidth, this.localize('avgWordsPerSentenceTip')
- 210 :
));
- 211 :
- 212 :
} else { // single document, we can still show word density and average words per sentence
- 213 :
var doc = docs[0];
- 214 :
if (doc) {
- 215 :
main.add({
- 216 :
cls: 'section',
- 217 :
html:"<b>"+this.localize("docsDensity")+"</b> "+Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000')
- 218 :
});
- 219 :
main.add({
- 220 :
cls: 'section',
- 221 :
html: "<b>"+this.localize("averageWordsPerSentence")+"</b> "+Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0')
- 222 :
});
- 223 :
}
- 224 :
}
- 225 :
- 226 :
// readability
- 227 :
this.getCorpus().getReadability().then(function(data) {
- 228 :
docs.forEach(function(doc) {
- 229 :
var readDoc = data.find(function(dataDoc) {
- 230 :
return dataDoc.docId === doc.getId();
- 231 :
});
- 232 :
if (readDoc) {
- 233 :
doc.set('readability', readDoc.readability);
- 234 :
}
- 235 :
});
- 236 :
- 237 :
var sectionIndex = main.items.length-2;
- 238 :
if (docs.length>1) {
- 239 :
docs.sort(function(d1, d2) {return d2.get('readability')-d1.get('readability')});
- 240 :
main.insert(sectionIndex, me.showSparklineSection(function(doc) {
- 241 :
return Ext.util.Format.number(doc.get('readability'),'0.000');
- 242 :
}, me.localize('readabilityIndex'), me.localize('highest'), me.localize('lowest'), docs, limit, docsLengthTpl, sparkWidth, me.localize('readabilityTip')));
- 243 :
} else {
- 244 :
main.insert(sectionIndex, {
- 245 :
cls: 'section',
- 246 :
html: '<b>'+me.localize('readabilityIndex')+'</b> '+ Ext.util.Format.number(docs[0].get('readability'),'0.000')
- 247 :
});
- 248 :
}
- 249 :
})
- 250 :
- 251 :
- 252 :
main.add({
- 253 :
cls: 'section',
- 254 :
items: [{
- 255 :
html: this.localize("mostFrequentWords"),
- 256 :
cls: 'header'
- 257 :
},{
- 258 :
cls: 'contents',
- 259 :
html: '<ul><li></li></ul>'
- 260 :
}],
- 261 :
listeners: {
- 262 :
afterrender: function(container) {
- 263 :
container.mask(me.localize("loading"));
- 264 :
me.getCorpus().getCorpusTerms().load({
- 265 :
params: {
- 266 :
limit: me.getApiParam('limit'),
- 267 :
stopList: me.getApiParam('stopList'),
- 268 :
forTool: 'summary'
- 269 :
},
- 270 :
callback: function(records, operation, success) {
- 271 :
if (success && records && records.length>0) {
- 272 :
container.unmask();
- 273 :
var contentsEl = container.down('panel[cls~=contents]').getTargetEl().selectNode('li');
- 274 :
Ext.dom.Helper.append(contentsEl,
- 275 :
new Ext.XTemplate('<tpl for="." between="; "><a href="#" onclick="return false" class="corpus-type keyword" voyant:recordId="{id}">{term}</a><span style="font-size: smaller"> ({val})</span></tpl>')
- 276 :
.apply(records.map(function(term) {
- 277 :
return {
- 278 :
id: term.getId(),
- 279 :
term: term.getTerm(),
- 280 :
val: term.getRawFreq()
- 281 :
}
- 282 :
}))
- 283 :
)
- 284 :
}
- 285 :
}
- 286 :
})
- 287 :
}
- 288 :
}
- 289 :
})
- 290 :
- 291 :
if (docs.length>1) {
- 292 :
main.add({
- 293 :
cls: 'section',
- 294 :
items: [{
- 295 :
html: this.localize("distinctiveWords"),
- 296 :
cls: 'header'
- 297 :
},{
- 298 :
cls: 'contents',
- 299 :
html: '<ol></ol>'
- 300 :
}],
- 301 :
itemId: 'distinctiveWords',
- 302 :
listeners: {
- 303 :
afterrender: function(container) {
- 304 :
me.showMoreDistinctiveWords();
- 305 :
}
- 306 :
},
- 307 :
scope: this
- 308 :
})
- 309 :
}
- 310 :
- 311 :
},
- 312 :
- 313 :
showSparklineSection: function(docDataFunc, headerText, topText, bottomText, docs, limit, docsLengthTpl, sparkWidth, valueTip) {
- 314 :
var me = this;
- 315 :
return {
- 316 :
cls: 'section',
- 317 :
items: [{
- 318 :
layout: 'hbox',
- 319 :
align: 'bottom',
- 320 :
items: [{
- 321 :
html: headerText,
- 322 :
cls: 'header'
- 323 :
}, {
- 324 :
xtype: 'sparklineline',
- 325 :
values: this.getCorpus().getDocuments().getRange().map(function(doc) {return docDataFunc.call(me, doc)}),
- 326 :
tipTpl: new Ext.XTemplate('{[this.getDocumentTitle(values.x,values.y)]}', {
- 327 :
getDocumentTitle: function(docIndex, len) {
- 328 :
return '('+len+') '+this.panel.getCorpus().getDocument(docIndex).getTitle()
- 329 :
},
- 330 :
panel: me
- 331 :
}),
- 332 :
height: 16,
- 333 :
width: sparkWidth
- 334 :
}]
- 335 :
},{
- 336 :
cls: 'contents',
- 337 :
html: '<ul><li>'+topText+" "+docsLengthTpl.apply(docs.slice(0, docs.length>limit ? limit : parseInt(docs.length/2)).map(function(doc) {return {
- 338 :
id: doc.getId(),
- 339 :
shortTitle: doc.getShortTitle(),
- 340 :
title: doc.getTitle(),
- 341 :
val: docDataFunc.call(me, doc),
- 342 :
valTip: valueTip
- 343 :
}}))+'</li>'+
- 344 :
'<li>'+bottomText+" "+docsLengthTpl.apply(docs.slice(-(docs.length>limit ? limit : parseInt(docs.length/2))).reverse().map(function(doc) {return {
- 345 :
id: doc.getId(),
- 346 :
shortTitle: doc.getShortTitle(),
- 347 :
title: doc.getTitle(),
- 348 :
val: docDataFunc.call(me, doc),
- 349 :
valTip: valueTip
- 350 :
}}))+'</li>'
- 351 :
}]
- 352 :
}
- 353 :
},
- 354 :
- 355 :
showMoreDistinctiveWords: function() {
- 356 :
var distinctiveWordsContainer = this.queryById('distinctiveWords');
- 357 :
var list = distinctiveWordsContainer.getTargetEl().selectNode("ol");
- 358 :
var count = Ext.dom.Query.select("li:not(.more)", list).length;
- 359 :
var numberOfDocumentsForDistinctiveWords = parseInt(this.getApiParam('numberOfDocumentsForDistinctiveWords'));
- 360 :
var range = this.getCorpus().getDocuments().getRange(count, count+numberOfDocumentsForDistinctiveWords-1);
- 361 :
if (range && Ext.isArray(range)) {
- 362 :
var docIndex = [];
- 363 :
range.forEach(function(doc) {
- 364 :
docIndex.push(doc.getIndex())
- 365 :
})
- 366 :
if (docIndex.length>0) {
- 367 :
this.getCorpus().getDocumentTerms().load({
- 368 :
addRecords: true,
- 369 :
params: {
- 370 :
docIndex: docIndex,
- 371 :
perDocLimit: parseInt(this.getApiParam("limit")),
- 372 :
limit: numberOfDocumentsForDistinctiveWords*parseInt(this.getApiParam("limit")),
- 373 :
stopList: this.getApiParam('stopList'),
- 374 :
sort: 'TFIDF',
- 375 :
dir: 'DESC',
- 376 :
forTool: 'summary'
- 377 :
},
- 378 :
scope: this,
- 379 :
callback: function(records, operation, success) {
- 380 :
var docs = {};
- 381 :
if (success && records && Ext.isArray(records)) { // TODO: why wouldn't we have records here?
- 382 :
records.forEach(function(r, index, array) {
- 383 :
var i = r.getDocIndex();
- 384 :
if (!(i in docs)) {docs[i]=[]};
- 385 :
docs[i].push({
- 386 :
id: r.getId(),
- 387 :
docIndex: r.getDocIndex(),
- 388 :
type: r.getTerm(),
- 389 :
val: Ext.util.Format.number(r.get('rawFreq'),'0,000'),
- 390 :
docId: r.get('docId')
- 391 :
});
- 392 :
- 393 :
});
- 394 :
var len;
- 395 :
docIndex.forEach(function(index) {
- 396 :
if (docs[index]) {
- 397 :
var doc = this.getCorpus().getDocument(index);
- 398 :
len = docs[index].length; // declare for template
- 399 :
Ext.dom.Helper.append(list, {tag: 'li', 'voyant:index': String(index), html:
- 400 :
'<a href="#" onclick="return false" class="document-id document-id-distinctive" voyant:val="'+doc.get('id')+'">'+doc.getShortTitle()+'</a>'+
- 401 :
this.localize('colon')+ " "+new Ext.XTemplate(this.localize('documentType')).apply({types: docs[index]})+'.'
- 402 :
});
- 403 :
}
- 404 :
}, this);
- 405 :
distinctiveWordsContainer.updateLayout()
- 406 :
len = numberOfDocumentsForDistinctiveWords;
- 407 :
remaining = this.getCorpus().getDocuments().getTotalCount() - count - docIndex.length;
- 408 :
if (remaining>0) {
- 409 :
var tpl = new Ext.Template(this.localize('moreDistinctiveWords'));
- 410 :
var more = Ext.dom.Helper.append(list, {tag: 'li', cls: 'more', html: tpl.apply([len>remaining ? remaining : len,remaining])}, true);
- 411 :
more.on("click", function() {
- 412 :
more.remove();
- 413 :
this.showMoreDistinctiveWords();
- 414 :
}, this)
- 415 :
}
- 416 :
}
- 417 :
}
- 418 :
});
- 419 :
}
- 420 :
}
- 421 :
},
- 422 :
- 423 :
// override because the doc sparklines are mostly useless as exports
- 424 :
getExportVisualization: function() {
- 425 :
return false;
- 426 :
},
- 427 :
- 428 :
getExtraDataExportItems: function() {
- 429 :
return [{
- 430 :
name: 'export',
- 431 :
inputValue: 'dataAsTsv',
- 432 :
boxLabel: this.localize('exportGridCurrentTsv')
- 433 :
}];
- 434 :
},
- 435 :
- 436 :
exportDataAsTsv: function(panel, form) {
- 437 :
var value = '';
- 438 :
var sections = panel.query('panel[cls~=section]');
- 439 :
sections.forEach(function(sp) {
- 440 :
var sectionData = '';
- 441 :
var header = sp.down('panel[cls~=header]');
- 442 :
var contents = sp.down('panel[cls~=contents]');
- 443 :
if (header) {
- 444 :
sectionData += header.getEl().dom.textContent + "\n";
- 445 :
if (contents) {
- 446 :
contents.getEl().select('li').elements.forEach(function(li) {
- 447 :
sectionData += li.textContent.replace(/:/, ":\t").replace(/\)[,;]/g, ")\t") + "\n";
- 448 :
});
- 449 :
}
- 450 :
} else {
- 451 :
sectionData = sp.getEl().dom.textContent + "\n";
- 452 :
}
- 453 :
value += sectionData + "\n";
- 454 :
});
- 455 :
Ext.Msg.show({
- 456 :
title: panel.localize('exportDataTitle'),
- 457 :
message: panel.localize('exportDataTsvMessage'),
- 458 :
buttons: Ext.Msg.OK,
- 459 :
icon: Ext.Msg.INFO,
- 460 :
prompt: true,
- 461 :
multiline: true,
- 462 :
value: value
- 463 :
});
- 464 :
}
- 465 :
});