From 6fc876a0ed14388910fc72fb46ddcbd8205fda32 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Thu, 20 Oct 2016 19:51:04 +0200 Subject: [PATCH 01/15] Final cleanup for settings handling --- lib/graph3d/Graph3d.js | 903 +++++++++++++++++++++-------------------- 1 file changed, 452 insertions(+), 451 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 102312a9..ccbba1d1 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -8,9 +8,6 @@ var Filter = require('./Filter'); var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); -// ----------------------------------------------------------------------------- -// Definitions private to module -// ----------------------------------------------------------------------------- /// enumerate the available styles Graph3d.STYLE = { @@ -28,59 +25,14 @@ Graph3d.STYLE = { /** - * Field names in the options hash which are of relevance to the user. - * - * Specifically, these are the fields which require no special handling, - * and can be directly copied over. - */ -var OPTIONKEYS = [ - 'width', - 'height', - 'filterLabel', - 'legendLabel', - 'xLabel', - 'yLabel', - 'zLabel', - 'xValueLabel', - 'yValueLabel', - 'zValueLabel', - 'showGrid', - 'showPerspective', - 'showShadow', - 'keepAspectRatio', - 'verticalRatio', - 'showAnimationControls', - 'animationInterval', - 'animationPreload', - 'animationAutoStart', - 'axisColor', - 'gridColor', - 'xCenter', - 'yCenter' -]; - - -/** - * Field names in the options hash which are of relevance to the user. - * - * Same as OPTIONKEYS, but internally these fields are stored with - * prefix 'default' in the name. + * Following label is used in the settings to describe values which + * should be determined by the code while running, from the current + * data and graph style. + * + * Using 'undefined' directly achieves the same thing, but this is + * more descriptive by describing the intent. */ -var PREFIXEDOPTIONKEYS = [ - 'xBarWidth', - 'yBarWidth', - 'valueMin', - 'valueMax', - 'xMin', - 'xMax', - 'xStep', - 'yMin', - 'yMax', - 'yStep', - 'zMin', - 'zMax', - 'zStep' -]; +var autoByDefault = undefined; /** @@ -89,15 +41,8 @@ var PREFIXEDOPTIONKEYS = [ * These are the values used when a Graph3d instance is initialized * without custom settings. * - * If a field is not in this list, a default value of 'undefined' can - * be assumed. Of course, it does no harm to set a field explicitly to - * 'undefined' here. - * - * A value of 'undefined' here normally means: - * - * 'derive from current data and graph style' - * - * In the code, this is indicated by the comment 'auto by default'. + * If a field is not in this list, a default value of 'autoByDefault' + * is assumed, which is just an alias for 'undefined'. */ var DEFAULTS = { width : '400px', @@ -114,29 +59,27 @@ var DEFAULTS = { showPerspective : true, showShadow : false, keepAspectRatio : true, - verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' - showAnimationControls: undefined, // auto by default - animationInterval : 1000, // milliseconds + showAnimationControls: autoByDefault, + animationInterval : 1000, // milliseconds animationPreload : false, - animationAutoStart : undefined, // auto by default + animationAutoStart : autoByDefault, axisColor : '#4D4D4D', gridColor : '#D3D3D3', xCenter : '55%', yCenter : '50%', - // Following require special handling, therefore not mentioned in the OPTIONKEYS tables. - style : Graph3d.STYLE.DOT, tooltip : false, - showLegend : undefined, // auto by default (based on graph style) - backgroundColor : undefined, + showLegend : autoByDefault, // determined by graph style + backgroundColor : autoByDefault, dataColor : { fill : '#7DC1FF', stroke : '#3267D2', - strokeWidth: 1 // px + strokeWidth: 1 // px }, cameraPosition : { @@ -145,102 +88,22 @@ var DEFAULTS = { distance : 1.7 }, - // Following stored internally with field prefix 'default' - // All these are 'auto by default' - - xBarWidth : undefined, - yBarWidth : undefined, - valueMin : undefined, - valueMax : undefined, - xMin : undefined, - xMax : undefined, - xStep : undefined, - yMin : undefined, - yMax : undefined, - yStep : undefined, - zMin : undefined, - zMax : undefined, - zStep : undefined + xBarWidth : autoByDefault, + yBarWidth : autoByDefault, + valueMin : autoByDefault, + valueMax : autoByDefault, + xMin : autoByDefault, + xMax : autoByDefault, + xStep : autoByDefault, + yMin : autoByDefault, + yMax : autoByDefault, + yStep : autoByDefault, + zMin : autoByDefault, + zMax : autoByDefault, + zStep : autoByDefault }; -/** - * Make first letter of parameter upper case. - * - * Source: http://stackoverflow.com/a/1026087 - */ -function capitalize(str) { - if (str === undefined || str === "") { - return str; - } - - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -/** - * Add a prefix to a field name, taking style guide into account - */ -function prefixFieldName(prefix, fieldName) { - if (prefix === undefined || prefix === "") { - return fieldName; - } - - return prefix + capitalize(fieldName); -} - - -/** - * Forcibly copy fields from src to dst in a controlled manner. - * - * A given field in dst will always be overwitten. If this field - * is undefined or not present in src, the field in dst will - * be explicitly set to undefined. - * - * The intention here is to be able to reset all option fields. - * - * Only the fields mentioned in array 'fields' will be handled. - * - * @param fields array with names of fields to copy - * @param prefix optional; prefix to use for the target fields. - */ -function forceCopy(src, dst, fields, prefix) { - var srcKey; - var dstKey; - - for (var i in fields) { - srcKey = fields[i]; - dstKey = prefixFieldName(prefix, srcKey); - - dst[dstKey] = src[srcKey]; - } -} - - -/** - * Copy fields from src to dst in a safe and controlled manner. - * - * Only the fields mentioned in array 'fields' will be copied over, - * and only if these are actually defined. - * - * @param fields array with names of fields to copy - * @param prefix optional; prefix to use for the target fields. - */ -function safeCopy(src, dst, fields, prefix) { - var srcKey; - var dstKey; - - for (var i in fields) { - srcKey = fields[i]; - if (src[srcKey] === undefined) continue; - - dstKey = prefixFieldName(prefix, srcKey); - - dst[dstKey] = src[srcKey]; - } -} - - // ----------------------------------------------------------------------------- // Class Graph3d @@ -272,27 +135,7 @@ function Graph3d(container, data, options) { // create a frame and canvas this.create(); - // - // Set Defaults - // - - // Handle the defaults which can be simply copied over - forceCopy(DEFAULTS, this, OPTIONKEYS); - forceCopy(DEFAULTS, this, PREFIXEDOPTIONKEYS, 'default'); - - // Following are internal fields, not part of the user settings - 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.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); - - // - // End Set Defaults - // + this._setDefaults(); // the column indexes this.colX = undefined; @@ -459,278 +302,87 @@ 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 + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found */ -Graph3d.prototype._setSpecialSettings = function(src, dst) { - if (src.backgroundColor !== undefined) { - this._setBackgroundColor(src.backgroundColor, dst); +Graph3d.prototype._getStyleNumber = function(styleName) { + switch (styleName) { + case 'dot': return Graph3d.STYLE.DOT; + case 'dot-line': return Graph3d.STYLE.DOTLINE; + case 'dot-color': return Graph3d.STYLE.DOTCOLOR; + case 'dot-size': return Graph3d.STYLE.DOTSIZE; + case 'line': return Graph3d.STYLE.LINE; + case 'grid': return Graph3d.STYLE.GRID; + case 'surface': return Graph3d.STYLE.SURFACE; + case 'bar': return Graph3d.STYLE.BAR; + case 'bar-color': return Graph3d.STYLE.BARCOLOR; + case 'bar-size': return Graph3d.STYLE.BARSIZE; } - 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; - } + return -1; }; - /** - * 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. + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ -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); +Graph3d.prototype._determineColumnIndexes = function(data, style) { + if (this.style === Graph3d.STYLE.DOT || + this.style === Graph3d.STYLE.DOTLINE || + this.style === Graph3d.STYLE.LINE || + this.style === Graph3d.STYLE.GRID || + this.style === Graph3d.STYLE.SURFACE || + this.style === Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; - if (isAutoByDefault) { - // these styles default to having legends - var isLegendGraphStyle = this.style === Graph3d.STYLE.DOTCOLOR - || this.style === Graph3d.STYLE.DOTSIZE; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } + else if (this.style === Graph3d.STYLE.DOTCOLOR || + this.style === Graph3d.STYLE.DOTSIZE || + this.style === Graph3d.STYLE.BARCOLOR || + this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; - this.showLegend = isLegendGraphStyle; - } else { - // Leave current value as is + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; } - } else { - dst.showLegend = showLegend; + } + else { + throw new Error('Unknown style "' + this.style + '"'); } }; +Graph3d.prototype.getNumberOfRows = function(data) { + return data.length; +} + -Graph3d.prototype._setStyle = function(style, dst) { - if (style === undefined) { - return; // Nothing to do +Graph3d.prototype.getNumberOfColumns = function(data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } } - - 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, dst) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; - - if (typeof(backgroundColor) === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } - else if (typeof(backgroundColor) === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } - else { - throw new Error('Unsupported type of backgroundColor'); - } - - dst.frame.style.backgroundColor = fill; - dst.frame.style.borderColor = stroke; - dst.frame.style.borderWidth = strokeWidth + 'px'; - dst.frame.style.borderStyle = 'solid'; -}; - - -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' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found - */ -Graph3d.prototype._getStyleNumber = function(styleName) { - switch (styleName) { - case 'dot': return Graph3d.STYLE.DOT; - case 'dot-line': return Graph3d.STYLE.DOTLINE; - case 'dot-color': return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': return Graph3d.STYLE.DOTSIZE; - case 'line': return Graph3d.STYLE.LINE; - case 'grid': return Graph3d.STYLE.GRID; - case 'surface': return Graph3d.STYLE.SURFACE; - case 'bar': return Graph3d.STYLE.BAR; - case 'bar-color': return Graph3d.STYLE.BARCOLOR; - case 'bar-size': return Graph3d.STYLE.BARSIZE; - } - - return -1; -}; - -/** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style - */ -Graph3d.prototype._determineColumnIndexes = function(data, style) { - if (this.style === Graph3d.STYLE.DOT || - this.style === Graph3d.STYLE.DOTLINE || - this.style === Graph3d.STYLE.LINE || - this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE || - this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; - - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } - else if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; - - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } - else { - throw new Error('Unknown style "' + this.style + '"'); - } -}; - -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; -} + return counter; +} Graph3d.prototype.getDistinctValues = function(data, column) { @@ -1203,16 +855,7 @@ Graph3d.prototype.setOptions = function (options) { this.animationStop(); - if (options !== undefined) { - // retrieve parameter values - - // Handle the parameters which can be simply copied over - safeCopy(options, this, OPTIONKEYS); - safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); - - // Handle the more complex ('special') fields - this._setSpecialSettings(options, this); - } + this._setOptions(options); this.setSize(this.width, this.height); @@ -2571,4 +2214,362 @@ function getMouseY (event) { return event.targetTouches[0] && event.targetTouches[0].clientY || 0; } + +// ----------------------------------------------------------------------------- +// Methods for handling settings +// ----------------------------------------------------------------------------- + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Specifically, these are the fields which require no special handling, + * and can be directly copied over. + */ +var OPTIONKEYS = [ + 'width', + 'height', + 'filterLabel', + 'legendLabel', + 'xLabel', + 'yLabel', + 'zLabel', + 'xValueLabel', + 'yValueLabel', + 'zValueLabel', + 'showGrid', + 'showPerspective', + 'showShadow', + 'keepAspectRatio', + 'verticalRatio', + 'showAnimationControls', + 'animationInterval', + 'animationPreload', + 'animationAutoStart', + 'axisColor', + 'gridColor', + 'xCenter', + 'yCenter' +]; + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Same as OPTIONKEYS, but internally these fields are stored with + * prefix 'default' in the name. + */ +var PREFIXEDOPTIONKEYS = [ + 'xBarWidth', + 'yBarWidth', + 'valueMin', + 'valueMax', + 'xMin', + 'xMax', + 'xStep', + 'yMin', + 'yMax', + 'yStep', + 'zMin', + 'zMax', + 'zStep' +]; + + +/** + * Make first letter of parameter upper case. + * + * Source: http://stackoverflow.com/a/1026087 + */ +function capitalize(str) { + if (str === undefined || str === "") { + return str; + } + + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +/** + * Add a prefix to a field name, taking style guide into account + */ +function prefixFieldName(prefix, fieldName) { + if (prefix === undefined || prefix === "") { + return fieldName; + } + + return prefix + capitalize(fieldName); +} + + +/** + * Forcibly copy fields from src to dst in a controlled manner. + * + * A given field in dst will always be overwitten. If this field + * is undefined or not present in src, the field in dst will + * be explicitly set to undefined. + * + * The intention here is to be able to reset all option fields. + * + * Only the fields mentioned in array 'fields' will be handled. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function forceCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + +/** + * Copy fields from src to dst in a safe and controlled manner. + * + * Only the fields mentioned in array 'fields' will be copied over, + * and only if these are actually defined. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function safeCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + if (src[srcKey] === undefined) continue; + + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + + +Graph3d.prototype._setDefaults = function() { + + // Handle the defaults which can be simply copied over + forceCopy(DEFAULTS, this, OPTIONKEYS); + forceCopy(DEFAULTS, this, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + this._setSpecialSettings(DEFAULTS, this); + + // Following are internal fields, not part of the user settings + 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.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? +}; + + +Graph3d.prototype._setOptions = function(options) { + if (options === undefined) { + return; + } + + // Handle the parameters which can be simply copied over + safeCopy(options, this, OPTIONKEYS); + safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + this._setSpecialSettings(options, this); +}; + + +/** + * 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, dst) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; + + if (typeof(backgroundColor) === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } + else if (typeof(backgroundColor) === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } + else { + throw new Error('Unsupported type of backgroundColor'); + } + + dst.frame.style.backgroundColor = fill; + dst.frame.style.borderColor = stroke; + dst.frame.style.borderWidth = strokeWidth + 'px'; + dst.frame.style.borderStyle = 'solid'; +}; + + +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 +// ----------------------------------------------------------------------------- + + module.exports = Graph3d; From 3e84c3557ee04e7ec9dcdde6a24fcb792d2eaabf Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Fri, 21 Oct 2016 09:07:18 +0200 Subject: [PATCH 02/15] Moved code for settings to separate module --- lib/graph3d/Graph3d.js | 376 +------------------------------------- lib/graph3d/Settings.js | 388 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+), 369 deletions(-) create mode 100644 lib/graph3d/Settings.js diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index ccbba1d1..1d8515d6 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -7,21 +7,11 @@ var Camera = require('./Camera'); var Filter = require('./Filter'); var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); +var Settings = require('./Settings'); /// 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.STYLE = Settings.STYLE; /** @@ -135,7 +125,7 @@ function Graph3d(container, data, options) { // create a frame and canvas this.create(); - this._setDefaults(); + Settings.setDefaults(this); // the column indexes this.colX = undefined; @@ -302,32 +292,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) { }; - - - -/** - * Retrieve the style index from given styleName - * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found - */ -Graph3d.prototype._getStyleNumber = function(styleName) { - switch (styleName) { - case 'dot': return Graph3d.STYLE.DOT; - case 'dot-line': return Graph3d.STYLE.DOTLINE; - case 'dot-color': return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': return Graph3d.STYLE.DOTSIZE; - case 'line': return Graph3d.STYLE.LINE; - case 'grid': return Graph3d.STYLE.GRID; - case 'surface': return Graph3d.STYLE.SURFACE; - case 'bar': return Graph3d.STYLE.BAR; - case 'bar-color': return Graph3d.STYLE.BARCOLOR; - case 'bar-size': return Graph3d.STYLE.BARSIZE; - } - - return -1; -}; - /** * Determine the indexes of the data columns, based on the given style and data * @param {DataSet} data @@ -855,7 +819,7 @@ Graph3d.prototype.setOptions = function (options) { this.animationStop(); - this._setOptions(options); + Settings.setOptions(options, this); this.setSize(this.width, this.height); @@ -2216,335 +2180,9 @@ function getMouseY (event) { // ----------------------------------------------------------------------------- -// Methods for handling settings +// Public methods for specific settings // ----------------------------------------------------------------------------- - -/** - * Field names in the options hash which are of relevance to the user. - * - * Specifically, these are the fields which require no special handling, - * and can be directly copied over. - */ -var OPTIONKEYS = [ - 'width', - 'height', - 'filterLabel', - 'legendLabel', - 'xLabel', - 'yLabel', - 'zLabel', - 'xValueLabel', - 'yValueLabel', - 'zValueLabel', - 'showGrid', - 'showPerspective', - 'showShadow', - 'keepAspectRatio', - 'verticalRatio', - 'showAnimationControls', - 'animationInterval', - 'animationPreload', - 'animationAutoStart', - 'axisColor', - 'gridColor', - 'xCenter', - 'yCenter' -]; - - -/** - * Field names in the options hash which are of relevance to the user. - * - * Same as OPTIONKEYS, but internally these fields are stored with - * prefix 'default' in the name. - */ -var PREFIXEDOPTIONKEYS = [ - 'xBarWidth', - 'yBarWidth', - 'valueMin', - 'valueMax', - 'xMin', - 'xMax', - 'xStep', - 'yMin', - 'yMax', - 'yStep', - 'zMin', - 'zMax', - 'zStep' -]; - - -/** - * Make first letter of parameter upper case. - * - * Source: http://stackoverflow.com/a/1026087 - */ -function capitalize(str) { - if (str === undefined || str === "") { - return str; - } - - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -/** - * Add a prefix to a field name, taking style guide into account - */ -function prefixFieldName(prefix, fieldName) { - if (prefix === undefined || prefix === "") { - return fieldName; - } - - return prefix + capitalize(fieldName); -} - - -/** - * Forcibly copy fields from src to dst in a controlled manner. - * - * A given field in dst will always be overwitten. If this field - * is undefined or not present in src, the field in dst will - * be explicitly set to undefined. - * - * The intention here is to be able to reset all option fields. - * - * Only the fields mentioned in array 'fields' will be handled. - * - * @param fields array with names of fields to copy - * @param prefix optional; prefix to use for the target fields. - */ -function forceCopy(src, dst, fields, prefix) { - var srcKey; - var dstKey; - - for (var i in fields) { - srcKey = fields[i]; - dstKey = prefixFieldName(prefix, srcKey); - - dst[dstKey] = src[srcKey]; - } -} - - -/** - * Copy fields from src to dst in a safe and controlled manner. - * - * Only the fields mentioned in array 'fields' will be copied over, - * and only if these are actually defined. - * - * @param fields array with names of fields to copy - * @param prefix optional; prefix to use for the target fields. - */ -function safeCopy(src, dst, fields, prefix) { - var srcKey; - var dstKey; - - for (var i in fields) { - srcKey = fields[i]; - if (src[srcKey] === undefined) continue; - - dstKey = prefixFieldName(prefix, srcKey); - - dst[dstKey] = src[srcKey]; - } -} - - - -Graph3d.prototype._setDefaults = function() { - - // Handle the defaults which can be simply copied over - forceCopy(DEFAULTS, this, OPTIONKEYS); - forceCopy(DEFAULTS, this, PREFIXEDOPTIONKEYS, 'default'); - - // Handle the more complex ('special') fields - this._setSpecialSettings(DEFAULTS, this); - - // Following are internal fields, not part of the user settings - 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.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? -}; - - -Graph3d.prototype._setOptions = function(options) { - if (options === undefined) { - return; - } - - // Handle the parameters which can be simply copied over - safeCopy(options, this, OPTIONKEYS); - safeCopy(options, this, PREFIXEDOPTIONKEYS, 'default'); - - // Handle the more complex ('special') fields - this._setSpecialSettings(options, this); -}; - - -/** - * 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, dst) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; - - if (typeof(backgroundColor) === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } - else if (typeof(backgroundColor) === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } - else { - throw new Error('Unsupported type of backgroundColor'); - } - - dst.frame.style.backgroundColor = fill; - dst.frame.style.borderColor = stroke; - dst.frame.style.borderWidth = strokeWidth + 'px'; - dst.frame.style.borderStyle = 'solid'; -}; - - -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 @@ -2562,13 +2200,13 @@ Graph3d.prototype._setCameraPosition = function(cameraPosition, dst) { * Optional, can be left undefined. */ Graph3d.prototype.setCameraPosition = function(pos) { - this._setCameraPosition(pos, this); + Settings.setCameraPosition(pos, this); this.redraw(); }; // ----------------------------------------------------------------------------- -// End methods for handling settings +// End public methods for specific settings // ----------------------------------------------------------------------------- diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js new file mode 100644 index 00000000..c8663a18 --- /dev/null +++ b/lib/graph3d/Settings.js @@ -0,0 +1,388 @@ +//////////////////////////////////////////////////////////////////////////////// +// This modules handles the options for Graph3d. +// +//////////////////////////////////////////////////////////////////////////////// + +// enumerate the available styles +var STYLE = { + BAR : 0, + BARCOLOR: 1, + BARSIZE : 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE : 6, + GRID : 7, + LINE : 8, + SURFACE : 9 +}; + + +// The string representations of the styles +var STYLENAME = { + 'dot' : STYLE.DOT, + 'dot-line' : STYLE.DOTLINE, + 'dot-color': STYLE.DOTCOLOR, + 'dot-size' : STYLE.DOTSIZE, + 'line' : STYLE.LINE, + 'grid' : STYLE.GRID, + 'surface' : STYLE.SURFACE, + 'bar' : STYLE.BAR, + 'bar-color': STYLE.BARCOLOR, + 'bar-size' : STYLE.BARSIZE +}; + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Specifically, these are the fields which require no special handling, + * and can be directly copied over. + */ +var OPTIONKEYS = [ + 'width', + 'height', + 'filterLabel', + 'legendLabel', + 'xLabel', + 'yLabel', + 'zLabel', + 'xValueLabel', + 'yValueLabel', + 'zValueLabel', + 'showGrid', + 'showPerspective', + 'showShadow', + 'keepAspectRatio', + 'verticalRatio', + 'showAnimationControls', + 'animationInterval', + 'animationPreload', + 'animationAutoStart', + 'axisColor', + 'gridColor', + 'xCenter', + 'yCenter' +]; + + +/** + * Field names in the options hash which are of relevance to the user. + * + * Same as OPTIONKEYS, but internally these fields are stored with + * prefix 'default' in the name. + */ +var PREFIXEDOPTIONKEYS = [ + 'xBarWidth', + 'yBarWidth', + 'valueMin', + 'valueMax', + 'xMin', + 'xMax', + 'xStep', + 'yMin', + 'yMax', + 'yStep', + 'zMin', + 'zMax', + 'zStep' +]; + + +/** + * Make first letter of parameter upper case. + * + * Source: http://stackoverflow.com/a/1026087 + */ +function capitalize(str) { + if (str === undefined || str === "") { + return str; + } + + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +/** + * Add a prefix to a field name, taking style guide into account + */ +function prefixFieldName(prefix, fieldName) { + if (prefix === undefined || prefix === "") { + return fieldName; + } + + return prefix + capitalize(fieldName); +} + + +/** + * Forcibly copy fields from src to dst in a controlled manner. + * + * A given field in dst will always be overwitten. If this field + * is undefined or not present in src, the field in dst will + * be explicitly set to undefined. + * + * The intention here is to be able to reset all option fields. + * + * Only the fields mentioned in array 'fields' will be handled. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function forceCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + +/** + * Copy fields from src to dst in a safe and controlled manner. + * + * Only the fields mentioned in array 'fields' will be copied over, + * and only if these are actually defined. + * + * @param fields array with names of fields to copy + * @param prefix optional; prefix to use for the target fields. + */ +function safeCopy(src, dst, fields, prefix) { + var srcKey; + var dstKey; + + for (var i in fields) { + srcKey = fields[i]; + if (src[srcKey] === undefined) continue; + + dstKey = prefixFieldName(prefix, srcKey); + + dst[dstKey] = src[srcKey]; + } +} + + +function setDefaults(dst) { + // Handle the defaults which can be simply copied over + forceCopy(DEFAULTS, dst, OPTIONKEYS); + forceCopy(DEFAULTS, dst, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + setSpecialSettings(DEFAULTS, dst); + + // Following are internal fields, not part of the user settings + dst.margin = 10; // px + dst.showGrayBottom = false; // TODO: this does not work correctly + dst.showTooltip = false; + dst.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width + dst.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? +} + + +function setOptions(options, dst) { + if (options === undefined) { + return; + } + + // Handle the parameters which can be simply copied over + safeCopy(options, dst, OPTIONKEYS); + safeCopy(options, dst, PREFIXEDOPTIONKEYS, 'default'); + + // Handle the more complex ('special') fields + setSpecialSettings(options, dst); +} + + +/** + * Special handling for certain parameters + * + * 'Special' here means: setting requires more than a simple copy + */ +function setSpecialSettings(src, dst) { + if (src.backgroundColor !== undefined) { + setBackgroundColor(src.backgroundColor, dst); + } + + setDataColor(src.dataColor, dst); + setStyle(src.style, dst); + setShowLegend(src.showLegend, dst); + 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. + */ +function setShowLegend(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 = dst.style === STYLE.DOTCOLOR + || dst.style === STYLE.DOTSIZE; + + dst.showLegend = isLegendGraphStyle; + } else { + // Leave current value as is + } + } else { + dst.showLegend = showLegend; + } +} + + +/** + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found + */ +function getStyleNumberByName(styleName) { + var number = STYLENAME[stylename]; + + if (number === undefined) { + return -1; + } + + return number; +} + + +/** + * Check if given number is a valid style number. + * + * @return true if valid, false otherwise + */ +function checkStyleNumber(style) { + var valid = false; + + for (var n in STYLE) { + if (STYLE[n] === style) { + valid = true; + break; + } + } + + return valid; +} + + +function setStyle(style, dst) { + if (style === undefined) { + return; // Nothing to do + } + + var styleNumber; + + if (typeof style === 'string') { + styleNumber = getStyleNumberByName(style); + + if (styleNumber === -1 ) { + throw new Error('Style \'' + style + '\' is invalid'); + } + } else { + // Do a pedantic check on style number value + if (!checkStyleNumber(style)) { + 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 + */ +function setBackgroundColor(backgroundColor, dst) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; + + if (typeof(backgroundColor) === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } + else if (typeof(backgroundColor) === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } + else { + throw new Error('Unsupported type of backgroundColor'); + } + + dst.frame.style.backgroundColor = fill; + dst.frame.style.borderColor = stroke; + dst.frame.style.borderWidth = strokeWidth + 'px'; + dst.frame.style.borderStyle = 'solid'; +} + + +function setDataColor(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; + } + } +} + + +function setCameraPosition(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); +} + + + + +module.exports.STYLE = STYLE; +module.exports.setDefaults = setDefaults; +module.exports.setOptions = setOptions; +module.exports.setCameraPosition = setCameraPosition; From baed96ee7ce2dd0344f57f61bde099894bb0473b Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Fri, 21 Oct 2016 09:58:51 +0200 Subject: [PATCH 03/15] Fixes and hardening for Settings.js --- lib/graph3d/Graph3d.js | 3 +-- lib/graph3d/Settings.js | 60 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 1d8515d6..fa8e2b37 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -94,7 +94,6 @@ var DEFAULTS = { }; - // ----------------------------------------------------------------------------- // Class Graph3d // ----------------------------------------------------------------------------- @@ -125,7 +124,7 @@ function Graph3d(container, data, options) { // create a frame and canvas this.create(); - Settings.setDefaults(this); + Settings.setDefaults(DEFAULTS, this); // the column indexes this.colX = undefined; diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js index c8663a18..f1b7da6a 100644 --- a/lib/graph3d/Settings.js +++ b/lib/graph3d/Settings.js @@ -2,6 +2,9 @@ // This modules handles the options for Graph3d. // //////////////////////////////////////////////////////////////////////////////// +var Camera = require('./Camera'); +var Point3d = require('./Point3d'); + // enumerate the available styles var STYLE = { @@ -89,6 +92,26 @@ var PREFIXEDOPTIONKEYS = [ ]; +// Placeholder for DEFAULTS reference +var DEFAULTS = undefined; + + +/** + * Check if given hash is empty. + * + * Source: http://stackoverflow.com/a/679937 + */ +function isEmpty(obj) { + for(var prop in obj) { + if (obj.hasOwnProperty(prop)) + return false; + } + + return true; +} + + + /** * Make first letter of parameter upper case. * @@ -166,13 +189,32 @@ function safeCopy(src, dst, fields, prefix) { } -function setDefaults(dst) { +/** + * Initialize dst with the values in src. + * + * src is the hash with the default values. + * A reference DEFAULTS to this hash is stored locally for + * further handling. + * + * For now, dst is assumed to be a Graph3d instance. + */ +function setDefaults(src, dst) { + if (src === undefined || isEmpty(src)) { + throw new Error('No DEFAULTS passed'); + } + if (dst === undefined) { + throw new Error('No dst passed'); + } + + // Remember defaults for future reference + DEFAULTS = src; + // Handle the defaults which can be simply copied over - forceCopy(DEFAULTS, dst, OPTIONKEYS); - forceCopy(DEFAULTS, dst, PREFIXEDOPTIONKEYS, 'default'); + forceCopy(src, dst, OPTIONKEYS); + forceCopy(src, dst, PREFIXEDOPTIONKEYS, 'default'); // Handle the more complex ('special') fields - setSpecialSettings(DEFAULTS, dst); + setSpecialSettings(src, dst); // Following are internal fields, not part of the user settings dst.margin = 10; // px @@ -187,6 +229,14 @@ function setOptions(options, dst) { if (options === undefined) { return; } + if (dst === undefined) { + throw new Error('No dst passed'); + } + + if (DEFAULTS === undefined || isEmpty(DEFAULTS)) { + throw new Error('DEFAULTS not set for module Settings'); + } + // Handle the parameters which can be simply copied over safeCopy(options, dst, OPTIONKEYS); @@ -253,7 +303,7 @@ function setShowLegend(showLegend, dst) { * when not found */ function getStyleNumberByName(styleName) { - var number = STYLENAME[stylename]; + var number = STYLENAME[styleName]; if (number === undefined) { return -1; From ea4d56c68b2396d3b2f9051b29cb2dc2848bc6f9 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Fri, 21 Oct 2016 16:48:49 +0300 Subject: [PATCH 04/15] vertical scroll when zoomKey isn't triggered (#2197) * Hide vertically hidden ranged items in groups that are not visible * Add vertical scroll when zoom is inactive * Fix review comments --- lib/timeline/Core.js | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index cca32f9c..7b5be3e6 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -157,9 +157,41 @@ Core.prototype._create = function (container) { if (me.isActive()) { me.emit('mousewheel', event); } + + // prevent scrolling when zoomKey defined or activated + if (!me.options.zoomKey || event[me.options.zoomKey]) return + + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } + + var current = me.props.scrollTop; + var adjusted = current + delta * 120; + if (me.isActive()) { + me._setScrollTop(adjusted); + me._redraw(); + me.emit('scroll', event); + } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both scroll) + event.preventDefault(); + } + + if (this.dom.root.addEventListener) { + // IE9, Chrome, Safari, Opera + this.dom.root.addEventListener("mousewheel", onMouseWheel, false); + // Firefox + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel, false); + } else { + // IE 6/7/8 + this.dom.root.attachEvent("onmousewheel", onMouseWheel); } - this.dom.root.addEventListener('mousewheel', onMouseWheel); - this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); // size properties of each of the panels this.props = { @@ -222,7 +254,7 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl' + 'locale', 'locales', 'moment', 'rtl', 'zoomKey' ]; util.selectiveExtend(fields, this.options, options); From 178e9dc915fe2057e3c924d77122db2a06e07878 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Fri, 21 Oct 2016 16:59:46 +0300 Subject: [PATCH 05/15] Horizontal scroll (#2201) * Hide vertically hidden ranged items in groups that are not visible * Add vertical scroll when zoom is inactive * Fix review comments * Add horizontalScroll option --- docs/timeline/index.html | 9 +++ examples/timeline/other/horizontalScroll.html | 77 +++++++++++++++++++ lib/timeline/Core.js | 5 +- lib/timeline/Range.js | 38 +++++---- lib/timeline/optionsTimeline.js | 1 + 5 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 examples/timeline/other/horizontalScroll.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index cd02f3d2..c45440ed 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -675,6 +675,15 @@ function (option, path) { + + horizontalScroll + Boolean + false + This option allows you to scroll horizontally to move backwards and forwards in the time range. + Only applicable when option zoomCtrl is defined or zoomable is false. + + + itemsAlwaysDraggable boolean diff --git a/examples/timeline/other/horizontalScroll.html b/examples/timeline/other/horizontalScroll.html new file mode 100644 index 00000000..a999cd51 --- /dev/null +++ b/examples/timeline/other/horizontalScroll.html @@ -0,0 +1,77 @@ + + + Timeline | Horizontal Scroll Option + + + + + + + + + +

Timeline horizontal scroll option

+ +
+ + + + + diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 7b5be3e6..76ed050b 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -161,6 +161,9 @@ Core.prototype._create = function (container) { // prevent scrolling when zoomKey defined or activated if (!me.options.zoomKey || event[me.options.zoomKey]) return + // prevent scrolling vertically when horizontalScroll is true + if (me.options.horizontalScroll) return + var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ delta = event.wheelDelta / 120; @@ -254,7 +257,7 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl', 'zoomKey' + 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll' ]; util.selectiveExtend(fields, this.options, options); diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index 41e500af..075b0baa 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -81,7 +81,7 @@ Range.prototype.setOptions = function (options) { // copy the options that we know var fields = [ 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', - 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl' + 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'horizontalScroll' ]; util.selectiveExtend(fields, this.options, options); @@ -483,15 +483,10 @@ Range.prototype._onDragEnd = function (event) { * @private */ Range.prototype._onMouseWheel = function(event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - // only zoom when the mouse is inside the current range - if (!this._isInsideRange(event)) return; + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); - // only zoom when the according key is pressed and the zoomKey option is set - if (this.options.zoomKey && !event[this.options.zoomKey]) return; - // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ @@ -502,6 +497,27 @@ Range.prototype._onMouseWheel = function(event) { delta = -event.detail / 3; } + // don't allow zoom when the according key is pressed and the zoomKey option or not zoomable but movable + if ((this.options.zoomKey && !event[this.options.zoomKey] && this.options.zoomable) + || (!this.options.zoomable && this.options.moveable)) { + if (this.options.horizontalScroll) { + // calculate a single scroll jump relative to the range scale + var diff = delta * (this.end - this.start) / 20; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); + } + return; + } + + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; + + // only zoom when the mouse is inside the current range + if (!this._isInsideRange(event)) return; + // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. @@ -524,10 +540,6 @@ Range.prototype._onMouseWheel = function(event) { this.zoom(scale, pointerDate, delta); } - - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); }; /** diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index a20d27f4..bfcac84b 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -26,6 +26,7 @@ let allOptions = { //globals : align: {string}, rtl: {boolean, 'undefined': 'undefined'}, + horizontalScroll: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, clickToUse: {boolean}, dataAttributes: {string, array}, From 3d0acba9944237ce12e82da59ce3f601a6e26dc8 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Fri, 21 Oct 2016 19:23:09 +0200 Subject: [PATCH 06/15] Small code cleanup and consolidation. --- lib/graph3d/Graph3d.js | 205 +++++++++++++++++----------------------- lib/graph3d/Settings.js | 2 - 2 files changed, 89 insertions(+), 118 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 896a1cdf..4a863628 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -202,21 +202,23 @@ Graph3d.prototype._convert3Dto2D = function(point3d) { * camera */ Graph3d.prototype._convertPointToTranslation = function(point3d) { - var ax = point3d.x * this.scale.x, + var cameraLocation = this.camera.getCameraLocation(), + cameraRotation = this.camera.getCameraRotation(), + ax = point3d.x * this.scale.x, ay = point3d.y * this.scale.y, az = point3d.z * this.scale.z, - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, + cx = CameraLocation.x, + cy = CameraLocation.y, + cz = CameraLocation.z, // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + sinTx = Math.sin(CameraRotation.x), + cosTx = Math.cos(CameraRotation.x), + sinTy = Math.sin(CameraRotation.y), + cosTy = Math.cos(CameraRotation.y), + sinTz = Math.sin(CameraRotation.z), + cosTz = Math.cos(CameraRotation.z), // calculate translation dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), @@ -291,47 +293,6 @@ Graph3d.prototype._calcTranslations = function(points, sort) { }; -/** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style - */ -Graph3d.prototype._determineColumnIndexes = function(data, style) { - if (this.style === Graph3d.STYLE.DOT || - this.style === Graph3d.STYLE.DOTLINE || - this.style === Graph3d.STYLE.LINE || - this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE || - this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; - - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } - else if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE || - this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; - - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } - else { - throw new Error('Unknown style "' + this.style + '"'); - } -}; - Graph3d.prototype.getNumberOfRows = function(data) { return data.length; } @@ -360,14 +321,57 @@ Graph3d.prototype.getDistinctValues = function(data, column) { Graph3d.prototype.getColumnRange = function(data,column) { - var minMax = {min:data[0][column],max:data[0][column]}; + var minMax; + for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { minMax.min = data[i][column]; } - if (minMax.max < data[i][column]) { minMax.max = data[i][column]; } + var item = data[i][column]; + + if (i === 0) { + minMax = { min: item, max: item}; + } else { + if (minMax.min > item) { minMax.min = item; } + if (minMax.max < item) { minMax.max = item; } + } } return minMax; }; + +/** + * Check if the state is consistent for the use of the value field. + * + * Throws if a problem is detected. + */ +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 + '\'' + ); + } +}; + + /** * Initialize the data from the data table. Calculate minimum and maximum values * and column index values @@ -409,12 +413,6 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { }; this.dataSet.on('*', this._onChange); - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange - // determine the location of x,y,z,value,filter columns this.colX = 'x'; this.colY = 'y'; @@ -422,7 +420,8 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { // 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! + // Only set this field if it's actually present + this.colFilter = 'filter'; if (this.dataFilter === undefined) { this.dataFilter = new Filter(rawData, this.colFilter, this); @@ -481,7 +480,6 @@ 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; - // 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); @@ -579,29 +577,7 @@ 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 + '\'' - ); - } - } - + this._checkValueField(data); // copy all values from the google data table to a list with Point3d objects for (i = 0; i < data.length; i++) { @@ -902,6 +878,7 @@ Graph3d.prototype._redrawClear = function() { */ Graph3d.prototype._getLegendWidth = function() { var width; + if (this.style === Graph3d.STYLE.DOTSIZE) { var dotSize = this.frame.clientWidth * this.dotSizeRatio; width = dotSize / 2 + dotSize * 2; @@ -920,12 +897,16 @@ Graph3d.prototype._getLegendWidth = function() { Graph3d.prototype._redrawLegend = function() { //Return without drawing anything, if no legend is specified - if (this.showLegend !== true) {return;} + if (this.showLegend !== true) { + return; + } // Do not draw legend when graph style does not support if (this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.BARSIZE //TODO add legend support for BARSIZE - ){return;} + ){ + return; + } // Legend types - size and color. Determine if size legend. var isSizeLegend = (this.style === Graph3d.STYLE.BARSIZE @@ -1000,11 +981,9 @@ Graph3d.prototype._redrawLegend = function() { var y; while (!step.end()) { y = bottom - (step.getCurrent() - legendMin) / (legendMax - legendMin) * height; - - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + from = new Point2d(left - gridLineLen, y); + to = new Point2d(left, y); + this._line(ctx, from, to); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; @@ -1018,7 +997,6 @@ Graph3d.prototype._redrawLegend = function() { ctx.textBaseline = 'top'; var label = this.legendLabel; ctx.fillText(label, right, bottom + this.margin); - }; /** @@ -1406,9 +1384,9 @@ Graph3d.prototype._redrawDataGrid = function() { if (this.style === Graph3d.STYLE.SURFACE) { for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - cross = this.dataPoints[i].pointCross; + right = point.pointRight; + top = point.pointTop; + cross = point.pointCross; if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { @@ -1466,30 +1444,25 @@ Graph3d.prototype._redrawDataGrid = function() { } } else { // grid style - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - - if (point !== undefined && right !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - this._line(ctx, point.screen, right.screen); + var drawGridLine = function(ctx, from, to) { + if (from === undefined || to === undefined) { + return; } - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (from.point.z + to.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - this._line(ctx, point.screen, top.screen); - } + ctx.lineWidth = this._getStrokeWidth(from) * 2; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + this._line(ctx, from.screen, to.screen); + }; + + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + drawGridLine(ctx, point, point.pointRight); + drawGridLine(ctx, point, point.pointTop); } } }; diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js index f1b7da6a..4be963cc 100644 --- a/lib/graph3d/Settings.js +++ b/lib/graph3d/Settings.js @@ -430,8 +430,6 @@ function setCameraPosition(cameraPosition, dst) { } - - module.exports.STYLE = STYLE; module.exports.setDefaults = setDefaults; module.exports.setOptions = setOptions; From 7424bfbfc28de94c076fd6ba7941105f9f3b98e2 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Fri, 21 Oct 2016 21:20:24 +0200 Subject: [PATCH 07/15] Graph3d: make setting dotSizeRatio public (#2202) * Graph3d: make setting dotSizeRatio public * Added method for dotSize * Fixed silly indent --- lib/graph3d/Graph3d.js | 12 +++++++++--- lib/graph3d/Settings.js | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 896a1cdf..43ac40ec 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -50,6 +50,7 @@ var DEFAULTS = { showShadow : false, keepAspectRatio : true, verticalRatio : 0.5, // 0.1 to 1.0, where 1.0 results in a 'cube' + dotSizeRatio : 0.02, // size of the dots as a fraction of the graph width showAnimationControls: autoByDefault, animationInterval : 1000, // milliseconds @@ -897,13 +898,18 @@ Graph3d.prototype._redrawClear = function() { }; +Graph3d.prototype._dotSize = function() { + return this.frame.clientWidth * this.dotSizeRatio; +}; + + /** * Get legend width */ Graph3d.prototype._getLegendWidth = function() { var width; if (this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * this.dotSizeRatio; + var dotSize = this._dotSize(); width = dotSize / 2 + dotSize * 2; } else if (this.style === Graph3d.STYLE.BARSIZE) { width = this.xBarWidth ; @@ -972,7 +978,7 @@ Graph3d.prototype._redrawLegend = function() { // draw the size legend box var widthMin; if (this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * this.dotSizeRatio; + var dotSize = this._dotSize(); widthMin = dotSize / 2; // px } else if (this.style === Graph3d.STYLE.BARSIZE) { //widthMin = this.xBarWidth * 0.2 this is wrong - barwidth measures in terms of xvalues @@ -1521,7 +1527,7 @@ Graph3d.prototype._redrawDataDot = function() { this._calcTranslations(this.dataPoints); // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * this.dotSizeRatio; // px + var dotSize = this._dotSize(); for (i = 0; i < this.dataPoints.length; i++) { var point = this.dataPoints[i]; diff --git a/lib/graph3d/Settings.js b/lib/graph3d/Settings.js index f1b7da6a..98f244c3 100644 --- a/lib/graph3d/Settings.js +++ b/lib/graph3d/Settings.js @@ -58,6 +58,7 @@ var OPTIONKEYS = [ 'showShadow', 'keepAspectRatio', 'verticalRatio', + 'dotSizeRatio', 'showAnimationControls', 'animationInterval', 'animationPreload', @@ -220,7 +221,6 @@ function setDefaults(src, dst) { dst.margin = 10; // px dst.showGrayBottom = false; // TODO: this does not work correctly dst.showTooltip = false; - dst.dotSizeRatio = 0.02; // size of the dots as a fraction of the graph width dst.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? } From b889a5057d027e8c94ac98f10b44c908dc1009d6 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Fri, 21 Oct 2016 22:21:09 +0300 Subject: [PATCH 08/15] Add a vertical scroll option for timeline [solves #273, #1060, #466] (#2196) * Add initial scroller without options * Add initial scroll without an option * Add verticalScroll option * Fix scrollbar positions * Add docs * fix example * remove jquery dependency * Fix example * Fix review comments --- docs/timeline/index.html | 8 ++ examples/timeline/other/verticalScroll.html | 92 ++++++++++++++ lib/timeline/Core.js | 126 +++++++++++++------- lib/timeline/Timeline.js | 2 +- lib/timeline/component/ItemSet.js | 17 ++- lib/timeline/component/css/panel.css | 23 +++- lib/timeline/optionsTimeline.js | 1 + lib/util.js | 26 ++++ 8 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 examples/timeline/other/verticalScroll.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index c45440ed..7cc75507 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -1024,6 +1024,14 @@ function (option, path) { + + verticalScroll + Boolean + false + Show a vertical scroll on the side of the group list. + + + width String or Number diff --git a/examples/timeline/other/verticalScroll.html b/examples/timeline/other/verticalScroll.html new file mode 100644 index 00000000..07a68ad4 --- /dev/null +++ b/examples/timeline/other/verticalScroll.html @@ -0,0 +1,92 @@ + + + Timeline | Vertical Scroll Option + + + + + + + + + +

Timeline vertical scroll option

+ +

With +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ +

With +horizontalScroll: true, +verticalScroll: true, +zoomKey: 'ctrlKey' +

+
+ + + + diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index 76ed050b..43f336e2 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -82,7 +82,6 @@ Core.prototype._create = function (container) { this.dom.centerContainer.appendChild(this.dom.center); this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); - this.dom.centerContainer.appendChild(this.dom.shadowTop); this.dom.centerContainer.appendChild(this.dom.shadowBottom); this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); @@ -90,9 +89,26 @@ Core.prototype._create = function (container) { this.dom.rightContainer.appendChild(this.dom.shadowTopRight); this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + this.on('rangechange', function () { if (this.initialDrawDone === true) { - this._redraw(); // this allows overriding the _redraw method + this._redraw(); } }.bind(this)); this.on('touch', this._onTouch.bind(this)); @@ -154,15 +170,15 @@ Core.prototype._create = function (container) { }.bind(this)); function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); + if (this.isActive()) { + this.emit('mousewheel', event); } // prevent scrolling when zoomKey defined or activated - if (!me.options.zoomKey || event[me.options.zoomKey]) return + if (!this.options.zoomKey || event[this.options.zoomKey]) return // prevent scrolling vertically when horizontalScroll is true - if (me.options.horizontalScroll) return + if (this.options.horizontalScroll) return var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ @@ -173,12 +189,12 @@ Core.prototype._create = function (container) { delta = -event.detail / 3; } - var current = me.props.scrollTop; + var current = this.props.scrollTop; var adjusted = current + delta * 120; - if (me.isActive()) { - me._setScrollTop(adjusted); - me._redraw(); - me.emit('scroll', event); + if (this.isActive()) { + this._setScrollTop(adjusted); + this._redraw(); + this.emit('scroll', event); } // Prevent default actions caused by mouse wheel @@ -188,30 +204,27 @@ Core.prototype._create = function (container) { if (this.dom.root.addEventListener) { // IE9, Chrome, Safari, Opera - this.dom.root.addEventListener("mousewheel", onMouseWheel, false); + this.dom.root.addEventListener("mousewheel", onMouseWheel.bind(this), false); // Firefox - this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel, false); + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel.bind(this), false); } else { // IE 6/7/8 - this.dom.root.attachEvent("onmousewheel", onMouseWheel); + this.dom.root.attachEvent("onmousewheel", onMouseWheel.bind(this)); } - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + function onMouseScrollSide(event) { + var current = this.scrollTop; + var adjusted = -current; + if (me.isActive()) { + me._setScrollTop(adjusted); + + me._redraw(); + me.emit('scroll', event); + } + } + + this.dom.left.parentNode.addEventListener('scroll', onMouseScrollSide); + this.dom.right.parentNode.addEventListener('scroll', onMouseScrollSide); this.customTimes = []; @@ -257,17 +270,23 @@ Core.prototype.setOptions = function (options) { var fields = [ 'width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates', - 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll' + 'locale', 'locales', 'moment', 'rtl', 'zoomKey', 'horizontalScroll', 'verticalScroll' ]; util.selectiveExtend(fields, this.options, options); if (this.options.rtl) { - var contentContainer = this.dom.leftContainer; - this.dom.leftContainer = this.dom.rightContainer; - this.dom.rightContainer = contentContainer; this.dom.container.style.direction = "rtl"; - this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; } + this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + this.dom.rightContainer.className = 'vis-panel vis-right vis-vertical-scroll'; + } else { + this.dom.leftContainer.className = 'vis-panel vis-left vis-vertical-scroll'; + } + } this.options.orientation = {item:undefined,axis:undefined}; if ('orientation' in options) { @@ -740,9 +759,25 @@ Core.prototype._redraw = function() { // calculate the widths of the panels props.root.width = dom.root.offsetWidth; props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; + + if (!this.initialDrawDone) { + props.scrollbarWidth = util.getScrollBarWidth(); + } + + if (this.options.verticalScroll) { + if (this.options.rtl) { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth + props.scrollbarWidth || -props.border.right; + } else { + props.left.width = dom.leftContainer.clientWidth + props.scrollbarWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + } else { + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + } + props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; props.rightContainer.width = props.right.width; var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; props.center.width = centerWidth; @@ -796,10 +831,8 @@ Core.prototype._redraw = function() { dom.center.style.left = '0'; dom.center.style.top = offset + 'px'; dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; - + // show shadows when vertical scrolling is available var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; @@ -810,6 +843,18 @@ Core.prototype._redraw = function() { dom.shadowTopRight.style.visibility = visibilityTop; dom.shadowBottomRight.style.visibility = visibilityBottom; + if (this.options.verticalScroll) { + this.dom.shadowTopRight.style.visibility = "hidden"; + this.dom.shadowBottomRight.style.visibility = "hidden"; + this.dom.shadowTopLeft.style.visibility = "hidden"; + this.dom.shadowBottomLeft.style.visibility = "hidden"; + document.getElementsByClassName('vis-left')[0].scrollTop = -offset; + document.getElementsByClassName('vis-right')[0].scrollTop = -offset; + } else { + dom.left.style.top = offset + 'px'; + dom.right.style.top = offset + 'px'; + } + // enable/disable vertical panning var contentsOverflow = this.props.center.height > this.props.centerContainer.height; this.hammer.get('pan').set({ @@ -832,6 +877,7 @@ Core.prototype._redraw = function() { } else { this.redrawCount = 0; } + this.initialDrawDone = true; //Emit public 'changed' event for UI updates, see issue #1592 diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index a8e55e92..a397498e 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -52,7 +52,6 @@ function Timeline (container, items, groups, options) { axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' item: 'bottom' // not relevant }, - rtl: false, moment: moment, width: null, @@ -61,6 +60,7 @@ function Timeline (container, items, groups, options) { minHeight: null }; this.options = util.deepExtend({}, this.defaultOptions); + this.options.rtl = options.rtl; // Create the DOM, props, and emitter this._create(container); diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 77912821..9eb46393 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -27,7 +27,6 @@ var BACKGROUND = '__background__'; // reserved group id for background items wit function ItemSet(body, options) { this.body = body; this.defaultOptions = { - rtl: false, type: null, // 'box', 'point', 'range', 'background' orientation: { item: 'bottom' // item orientation: 'top' or 'bottom' @@ -96,7 +95,8 @@ function ItemSet(body, options) { // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); - + this.options.rtl = options.rtl; + // options for getting items from the DataSet with the correct type this.itemOptions = { type: {start: 'Date', end: 'Date'} @@ -230,7 +230,12 @@ ItemSet.prototype._create = function(){ // add item on doubletap this.hammer.on('doubletap', this._onAddItem.bind(this)); - this.groupHammer = new Hammer(this.body.dom.leftContainer); + + if (this.options.rtl) { + this.groupHammer = new Hammer(this.body.dom.rightContainer); + } else { + this.groupHammer = new Hammer(this.body.dom.leftContainer); + } this.groupHammer.on('panstart', this._onGroupDragStart.bind(this)); this.groupHammer.on('panmove', this._onGroupDrag.bind(this)); @@ -451,7 +456,11 @@ ItemSet.prototype.show = function() { // show labelset containing labels if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); + if (this.options.rtl) { + this.body.dom.right.appendChild(this.dom.labelSet); + } else { + this.body.dom.left.appendChild(this.dom.labelSet); + } } }; diff --git a/lib/timeline/component/css/panel.css b/lib/timeline/component/css/panel.css index 4c5088a9..a02ee5bc 100644 --- a/lib/timeline/component/css/panel.css +++ b/lib/timeline/component/css/panel.css @@ -1,4 +1,3 @@ - .vis-panel { position: absolute; @@ -24,6 +23,28 @@ overflow: hidden; } +.vis-left.vis-panel.vis-vertical-scroll, .vis-right.vis-panel.vis-vertical-scroll { + height: 100%; + overflow-x: hidden; + overflow-y: scroll; +} + +.vis-left.vis-panel.vis-vertical-scroll { + direction: rtl; +} + +.vis-left.vis-panel.vis-vertical-scroll .vis-content { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll { + direction: ltr; +} + +.vis-right.vis-panel.vis-vertical-scroll .vis-content { + direction: rtl; +} + .vis-panel.vis-center, .vis-panel.vis-top, .vis-panel.vis-bottom { diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index bfcac84b..4c976d85 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -26,6 +26,7 @@ let allOptions = { //globals : align: {string}, rtl: {boolean, 'undefined': 'undefined'}, + verticalScroll: {boolean, 'undefined': 'undefined'}, horizontalScroll: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, clickToUse: {boolean}, diff --git a/lib/util.js b/lib/util.js index c1dc1c21..f9506a6a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1452,3 +1452,29 @@ exports.easingFunctions = { return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t } }; + +exports.getScrollBarWidth = function () { + var inner = document.createElement('p'); + inner.style.width = "100%"; + inner.style.height = "200px"; + + var outer = document.createElement('div'); + outer.style.position = "absolute"; + outer.style.top = "0px"; + outer.style.left = "0px"; + outer.style.visibility = "hidden"; + outer.style.width = "200px"; + outer.style.height = "150px"; + outer.style.overflow = "hidden"; + outer.appendChild (inner); + + document.body.appendChild (outer); + var w1 = inner.offsetWidth; + outer.style.overflow = 'scroll'; + var w2 = inner.offsetWidth; + if (w1 == w2) w2 = outer.clientWidth; + + document.body.removeChild (outer); + + return (w1 - w2); +}; \ No newline at end of file From 13752ed435b6b667d721411771e40b68b777e9e7 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 09:46:37 +0200 Subject: [PATCH 09/15] Fixes after testing --- lib/graph3d/Graph3d.js | 54 ++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 4a863628..44c62ef3 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -208,17 +208,17 @@ Graph3d.prototype._convertPointToTranslation = function(point3d) { ay = point3d.y * this.scale.y, az = point3d.z * this.scale.z, - cx = CameraLocation.x, - cy = CameraLocation.y, - cz = CameraLocation.z, + cx = cameraLocation.x, + cy = cameraLocation.y, + cz = cameraLocation.z, // calculate angles - sinTx = Math.sin(CameraRotation.x), - cosTx = Math.cos(CameraRotation.x), - sinTy = Math.sin(CameraRotation.y), - cosTy = Math.cos(CameraRotation.y), - sinTz = Math.sin(CameraRotation.z), - cosTz = Math.cos(CameraRotation.z), + sinTx = Math.sin(cameraRotation.x), + cosTx = Math.cos(cameraRotation.x), + sinTy = Math.sin(cameraRotation.y), + cosTy = Math.cos(cameraRotation.y), + sinTz = Math.sin(cameraRotation.z), + cosTz = Math.cos(cameraRotation.z), // calculate translation dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), @@ -979,6 +979,8 @@ Graph3d.prototype._redrawLegend = function() { step.start(true); var y; + var from; + var to; while (!step.end()) { y = bottom - (step.getCurrent() - legendMin) / (legendMax - legendMin) * height; from = new Point2d(left - gridLineLen, y); @@ -1362,6 +1364,21 @@ Graph3d.prototype._hsv2rgb = function(H, S, V) { }; +Graph3d.prototype._drawGridLine = function(ctx, from, to) { + if (from === undefined || to === undefined) { + return; + } + + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + var zAvg = (from.point.z + to.point.z) / 2; + var h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = this._getStrokeWidth(from) * 2; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + this._line(ctx, from.screen, to.screen); +}; + + /** * Draw all datapoints as a grid * This function can be used when the style is 'grid' @@ -1444,25 +1461,10 @@ Graph3d.prototype._redrawDataGrid = function() { } } else { // grid style - - var drawGridLine = function(ctx, from, to) { - if (from === undefined || to === undefined) { - return; - } - - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (from.point.z + to.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - - ctx.lineWidth = this._getStrokeWidth(from) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - this._line(ctx, from.screen, to.screen); - }; - for (i = 0; i < this.dataPoints.length; i++) { point = this.dataPoints[i]; - drawGridLine(ctx, point, point.pointRight); - drawGridLine(ctx, point, point.pointTop); + this._drawGridLine(ctx, point, point.pointRight); + this._drawGridLine(ctx, point, point.pointTop); } } }; From d3b174da09502a86fe104581d2f25da6d6a7bf76 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 10:16:23 +0200 Subject: [PATCH 10/15] Interim save --- lib/graph3d/Graph3d.js | 125 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 43ac40ec..9e7dc4ac 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -1588,6 +1588,131 @@ Graph3d.prototype._redrawDataDot = function() { } }; + +/** + * Draw all datapoints for currently selected graph style. + */ +Graph3d.prototype._redrawDataBar = function() { + var ctx = this._getContext(); + var i, j, surface, corners; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + this._calcTranslations(this.dataPoints); + + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + // draw the datapoints as bars + var xWidth = this.xBarWidth / 2; + var yWidth = this.yBarWidth / 2; + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; + + // determine color + var hue, color, borderColor; + if (this.style === Graph3d.STYLE.BARCOLOR ) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + else if (this.style === Graph3d.STYLE.BARSIZE) { + color = this.dataColor.fill; + borderColor = this.dataColor.stroke; + } + else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + + // calculate size for the bar + if (this.style === Graph3d.STYLE.BARSIZE) { + xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + } + + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} + ]; + var bottom = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} + ]; + + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + + // create five sides, calculate both corner points and center points + var surfaces = [ + {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, + {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, + {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, + {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, + {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} + ]; + point.surfaces = surfaces; + + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); + } + } +}; + + /** * Draw all datapoints as bars. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' From cd1b18fbe9a5cbc9269456e157925ddcaa270224 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 10:19:45 +0200 Subject: [PATCH 11/15] Fixed another silly indent --- lib/graph3d/Graph3d.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 44c62ef3..1000b086 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -985,7 +985,7 @@ Graph3d.prototype._redrawLegend = function() { y = bottom - (step.getCurrent() - legendMin) / (legendMax - legendMin) * height; from = new Point2d(left - gridLineLen, y); to = new Point2d(left, y); - this._line(ctx, from, to); + this._line(ctx, from, to); ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; From 5d0a70e1f1fc5459adc7c946276ab8239a72af85 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 11:03:22 +0200 Subject: [PATCH 12/15] Added generic graph drawing loop; isolated point drawing of graph style 'Bar' --- lib/graph3d/Graph3d.js | 216 ++++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 102 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index aea7bbc0..4e4dc10f 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -838,8 +838,12 @@ Graph3d.prototype.redraw = function() { else if (this.style === Graph3d.STYLE.LINE) { this._redrawDataLine(); } - else if (this.style === Graph3d.STYLE.BAR || - this.style === Graph3d.STYLE.BARCOLOR || + else if (this.style === Graph3d.STYLE.BAR) { + + // Pass a method reference here + this._redrawDataGraph(Graph3d.prototype._redrawBarGraphPoint); + + } else if (this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { this._redrawDataBar(); } @@ -1565,125 +1569,131 @@ Graph3d.prototype._redrawDataDot = function() { /** - * Draw all datapoints for currently selected graph style. + * Draw a bar element in the view with the given properties. */ -Graph3d.prototype._redrawDataBar = function() { - var ctx = this._getContext(); +Graph3d.prototype._redrawBar = function(ctx, point, xWidth, yWidth, color, borderColor) { var i, j, surface, corners; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} + ]; + var bottom = [ + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} + ]; + + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + + // create five sides, calculate both corner points and center points + var surfaces = [ + {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, + {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, + {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, + {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, + {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} + ]; + point.surfaces = surfaces; + + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); + } +}; - this._calcTranslations(this.dataPoints); + +/** + * Draw single datapoint for graph style 'Bar'. + */ +Graph3d.prototype._redrawBarGraphPoint = function(ctx, point) { + var i, j, surface, corners; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; - // draw the datapoints as bars var xWidth = this.xBarWidth / 2; var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; - - // determine color - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR ) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - else if (this.style === Graph3d.STYLE.BARSIZE) { - color = this.dataColor.fill; - borderColor = this.dataColor.stroke; - } - else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - } - // calculate all corner points - var me = this; - var point3d = point.point; - var top = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} - ]; - var bottom = [ - {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, - {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, - {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} - ]; + // determine color + var hue, color, borderColor; + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); - // calculate screen location of the points - top.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - bottom.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); + this._redrawBar(ctx, point, xWidth, yWidth, color, borderColor); +}; - // create five sides, calculate both corner points and center points - var surfaces = [ - {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)}, - {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)}, - {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)}, - {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)}, - {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)} - ]; - point.surfaces = surfaces; - // calculate the distance of each of the surface centers to the camera - for (j = 0; j < surfaces.length; j++) { - surface = surfaces[j]; - var transCenter = this._convertPointToTranslation(surface.center); - surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; - // TODO: this dept calculation doesn't work 100% of the cases due to perspective, - // but the current solution is fast/simple and works in 99.9% of all cases - // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) - } +/** + * Draw all datapoints for currently selected graph style. + * + * @param pointDrawMethod - method reference to draw a point in a specific graph style. + */ +Graph3d.prototype._redrawDataGraph = function(pointDrawMethod) { + var ctx = this._getContext(); + var i; - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + this._calcTranslations(this.dataPoints); - // both are equal - return 0; - }); + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; - // draw the ordered surfaces - ctx.lineWidth = this._getStrokeWidth(point); - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside - for (j = 2; j < surfaces.length; j++) { - surface = surfaces[j]; - corners = surface.corners; - ctx.beginPath(); - ctx.moveTo(corners[3].screen.x, corners[3].screen.y); - ctx.lineTo(corners[0].screen.x, corners[0].screen.y); - ctx.lineTo(corners[1].screen.x, corners[1].screen.y); - ctx.lineTo(corners[2].screen.x, corners[2].screen.y); - ctx.lineTo(corners[3].screen.x, corners[3].screen.y); - ctx.fill(); - ctx.stroke(); - } + // Using call() ensures that the correct context is used + pointDrawMethod.call(this, ctx, point); } }; @@ -1710,6 +1720,8 @@ Graph3d.prototype._redrawDataBar = function() { for (i = 0; i < this.dataPoints.length; i++) { var point = this.dataPoints[i]; + // TODO: Remove code for style `Bar` here - it has been refactored to separate routine + // determine color var hue, color, borderColor; if (this.style === Graph3d.STYLE.BARCOLOR ) { From 124b70ffeb1aae29726ece52fee505e728e11a0a Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 12:30:53 +0200 Subject: [PATCH 13/15] Undid last commit in a very roundabout way. --- lib/graph3d/Graph3d.js | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 4e4dc10f..95be0634 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -831,25 +831,34 @@ Graph3d.prototype.redraw = function() { this._redrawClear(); this._redrawAxis(); - if (this.style === Graph3d.STYLE.GRID || - this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } - else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); + var pointDrawingMethod = undefined; + switch (this.style) { + case Graph3d.STYLE.BAR: + pointDrawingMethod = Graph3d.prototype._redrawBarGraphPoint; + break; } - else if (this.style === Graph3d.STYLE.BAR) { - // Pass a method reference here - this._redrawDataGraph(Graph3d.prototype._redrawBarGraphPoint); + if (pointDrawingMethod !== undefined) { + // Use generic drawing loop + // Pass the method reference here + this._redrawDataGraph(pointDrawingMethod); + } else { + // Use the old style drawing methods - } else if (this.style === Graph3d.STYLE.BARCOLOR || - this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); - } - else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); + if (this.style === Graph3d.STYLE.GRID || + this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } + else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } else if (this.style === Graph3d.STYLE.BARCOLOR || + this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } + else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); + } } this._redrawInfo(); From 44d6d8672165dec6603203c7f8ac4a90d2574386 Mon Sep 17 00:00:00 2001 From: Wim Rijnders Date: Sat, 22 Oct 2016 15:04:48 +0200 Subject: [PATCH 14/15] Added explanatory text for graph3d tooltip demo --- examples/graph3d/11_tooltips.html | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/graph3d/11_tooltips.html b/examples/graph3d/11_tooltips.html index 39af19aa..c37b35f4 100644 --- a/examples/graph3d/11_tooltips.html +++ b/examples/graph3d/11_tooltips.html @@ -5,6 +5,12 @@ @@ -24,11 +30,11 @@ // Create and populate a data table. data = new vis.DataSet(); - var extra_content = [ - 'Arbitrary information', - 'You can access data from the point source object', - 'Tooltip example content', - ]; + var extra_content = [ + 'Arbitrary information', + 'You can access data from the point source object', + 'Tooltip example content', + ]; // create some nice looking data with sin/cos var steps = 5; // number of datapoints will be steps*steps @@ -42,7 +48,7 @@ data.add({x:x, y:y, z: z, style:value, extra: extra_content[(x*y) % extra_content.length]}); } else { - data.add({x:x, y:y, z: z, extra: extra_content[(x*y) % extra_content.length]}); + data.add({x:x, y:y, z: z, extra: extra_content[(x*y) % extra_content.length]}); } } } @@ -61,8 +67,8 @@ //tooltip: true, tooltip: function (point) { // parameter point contains properties x, y, z, and data - // data is the original object passed to the point constructor - return 'value: ' + point.z + '
' + point.data.extra; + // data is the original object passed to the point constructor + return 'value: ' + point.z + '
' + point.data.extra; }, keepAspectRatio: true, @@ -106,6 +112,7 @@
-
+
Hover the mouse cursor over the graph to see tooltips.
+ From d9598432d37fd5be56cfb2976c45963e97acf539 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Sat, 22 Oct 2016 18:09:40 +0300 Subject: [PATCH 15/15] Fix React support for group and item templates and add example (#2206) * Hide vertically hidden ranged items in groups that are not visible * Add element to templates options * Fix comment typo * Add documentation for react mounting * add react example * Fix typo * fix title * Fix review comments --- examples/timeline/other/usingReact.html | 123 ++++++++++++++++++++++++ lib/timeline/component/Group.js | 16 +-- lib/timeline/component/item/Item.js | 42 ++++---- 3 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 examples/timeline/other/usingReact.html diff --git a/examples/timeline/other/usingReact.html b/examples/timeline/other/usingReact.html new file mode 100644 index 00000000..f6d1e1f7 --- /dev/null +++ b/examples/timeline/other/usingReact.html @@ -0,0 +1,123 @@ + + + + + React Components in templates + + + +
+ + + + + + + + + + + + + diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index f522fd82..691d24b1 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -87,10 +87,12 @@ Group.prototype._create = function() { Group.prototype.setData = function(data) { // update contents var content; + var templateFunction; + if (this.itemSet.options && this.itemSet.options.groupTemplate) { - content = this.itemSet.options.groupTemplate(data, this.dom.inner); - } - else { + templateFunction = this.itemSet.options.groupTemplate.bind(this); + content = templateFunction(data, this.dom.inner); + } else { content = data && data.content; } @@ -100,11 +102,11 @@ Group.prototype.setData = function(data) { this.dom.inner.removeChild(this.dom.inner.firstChild); } this.dom.inner.appendChild(content); - } - else if (content !== undefined && content !== null) { + } else if (content instanceof Object) { + templateFunction(data, this.dom.inner); + } else if (content !== undefined && content !== null) { this.dom.inner.innerHTML = content; - } - else { + } else { this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null } diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index e50918a8..c70cc0a4 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -187,31 +187,37 @@ Item.prototype._repaintDeleteButton = function (anchor) { */ Item.prototype._updateContents = function (element) { var content; + var templateFunction; + if (this.options.template) { var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData, element); - } - else { + templateFunction = this.options.template.bind(this); + content = templateFunction(itemData, element); + } else { content = this.data.content; } - var changed = this._contentToString(this.content) !== this._contentToString(content); - if (changed) { - // only replace the content when changed - if (content instanceof Element) { - element.innerHTML = ''; - element.appendChild(content); - } - else if (content != undefined) { - element.innerHTML = content; - } - else { - if (!(this.data.type == 'background' && this.data.content === undefined)) { - throw new Error('Property "content" missing in item ' + this.id); + if (content instanceof Object) { + templateFunction(itemData, element) + } else { + var changed = this._contentToString(this.content) !== this._contentToString(content); + if (changed) { + // only replace the content when changed + if (content instanceof Element) { + element.innerHTML = ''; + element.appendChild(content); + } + else if (content != undefined) { + element.innerHTML = content; + } + else { + if (!(this.data.type == 'background' && this.data.content === undefined)) { + throw new Error('Property "content" missing in item ' + this.id); + } } - } - this.content = content; + this.content = content; + } } };