diff --git a/lib/graph3d/DataGroup.js b/lib/graph3d/DataGroup.js new file mode 100644 index 00000000..8f2a615b --- /dev/null +++ b/lib/graph3d/DataGroup.js @@ -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; diff --git a/lib/graph3d/Filter.js b/lib/graph3d/Filter.js index 41d734a3..56f0c453 100644 --- a/lib/graph3d/Filter.js +++ b/lib/graph3d/Filter.js @@ -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); diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 29371ff7..eb4d61a6 100755 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -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