From 088980bc4cf147eab1e08a73234d69de8b59c764 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Thu, 27 Oct 2016 13:31:57 +0200 Subject: [PATCH] Added class Range for Min/Max value pairs. (#2230) * Added class Range for Min/Max value pairs. * Processed review feedback, added comments. * Fixed jsdoc tags; reviewed commenting * Final fixes to commenting --- lib/graph3d/Graph3d.js | 386 +++++++++++++++++++++++------------------ lib/graph3d/Range.js | 97 +++++++++++ 2 files changed, 316 insertions(+), 167 deletions(-) create mode 100644 lib/graph3d/Range.js diff --git a/lib/graph3d/Graph3d.js b/lib/graph3d/Graph3d.js index 10e5f5e8..f66912e8 100644 --- a/lib/graph3d/Graph3d.js +++ b/lib/graph3d/Graph3d.js @@ -7,6 +7,7 @@ var Camera = require('./Camera'); var Filter = require('./Filter'); var Slider = require('./Slider'); var StepNumber = require('./StepNumber'); +var Range = require('./Range'); var Settings = require('./Settings'); @@ -15,12 +16,11 @@ Graph3d.STYLE = Settings.STYLE; /** - * 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. + * 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. + * Using 'undefined' directly achieves the same thing, but this is more + * descriptive by describing the intent. */ var autoByDefault = undefined; @@ -28,11 +28,11 @@ var autoByDefault = undefined; /** * Default values for option settings. * - * These are the values used when a Graph3d instance is initialized - * without custom settings. + * 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 'autoByDefault' - * is assumed, which is just an alias for 'undefined'. + * 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', @@ -49,11 +49,11 @@ var DEFAULTS = { showPerspective : true, 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 + 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 + animationInterval : 1000, // milliseconds animationPreload : false, animationAutoStart : autoByDefault, @@ -64,13 +64,13 @@ var DEFAULTS = { style : Graph3d.STYLE.DOT, tooltip : false, - showLegend : autoByDefault, // determined by graph style + showLegend : autoByDefault, // determined by graph style backgroundColor : autoByDefault, dataColor : { fill : '#7DC1FF', stroke : '#3267D2', - strokeWidth: 1 // px + strokeWidth: 1 // px }, cameraPosition : { @@ -152,9 +152,11 @@ Emitter(Graph3d.prototype); * Calculate the scaling values, dependent on the range in x, y, and z direction */ Graph3d.prototype._setScale = function() { - this.scale = new Point3d(1 / (this.xMax - this.xMin), - 1 / (this.yMax - this.yMin), - 1 / (this.zMax - this.zMin)); + this.scale = new Point3d( + 1 / this.xRange.range(), + 1 / this.yRange.range(), + 1 / this.zRange.range() + ); // keep aspect ration between x and y scale if desired if (this.keepAspectRatio) { @@ -173,21 +175,24 @@ Graph3d.prototype._setScale = function() { // TODO: can this be automated? verticalRatio? // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + if (this.valueRange !== undefined) { + this.scale.value = 1 / this.valueRange.range(); + } // position the camera arm - var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; - var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; - var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + var xCenter = this.xRange.center() * this.scale.x; + var yCenter = this.yRange.center() * this.scale.y; + var zCenter = this.zRange.center() * this.scale.z; this.camera.setArmLocation(xCenter, yCenter, zCenter); }; /** * Convert a 3D location to a 2D location on screen - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point2d} point2d A 2D point with parameters x, y + * Source: ttp://en.wikipedia.org/wiki/3D_projection + * + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @returns {Point2d} point2d A 2D point with parameters x, y */ Graph3d.prototype._convert3Dto2D = function(point3d) { var translation = this._convertPointToTranslation(point3d); @@ -196,11 +201,12 @@ Graph3d.prototype._convert3Dto2D = function(point3d) { /** * Convert a 3D location its translation seen from the camera - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera + * Source: http://en.wikipedia.org/wiki/3D_projection + * + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @returns {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera. */ Graph3d.prototype._convertPointToTranslation = function(point3d) { var cameraLocation = this.camera.getCameraLocation(), @@ -231,10 +237,11 @@ Graph3d.prototype._convertPointToTranslation = function(point3d) { /** * Convert a translation point to a point on the screen - * @param {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - * @return {Point2d} point2d A 2D point with parameters x, y + * + * @param {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera. + * @returns {Point2d} point2d A 2D point with parameters x, y */ Graph3d.prototype._convertTranslationToScreen = function(translation) { var ex = this.eye.x, @@ -321,20 +328,21 @@ Graph3d.prototype.getDistinctValues = function(data, column) { } +/** + * Get the absolute min/max values for the passed data column. + * + * @returns {Range} A Range instance with min/max members properly set. + */ Graph3d.prototype.getColumnRange = function(data,column) { - var minMax; + var range = new Range(); + // Adjust the range so that it covers all values in the passed data elements. for (var i = 0; i < data.length; i++) { var item = data[i][column]; - - if (i === 0) { - minMax = { min: item, max: item}; - } else { - if (minMax.min > item) { minMax.min = item; } - if (minMax.max < item) { minMax.max = item; } - } + range.adjust(item); } - return minMax; + + return range; }; @@ -363,7 +371,7 @@ Graph3d.prototype._checkValueField = function (data) { } // The data must also contain this field. - // Note that only first data element is checked + // Note that only first data element is checked. if (data[0][this.colValue] === undefined) { throw new Error('Expected data to have ' + ' field \'' + this.colValue + '\' ' @@ -373,11 +381,37 @@ Graph3d.prototype._checkValueField = function (data) { }; +/** + * Set default values for range + * + * The default values override the range values, if defined. + * + * Because it's possible that only defaultMin or defaultMax is set, it's better + * to pass in a range already set with the min/max set from the data. Otherwise, + * it's quite hard to process the min/max properly. + */ +Graph3d.prototype._setRangeDefaults = function (range, defaultMin, defaultMax) { + if (defaultMin !== undefined) { + range.min = defaultMin; + } + + if (defaultMax !== undefined) { + range.max = defaultMax; + } + + // This is the original way that the default min/max values were adjusted. + // TODO: Perhaps it's better if an error is thrown if the values do not agree. + // But this will change the behaviour. + if (range.max <= range.min) range.max = range.min + 1; +}; + + /** * Initialize the data from the data table. Calculate minimum and maximum values * and column index values - * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. - * @param {Number} style Style Number + * @param {Array | DataSet | DataView} rawData The data containing the items for + * the Graph. + * @param {Number} style Style Number */ Graph3d.prototype._dataInitialize = function (rawData, style) { var me = this; @@ -419,17 +453,6 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { this.colY = 'y'; this.colZ = 'z'; - // check if a filter column is provided - if (data[0].hasOwnProperty('filter')) { - // Only set this field if it's actually present - this.colFilter = 'filter'; - - if (this.dataFilter === undefined) { - this.dataFilter = new Filter(rawData, this.colFilter, this); - this.dataFilter.setOnLoadCallback(function() {me.redraw();}); - } - } - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || @@ -455,39 +478,49 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { } // calculate minimums and maximums - var xRange = this.getColumnRange(data,this.colX); + var NUMSTEPS = 5; + + var xRange = this.getColumnRange(data, this.colX); if (withBars) { - xRange.min -= this.xBarWidth / 2; - xRange.max += this.xBarWidth / 2; + xRange.expand(this.xBarWidth / 2); } - this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min; - this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max; - if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; - this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5; + this._setRangeDefaults(xRange, this.defaultXMin, this.defaultXMax); + this.xRange = xRange; + this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : xRange.range()/NUMSTEPS; - var yRange = this.getColumnRange(data,this.colY); + var yRange = this.getColumnRange(data, this.colY); if (withBars) { - yRange.min -= this.yBarWidth / 2; - yRange.max += this.yBarWidth / 2; + yRange.expand(this.yBarWidth / 2); } - this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min; - this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max; - if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; - this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5; + this._setRangeDefaults(yRange, this.defaultYMin, this.defaultYMax); + this.yRange = yRange; + this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : yRange.range()/NUMSTEPS; - var zRange = this.getColumnRange(data,this.colZ); - this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min; - this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max; - if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; + var zRange = this.getColumnRange(data, this.colZ); + this._setRangeDefaults(zRange, this.defaultZMin, this.defaultZMax); + this.zRange = zRange; + this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : zRange.range()/NUMSTEPS; if (data[0].hasOwnProperty('style')) { this.colValue = 'style'; var valueRange = this.getColumnRange(data,this.colValue); - this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; - this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; - if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + this._setRangeDefaults(valueRange, this.defaultValueMin, this.defaultValueMax); + this.valueRange = valueRange; + } + + + // check if a filter column is provided + // Needs to be started after zRange is defined + if (data[0].hasOwnProperty('filter')) { + // Only set this field if it's actually present + this.colFilter = 'filter'; + + if (this.dataFilter === undefined) { + this.dataFilter = new Filter(rawData, this.colFilter, this); + this.dataFilter.setOnLoadCallback(function() {me.redraw();}); + } } + // set the scale dependent on the ranges. this._setScale(); @@ -497,11 +530,14 @@ Graph3d.prototype._dataInitialize = function (rawData, style) { /** * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen + * + * @param {Array} data + * @returns {Array} dataPoints Array with point objects which can be drawn on + * screen */ Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time + // TODO: store the created matrix dataPoints in the filters instead of + // reloading each time. var x, y, i, z, obj, point; var dataPoints = []; @@ -539,7 +575,8 @@ Graph3d.prototype._getDataPoints = function (data) { y = data[i][this.colY] || 0; z = data[i][this.colZ] || 0; - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + // TODO: implement Array().indexOf() for Internet Explorer + var xIndex = dataX.indexOf(x); var yIndex = dataY.indexOf(y); if (dataMatrix[xIndex] === undefined) { @@ -556,7 +593,7 @@ Graph3d.prototype._getDataPoints = function (data) { obj.point = point3d; obj.trans = undefined; obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + obj.bottom = new Point3d(x, y, this.zRange.min); dataMatrix[xIndex][yIndex] = obj; @@ -594,7 +631,7 @@ Graph3d.prototype._getDataPoints = function (data) { obj = {}; obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.bottom = new Point3d(point.x, point.y, this.zRange.min); obj.trans = undefined; obj.screen = undefined; @@ -614,6 +651,7 @@ Graph3d.prototype._getDataPoints = function (data) { /** * Create the main frame for the Graph3d. + * * This function is executed once when a Graph3d object is created. The frame * contains a canvas, and this canvas contains all objects like the axis and * nodes. @@ -670,10 +708,11 @@ Graph3d.prototype.create = function () { /** * Set a new size for the graph - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') + * + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ Graph3d.prototype.setSize = function(width, height) { this.frame.style.width = width; @@ -749,8 +788,9 @@ Graph3d.prototype._resizeCenter = function() { /** * Retrieve the current camera rotation - * @return {object} An object with parameters horizontal, vertical, and - * distance + * + * @returns {object} An object with parameters horizontal, vertical, and + * distance */ Graph3d.prototype.getCameraPosition = function() { var pos = this.camera.getArmRotation(); @@ -781,6 +821,7 @@ Graph3d.prototype._readData = function(data) { /** * Replace the dataset of the Graph3d + * * @param {Array | DataSet | DataView} data */ Graph3d.prototype.setData = function (data) { @@ -795,6 +836,7 @@ Graph3d.prototype.setData = function (data) { /** * Update the options. Options will be merged with current options + * * @param {Object} options */ Graph3d.prototype.setOptions = function (options) { @@ -1017,8 +1059,8 @@ Graph3d.prototype._redrawLegend = function() { // print value text along the legend edge var gridLineLen = 5; // px - var legendMin = isValueLegend ? this.valueMin : this.zMin; - var legendMax = isValueLegend ? this.valueMax : this.zMax; + var legendMin = isValueLegend ? this.valueRange.min : this.zRange.min; + var legendMax = isValueLegend ? this.valueRange.max : this.zRange.max; var step = new StepNumber(legendMin, legendMax, (legendMax-legendMin)/5, true); step.start(true); @@ -1217,8 +1259,7 @@ Graph3d.prototype._redrawAxis = function() { var ctx = this._getContext(), from, to, step, prettyStep, text, xText, yText, zText, - offset, xOffset, yOffset, - xMin2d, xMax2d; + offset, xOffset, yOffset; // TODO: get the actual rendered style of the containerElement //ctx.font = this.containerElement.style.font; @@ -1231,32 +1272,36 @@ Graph3d.prototype._redrawAxis = function() { var armAngle = this.camera.getArmRotation().horizontal; var armVector = new Point2d(Math.cos(armAngle), Math.sin(armAngle)); + var xRange = this.xRange; + var yRange = this.yRange; + var zRange = this.zRange; + // draw x-grid lines ctx.lineWidth = 1; prettyStep = (this.defaultXStep === undefined); - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step = new StepNumber(xRange.min, xRange.max, this.xStep, prettyStep); step.start(true); while (!step.end()) { var x = step.getCurrent(); if (this.showGrid) { - from = new Point3d(x, this.yMin, this.zMin); - to = new Point3d(x, this.yMax, this.zMin); + from = new Point3d(x, yRange.min, zRange.min); + to = new Point3d(x, yRange.max, zRange.min); this._line3d(ctx, from, to, this.gridColor); } else { - from = new Point3d(x, this.yMin, this.zMin); - to = new Point3d(x, this.yMin+gridLenX, this.zMin); + from = new Point3d(x, yRange.min, zRange.min); + to = new Point3d(x, yRange.min+gridLenX, zRange.min); this._line3d(ctx, from, to, this.axisColor); - from = new Point3d(x, this.yMax, this.zMin); - to = new Point3d(x, this.yMax-gridLenX, this.zMin); + from = new Point3d(x, yRange.max, zRange.min); + to = new Point3d(x, yRange.max-gridLenX, zRange.min); this._line3d(ctx, from, to, this.axisColor); } - yText = (armVector.x > 0) ? this.yMin : this.yMax; - var point3d = new Point3d(x, yText, this.zMin); + yText = (armVector.x > 0) ? yRange.min : yRange.max; + var point3d = new Point3d(x, yText, zRange.min); var msg = ' ' + this.xValueLabel(x) + ' '; this.drawAxisLabelX(ctx, point3d, msg, armAngle, textMargin); @@ -1266,29 +1311,29 @@ Graph3d.prototype._redrawAxis = function() { // draw y-grid lines ctx.lineWidth = 1; prettyStep = (this.defaultYStep === undefined); - step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); + step = new StepNumber(yRange.min, yRange.max, this.yStep, prettyStep); step.start(true); while (!step.end()) { var y = step.getCurrent(); if (this.showGrid) { - from = new Point3d(this.xMin, y, this.zMin); - to = new Point3d(this.xMax, y, this.zMin); + from = new Point3d(xRange.min, y, zRange.min); + to = new Point3d(xRange.max, y, zRange.min); this._line3d(ctx, from, to, this.gridColor); } else { - from = new Point3d(this.xMin, y, this.zMin); - to = new Point3d(this.xMin+gridLenY, y, this.zMin); + from = new Point3d(xRange.min, y, zRange.min); + to = new Point3d(xRange.min+gridLenY, y, zRange.min); this._line3d(ctx, from, to, this.axisColor); - from = new Point3d(this.xMax, y, this.zMin); - to = new Point3d(this.xMax-gridLenY, y, this.zMin); + from = new Point3d(xRange.max, y, zRange.min); + to = new Point3d(xRange.max-gridLenY, y, zRange.min); this._line3d(ctx, from, to, this.axisColor); } - xText = (armVector.y > 0) ? this.xMin : this.xMax; - point3d = new Point3d(xText, y, this.zMin); + xText = (armVector.y > 0) ? xRange.min : xRange.max; + point3d = new Point3d(xText, y, zRange.min); var msg = ' ' + this.yValueLabel(y) + ' '; this.drawAxisLabelY(ctx, point3d, msg, armAngle, textMargin); @@ -1298,11 +1343,11 @@ Graph3d.prototype._redrawAxis = function() { // draw z-grid lines and axis ctx.lineWidth = 1; prettyStep = (this.defaultZStep === undefined); - step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); + step = new StepNumber(zRange.min, zRange.max, this.zStep, prettyStep); step.start(true); - xText = (armVector.x > 0) ? this.xMin : this.xMax; - yText = (armVector.y < 0) ? this.yMin : this.yMax; + xText = (armVector.x > 0) ? xRange.min : xRange.max; + yText = (armVector.y < 0) ? yRange.min : yRange.max; while (!step.end()) { var z = step.getCurrent(); @@ -1320,39 +1365,42 @@ Graph3d.prototype._redrawAxis = function() { } ctx.lineWidth = 1; - from = new Point3d(xText, yText, this.zMin); - to = new Point3d(xText, yText, this.zMax); + from = new Point3d(xText, yText, zRange.min); + to = new Point3d(xText, yText, zRange.max); this._line3d(ctx, from, to, this.axisColor); // draw x-axis + var xMin2d; + var xMax2d; ctx.lineWidth = 1; + // line at yMin - xMin2d = new Point3d(this.xMin, this.yMin, this.zMin); - xMax2d = new Point3d(this.xMax, this.yMin, this.zMin); + xMin2d = new Point3d(xRange.min, yRange.min, zRange.min); + xMax2d = new Point3d(xRange.max, yRange.min, zRange.min); this._line3d(ctx, xMin2d, xMax2d, this.axisColor); // line at ymax - xMin2d = new Point3d(this.xMin, this.yMax, this.zMin); - xMax2d = new Point3d(this.xMax, this.yMax, this.zMin); + xMin2d = new Point3d(xRange.min, yRange.max, zRange.min); + xMax2d = new Point3d(xRange.max, yRange.max, zRange.min); this._line3d(ctx, xMin2d, xMax2d, this.axisColor); // draw y-axis ctx.lineWidth = 1; // line at xMin - from = new Point3d(this.xMin, this.yMin, this.zMin); - to = new Point3d(this.xMin, this.yMax, this.zMin); + from = new Point3d(xRange.min, yRange.min, zRange.min); + to = new Point3d(xRange.min, yRange.max, zRange.min); this._line3d(ctx, from, to, this.axisColor); // line at xMax - from = new Point3d(this.xMax, this.yMin, this.zMin); - to = new Point3d(this.xMax, this.yMax, this.zMin); + from = new Point3d(xRange.max, yRange.min, zRange.min); + to = new Point3d(xRange.max, yRange.max, zRange.min); this._line3d(ctx, from, to, this.axisColor); // draw x-label var xLabel = this.xLabel; if (xLabel.length > 0) { yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = (armVector.x > 0) ? this.yMin - yOffset: this.yMax + yOffset; - text = new Point3d(xText, yText, this.zMin); + xText = xRange.center() / 2; + yText = (armVector.x > 0) ? yRange.min - yOffset: yRange.max + yOffset; + text = new Point3d(xText, yText, zRange.min); this.drawAxisLabelX(ctx, text, xLabel, armAngle); } @@ -1360,9 +1408,9 @@ Graph3d.prototype._redrawAxis = function() { var yLabel = this.yLabel; if (yLabel.length > 0) { xOffset = 0.1 / this.scale.x; - xText = (armVector.y > 0) ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = new Point3d(xText, yText, this.zMin); + xText = (armVector.y > 0) ? xRange.min - xOffset : xRange.max + xOffset; + yText = yRange.center() / 2; + text = new Point3d(xText, yText, zRange.min); this.drawAxisLabelY(ctx, text, yLabel, armAngle); } @@ -1371,9 +1419,9 @@ Graph3d.prototype._redrawAxis = function() { var zLabel = this.zLabel; if (zLabel.length > 0) { offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = (armVector.x > 0) ? this.xMin : this.xMax; - yText = (armVector.y < 0) ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; + xText = (armVector.x > 0) ? xRange.min : xRange.max; + yText = (armVector.y < 0) ? yRange.min : yRange.max; + zText = zRange.center() / 2; text = new Point3d(xText, yText, zText); this.drawAxisLabelZ(ctx, text, zLabel, offset); @@ -1436,6 +1484,7 @@ Graph3d.prototype._redrawBar = function(ctx, point, xWidth, yWidth, color, borde // calculate all corner points var me = this; var point3d = point.point; + var zMin = this.zRange.min; var top = [ {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, @@ -1443,10 +1492,10 @@ Graph3d.prototype._redrawBar = function(ctx, point, xWidth, yWidth, color, borde {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)} + {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, zMin)}, + {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, zMin)}, + {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, zMin)} ]; // calculate screen location of the points @@ -1555,7 +1604,7 @@ Graph3d.prototype._drawCircle = function(ctx, point, color, borderColor, size) { */ Graph3d.prototype._getColorsRegular = function(point) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - var hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + var hue = (1 - (point.point.z - this.zRange.min) * this.scale.z / this.verticalRatio) * 240; var color = this._hsv2rgb(hue, 1, 1); var borderColor = this._hsv2rgb(hue, 1, 0.8); @@ -1572,7 +1621,7 @@ Graph3d.prototype._getColorsRegular = function(point) { */ Graph3d.prototype._getColorsColor = function(point) { // calculate the color based on the value - var hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + var hue = (1 - (point.point.value - this.valueRange.min) * this.scale.value) * 240; var color = this._hsv2rgb(hue, 1, 1); var borderColor = this._hsv2rgb(hue, 1, 0.8); @@ -1656,7 +1705,7 @@ Graph3d.prototype._redrawBarColorGraphPoint = function(ctx, point) { */ Graph3d.prototype._redrawBarSizeGraphPoint = function(ctx, point) { // calculate size for the bar - var fraction = (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range(); var xWidth = (this.xBarWidth / 2) * (fraction * 0.8 + 0.2); var yWidth = (this.yBarWidth / 2) * (fraction * 0.8 + 0.2); @@ -1704,7 +1753,7 @@ Graph3d.prototype._redrawDotColorGraphPoint = function(ctx, point) { */ Graph3d.prototype._redrawDotSizeGraphPoint = function(ctx, point) { var dotSize = this._dotSize(); - var fraction = (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + var fraction = (point.point.value - this.valueRange.min) / this.valueRange.range(); var size = dotSize/2 + 2*dotSize * fraction; var colors = this._getColorsSize(); @@ -1747,7 +1796,7 @@ Graph3d.prototype._redrawSurfaceGraphPoint = function(ctx, point) { // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 var zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - var h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + var h = (1 - (zAvg - this.zRange.min) * this.scale.z / this.verticalRatio) * 240; var s = 1; // saturation var v; @@ -1785,7 +1834,7 @@ Graph3d.prototype._drawGridLine = function(ctx, from, to) { // 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; + var h = (1 - (zAvg - this.zRange.min) * this.scale.z / this.verticalRatio) * 240; ctx.lineWidth = this._getStrokeWidth(from) * 2; ctx.strokeStyle = this._hsv2rgb(h, 1, 1); @@ -2076,9 +2125,11 @@ Graph3d.prototype._onWheel = function(event) { /** * Test whether a point lies inside given 2D triangle - * @param {Point2d} point - * @param {Point2d[]} triangle - * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * + * @param {Point2d} point + * @param {Point2d[]} triangle + * @returns {boolean} true if given point lies inside or on the edge of the + * triangle, false otherwise * @private */ Graph3d.prototype._insideTriangle = function (point, triangle) { @@ -2102,9 +2153,11 @@ Graph3d.prototype._insideTriangle = function (point, triangle) { /** * Find a data point close to given screen position (x, y) - * @param {Number} x - * @param {Number} y - * @return {Object | null} The closest data point or null if not close to any data point + * + * @param {Number} x + * @param {Number} y + * @returns {Object | null} The closest data point or null if not close to any + * data point * @private */ Graph3d.prototype._dataPointFromXY = function (x, y) { @@ -2268,8 +2321,9 @@ Graph3d.prototype._hideTooltip = function () { /** * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x + * + * @param {Event} event + * @returns {Number} mouse x */ function getMouseX (event) { if ('clientX' in event) return event.clientX; @@ -2278,8 +2332,9 @@ function getMouseX (event) { /** * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * + * @param {Event} event + * @returns {Number} mouse y */ function getMouseY (event) { if ('clientY' in event) return event.clientY; @@ -2293,19 +2348,16 @@ function getMouseY (event) { /** * 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. + * + * @param {Object} pos An object with the camera position + * @param {?Number} pos.horizontal The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * @param {?Number} pos.vertical 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. + * @param {?Number} pos.distance 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) { Settings.setCameraPosition(pos, this); diff --git a/lib/graph3d/Range.js b/lib/graph3d/Range.js new file mode 100644 index 00000000..2866c7ea --- /dev/null +++ b/lib/graph3d/Range.js @@ -0,0 +1,97 @@ +/** + * @prototype Range + * + * Helper class to make working with related min and max values easier. + * + * The range is inclusive; a given value is considered part of the range if: + * + * this.min <= value <= this.max + */ +function Range() { + this.min = undefined; + this.max = undefined; +} + + +/** + * Adjust the range so that the passed value fits in it. + * + * If the value is outside of the current extremes, adjust + * the min or max so that the value is within the range. + * + * @param {number} value Numeric value to fit in range + */ +Range.prototype.adjust = function(value) { + if (value === undefined) return; + + if (this.min === undefined || this.min > value ) { + this.min = value; + } + + if (this.max === undefined || this.max < value) { + this.max = value; + } +}; + + +/** + * Adjust the current range so that the passed range fits in it. + * + * @param {Range} range Range instance to fit in current instance + */ +Range.prototype.combine = function(range) { + this.add(range.min); + this.add(range.max); +}; + + +/** + * Expand the range by the given value + * + * min will be lowered by given value; + * max will be raised by given value + * + * Shrinking by passing a negative value is allowed. + * + * @param {number} val Amount by which to expand or shrink current range with + */ +Range.prototype.expand = function(val) { + if (val === undefined) { + return; + } + + var newMin = this.min - val; + var newMax = this.max + val; + + // Note that following allows newMin === newMax. + // This should be OK, since method expand() allows this also. + if (newMin > newMax) { + throw new Error('Passed expansion value makes range invalid'); + } + + this.min = newMin; + this.max = newMax; +}; + + +/** + * Determine the full range width of current instance. + * + * @returns {num} The calculated width of this range + */ +Range.prototype.range = function() { + return this.max - this.min; +}; + + +/** + * Determine the central point of current instance. + * + * @returns {number} the value in the middle of min and max + */ +Range.prototype.center = function() { + return (this.min + this.max) / 2; +}; + + +module.exports = Range;