Browse Source

Graph3d: move Filter into DataGroup (#3159)

* Graph3D: move Filter into DataGroup

The `Filter` instance within `Graph3d` is intimately connected to the graph data,
contained in a `DataGroup` instance. As such, it needs to be placed within `DataGroup`.

A consequence of this is that, in the final case of multiple graphs, each graph can be
animation separately. I regard this as an advantage, even though it will means more
initialization for the graphs (you have to initialize each separately for an animation.

An effort has been made to decouple `Graph3d` and `Filter` as much as possible. There
are still some relationships present, but it's more bother than it's worth to dissolve these.

In addition to moving the `Filter` instance, the following has been done:

- Added variable `style` to `DataGroup`
- Moved certain data-specific methods from `Graph3d` to `DataGroup`
- cleaned up some code and commenting

These changes have been tested with the following examples:

- `graph3d/10_styling`
- `graph3d/03_filter_data`
- `graph3d/04_animation`

* Add changes to filter
revert-3409-performance
wimrijnders 7 years ago
committed by yotamberk
parent
commit
b9aec48b03
3 changed files with 266 additions and 129 deletions
  1. +196
    -0
      lib/graph3d/DataGroup.js
  2. +3
    -3
      lib/graph3d/Filter.js
  3. +67
    -126
      lib/graph3d/Graph3d.js

+ 196
- 0
lib/graph3d/DataGroup.js View File

@ -1,6 +1,9 @@
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Range = require('./Range');
var Filter = require('./Filter');
var Settings = require('./Settings');
var Point3d = require('./Point3d');
/**
@ -51,6 +54,8 @@ DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
if (data.length == 0) return;
this.style = style;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
@ -102,6 +107,27 @@ DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
this._setRangeDefaults(valueRange, graph3d.defaultValueMin, graph3d.defaultValueMax);
this.valueRange = valueRange;
}
// Initialize data filter if a filter column is provided
var table = this.getDataTable();
if (table[0].hasOwnProperty('filter')) {
if (this.dataFilter === undefined) {
this.dataFilter = new Filter(this, 'filter', graph3d);
this.dataFilter.setOnLoadCallback(function() { graph3d.redraw(); });
}
}
var dataPoints;
if (this.dataFilter) {
// apply filtering
dataPoints = this.dataFilter._getDataPoints();
}
else {
// no filtering. load all data
dataPoints = this._getDataPoints(this.getDataTable());
}
return dataPoints;
};
@ -290,4 +316,174 @@ DataGroup.prototype.getDataSet = function() {
};
/**
* Return all data values as a list of Point3d objects
*/
DataGroup.prototype.getDataPoints = function(data) {
var dataPoints = [];
for (var i = 0; i < data.length; i++) {
var point = new Point3d();
point.x = data[i][this.colX] || 0;
point.y = data[i][this.colY] || 0;
point.z = data[i][this.colZ] || 0;
point.data = data[i];
if (this.colValue !== undefined) {
point.value = data[i][this.colValue] || 0;
}
var obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
dataPoints.push(obj);
}
return dataPoints;
};
/**
* Copy all values from the data table to a matrix.
*
* The provided values are supposed to form a grid of (x,y) positions.
* @private
*/
DataGroup.prototype.initDataAsMatrix = function(data) {
// TODO: store the created matrix dataPoints in the filters instead of
// reloading each time.
var x, y, i, obj;
// create two lists with all present x and y values
var dataX = this.getDistinctValues(this.colX, data);
var dataY = this.getDistinctValues(this.colY, data);
var dataPoints = this.getDataPoints(data);
// create a grid, a 2d matrix, with all values.
var dataMatrix = []; // temporary data matrix
for (i = 0; i < dataPoints.length; i++) {
obj = dataPoints[i];
// TODO: implement Array().indexOf() for Internet Explorer
var xIndex = dataX.indexOf(obj.point.x);
var yIndex = dataY.indexOf(obj.point.y);
if (dataMatrix[xIndex] === undefined) {
dataMatrix[xIndex] = [];
}
dataMatrix[xIndex][yIndex] = obj;
}
// fill in the pointers to the neighbors.
for (x = 0; x < dataMatrix.length; x++) {
for (y = 0; y < dataMatrix[x].length; y++) {
if (dataMatrix[x][y]) {
dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
dataMatrix[x][y].pointCross =
(x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
dataMatrix[x+1][y+1] :
undefined;
}
}
}
return dataPoints;
}
/**
* Return common information, if present
*/
DataGroup.prototype.getInfo = function() {
var dataFilter = this.dataFilter;
if (!dataFilter) return undefined;
return dataFilter.getLabel() + ': ' + dataFilter.getSelectedValue();
};
/**
* Reload the data
*/
DataGroup.prototype.reload = function() {
if (this.dataTable) {
this.setData(this.dataTable);
}
};
/**
* Filter the data based on the current filter
*
* @param {Array} data
* @returns {Array} dataPoints Array with point objects which can be drawn on
* screen
*/
DataGroup.prototype._getDataPoints = function (data) {
var dataPoints = [];
if (this.style === Settings.STYLE.GRID || this.style === Settings.STYLE.SURFACE) {
dataPoints = this.initDataAsMatrix(data);
}
else { // 'dot', 'dot-line', etc.
this._checkValueField(data);
dataPoints = this.getDataPoints(data);
if (this.style === Settings.STYLE.LINE) {
// Add next member points for line drawing
for (var i = 0; i < dataPoints.length; i++) {
if (i > 0) {
dataPoints[i - 1].pointNext = dataPoints[i];
}
}
}
}
return dataPoints;
};
/**
* Check if the state is consistent for the use of the value field.
*
* Throws if a problem is detected.
* @private
*/
DataGroup.prototype._checkValueField = function (data) {
var hasValueField = this.style === Settings.STYLE.BARCOLOR
|| this.style === Settings.STYLE.BARSIZE
|| this.style === Settings.STYLE.DOTCOLOR
|| this.style === Settings.STYLE.DOTSIZE;
if (!hasValueField) {
return; // No need to check further
}
// Following field must be present for the current graph style
if (this.colValue === undefined) {
throw new Error('Expected data to have '
+ ' field \'style\' '
+ ' for graph style \'' + this.style + '\''
);
}
// The data must also contain this field.
// Note that only first data element is checked.
if (data[0][this.colValue] === undefined) {
throw new Error('Expected data to have '
+ ' field \'' + this.colValue + '\' '
+ ' for graph style \'' + this.style + '\''
);
}
};
module.exports = DataGroup;

+ 3
- 3
lib/graph3d/Filter.js View File

@ -8,7 +8,7 @@ var DataView = require('../DataView');
* @param {Graph} graph The graph
*/
function Filter (dataGroup, column, graph) {
this.data = dataGroup.getDataSet();
this.dataGroup = dataGroup;
this.column = column;
this.graph = graph; // the parent graph
@ -133,8 +133,8 @@ Filter.prototype._getDataPoints = function(index) {
f.column = this.column;
f.value = this.values[index];
var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
dataPoints = this.graph._getDataPoints(dataView);
var dataView = new DataView(this.dataGroup.getDataSet(), {filter: function (item) {return (item[f.column] == f.value);}}).get();
dataPoints = this.dataGroup._getDataPoints(dataView);
this.dataPoints[index] = dataPoints;
}

+ 67
- 126
lib/graph3d/Graph3d.js View File

@ -2,7 +2,6 @@ var Emitter = require('emitter-component');
var util = require('../util');
var Point3d = require('./Point3d');
var Point2d = require('./Point2d');
var Filter = require('./Filter');
var Slider = require('./Slider');
var StepNumber = require('./StepNumber');
var Settings = require('./Settings');
@ -159,7 +158,6 @@ function Graph3d(container, data, options) {
this.colY = undefined;
this.colZ = undefined;
this.colValue = undefined;
this.colFilter = undefined;
// TODO: customize axis range
@ -319,76 +317,28 @@ Graph3d.prototype._calcTranslations = function(points) {
/**
* Check if the state is consistent for the use of the value field.
*
* Throws if a problem is detected.
* Transfer min/max values to the Graph3d instance.
*/
Graph3d.prototype._checkValueField = function (data) {
var hasValueField = this.style === Graph3d.STYLE.BARCOLOR
|| this.style === Graph3d.STYLE.BARSIZE
|| this.style === Graph3d.STYLE.DOTCOLOR
|| this.style === Graph3d.STYLE.DOTSIZE;
if (!hasValueField) {
return; // No need to check further
}
// Following field must be present for the current graph style
if (this.colValue === undefined) {
throw new Error('Expected data to have '
+ ' field \'style\' '
+ ' for graph style \'' + this.style + '\''
);
}
// The data must also contain this field.
// Note that only first data element is checked.
if (data[0][this.colValue] === undefined) {
throw new Error('Expected data to have '
+ ' field \'' + this.colValue + '\' '
+ ' for graph style \'' + this.style + '\''
);
}
};
Graph3d.prototype._initializeData = function(rawData, style) {
this.dataGroup.initializeData(this, rawData, style);
// Transfer min/max values to the Graph3d instance.
Graph3d.prototype._initializeRanges = function() {
// TODO: later on, all min/maxes of all datagroups will be combined here
this.xRange = this.dataGroup.xRange;
this.yRange = this.dataGroup.yRange;
this.zRange = this.dataGroup.zRange;
this.valueRange = this.dataGroup.valueRange;
var dg = this.dataGroup;
this.xRange = dg.xRange;
this.yRange = dg.yRange;
this.zRange = dg.zRange;
this.valueRange = dg.valueRange;
// Values currently needed but which need to be sorted out for
// the multiple graph case.
this.xStep = this.dataGroup.xStep;
this.yStep = this.dataGroup.yStep;
this.zStep = this.dataGroup.zStep;
this.xBarWidth = this.dataGroup.xBarWidth;
this.yBarWidth = this.dataGroup.yBarWidth;
this.colX = this.dataGroup.colX;
this.colY = this.dataGroup.colY;
this.colZ = this.dataGroup.colZ;
this.colValue = this.dataGroup.colValue;
// Check if a filter column is provided
var data = this.dataGroup.getDataTable();
if (data[0].hasOwnProperty('filter')) {
// Only set this field if it's actually present
this.colFilter = 'filter';
this.xStep = dg.xStep;
this.yStep = dg.yStep;
this.zStep = dg.zStep;
this.xBarWidth = dg.xBarWidth;
this.yBarWidth = dg.yBarWidth;
this.colX = dg.colX;
this.colY = dg.colY;
this.colZ = dg.colZ;
this.colValue = dg.colValue;
var me = this;
if (this.dataFilter === undefined) {
this.dataFilter = new Filter(this.dataGroup, this.colFilter, this);
this.dataFilter.setOnLoadCallback(function() {me.redraw();});
}
}
// set the scale dependent on the ranges.
this._setScale();
@ -581,10 +531,14 @@ Graph3d.prototype._resizeCanvas = function() {
this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
};
/**
* Start animation
* Start animation, if requested and filter present
*/
Graph3d.prototype.animationStart = function() {
// start animation when option is true
if (!this.animationAutoStart || !this.dataGroup.dataFilter) return;
if (!this.frame.filter || !this.frame.filter.slider)
throw new Error('No animation available');
@ -649,19 +603,9 @@ Graph3d.prototype.getCameraPosition = function() {
*/
Graph3d.prototype._readData = function(data) {
// read the data
this._initializeData(data, this.style);
this.dataPoints = this.dataGroup.initializeData(this, data, this.style);
if (this.dataFilter) {
// apply filtering
this.dataPoints = this.dataFilter._getDataPoints();
}
else {
// no filtering. load all data
this.dataPoints = this._getDataPoints(this.dataGroup.getDataTable());
}
// draw the filter
this._initializeRanges();
this._redrawFilter();
};
@ -675,11 +619,7 @@ Graph3d.prototype.setData = function (data) {
this._readData(data);
this.redraw();
// start animation when option is true
if (this.animationAutoStart && this.dataFilter) {
this.animationStart();
}
this.animationStart();
};
/**
@ -691,16 +631,11 @@ Graph3d.prototype.setOptions = function (options) {
this.animationStop();
Settings.setOptions(options, this);
this.setPointDrawingMethod();
this._setSize(this.width, this.height);
this.setData(this.dataGroup.getDataTable());
// start animation when option is true
if (this.animationAutoStart && this.dataFilter) {
this.animationStart();
}
this.animationStart();
};
@ -934,39 +869,44 @@ Graph3d.prototype._redrawLegend = function() {
* Redraw the filter
*/
Graph3d.prototype._redrawFilter = function() {
this.frame.filter.innerHTML = '';
var dataFilter = this.dataGroup.dataFilter;
var filter = this.frame.filter;
filter.innerHTML = '';
if (this.dataFilter) {
var options = {
'visible': this.showAnimationControls
};
var slider = new Slider(this.frame.filter, options);
this.frame.filter.slider = slider;
if (!dataFilter) {
filter.slider = undefined;
return;
}
// TODO: css here is not nice here...
this.frame.filter.style.padding = '10px';
//this.frame.filter.style.backgroundColor = '#EFEFEF';
var options = {
'visible': this.showAnimationControls
};
var slider = new Slider(filter, options);
filter.slider = slider;
slider.setValues(this.dataFilter.values);
slider.setPlayInterval(this.animationInterval);
// TODO: css here is not nice here...
filter.style.padding = '10px';
//this.frame.filter.style.backgroundColor = '#EFEFEF';
// create an event handler
var me = this;
var onchange = function () {
var index = slider.getIndex();
slider.setValues(dataFilter.values);
slider.setPlayInterval(this.animationInterval);
me.dataFilter.selectValue(index);
me.dataPoints = me.dataFilter._getDataPoints();
// create an event handler
var me = this;
var onchange = function () {
var dataFilter = me.dataGroup.dataFilter;
var index = slider.getIndex();
me.redraw();
};
slider.setOnChangeCallback(onchange);
}
else {
this.frame.filter.slider = undefined;
}
dataFilter.selectValue(index);
me.dataPoints = dataFilter._getDataPoints();
me.redraw();
};
slider.setOnChangeCallback(onchange);
};
/**
* Redraw the slider
*/
@ -981,19 +921,20 @@ Graph3d.prototype._redrawSlider = function() {
* Redraw common information
*/
Graph3d.prototype._redrawInfo = function() {
if (this.dataFilter) {
var ctx = this._getContext();
var info = this.dataGroup.getInfo();
if (info === undefined) return;
ctx.font = '14px arial'; // TODO: put in options
ctx.lineStyle = 'gray';
ctx.fillStyle = 'gray';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
var ctx = this._getContext();
var x = this.margin;
var y = this.margin;
ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
}
ctx.font = '14px arial'; // TODO: put in options
ctx.lineStyle = 'gray';
ctx.fillStyle = 'gray';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
var x = this.margin;
var y = this.margin;
ctx.fillText(info, x, y);
};

Loading…
Cancel
Save