diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 19bc59ef..695be17c 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -12,6 +12,21 @@ var StepNumber = require('./StepNumber'); // Definitions private to module // ----------------------------------------------------------------------------- +/// enumerate the available styles +Graph3d.STYLE = { + BAR : 0, + BARCOLOR: 1, + BARSIZE : 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE : 6, + GRID : 7, + LINE : 8, + SURFACE : 9 +}; + + /** * Field names in the options hash which are of relevance to the user. * @@ -32,9 +47,9 @@ var OPTIONKEYS = [ 'showGrid', 'showPerspective', 'showShadow', - 'showAnimationControls', 'keepAspectRatio', 'verticalRatio', + 'showAnimationControls', 'animationInterval', 'animationPreload', 'animationAutoStart', @@ -76,19 +91,37 @@ var DEFAULTS = { showPerspective : true, showShadow : false, keepAspectRatio : true, - verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' - animationInterval: 1000, // milliseconds - animationPreload : false, + verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + + showAnimationControls: undefined, // auto by default + animationInterval : 1000, // milliseconds + animationPreload : false, + animationAutoStart : undefined, // auto by default + axisColor : '#4D4D4D', gridColor : '#D3D3D3', xCenter : '55%', - yCenter : '50%' + yCenter : '50%', + + + // Following not in OPTIONKEYS because they require special handling, + style : Graph3d.STYLE.DOT, + tooltip : false, + showLegend : undefined, // auto by default (based on graph style) + backgroundColor : undefined, - // Following not in defaults (yet) but present in user settings - // These will be initialized as 'undefined' - //'showAnimationControls', - //'animationAutoStart' + dataColor : { + fill : '#7DC1FF', + stroke : '#3267D2', + strokeWidth: 1 // px + }, + + cameraPosition : { + horizontal: 1.0, + vertical : 0.5, + distance : 1.7 + } }; @@ -138,6 +171,7 @@ function safeCopy(src, dst, fields) { // Class Graph3d // ----------------------------------------------------------------------------- + /** * @constructor Graph3d * Graph3d displays data in 3d. @@ -160,6 +194,8 @@ function Graph3d(container, data, options) { this.dataTable = null; // The original data table this.dataPoints = null; // The table with point objects + // create a frame and canvas + this.create(); // // Start Settings @@ -169,24 +205,19 @@ function Graph3d(container, data, options) { forceCopy(DEFAULTS, this, OPTIONKEYS); // Following are internal fields, not part of the user settings - this.margin = 10; // px - this.showGrayBottom = false; // TODO: this does not work correctly + this.margin = 10; // px + this.showGrayBottom = false; // TODO: this does not work correctly this.showTooltip = false; - this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width + this.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + + // Handle the more complex ('special') fields + this._setSpecialSettings(DEFAULTS, this); // The rest of the fields. // These require special attention in some way // TODO: handle these - this.showLegend = undefined; // auto by default (based on graph style) - - this.style = Graph3d.STYLE.DOT; - - - this.camera = new Camera(); - this.camera.setArmRotation(1.0, 0.5); - this.camera.setArmLength(1.7); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? // the column indexes this.colX = undefined; @@ -210,21 +241,10 @@ function Graph3d(container, data, options) { this.yBarWidth = 1; // TODO: customize axis range - // colors - this.dataColor = { - fill: '#7DC1FF', - stroke: '#3267D2', - strokeWidth: 1 // px - }; - - // // End Settings // - // create a frame and canvas - this.create(); - // apply options (also when undefined) this.setOptions(options); @@ -381,11 +401,100 @@ Graph3d.prototype._calcTranslations = function(points, sort) { }; +// ----------------------------------------------------------------------------- +// Methods for handling settings +// ----------------------------------------------------------------------------- + + +/** + * Special handling for certain parameters + * + * 'Special' here means: setting requires more than a simple copy + */ +Graph3d.prototype._setSpecialSettings = function(src, dst) { + if (src.backgroundColor !== undefined) { + this._setBackgroundColor(src.backgroundColor, dst); + } + + this._setDataColor(src.dataColor, dst); + this._setStyle(src.style, dst); + this._setShowLegend(src.showLegend, dst); + this._setCameraPosition(src.cameraPosition, dst); + + // As special fields go, this is an easy one; just a translation of the name. + // Can't use this.tooltip directly, because that field exists internally + if (src.tooltip !== undefined) { + dst.showTooltip = src.tooltip; + } +}; + + +/** + * Set the value of setting 'showLegend' + * + * This depends on the value of the style fields, so it must be called + * after the style field has been initialized. + */ +Graph3d.prototype._setShowLegend = function(showLegend, dst) { + if (showLegend === undefined) { + // If the default was auto, make a choice for this field + var isAutoByDefault = (DEFAULTS.showLegend === undefined); + + if (isAutoByDefault) { + // these styles default to having legends + var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR + || this.style === Graph3d.STYLE.DOTSIZE; + + this.showLegend = isLegendGraphStyle; + } else { + // Leave current value as is + } + } else { + dst.showLegend = showLegend; + } +}; + + +Graph3d.prototype._setStyle = function(style, dst) { + if (style === undefined) { + return; // Nothing to do + } + + var styleNumber; + + if (typeof style === 'string') { + styleNumber = this._getStyleNumber(style); + + if (styleNumber === -1 ) { + throw new Error('Style \'' + style + '\' is invalid'); + } + } else { + // Do a pedantic check on style number value + var valid = false; + for (var n in Graph3d.STYLE) { + if (Graph3d.STYLE[n] === style) { + valid = true; + break; + } + } + + if (!valid) { + throw new Error('Style \'' + style + '\' is invalid'); + } + + styleNumber = style; + } + + dst.style = styleNumber; +}; + + + /** * Set the background styling for the graph * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ -Graph3d.prototype._setBackgroundColor = function(backgroundColor) { +Graph3d.prototype._setBackgroundColor = function(backgroundColor, dst) { var fill = 'white'; var stroke = 'gray'; var strokeWidth = 1; @@ -400,34 +509,92 @@ Graph3d.prototype._setBackgroundColor = function(backgroundColor) { if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; } - else if (backgroundColor === undefined) { - // use use defaults - } else { throw new Error('Unsupported type of backgroundColor'); } - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; + dst.frame.style.backgroundColor = fill; + dst.frame.style.borderColor = stroke; + dst.frame.style.borderWidth = strokeWidth + 'px'; + dst.frame.style.borderStyle = 'solid'; }; -/// enumerate the available styles -Graph3d.STYLE = { - BAR: 0, - BARCOLOR: 1, - BARSIZE: 2, - DOT : 3, - DOTLINE : 4, - DOTCOLOR: 5, - DOTSIZE: 6, - GRID : 7, - LINE: 8, - SURFACE : 9 +Graph3d.prototype._setDataColor = function(dataColor, dst) { + if (dataColor === undefined) { + return; // Nothing to do + } + + if (dst.dataColor === undefined) { + dst.dataColor = {}; + } + + if (typeof dataColor === 'string') { + dst.dataColor.fill = dataColor; + dst.dataColor.stroke = dataColor; + } + else { + if (dataColor.fill) { + dst.dataColor.fill = dataColor.fill; + } + if (dataColor.stroke) { + dst.dataColor.stroke = dataColor.stroke; + } + if (dataColor.strokeWidth !== undefined) { + dst.dataColor.strokeWidth = dataColor.strokeWidth; + } + } }; + +Graph3d.prototype._setCameraPosition = function(cameraPosition, dst) { + var camPos = cameraPosition; + if (camPos === undefined) { + return; + } + + if (dst.camera === undefined) { + dst.camera = new Camera(); + } + + dst.camera.setArmRotation(camPos.horizontal, camPos.vertical); + dst.camera.setArmLength(camPos.distance); +}; + + +// +// Public methods for specific settings +// + +/** + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. + */ +Graph3d.prototype.setCameraPosition = function(pos) { + this._setCameraPosition(pos, this); + this.redraw(); +}; + + +// ----------------------------------------------------------------------------- +// End methods for handling settings +// ----------------------------------------------------------------------------- + + + + /** * Retrieve the style index from given styleName * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' @@ -579,13 +746,11 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { this.colX = 'x'; this.colY = 'y'; this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; - - // check if a filter column is provided if (data[0].hasOwnProperty('filter')) { + this.colFilter = 'filter'; // Bugfix: only set this field if it's actually present! + if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); this.dataFilter.setOnLoadCallback(function() {me.redraw();}); @@ -643,17 +808,15 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; - if (this.colValue !== undefined) { + // Bugfix: Only handle field 'style' if it's actually present + if (data[0].hasOwnProperty('style')) { + this.colValue = 'style'; var valueRange = this.getColumnRange(data,this.colValue); this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; } - // these styles default to having legends - var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE; - this.showLegend = (this.defaultShowLegend !== undefined) ? this.defaultShowLegend : isLegendGraphStyle; - // set the scale dependent on the ranges. this._setScale(); }; @@ -743,6 +906,30 @@ Graph3d.prototype._getDataPoints = function (data) { } } else { // 'dot', 'dot-line', etc. + + // Bugfix: ensure value field is present in data if expected + var hasValueField = this.style === Graph3d.STYLE.BARCOLOR + || this.style === Graph3d.STYLE.BARSIZE + || this.style === Graph3d.STYLE.DOTCOLOR + || this.style === Graph3d.STYLE.DOTSIZE; + + if (hasValueField) { + if (this.colValue === undefined) { + throw new Error('Expected data to have ' + + ' field \'style\' ' + + ' for graph style \'' + this.style + '\'' + ); + } + + if (data[0][this.colValue] === undefined) { + throw new Error('Expected data to have ' + + ' field \'' + this.colValue + '\' ' + + ' for graph style \'' + this.style + '\'' + ); + } + } + + // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { point = new Point3d(); @@ -901,37 +1088,6 @@ Graph3d.prototype._resizeCenter = function() { } }; -/** - * Set the rotation and distance of the camera - * @param {Object} pos An object with the camera position. The object - * contains three parameters: - * - horizontal {Number} - * The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * - vertical {Number} - * The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - * - distance {Number} - * The (normalized) distance of the camera to the - * center of the graph, a value between 0.71 and 5.0. - * Optional, can be left undefined. - */ -Graph3d.prototype.setCameraPosition = function(pos) { - if (pos === undefined) { - return; - } - - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } - - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } - - this.redraw(); -}; /** @@ -995,17 +1151,10 @@ Graph3d.prototype.setOptions = function (options) { // Handle the parameters which can be simply copied over safeCopy(options, this, OPTIONKEYS); - // Handle the rest of the parameters - if (options.showLegend !== undefined) this.defaultShowLegend = options.showLegend; - - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } - } - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + // Handle the more complex ('special') fields + this._setSpecialSettings(options, this); + // Handle the rest of the parameters if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; if (options.xMin !== undefined) this.defaultXMin = options.xMin; @@ -1019,34 +1168,6 @@ Graph3d.prototype.setOptions = function (options) { if (options.zMax !== undefined) this.defaultZMax = options.zMax; if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor); - - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); - } - - // colors - if (options.dataColor) { - if (typeof options.dataColor === 'string') { - this.dataColor.fill = options.dataColor; - this.dataColor.stroke = options.dataColor; - } - else { - if (options.dataColor.fill) { - this.dataColor.fill = options.dataColor.fill; - } - if (options.dataColor.stroke) { - this.dataColor.stroke = options.dataColor.stroke; - } - if (options.dataColor.strokeWidth !== undefined) { - this.dataColor.strokeWidth = options.dataColor.strokeWidth; - } - } - } - } this.setSize(this.width, this.height); diff --git a/lib/graph3d/StepNumber.js b/lib/graph3d/StepNumber.js index 72a73839..9675b4f8 100644 --- a/lib/graph3d/StepNumber.js +++ b/lib/graph3d/StepNumber.js @@ -35,6 +35,17 @@ function StepNumber(start, end, step, prettyStep) { this.setRange(start, end, step, prettyStep); }; + +/** + * Check for input values, to prevent disasters from happening + * + * Source: http://stackoverflow.com/a/1830844 + */ +StepNumber.prototype.isNumeric = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +}; + + /** * Set a new range: start, end and step. * @@ -45,6 +56,16 @@ function StepNumber(start, end, step, prettyStep) { * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ StepNumber.prototype.setRange = function(start, end, step, prettyStep) { + if (!this.isNumeric(start)) { + throw new Error('Parameter \'start\' is not numeric; value: ' + start); + } + if (!this.isNumeric(end)) { + throw new Error('Parameter \'end\' is not numeric; value: ' + start); + } + if (!this.isNumeric(step)) { + throw new Error('Parameter \'step\' is not numeric; value: ' + start); + } + this._start = start ? start : 0; this._end = end ? end : 0;