var DataSet = require('../DataSet'); var DataView = require('../DataView'); var Range = require('./Range'); var Filter = require('./Filter'); var Settings = require('./Settings'); var Point3d = require('./Point3d'); /** * 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) { 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.style = style; // unsubscribe from the dataTable if (this.dataSet) { this.dataSet.off('*', this._onChange); } this.dataSet = rawData; this.dataTable = data; // subscribe to changes in the dataset var me = this; this._onChange = function () { graph3d.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; } // 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; }; /** * 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; }; /** * 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;