/**
* The Summary panel provides an overview of a corpus, and the content will
* depend on whether the corpus includes one document or many.
*
* @example
*
* let config = {
* "limit": null,
* "numberOfDocumentsForDistinctiveWords": null,
* "start": null,
* "stopList": null,
* };
*
* loadCorpus("austen").tool("Summary", config);
*
* @class Summary
* @tutorial summary
* @memberof Tools
*/
Ext.define('Voyant.panel.Summary', {
extend: 'Ext.panel.Panel',
mixins: ['Voyant.panel.Panel'],
alias: 'widget.summary',
statics: {
i18n: {
readabilityIndex: 'Readability Index:',
docsDensityTip: 'ratio of unique words in this document',
avgWordsPerSentenceTip: 'average words per sentence in this document',
readabilityTip: 'the Coleman-Liau readability index for this document'
},
api: {
/**
* @memberof Tools.Summary
* @instance
* @property {stopList}
* @default
*/
stopList: 'auto',
/**
* @memberof Tools.Summary
* @instance
* @property {start}
* @default
*/
start: 0,
/**
* @memberof Tools.Summary
* @instance
* @property {limit}
* @default
*/
limit: 5,
/**
* @memberof Tools.Summary
* @instance
* @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).
*/
numberOfDocumentsForDistinctiveWords: 10
},
glyph: 'xf1ea@FontAwesome'
},
config: {
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
},
autoScroll: true,
cls: 'corpus-summary',
constructor: function(config ) {
Ext.apply(this, {
title: this.localize('title'),
items: {
itemId: 'main',
cls: 'main',
margin: 10
},
dockedItems: [{
dock: 'bottom',
xtype: 'toolbar',
overflowHandler: 'scroller',
items: [{
fieldLabel: this.localize('items'),
labelWidth: 40,
width: 120,
xtype: 'slider',
increment: 5,
minValue: 5,
maxValue: 59,
listeners: {
afterrender: function(slider) {
slider.setValue(this.getApiParam("limit"))
},
changecomplete: function(slider, newvalue) {
this.setApiParams({limit: newvalue});
this.loadSummary();
},
scope: this
}
}]
}]
});
this.callParent(arguments);
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
this.on("afterrender", function() {
this.body.addListener('click', function(e) {
var target = e.getTarget(null, null, true);
if (target && target.dom.tagName == 'A') {
if (target.hasCls('document-id')) {
var docId = target.getAttribute('val', 'voyant');
var doc = this.getCorpus().getDocuments().getById(docId);
this.dispatchEvent('documentsClicked', this, [doc]);
} else if (target.hasCls('corpus-type')) {
this.dispatchEvent('termsClicked', this, [target.getHtml()]);
} else if (target.hasCls('document-type')) {
this.dispatchEvent('documentIndexTermsClicked', this, [{
term: target.getHtml(),
docIndex: target.getAttribute("docIndex", 'voyant')
}]);
}
}
}, this);
})
// create a listener for corpus loading (defined here, in case we need to load it next)
this.on('loadedCorpus', function(src, corpus) {
if (this.rendered) {
this.loadSummary();
}
else {
this.on("afterrender", function() {
this.loadSummary();
}, this)
}
});
// if we have a corpus, load it
if (config && config.corpus) {
this.fireEvent('loadedCorpus', this, config.corpus);
}
this.on("resize", function() {
var available = this.getWidth()-200;
this.query("sparklineline").forEach(function(spark) {
if (spark.getWidth()>available) {
spark.setWidth(available);
}
})
}, this)
},
loadSummary: function() {
var me = this;
var main = this.queryById('main');
main.removeAll();
main.add({
cls: 'section',
html: this.getCorpus().getString()
});
var docs = this.getCorpus().getDocuments().getRange();
var limit = this.getApiParam('limit');
if (docs.length>1) {
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>')
var sparkWidth;
if (docs.length<25) {sparkWidth=docs.length*4;}
else if (docs.length<50) {sparkWidth=docs.length*2;}
else if (docs.length>100) {
var available = main.getWidth()-200;
sparkWidth = available < docs.length ? docs.length : available;
}
var numberOfTerms = this.localize('numberOfTerms');
// document length
docs.sort(function(d1, d2) {return d2.getLexicalTokensCount()-d1.getLexicalTokensCount()});
main.add(this.showSparklineSection(
function(doc) { return doc.getLexicalTokensCount(); },
this.localize('docsLength'), this.localize('longest'), this.localize('shortest'),
docs, limit, docsLengthTpl, sparkWidth, this.localize('numberOfTerms')
));
// vocabulary density
docs.sort(function(d1, d2) {return d2.getLexicalTypeTokenRatio()-d1.getLexicalTypeTokenRatio()});
main.add(this.showSparklineSection(
function(doc) { return Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000'); },
this.localize('docsDensity'), this.localize('highest'), this.localize('lowest'),
docs, limit, docsLengthTpl, sparkWidth, this.localize('docsDensityTip')
));
// words per sentence
docs.sort(function(d1, d2) {return d2.getAverageWordsPerSentence()-d1.getAverageWordsPerSentence()});
main.add(this.showSparklineSection(
function(doc) { return Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0'); },
this.localize('averageWordsPerSentence'), this.localize('highest'), this.localize('lowest'),
docs, limit, docsLengthTpl, sparkWidth, this.localize('avgWordsPerSentenceTip')
));
} else { // single document, we can still show word density and average words per sentence
var doc = docs[0];
if (doc) {
main.add({
cls: 'section',
html:"<b>"+this.localize("docsDensity")+"</b> "+Ext.util.Format.number(doc.getLexicalTypeTokenRatio(),'0.000')
});
main.add({
cls: 'section',
html: "<b>"+this.localize("averageWordsPerSentence")+"</b> "+Ext.util.Format.number(doc.getAverageWordsPerSentence(),'0.0')
});
}
}
// readability
this.getCorpus().getReadability().then(function(data) {
docs.forEach(function(doc) {
var readDoc = data.find(function(dataDoc) {
return dataDoc.docId === doc.getId();
});
if (readDoc) {
doc.set('readability', readDoc.readability);
}
});
var sectionIndex = main.items.length-2;
if (docs.length>1) {
docs.sort(function(d1, d2) {return d2.get('readability')-d1.get('readability')});
main.insert(sectionIndex, me.showSparklineSection(function(doc) {
return Ext.util.Format.number(doc.get('readability'),'0.000');
}, me.localize('readabilityIndex'), me.localize('highest'), me.localize('lowest'), docs, limit, docsLengthTpl, sparkWidth, me.localize('readabilityTip')));
} else {
main.insert(sectionIndex, {
cls: 'section',
html: '<b>'+me.localize('readabilityIndex')+'</b> '+ Ext.util.Format.number(docs[0].get('readability'),'0.000')
});
}
})
main.add({
cls: 'section',
items: [{
html: this.localize("mostFrequentWords"),
cls: 'header'
},{
cls: 'contents',
html: '<ul><li></li></ul>'
}],
listeners: {
afterrender: function(container) {
container.mask(me.localize("loading"));
me.getCorpus().getCorpusTerms().load({
params: {
limit: me.getApiParam('limit'),
stopList: me.getApiParam('stopList'),
forTool: 'summary'
},
callback: function(records, operation, success) {
if (success && records && records.length>0) {
container.unmask();
var contentsEl = container.down('panel[cls~=contents]').getTargetEl().selectNode('li');
Ext.dom.Helper.append(contentsEl,
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>')
.apply(records.map(function(term) {
return {
id: term.getId(),
term: term.getTerm(),
val: term.getRawFreq()
}
}))
)
}
}
})
}
}
})
if (docs.length>1) {
main.add({
cls: 'section',
items: [{
html: this.localize("distinctiveWords"),
cls: 'header'
},{
cls: 'contents',
html: '<ol></ol>'
}],
itemId: 'distinctiveWords',
listeners: {
afterrender: function(container) {
me.showMoreDistinctiveWords();
}
},
scope: this
})
}
},
showSparklineSection: function(docDataFunc, headerText, topText, bottomText, docs, limit, docsLengthTpl, sparkWidth, valueTip) {
var me = this;
return {
cls: 'section',
items: [{
layout: 'hbox',
align: 'bottom',
items: [{
html: headerText,
cls: 'header'
}, {
xtype: 'sparklineline',
values: this.getCorpus().getDocuments().getRange().map(function(doc) {return docDataFunc.call(me, doc)}),
tipTpl: new Ext.XTemplate('{[this.getDocumentTitle(values.x,values.y)]}', {
getDocumentTitle: function(docIndex, len) {
return '('+len+') '+this.panel.getCorpus().getDocument(docIndex).getTitle()
},
panel: me
}),
height: 16,
width: sparkWidth
}]
},{
cls: 'contents',
html: '<ul><li>'+topText+" "+docsLengthTpl.apply(docs.slice(0, docs.length>limit ? limit : parseInt(docs.length/2)).map(function(doc) {return {
id: doc.getId(),
shortTitle: doc.getShortTitle(),
title: doc.getTitle(),
val: docDataFunc.call(me, doc),
valTip: valueTip
}}))+'</li>'+
'<li>'+bottomText+" "+docsLengthTpl.apply(docs.slice(-(docs.length>limit ? limit : parseInt(docs.length/2))).reverse().map(function(doc) {return {
id: doc.getId(),
shortTitle: doc.getShortTitle(),
title: doc.getTitle(),
val: docDataFunc.call(me, doc),
valTip: valueTip
}}))+'</li>'
}]
}
},
showMoreDistinctiveWords: function() {
var distinctiveWordsContainer = this.queryById('distinctiveWords');
var list = distinctiveWordsContainer.getTargetEl().selectNode("ol");
var count = Ext.dom.Query.select("li:not(.more)", list).length;
var numberOfDocumentsForDistinctiveWords = parseInt(this.getApiParam('numberOfDocumentsForDistinctiveWords'));
var range = this.getCorpus().getDocuments().getRange(count, count+numberOfDocumentsForDistinctiveWords-1);
if (range && Ext.isArray(range)) {
var docIndex = [];
range.forEach(function(doc) {
docIndex.push(doc.getIndex())
})
if (docIndex.length>0) {
this.getCorpus().getDocumentTerms().load({
addRecords: true,
params: {
docIndex: docIndex,
perDocLimit: parseInt(this.getApiParam("limit")),
limit: numberOfDocumentsForDistinctiveWords*parseInt(this.getApiParam("limit")),
stopList: this.getApiParam('stopList'),
sort: 'TFIDF',
dir: 'DESC',
forTool: 'summary'
},
scope: this,
callback: function(records, operation, success) {
var docs = {};
if (success && records && Ext.isArray(records)) { // TODO: why wouldn't we have records here?
records.forEach(function(r, index, array) {
var i = r.getDocIndex();
if (!(i in docs)) {docs[i]=[]};
docs[i].push({
id: r.getId(),
docIndex: r.getDocIndex(),
type: r.getTerm(),
val: Ext.util.Format.number(r.get('rawFreq'),'0,000'),
docId: r.get('docId')
});
});
var len;
docIndex.forEach(function(index) {
if (docs[index]) {
var doc = this.getCorpus().getDocument(index);
len = docs[index].length; // declare for template
Ext.dom.Helper.append(list, {tag: 'li', 'voyant:index': String(index), html:
'<a href="#" onclick="return false" class="document-id document-id-distinctive" voyant:val="'+doc.get('id')+'">'+doc.getShortTitle()+'</a>'+
this.localize('colon')+ " "+new Ext.XTemplate(this.localize('documentType')).apply({types: docs[index]})+'.'
});
}
}, this);
distinctiveWordsContainer.updateLayout()
len = numberOfDocumentsForDistinctiveWords;
remaining = this.getCorpus().getDocuments().getTotalCount() - count - docIndex.length;
if (remaining>0) {
var tpl = new Ext.Template(this.localize('moreDistinctiveWords'));
var more = Ext.dom.Helper.append(list, {tag: 'li', cls: 'more', html: tpl.apply([len>remaining ? remaining : len,remaining])}, true);
more.on("click", function() {
more.remove();
this.showMoreDistinctiveWords();
}, this)
}
}
}
});
}
}
},
// override because the doc sparklines are mostly useless as exports
getExportVisualization: function() {
return false;
},
getExtraDataExportItems: function() {
return [{
name: 'export',
inputValue: 'dataAsTsv',
boxLabel: this.localize('exportGridCurrentTsv')
}];
},
exportDataAsTsv: function(panel, form) {
var value = '';
var sections = panel.query('panel[cls~=section]');
sections.forEach(function(sp) {
var sectionData = '';
var header = sp.down('panel[cls~=header]');
var contents = sp.down('panel[cls~=contents]');
if (header) {
sectionData += header.getEl().dom.textContent + "\n";
if (contents) {
contents.getEl().select('li').elements.forEach(function(li) {
sectionData += li.textContent.replace(/:/, ":\t").replace(/\)[,;]/g, ")\t") + "\n";
});
}
} else {
sectionData = sp.getEl().dom.textContent + "\n";
}
value += sectionData + "\n";
});
Ext.Msg.show({
title: panel.localize('exportDataTitle'),
message: panel.localize('exportDataTsvMessage'),
buttons: Ext.Msg.OK,
icon: Ext.Msg.INFO,
prompt: true,
multiline: true,
value: value
});
}
});