- 1 :
/**
- 2 :
* Microsearch visualizes the frequency and distribution of terms in a corpus.
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "query": null,
- 8 :
* "stopList": "auto"
- 9 :
* };
- 10 :
*
- 11 :
* loadCorpus("austen").tool("microsearch", config);
- 12 :
*
- 13 :
* @class MicroSearch
- 14 :
* @tutorial microsearch
- 15 :
* @memberof Tools
- 16 :
*/
- 17 :
Ext.define('Voyant.panel.MicroSearch', {
- 18 :
extend: 'Ext.panel.Panel',
- 19 :
mixins: ['Voyant.panel.Panel'],
- 20 :
alias: 'widget.microsearch',
- 21 :
statics: {
- 22 :
i18n: {
- 23 :
},
- 24 :
api: {
- 25 :
/**
- 26 :
* @memberof Tools.MicroSearch
- 27 :
* @instance
- 28 :
* @property {stopList}
- 29 :
* @default
- 30 :
*/
- 31 :
stopList: 'auto',
- 32 :
- 33 :
/**
- 34 :
* @memberof Tools.MicroSearch
- 35 :
* @instance
- 36 :
* @property {query}
- 37 :
*/
- 38 :
query: undefined
- 39 :
},
- 40 :
glyph: 'xf1ea@FontAwesome'
- 41 :
},
- 42 :
config: {
- 43 :
/**
- 44 :
* @private
- 45 :
*/
- 46 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
- 47 :
- 48 :
/**
- 49 :
* @private
- 50 :
*/
- 51 :
maxTokens: 0,
- 52 :
- 53 :
/**
- 54 :
* @private
- 55 :
*/
- 56 :
tokensPerSegment: 0,
- 57 :
- 58 :
/**
- 59 :
* @private
- 60 :
*/
- 61 :
maxVerticalLines: 0,
- 62 :
- 63 :
/**
- 64 :
* @private
- 65 :
*/
- 66 :
maxSegments: 0
- 67 :
},
- 68 :
constructor: function(config ) {
- 69 :
- 70 :
Ext.apply(this, {
- 71 :
title: this.localize('title'),
- 72 :
dockedItems: [{
- 73 :
dock: 'bottom',
- 74 :
xtype: 'toolbar',
- 75 :
overflowHandler: 'scroller',
- 76 :
items: [{
- 77 :
xtype: 'querysearchfield'
- 78 :
}]
- 79 :
}]
- 80 :
});
- 81 :
- 82 :
this.callParent(arguments);
- 83 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 84 :
- 85 :
// create a listener for corpus loading (defined here, in case we need to load it next)
- 86 :
this.on('loadedCorpus', function(src, corpus) {
- 87 :
if (this.rendered) {
- 88 :
this.initialize();
- 89 :
}
- 90 :
else {
- 91 :
this.on("afterrender", function() {
- 92 :
this.initialize();
- 93 :
}, this)
- 94 :
}
- 95 :
- 96 :
});
- 97 :
- 98 :
this.on('query', function(src, query) {
- 99 :
this.setApiParam('query', query);
- 100 :
this.updateSearchResults();
- 101 :
})
- 102 :
- 103 :
},
- 104 :
- 105 :
initialize: function() {
- 106 :
- 107 :
var el = this.getTargetEl(), corpus = this.getCorpus();
- 108 :
- 109 :
var lineSize = 5; // pixels, including margins below and above
- 110 :
this.setMaxVerticalLines(Math.floor((el.getHeight() - 10 /* margin of 5px */) / lineSize));
- 111 :
- 112 :
// max segments
- 113 :
var gutterSize = 10;
- 114 :
var corpusSize = corpus.getDocumentsCount();
- 115 :
var gutter = corpusSize * gutterSize;
- 116 :
var columnSize = Math.floor((el.getWidth() - gutter - 10 /* margin of 5px */) / corpusSize);
- 117 :
if (columnSize>200) {columnSize=200;}
- 118 :
var segmentWidth = 3; // each segment is 3 pixels
- 119 :
var maxSegmentsPerLine = Math.floor(columnSize / segmentWidth);
- 120 :
if (maxSegmentsPerLine<1) {maxSegmentsPerLine=1;}
- 121 :
- 122 :
// and the answer is...
- 123 :
this.setMaxSegments(maxSegmentsPerLine * this.getMaxVerticalLines());
- 124 :
- 125 :
var documentsStore = corpus.getDocuments();
- 126 :
this.setMaxTokens(documentsStore.max('tokensCount-lexical'));
- 127 :
- 128 :
this.setTokensPerSegment(this.getMaxTokens() < this.getMaxSegments() ? 1 : Math.ceil(this.getMaxTokens()/this.getMaxSegments()));
- 129 :
- 130 :
- 131 :
var canvas = "<table cellpadding='0' cellspacing='0' style='height: 100%'><tr>";
- 132 :
this.segments = [];
- 133 :
documentsStore.each(function(document) {
- 134 :
docIndex = document.getIndex();
- 135 :
canvas+='<td style="overflow: hidden; vertical-align: top; width: '+columnSize+'px;">'+
- 136 :
'<div class="docLabel" style="white-space: nowrap; width: '+columnSize+'px;" data-qtip="'+document.getFullLabel()+'">'+document.getFullLabel()+"</div>"+
- 137 :
'<canvas style="display: block;" width="'+columnSize+'" height="'+el.getHeight()+'" id="'+this.body.id+'-'+docIndex+'">'+
- 138 :
'</td>';
- 139 :
if (docIndex+1<corpusSize) {canvas+='<td style="width: '+gutterSize+'px;"> </td>';}
- 140 :
}, this);
- 141 :
canvas+='</tr></table>';
- 142 :
el.update(canvas);
- 143 :
- 144 :
this.updateSearchResults();
- 145 :
- 146 :
if (!this.getApiParam('query')) {
- 147 :
var me = this;
- 148 :
return this.getCorpus().loadCorpusTerms({limit: 1, stopList: this.getApiParam('stopList'), categories: this.getApiParam("categories")}).then(function(corpusTerms) {
- 149 :
var term = corpusTerms.getAt(0).getTerm();
- 150 :
var q = me.down('querysearchfield');
- 151 :
q.addValue(new Voyant.data.model.CorpusTerm({term: term}));
- 152 :
me.fireEvent("query", me, [term])
- 153 :
});
- 154 :
}
- 155 :
- 156 :
},
- 157 :
- 158 :
updateSearchResults: function() {
- 159 :
query = this.getApiParam('query');
- 160 :
- 161 :
// draw background
- 162 :
this.getCorpus().getDocuments().each(function(document) {
- 163 :
var distributions = this.redistributeDistributions(document, new Array(this.getMaxSegments()));
- 164 :
this.drawDocumentDistributions(document, null, distributions);
- 165 :
}, this);
- 166 :
- 167 :
if (Ext.Array.from(query).length > 0) {
- 168 :
this.mask(this.localize('loading'))
- 169 :
this.getCorpus().getDocumentTerms().load({
- 170 :
params: {
- 171 :
query: Ext.Array.from(query),
- 172 :
withDistributions: 'relative',
- 173 :
bins: this.getMaxSegments(),
- 174 :
categories: this.getApiParam('categories')
- 175 :
},
- 176 :
callback: function(records, operation, success) {
- 177 :
this.unmask();
- 178 :
var max = 0;
- 179 :
var min = Number.MAX_VALUE;
- 180 :
var docs = [];
- 181 :
records.forEach(function(record) {
- 182 :
var doc = this.getCorpus().getDocument(record.getDocIndex());
- 183 :
var distributions = this.redistributeDistributions(doc, record.getDistributions());
- 184 :
var m = Ext.Array.max(distributions);
- 185 :
if (m>max) {max=m;}
- 186 :
distributions.forEach(function(d) {
- 187 :
if (d && d<min) {
- 188 :
min = d;
- 189 :
}
- 190 :
})
- 191 :
if (docs[record.getDocIndex()] === undefined) {
- 192 :
docs[record.getDocIndex()] = {};
- 193 :
}
- 194 :
docs[record.getDocIndex()][record.getTerm()] = this.redistributeDistributions(doc, record.getDistributions());
- 195 :
}, this);
- 196 :
docs.forEach(function(termDistributions, i) {
- 197 :
for (var term in termDistributions) {
- 198 :
var distributions = termDistributions[term];
- 199 :
this.drawDocumentDistributions(this.getCorpus().getDocument(i), term, distributions, min || Ext.Array.min(distributions), max || Ext.Array.max(distributions));
- 200 :
}
- 201 :
}, this)
- 202 :
},
- 203 :
scope: this
- 204 :
})
- 205 :
}
- 206 :
},
- 207 :
- 208 :
redistributeDistributions: function(doc, distributions) {
- 209 :
var segments = Math.ceil(doc.getLexicalTokensCount() / this.getTokensPerSegment());
- 210 :
- 211 :
// redistribute if needed, we'll take the mean of the distribution values to maintain comparison across segments
- 212 :
if (distributions.length>segments) {
- 213 :
var newdistributions = [];
- 214 :
for (var i=0; i<distributions.length; i++) {
- 215 :
var a = parseInt(i*segments/distributions.length);
- 216 :
if (newdistributions[a]) {newdistributions[a].push(distributions[i])}
- 217 :
else {newdistributions[a]=[distributions[i]];}
- 218 :
}
- 219 :
distributions = newdistributions
- 220 :
for (var i=0; i<distributions.length; i++) {
- 221 :
distributions[i] = Ext.Array.mean(distributions[i]);
- 222 :
}
- 223 :
}
- 224 :
return distributions;
- 225 :
},
- 226 :
- 227 :
drawDocumentDistributions: function(doc, term, distributions, min, max) {
- 228 :
var canvas = this.getTargetEl().dom.querySelector("#"+this.body.id+"-"+doc.getIndex());
- 229 :
var c = canvas.getContext('2d');
- 230 :
var x = 0, w = canvas.clientWidth, y = 0;
- 231 :
- 232 :
var isBlank = term === null;
- 233 :
var color = [230, 230, 230];
- 234 :
if (!isBlank) {
- 235 :
color = this.getApplication().getColorForTerm(term);
- 236 :
}
- 237 :
- 238 :
for (var j=0; j<distributions.length;j++) {
- 239 :
if (isBlank) {
- 240 :
c.fillStyle = "rgb(230,230,230)";
- 241 :
c.fillRect(x,y,3,3);
- 242 :
} else if (distributions[j]) {
- 243 :
var alpha = ((distributions[j]-min)*.7/(max-min))+.3;
- 244 :
c.fillStyle = "rgba("+color[0]+","+color[1]+","+color[2]+","+alpha+")";
- 245 :
c.fillRect(x,y,3,3);
- 246 :
}
- 247 :
x+=3;
- 248 :
if (x>=w) {x=0; y+=5}
- 249 :
}
- 250 :
- 251 :
}
- 252 :
});