- 1 :
/**
- 2 :
* StreamGraph is a visualization that depicts the change of the frequency of words in a corpus (or within a single document).
- 3 :
*
- 4 :
* @example
- 5 :
*
- 6 :
* let config = {
- 7 :
* "bins": null,
- 8 :
* "docId": null,
- 9 :
* "docIndex": null,
- 10 :
* "limit": null,
- 11 :
* "query": null,
- 12 :
* "stopList": null,
- 13 :
* "withDistributions": null
- 14 :
* };
- 15 :
*
- 16 :
* loadCorpus("austen").tool("streamgraph", config);
- 17 :
*
- 18 :
* @class StreamGraph
- 19 :
* @tutorial streamgraph
- 20 :
* @memberof Tools
- 21 :
*/
- 22 :
Ext.define('Voyant.panel.StreamGraph', {
- 23 :
extend: 'Ext.panel.Panel',
- 24 :
mixins: ['Voyant.panel.Panel'],
- 25 :
alias: 'widget.streamgraph',
- 26 :
statics: {
- 27 :
i18n: {
- 28 :
},
- 29 :
api: {
- 30 :
/**
- 31 :
* @memberof Tools.StreamGraph
- 32 :
* @instance
- 33 :
* @property {limit}
- 34 :
* @default
- 35 :
*/
- 36 :
limit: 5,
- 37 :
- 38 :
/**
- 39 :
* @memberof Tools.StreamGraph
- 40 :
* @instance
- 41 :
* @property {stopList}
- 42 :
* @default
- 43 :
*/
- 44 :
stopList: 'auto',
- 45 :
- 46 :
/**
- 47 :
* @memberof Tools.StreamGraph
- 48 :
* @instance
- 49 :
* @property {query}
- 50 :
*/
- 51 :
query: undefined,
- 52 :
- 53 :
/**
- 54 :
* @memberof Tools.StreamGraph
- 55 :
* @instance
- 56 :
* @property {withDistributions}
- 57 :
* @default
- 58 :
*/
- 59 :
withDistributions: 'relative',
- 60 :
- 61 :
/**
- 62 :
* @memberof Tools.StreamGraph
- 63 :
* @instance
- 64 :
* @property {bins}
- 65 :
* @default
- 66 :
*/
- 67 :
bins: 50,
- 68 :
- 69 :
/**
- 70 :
* @memberof Tools.StreamGraph
- 71 :
* @instance
- 72 :
* @property {docIndex}
- 73 :
*/
- 74 :
docIndex: undefined,
- 75 :
- 76 :
/**
- 77 :
* @memberof Tools.StreamGraph
- 78 :
* @instance
- 79 :
* @property {docId}
- 80 :
*/
- 81 :
docId: undefined
- 82 :
},
- 83 :
glyph: 'xf1fe@FontAwesome'
- 84 :
},
- 85 :
- 86 :
config: {
- 87 :
visLayout: undefined,
- 88 :
vis: undefined,
- 89 :
mode: 'corpus',
- 90 :
- 91 :
layerData: undefined,
- 92 :
- 93 :
graphId: undefined,
- 94 :
options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
- 95 :
},
- 96 :
- 97 :
graphMargin: {top: 20, right: 60, bottom: 110, left: 80},
- 98 :
- 99 :
MODE_CORPUS: 'corpus',
- 100 :
MODE_DOCUMENT: 'document',
- 101 :
- 102 :
constructor: function(config) {
- 103 :
this.callParent(arguments);
- 104 :
this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
- 105 :
- 106 :
this.setGraphId(Ext.id(null, 'streamgraph_'));
- 107 :
},
- 108 :
- 109 :
initComponent: function() {
- 110 :
var me = this;
- 111 :
- 112 :
Ext.apply(me, {
- 113 :
title: this.localize('title'),
- 114 :
tbar: new Ext.Toolbar({
- 115 :
overflowHandler: 'scroller',
- 116 :
items: ['->',{
- 117 :
xtype: 'legend',
- 118 :
store: new Ext.data.JsonStore({
- 119 :
fields: ['name', 'mark', 'active']
- 120 :
}),
- 121 :
listeners: {
- 122 :
itemclick: function(view, record, el, index) {
- 123 :
var isActive = Ext.fly(el.firstElementChild).hasCls('x-legend-inactive');
- 124 :
record.set('active', isActive);
- 125 :
var terms = this.getCurrentTerms();
- 126 :
this.setApiParams({query: terms, limit: terms.length, stopList: undefined, categories: this.getApiParam("categories")});
- 127 :
this.loadFromCorpus();
- 128 :
},
- 129 :
scope: this
- 130 :
}
- 131 :
},'->']
- 132 :
}),
- 133 :
bbar: {
- 134 :
overflowHandler: 'scroller',
- 135 :
items: [{
- 136 :
xtype: 'querysearchfield'
- 137 :
},{
- 138 :
xtype: 'button',
- 139 :
text: this.localize('clearTerms'),
- 140 :
handler: function() {
- 141 :
this.setApiParams({query: undefined});
- 142 :
this.loadFromRecords([]);
- 143 :
},
- 144 :
scope: this
- 145 :
},{
- 146 :
xtype: 'corpusdocumentselector',
- 147 :
singleSelect: true
- 148 :
},{
- 149 :
text: this.localize('freqsMode'),
- 150 :
glyph: 'xf201@FontAwesome',
- 151 :
tooltip: this.localize('freqsModeTip'),
- 152 :
menu: {
- 153 :
items: [{
- 154 :
text: this.localize('relativeFrequencies'),
- 155 :
checked: true,
- 156 :
itemId: 'relative',
- 157 :
group: 'freqsMode',
- 158 :
checkHandler: function(item, checked) {
- 159 :
if (checked) {
- 160 :
this.setApiParam('withDistributions', 'relative');
- 161 :
this.loadFromCorpus();
- 162 :
}
- 163 :
},
- 164 :
scope: this
- 165 :
}, {
- 166 :
text: this.localize('rawFrequencies'),
- 167 :
checked: false,
- 168 :
itemId: 'raw',
- 169 :
group: 'freqsMode',
- 170 :
checkHandler: function(item, checked) {
- 171 :
if (checked) {
- 172 :
this.setApiParam('withDistributions', 'raw');
- 173 :
this.loadFromCorpus();
- 174 :
}
- 175 :
},
- 176 :
scope: this
- 177 :
}]
- 178 :
}
- 179 :
},{
- 180 :
xtype: 'slider',
- 181 :
itemId: 'segmentsSlider',
- 182 :
fieldLabel: this.localize('segments'),
- 183 :
labelAlign: 'right',
- 184 :
labelWidth: 70,
- 185 :
width: 150,
- 186 :
increment: 10,
- 187 :
minValue: 10,
- 188 :
maxValue: 300,
- 189 :
listeners: {
- 190 :
afterrender: function(slider) {
- 191 :
slider.setValue(this.getApiParam('bins'));
- 192 :
},
- 193 :
changecomplete: function(slider, newvalue) {
- 194 :
this.setApiParams({bins: newvalue});
- 195 :
this.loadFromCorpus();
- 196 :
},
- 197 :
scope: this
- 198 :
}
- 199 :
}]
- 200 :
}
- 201 :
});
- 202 :
- 203 :
this.on('loadedCorpus', function(src, corpus) {
- 204 :
if (this.getCorpus().getDocumentsCount() == 1 && this.getMode() != this.MODE_DOCUMENT) {
- 205 :
this.setMode(this.MODE_DOCUMENT);
- 206 :
}
- 207 :
if (!('bins' in this.getModifiedApiParams())) {
- 208 :
if (this.getMode() == this.MODE_CORPUS) {
- 209 :
var count = corpus.getDocumentsCount();
- 210 :
var binsMax = 100;
- 211 :
this.setApiParam('bins', count > binsMax ? binsMax : count);
- 212 :
}
- 213 :
}
- 214 :
if (this.isVisible()) {
- 215 :
this.loadFromCorpus();
- 216 :
}
- 217 :
}, this);
- 218 :
- 219 :
this.on('corpusSelected', function(src, corpus) {
- 220 :
if (src.isXType('corpusdocumentselector')) {
- 221 :
this.setMode(this.MODE_CORPUS);
- 222 :
this.setApiParams({docId: undefined, docIndex: undefined});
- 223 :
this.setCorpus(corpus);
- 224 :
this.loadFromCorpus();
- 225 :
}
- 226 :
});
- 227 :
- 228 :
this.on('documentSelected', function(src, doc) {
- 229 :
var docId = doc.getId();
- 230 :
this.setApiParam('docId', docId);
- 231 :
this.loadFromDocumentTerms();
- 232 :
}, this);
- 233 :
- 234 :
this.on('query', function(src, query) {
- 235 :
var terms = this.getCurrentTerms();
- 236 :
terms.push(query);
- 237 :
this.setApiParams({query: terms, limit: terms.length, stopList: undefined});
- 238 :
if (this.getMode() === this.MODE_DOCUMENT) {
- 239 :
this.loadFromDocumentTerms();
- 240 :
} else {
- 241 :
this.loadFromCorpusTerms(this.getCorpus().getCorpusTerms());
- 242 :
}
- 243 :
}, this);
- 244 :
- 245 :
this.on('resize', this.resizeGraph, this);
- 246 :
- 247 :
this.on('boxready', this.initGraph, this);
- 248 :
- 249 :
me.callParent(arguments);
- 250 :
},
- 251 :
- 252 :
loadFromCorpus: function() {
- 253 :
var corpus = this.getCorpus();
- 254 :
if (this.getApiParam('docId') || this.getApiParam('docIndex')) {
- 255 :
this.loadFromDocumentTerms();
- 256 :
} else if (corpus.getDocumentsCount() == 1) {
- 257 :
this.loadFromDocument(corpus.getDocument(0));
- 258 :
} else {
- 259 :
this.loadFromCorpusTerms(corpus.getCorpusTerms());
- 260 :
}
- 261 :
},
- 262 :
- 263 :
loadFromCorpusTerms: function(corpusTerms) {
- 264 :
var params = this.getApiParams(['limit','stopList','query','withDistributions','bins','categories']);
- 265 :
// ensure that we're not beyond the number of documents
- 266 :
if (params.bins && params.bins > this.getCorpus().getDocumentsCount()) {
- 267 :
params.bins = this.getCorpus().getDocumentsCount();
- 268 :
}
- 269 :
corpusTerms.load({
- 270 :
callback: function(records, operation, success) {
- 271 :
if (success) {
- 272 :
this.setMode(this.MODE_CORPUS);
- 273 :
this.loadFromRecords(records);
- 274 :
} else {
- 275 :
Voyant.application.showResponseError(this.localize('failedGetCorpusTerms'), operation);
- 276 :
}
- 277 :
},
- 278 :
scope: this,
- 279 :
params: params
- 280 :
});
- 281 :
},
- 282 :
- 283 :
loadFromDocument: function(document) {
- 284 :
if (document.then) {
- 285 :
var me = this;
- 286 :
document.then(function(document) {me.loadFromDocument(document);});
- 287 :
} else {
- 288 :
var ids = [];
- 289 :
if (Ext.getClassName(document)=="Voyant.data.model.Document") {
- 290 :
this.setApiParams({
- 291 :
docIndex: undefined,
- 292 :
query: undefined,
- 293 :
docId: document.getId()
- 294 :
});
- 295 :
if (this.isVisible()) {
- 296 :
this.loadFromDocumentTerms();
- 297 :
}
- 298 :
}
- 299 :
}
- 300 :
},
- 301 :
- 302 :
loadFromDocumentTerms: function(documentTerms) {
- 303 :
if (this.getCorpus()) {
- 304 :
documentTerms = documentTerms || this.getCorpus().getDocumentTerms({autoLoad: false});
- 305 :
documentTerms.load({
- 306 :
callback: function(records, operation, success) {
- 307 :
if (success) {
- 308 :
this.setMode(this.MODE_DOCUMENT);
- 309 :
this.loadFromRecords(records);
- 310 :
}
- 311 :
else {
- 312 :
Voyant.application.showResponseError(this.localize('failedGetDocumentTerms'), operation);
- 313 :
}
- 314 :
},
- 315 :
scope: this,
- 316 :
params: this.getApiParams(['docId','docIndex','limit','stopList','query','withDistributions','bins','categories'])
- 317 :
});
- 318 :
}
- 319 :
},
- 320 :
- 321 :
loadFromRecords: function(records) {
- 322 :
var legendData = [];
- 323 :
var layers = [];
- 324 :
records.forEach(function(record, index) {
- 325 :
var key = record.getTerm();
- 326 :
var values = record.get('distributions');
- 327 :
for (var i = 0; i < values.length; i++) {
- 328 :
if (layers[i] === undefined) {
- 329 :
layers[i] = {};
- 330 :
}
- 331 :
layers[i][key] = values[i];
- 332 :
}
- 333 :
legendData.push({id: key, name: key, mark: this.getApplication().getColorForTerm(key, true), active: true});
- 334 :
}, this);
- 335 :
- 336 :
this.setLayerData(layers);
- 337 :
- 338 :
this.down('[xtype=legend]').getStore().loadData(legendData);
- 339 :
- 340 :
this.doLayout();
- 341 :
},
- 342 :
- 343 :
doLayout: function(layers) {
- 344 :
var layers = this.getLayerData();
- 345 :
if (layers !== undefined) {
- 346 :
var me = this;
- 347 :
- 348 :
var keys = [];
- 349 :
this.down('[xtype=legend]').getStore().each(function(r) { keys.push(r.getId()); });
- 350 :
- 351 :
var steps;
- 352 :
if (this.getMode() === this.MODE_DOCUMENT) {
- 353 :
steps = this.getApiParam('bins');
- 354 :
} else {
- 355 :
var bins = this.getApiParam('bins');
- 356 :
var docsCount = this.getCorpus().getDocumentsCount();
- 357 :
- 358 :
steps = bins < docsCount ? bins : docsCount;
- 359 :
}
- 360 :
- 361 :
this.getVisLayout().keys(keys);
- 362 :
var processedLayers = this.getVisLayout()(layers);
- 363 :
- 364 :
var width = this.body.down('svg').getWidth() - this.graphMargin.left - this.graphMargin.right;
- 365 :
var x = d3.scaleLinear().domain([0, steps-1]).range([0, width]);
- 366 :
- 367 :
var min = d3.min(processedLayers, function(layer) {
- 368 :
return d3.min(layer, function(d) { return d[0]; });
- 369 :
});
- 370 :
var max = d3.max(processedLayers, function(layer) {
- 371 :
return d3.max(layer, function(d) { return d[1]; });
- 372 :
});
- 373 :
- 374 :
var height = this.body.down('svg').getHeight() - this.graphMargin.top - this.graphMargin.bottom;
- 375 :
var y = d3.scaleLinear().domain([min, max]).range([height, 0]);
- 376 :
- 377 :
var area = d3.area()
- 378 :
.x(function(d, i) { return x(i); })
- 379 :
.y0(function(d) { return y(d[0]); })
- 380 :
.y1(function(d) { return y(d[1]); })
- 381 :
.curve(d3.curveCatmullRom);
- 382 :
- 383 :
var xAxis;
- 384 :
if (this.getMode() === this.MODE_CORPUS) {
- 385 :
var xAxisDomain = [];
- 386 :
this.getCorpus().getDocuments().each(function(doc) {
- 387 :
xAxisDomain.push(doc.getTinyLabel());
- 388 :
});
- 389 :
var xAxisScale = d3.scalePoint().domain(xAxisDomain).range([0, width]);
- 390 :
xAxis = d3.axisBottom(xAxisScale);
- 391 :
} else {
- 392 :
xAxis = d3.axisBottom(x);
- 393 :
}
- 394 :
- 395 :
var yAxis = d3.axisLeft(y);
- 396 :
- 397 :
var paths = this.getVis().selectAll('path').data(processedLayers, function(d) { return d; });
- 398 :
- 399 :
paths
- 400 :
.attr('d', function(d) { return area(d); })
- 401 :
.style('fill', function(d) { return me.getApplication().getColorForTerm(d.key, true); })
- 402 :
.select('title').text(function (d) { return d.key; });
- 403 :
- 404 :
paths.enter().append('path')
- 405 :
.attr('d', function(d) { return area(d); })
- 406 :
.style('fill', function(d) { return me.getApplication().getColorForTerm(d.key, true); })
- 407 :
.append('title').text(function (d) { return d.key; });
- 408 :
- 409 :
paths.exit().remove();
- 410 :
- 411 :
this.getVis().selectAll('g.axis').remove();
- 412 :
- 413 :
this.getVis().append('g')
- 414 :
.attr('class', 'axis x')
- 415 :
.attr('transform', 'translate(0,'+height+')')
- 416 :
.call(xAxis);
- 417 :
- 418 :
var xAxisText;
- 419 :
if (this.getMode() === this.MODE_CORPUS) {
- 420 :
this.getVis().select('g.axis.x').selectAll('text').each(function() {
- 421 :
d3.select(this)
- 422 :
.attr('text-anchor', 'end')
- 423 :
.attr('transform', 'rotate(-45)');
- 424 :
});
- 425 :
- 426 :
xAxisText = this.localize('documents');
- 427 :
} else {
- 428 :
xAxisText = this.localize('documentSegments');
- 429 :
}
- 430 :
this.getVis().select('g.axis.x').append("text")
- 431 :
.attr('text-anchor', 'middle')
- 432 :
.attr('transform', 'translate('+width/2+', '+(this.graphMargin.bottom-30)+')')
- 433 :
.attr('fill', '#000')
- 434 :
.text(xAxisText);
- 435 :
- 436 :
this.getVis().append('g')
- 437 :
.attr('class', 'axis y')
- 438 :
.attr('transform', 'translate(0,0)')
- 439 :
.call(yAxis);
- 440 :
- 441 :
var yAxisText;
- 442 :
if (this.getApiParam('withDistributions') === 'raw') {
- 443 :
yAxisText = this.localize('rawFrequencies');
- 444 :
} else {
- 445 :
yAxisText = this.localize('relativeFrequencies');
- 446 :
}
- 447 :
this.getVis().select('g.axis.y').append("text")
- 448 :
.attr('text-anchor', 'middle')
- 449 :
.attr('transform', 'translate(-'+(this.graphMargin.left-20)+', '+height/2+') rotate(-90)')
- 450 :
.attr('fill', '#000')
- 451 :
.text(yAxisText);
- 452 :
}
- 453 :
},
- 454 :
- 455 :
getCurrentTerms: function() {
- 456 :
var terms = [];
- 457 :
this.down('[xtype=legend]').getStore().each(function(record) {
- 458 :
if (record.get('active')) {
- 459 :
terms.push(record.get('name'));
- 460 :
}
- 461 :
}, this);
- 462 :
return terms;
- 463 :
},
- 464 :
- 465 :
initGraph: function() {
- 466 :
if (this.getVisLayout() === undefined) {
- 467 :
var el = this.getLayout().getRenderTarget();
- 468 :
- 469 :
this.setVisLayout(d3.stack().offset(d3.stackOffsetWiggle).order(d3.stackOrderInsideOut));
- 470 :
this.setVis(d3.select(el.dom).append('svg').attr('id',this.getGraphId()).append('g').attr('transform', 'translate('+this.graphMargin.left+','+this.graphMargin.top+')'));
- 471 :
- 472 :
this.resizeGraph();
- 473 :
}
- 474 :
},
- 475 :
- 476 :
resizeGraph: function() {
- 477 :
var el = this.body;//getLayout().getRenderTarget();
- 478 :
var width = el.getWidth();
- 479 :
var height = el.getHeight();
- 480 :
- 481 :
d3.select(el.dom).select('svg').attr('width', width).attr('height', height);
- 482 :
- 483 :
this.doLayout();
- 484 :
}
- 485 :
});
- 486 :