Browse Source

Add data group class to Graph3d (#3152)

* First working version of new class DataGroup.

* Adjustments for review - done all points except last

* DRY distinct values, clean up sort code

* Added missing @param's to comments in DataGroup
gemini
wimrijnders 7 years ago
committed by yotamberk
parent
commit
33c3e3d8e4
3 changed files with 402 additions and 286 deletions
  1. +307
    -0
      lib/graph3d/DataGroup.js
  2. +4
    -9
      lib/graph3d/Filter.js
  3. +91
    -277
      lib/graph3d/Graph3d.js

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

@ -0,0 +1,307 @@
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Point3d = require('./Point3d');
var Range = require('./Range');
/**
* Creates a container for all data of one specific 3D-graph.
*
* On construction, the container is totally empty; the data
* needs to be initialized with method initializeData().
* Failure to do so will result in the following exception begin thrown
* on instantiation of Graph3D:
*
* Error: Array, DataSet, or DataView expected
*
* @constructor
*/
function DataGroup() {
this.dataTable = null; // The original data table
}
/**
* Initializes the instance from the passed data.
*
* Calculates minimum and maximum values and column index values.
*
* The graph3d instance is used internally to access the settings for
* the given instance.
* TODO: Pass settings only instead.
*
* @param {Graph3D} graph3d Reference to the calling Graph3D instance.
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
DataGroup.prototype.initializeData = function(graph3d, rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
if (rawData === undefined)
return;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = graph3d.hasBars(style);
// determine barWidth from data
if (withBars) {
if (graph3d.defaultXBarWidth !== undefined) {
this.xBarWidth = graph3d.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (graph3d.defaultYBarWidth !== undefined) {
this.yBarWidth = graph3d.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minima and maxima
this._initializeRange(data, this.colX, graph3d, withBars);
this._initializeRange(data, this.colY, graph3d, withBars);
this._initializeRange(data, this.colZ, graph3d, false);
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data, this.colValue);
this._setRangeDefaults(valueRange, graph3d.defaultValueMin, graph3d.defaultValueMax);
this.valueRange = valueRange;
}
};
/**
* Collect the range settings for the given data column.
*
* This internal method is intended to make the range
* initalization more generic.
*
* TODO: if/when combined settings per axis defined, get rid of this.
*
* @private
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
*/
DataGroup.prototype._collectRangeSettings = function(column, graph3d) {
var index = ['x', 'y', 'z'].indexOf(column);
if (index == -1) {
throw new Error('Column \'' + column + '\' invalid');
}
var upper = column.toUpperCase();
return {
barWidth : this[column + 'BarWidth'],
min : graph3d['default' + upper + 'Min'],
max : graph3d['default' + upper + 'Max'],
step : graph3d['default' + upper + 'Step'],
range_label: column + 'Range', // Name of instance field to write to
step_label : column + 'Step' // Name of instance field to write to
};
}
/**
* Initializes the settings per given column.
*
* TODO: if/when combined settings per axis defined, rewrite this.
*
* @private
*
* @param {DataSet | DataView} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
* @param {Graph3D} graph3d Reference to the calling Graph3D instance;
* required for access to settings
* @param {Boolean} withBars True if initializing for bar graph
*/
DataGroup.prototype._initializeRange = function(data, column, graph3d, withBars) {
var NUMSTEPS = 5;
var settings = this._collectRangeSettings(column, graph3d);
var range = this.getColumnRange(data, column);
if (withBars && column != 'z') { // Safeguard for 'z'; it doesn't have a bar width
range.expand(settings.barWidth / 2);
}
this._setRangeDefaults(range, settings.min, settings.max);
this[settings.range_label] = range;
this[settings.step_label ] = (settings.step !== undefined) ? settings.step : range.range()/NUMSTEPS;
}
/**
* Creates a list with all the different values in the data for the given column.
*
* If no data passed, use the internal data of this instance.
*
* @param {'x'|'y'|'z'} column The data column to process
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
*
* @returns {Array} All distinct values in the given column data, sorted ascending.
*/
DataGroup.prototype.getDistinctValues = function(column, data) {
if (data === undefined) {
data = this.dataTable;
}
var values = [];
for (var i = 0; i < data.length; i++) {
var value = data[i][column] || 0;
if (values.indexOf(value) === -1) {
values.push(value);
}
}
return values.sort(function(a,b) { return a - b; });
};
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
DataGroup.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @param {DataSet|DataView|undefined} data The data containing the items for the Graph
* @param {'x'|'y'|'z'} column The data column to process
*
* @returns {Range} A Range instance with min/max members properly set.
*/
DataGroup.prototype.getColumnRange = function(data, column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Determines the number of rows in the current data.
*
* @returns {Number}
*/
DataGroup.prototype.getNumberOfRows = function() {
return this.dataTable.length;
}
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
DataGroup.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
DataGroup.prototype.getDataTable = function() {
return this.dataTable;
};
DataGroup.prototype.getDataSet = function() {
return this.dataSet;
};
/**
* Reload the data
*/
DataGroup.prototype.reload = function() {
if (this.dataTable) {
this.setData(this.dataTable);
}
};
module.exports = DataGroup;

+ 4
- 9
lib/graph3d/Filter.js View File

@ -3,12 +3,12 @@ var DataView = require('../DataView');
/**
* @class Filter
*
* @param {DataSet} data The google data table
* @param {DataGroup} dataGroup the data group
* @param {Number} column The index of the column to be filtered
* @param {Graph} graph The graph
*/
function Filter (data, column, graph) {
this.data = data;
function Filter (dataGroup, column, graph) {
this.data = dataGroup.getDataSet();
this.column = column;
this.graph = graph; // the parent graph
@ -16,12 +16,7 @@ function Filter (data, column, graph) {
this.value = undefined;
// read all distinct values and select the first one
this.values = graph.getDistinctValues(data.get(), this.column);
// sort both numeric and string values correctly
this.values.sort(function (a, b) {
return a > b ? 1 : a < b ? -1 : 0;
});
this.values = dataGroup.getDistinctValues(this.column);
if (this.values.length > 0) {
this.selectValue(0);

+ 91
- 277
lib/graph3d/Graph3d.js View File

@ -1,4 +1,5 @@
var Emitter = require('emitter-component'); var DataSet = require('../DataSet');
var Emitter = require('emitter-component');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var util = require('../util');
var Point3d = require('./Point3d');
@ -9,6 +10,7 @@ var Slider = require('./Slider');
var StepNumber = require('./StepNumber');
var Range = require('./Range');
var Settings = require('./Settings');
var DataGroup = require('./DataGroup');
/// enumerate the available styles
@ -148,7 +150,7 @@ function Graph3d(container, data, options) {
// create variables and set default values
this.containerElement = container;
this.dataTable = null; // The original data table
this.dataGroup = new DataGroup();
this.dataPoints = null; // The table with point objects
// create a frame and canvas
@ -303,11 +305,7 @@ Graph3d.prototype._convertTranslationToScreen = function(translation) {
/**
* Calculate the translations and screen positions of all points
*/
Graph3d.prototype._calcTranslations = function(points, sort) {
if (sort === undefined) {
sort = true;
}
Graph3d.prototype._calcTranslations = function(points) {
for (var i = 0; i < points.length; i++) {
var point = points[i];
point.trans = this._convertPointToTranslation(point.point);
@ -318,10 +316,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
point.dist = this.showPerspective ? transBottom.length() : -transBottom.z;
}
if (!sort) {
return;
}
// sort the points on depth of their (x,y) position (not on z)
var sortDepth = function (a, b) {
return b.dist - a.dist;
@ -330,78 +324,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) {
};
Graph3d.prototype.getNumberOfRows = function(data) {
return data.length;
}
Graph3d.prototype.getNumberOfColumns = function(data) {
var counter = 0;
for (var column in data[0]) {
if (data[0].hasOwnProperty(column)) {
counter++;
}
}
return counter;
}
Graph3d.prototype.getDistinctValues = function(data, column) {
var distinctValues = [];
for (var i = 0; i < data.length; i++) {
if (distinctValues.indexOf(data[i][column]) == -1) {
distinctValues.push(data[i][column]);
}
}
return distinctValues.sort(function(a,b) { return a - b; });
}
/**
* Determine the smallest difference between the values for given
* column in the passed data set.
*
* @returns {Number|null} Smallest difference value or
* null, if it can't be determined.
*/
Graph3d.prototype.getSmallestDifference = function(data, column) {
var values = this.getDistinctValues(data, column);
var diffs = [];
// Get all the distinct diffs
// Array values is assumed to be sorted here
var smallest_diff = null;
for (var i = 1; i < values.length; i++) {
var diff = values[i] - values[i - 1];
if (smallest_diff == null || smallest_diff > diff ) {
smallest_diff = diff;
}
}
return smallest_diff;
}
/**
* Get the absolute min/max values for the passed data column.
*
* @returns {Range} A Range instance with min/max members properly set.
*/
Graph3d.prototype.getColumnRange = function(data,column) {
var range = new Range();
// Adjust the range so that it covers all values in the passed data elements.
for (var i = 0; i < data.length; i++) {
var item = data[i][column];
range.adjust(item);
}
return range;
};
/**
* Check if the state is consistent for the use of the value field.
*
@ -418,6 +340,7 @@ Graph3d.prototype._checkValueField = function (data) {
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 '
@ -437,150 +360,76 @@ Graph3d.prototype._checkValueField = function (data) {
};
/**
* Set default values for range
*
* The default values override the range values, if defined.
*
* Because it's possible that only defaultMin or defaultMax is set, it's better
* to pass in a range already set with the min/max set from the data. Otherwise,
* it's quite hard to process the min/max properly.
*/
Graph3d.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) {
if (defaultMin !== undefined) {
range.min = defaultMin;
}
if (defaultMax !== undefined) {
range.max = defaultMax;
}
// This is the original way that the default min/max values were adjusted.
// TODO: Perhaps it's better if an error is thrown if the values do not agree.
// But this will change the behaviour.
if (range.max <= range.min) range.max = range.min + 1;
};
/**
* Initialize the data from the data table. Calculate minimum and maximum values
* and column index values
* @param {Array | DataSet | DataView} rawData The data containing the items for
* the Graph.
* @param {Number} style Style Number
*/
Graph3d.prototype._dataInitialize = function (rawData, style) {
var me = this;
// unsubscribe from the dataTable
if (this.dataSet) {
this.dataSet.off('*', this._onChange);
}
Graph3d.prototype._initializeData = function(rawData, style) {
this.dataGroup.initializeData(this, rawData, style);
if (rawData === undefined)
return;
// Transfer min/max values to the Graph3d instance.
// 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;
if (Array.isArray(rawData)) {
rawData = new DataSet(rawData);
}
// 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;
var data;
if (rawData instanceof DataSet || rawData instanceof DataView) {
data = rawData.get();
}
else {
throw new Error('Array, DataSet, or DataView expected');
}
// Check if a filter column is provided
var data = this.dataGroup.getDataTable();
if (data.length == 0)
return;
this.dataSet = rawData;
this.dataTable = data;
// subscribe to changes in the dataset
this._onChange = function () {
me.setData(me.dataSet);
};
this.dataSet.on('*', this._onChange);
// determine the location of x,y,z,value,filter columns
this.colX = 'x';
this.colY = 'y';
this.colZ = 'z';
var withBars = this.style == Graph3d.STYLE.BAR ||
this.style == Graph3d.STYLE.BARCOLOR ||
this.style == Graph3d.STYLE.BARSIZE;
// determine barWidth from data
if (withBars) {
if (this.defaultXBarWidth !== undefined) {
this.xBarWidth = this.defaultXBarWidth;
}
else {
this.xBarWidth = this.getSmallestDifference(data, this.colX) || 1;
}
if (this.defaultYBarWidth !== undefined) {
this.yBarWidth = this.defaultYBarWidth;
}
else {
this.yBarWidth = this.getSmallestDifference(data, this.colY) || 1;
}
}
// calculate minimums and maximums
var NUMSTEPS = 5;
var xRange = this.getColumnRange(data, this.colX);
if (withBars) {
xRange.expand(this.xBarWidth / 2);
}
this._setRangeDefaults(xRange, this.defaultXMin, this.defaultXMax);
this.xRange = xRange;
this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : xRange.range()/NUMSTEPS;
var yRange = this.getColumnRange(data, this.colY);
if (withBars) {
yRange.expand(this.yBarWidth / 2);
}
this._setRangeDefaults(yRange, this.defaultYMin, this.defaultYMax);
this.yRange = yRange;
this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : yRange.range()/NUMSTEPS;
var zRange = this.getColumnRange(data, this.colZ);
this._setRangeDefaults(zRange, this.defaultZMin, this.defaultZMax);
this.zRange = zRange;
this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : zRange.range()/NUMSTEPS;
if (data[0].hasOwnProperty('style')) {
this.colValue = 'style';
var valueRange = this.getColumnRange(data,this.colValue);
this._setRangeDefaults(valueRange, this.defaultValueMin, this.defaultValueMax);
this.valueRange = valueRange;
}
// check if a filter column is provided
// Needs to be started after zRange is defined
if (data[0].hasOwnProperty('filter')) {
// Only set this field if it's actually present
this.colFilter = 'filter';
var me = this;
if (this.dataFilter === undefined) {
this.dataFilter = new Filter(rawData, this.colFilter, this);
this.dataFilter = new Filter(this.dataGroup, this.colFilter, this);
this.dataFilter.setOnLoadCallback(function() {me.redraw();});
}
}
// set the scale dependent on the ranges.
this._setScale();
};
/**
* Return all data values as a list of Point3d objects
*/
Graph3d.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;
};
/**
* Filter the data based on the current filter
@ -598,60 +447,29 @@ Graph3d.prototype._getDataPoints = function (data) {
if (this.style === Graph3d.STYLE.GRID ||
this.style === Graph3d.STYLE.SURFACE) {
// copy all values from the google data table to a matrix
// copy all values from the data table to a matrix
// the provided values are supposed to form a grid of (x,y) positions
// create two lists with all present x and y values
var dataX = [];
var dataY = [];
for (i = 0; i < this.getNumberOfRows(data); i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
if (dataX.indexOf(x) === -1) {
dataX.push(x);
}
if (dataY.indexOf(y) === -1) {
dataY.push(y);
}
}
var dataX = this.dataGroup.getDistinctValues(this.colX, data);
var dataY = this.dataGroup.getDistinctValues(this.colY, data);
var sortNumber = function (a, b) {
return a - b;
};
dataX.sort(sortNumber);
dataY.sort(sortNumber);
dataPoints = this.getDataPoints(data);
// create a grid, a 2d matrix, with all values.
var dataMatrix = []; // temporary data matrix
for (i = 0; i < data.length; i++) {
x = data[i][this.colX] || 0;
y = data[i][this.colY] || 0;
z = data[i][this.colZ] || 0;
for (i = 0; i < dataPoints.length; i++) {
obj = dataPoints[i];
// TODO: implement Array().indexOf() for Internet Explorer
var xIndex = dataX.indexOf(x);
var yIndex = dataY.indexOf(y);
var xIndex = dataX.indexOf(obj.point.x);
var yIndex = dataY.indexOf(obj.point.y);
if (dataMatrix[xIndex] === undefined) {
dataMatrix[xIndex] = [];
}
var point3d = new Point3d();
point3d.x = x;
point3d.y = y;
point3d.z = z;
point3d.data = data[i];
obj = {};
obj.point = point3d;
obj.trans = undefined;
obj.screen = undefined;
obj.bottom = new Point3d(x, y, this.zRange.min);
dataMatrix[xIndex][yIndex] = obj;
dataPoints.push(obj);
}
// fill in the pointers to the neighbors.
@ -670,39 +488,22 @@ Graph3d.prototype._getDataPoints = function (data) {
}
else { // 'dot', 'dot-line', etc.
this._checkValueField(data);
dataPoints = this.getDataPoints(data);
// copy all values from the google data table to a list with Point3d objects
for (i = 0; i < data.length; i++) {
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;
}
obj = {};
obj.point = point;
obj.bottom = new Point3d(point.x, point.y, this.zRange.min);
obj.trans = undefined;
obj.screen = undefined;
if (this.style === Graph3d.STYLE.LINE) {
if (this.style === Graph3d.STYLE.LINE) {
// Add next member points for line drawing
for (i = 0; i < dataPoints.length; i++) {
if (i > 0) {
// Add next point for line drawing
dataPoints[i - 1].pointNext = obj;
dataPoints[i - 1].pointNext = dataPoints[i];;
}
}
dataPoints.push(obj);
}
}
return dataPoints;
};
/**
* Create the main frame for the Graph3d.
*
@ -854,7 +655,7 @@ Graph3d.prototype.getCameraPosition = function() {
*/
Graph3d.prototype._readData = function(data) {
// read the data
this._dataInitialize(data, this.style);
this._initializeData(data, this.style);
if (this.dataFilter) {
@ -863,7 +664,7 @@ Graph3d.prototype._readData = function(data) {
}
else {
// no filtering. load all data
this.dataPoints = this._getDataPoints(this.dataTable);
this.dataPoints = this._getDataPoints(this.dataGroup.getDataTable());
}
// draw the filter
@ -901,9 +702,7 @@ Graph3d.prototype.setOptions = function (options) {
this._setSize(this.width, this.height);
// re-load the data
if (this.dataTable) {
this.setData(this.dataTable);
}
this.dataGroup.reload();
// start animation when option is true
if (this.animationAutoStart && this.dataFilter) {
@ -1138,6 +937,7 @@ Graph3d.prototype._redrawLegend = function() {
ctx.fillText(label, right, bottom + this.margin);
};
/**
* Redraw the filter
*/
@ -2335,6 +2135,20 @@ Graph3d.prototype._dataPointFromXY = function (x, y) {
return closestDataPoint;
};
/**
* Determine if the given style has bars
*
* @param {number} style the style to check
* @returns {boolean} true if bar style, false otherwise
*/
Graph3d.prototype.hasBars = function(style) {
return style == Graph3d.STYLE.BAR ||
style == Graph3d.STYLE.BARCOLOR ||
style == Graph3d.STYLE.BARSIZE;
};
/**
* Display a tooltip for given data point
* @param {Object} dataPoint

Loading…
Cancel
Save