Browse Source

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
codeClimate
wimrijnders 8 years ago
committed by Alexander Wunschik
parent
commit
088980bc4c
2 changed files with 316 additions and 167 deletions
  1. +219
    -167
      lib/graph3d/Graph3d.js
  2. +97
    -0
      lib/graph3d/Range.js

+ 219
- 167
lib/graph3d/Graph3d.js View File

@ -7,6 +7,7 @@ var Camera = require('./Camera');
var Filter = require('./Filter'); var Filter = require('./Filter');
var Slider = require('./Slider'); var Slider = require('./Slider');
var StepNumber = require('./StepNumber'); var StepNumber = require('./StepNumber');
var Range = require('./Range');
var Settings = require('./Settings'); 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; var autoByDefault = undefined;
@ -28,11 +28,11 @@ var autoByDefault = undefined;
/** /**
* Default values for option settings. * 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 = { var DEFAULTS = {
width : '400px', width : '400px',
@ -49,11 +49,11 @@ var DEFAULTS = {
showPerspective : true, showPerspective : true,
showShadow : false, showShadow : false,
keepAspectRatio : true, 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, showAnimationControls: autoByDefault,
animationInterval : 1000, // milliseconds
animationInterval : 1000, // milliseconds
animationPreload : false, animationPreload : false,
animationAutoStart : autoByDefault, animationAutoStart : autoByDefault,
@ -64,13 +64,13 @@ var DEFAULTS = {
style : Graph3d.STYLE.DOT, style : Graph3d.STYLE.DOT,
tooltip : false, tooltip : false,
showLegend : autoByDefault, // determined by graph style
showLegend : autoByDefault, // determined by graph style
backgroundColor : autoByDefault, backgroundColor : autoByDefault,
dataColor : { dataColor : {
fill : '#7DC1FF', fill : '#7DC1FF',
stroke : '#3267D2', stroke : '#3267D2',
strokeWidth: 1 // px
strokeWidth: 1 // px
}, },
cameraPosition : { cameraPosition : {
@ -152,9 +152,11 @@ Emitter(Graph3d.prototype);
* Calculate the scaling values, dependent on the range in x, y, and z direction * Calculate the scaling values, dependent on the range in x, y, and z direction
*/ */
Graph3d.prototype._setScale = function() { 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 // keep aspect ration between x and y scale if desired
if (this.keepAspectRatio) { if (this.keepAspectRatio) {
@ -173,21 +175,24 @@ Graph3d.prototype._setScale = function() {
// TODO: can this be automated? verticalRatio? // TODO: can this be automated? verticalRatio?
// determine scale for (optional) value // 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 // 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); this.camera.setArmLocation(xCenter, yCenter, zCenter);
}; };
/** /**
* Convert a 3D location to a 2D location on screen * 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) { Graph3d.prototype._convert3Dto2D = function(point3d) {
var translation = this._convertPointToTranslation(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 * 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) { Graph3d.prototype._convertPointToTranslation = function(point3d) {
var cameraLocation = this.camera.getCameraLocation(), var cameraLocation = this.camera.getCameraLocation(),
@ -231,10 +237,11 @@ Graph3d.prototype._convertPointToTranslation = function(point3d) {
/** /**
* Convert a translation point to a point on the screen * 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) { Graph3d.prototype._convertTranslationToScreen = function(translation) {
var ex = this.eye.x, 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) { 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++) { for (var i = 0; i < data.length; i++) {
var item = 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; }
}
range.adjust(item);
} }
return minMax;
return range;
}; };
@ -363,7 +371,7 @@ Graph3d.prototype._checkValueField = function (data) {
} }
// The data must also contain this field. // 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) { if (data[0][this.colValue] === undefined) {
throw new Error('Expected data to have ' throw new Error('Expected data to have '
+ ' field \'' + this.colValue + '\' ' + ' 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 * Initialize the data from the data table. Calculate minimum and maximum values
* and column index 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) { Graph3d.prototype._dataInitialize = function (rawData, style) {
var me = this; var me = this;
@ -419,17 +453,6 @@ Graph3d.prototype._dataInitialize = function (rawData, style) {
this.colY = 'y'; this.colY = 'y';
this.colZ = 'z'; 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 || var withBars = this.style == Graph3d.STYLE.BAR ||
this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARCOLOR ||
@ -455,39 +478,49 @@ Graph3d.prototype._dataInitialize = function (rawData, style) {
} }
// calculate minimums and maximums // calculate minimums and maximums
var xRange = this.getColumnRange(data,this.colX);
var NUMSTEPS = 5;
var xRange = this.getColumnRange(data, this.colX);
if (withBars) { 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) { 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')) { if (data[0].hasOwnProperty('style')) {
this.colValue = 'style'; this.colValue = 'style';
var valueRange = this.getColumnRange(data,this.colValue); 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. // set the scale dependent on the ranges.
this._setScale(); this._setScale();
@ -497,11 +530,14 @@ Graph3d.prototype._dataInitialize = function (rawData, style) {
/** /**
* Filter the data based on the current filter * 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) { 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 x, y, i, z, obj, point;
var dataPoints = []; var dataPoints = [];
@ -539,7 +575,8 @@ Graph3d.prototype._getDataPoints = function (data) {
y = data[i][this.colY] || 0; y = data[i][this.colY] || 0;
z = data[i][this.colZ] || 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); var yIndex = dataY.indexOf(y);
if (dataMatrix[xIndex] === undefined) { if (dataMatrix[xIndex] === undefined) {
@ -556,7 +593,7 @@ Graph3d.prototype._getDataPoints = function (data) {
obj.point = point3d; obj.point = point3d;
obj.trans = undefined; obj.trans = undefined;
obj.screen = 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; dataMatrix[xIndex][yIndex] = obj;
@ -594,7 +631,7 @@ Graph3d.prototype._getDataPoints = function (data) {
obj = {}; obj = {};
obj.point = point; 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.trans = undefined;
obj.screen = undefined; obj.screen = undefined;
@ -614,6 +651,7 @@ Graph3d.prototype._getDataPoints = function (data) {
/** /**
* Create the main frame for the Graph3d. * Create the main frame for the Graph3d.
*
* This function is executed once when a Graph3d object is created. The frame * 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 * contains a canvas, and this canvas contains all objects like the axis and
* nodes. * nodes.
@ -670,10 +708,11 @@ Graph3d.prototype.create = function () {
/** /**
* Set a new size for the graph * 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) { Graph3d.prototype.setSize = function(width, height) {
this.frame.style.width = width; this.frame.style.width = width;
@ -749,8 +788,9 @@ Graph3d.prototype._resizeCenter = function() {
/** /**
* Retrieve the current camera rotation * 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() { Graph3d.prototype.getCameraPosition = function() {
var pos = this.camera.getArmRotation(); var pos = this.camera.getArmRotation();
@ -781,6 +821,7 @@ Graph3d.prototype._readData = function(data) {
/** /**
* Replace the dataset of the Graph3d * Replace the dataset of the Graph3d
*
* @param {Array | DataSet | DataView} data * @param {Array | DataSet | DataView} data
*/ */
Graph3d.prototype.setData = function (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 * Update the options. Options will be merged with current options
*
* @param {Object} options * @param {Object} options
*/ */
Graph3d.prototype.setOptions = function (options) { Graph3d.prototype.setOptions = function (options) {
@ -1017,8 +1059,8 @@ Graph3d.prototype._redrawLegend = function() {
// print value text along the legend edge // print value text along the legend edge
var gridLineLen = 5; // px 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); var step = new StepNumber(legendMin, legendMax, (legendMax-legendMin)/5, true);
step.start(true); step.start(true);
@ -1217,8 +1259,7 @@ Graph3d.prototype._redrawAxis = function() {
var ctx = this._getContext(), var ctx = this._getContext(),
from, to, step, prettyStep, from, to, step, prettyStep,
text, xText, yText, zText, text, xText, yText, zText,
offset, xOffset, yOffset,
xMin2d, xMax2d;
offset, xOffset, yOffset;
// TODO: get the actual rendered style of the containerElement // TODO: get the actual rendered style of the containerElement
//ctx.font = this.containerElement.style.font; //ctx.font = this.containerElement.style.font;
@ -1231,32 +1272,36 @@ Graph3d.prototype._redrawAxis = function() {
var armAngle = this.camera.getArmRotation().horizontal; var armAngle = this.camera.getArmRotation().horizontal;
var armVector = new Point2d(Math.cos(armAngle), Math.sin(armAngle)); 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 // draw x-grid lines
ctx.lineWidth = 1; ctx.lineWidth = 1;
prettyStep = (this.defaultXStep === undefined); 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); step.start(true);
while (!step.end()) { while (!step.end()) {
var x = step.getCurrent(); var x = step.getCurrent();
if (this.showGrid) { 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); this._line3d(ctx, from, to, this.gridColor);
} }
else { 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); 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); 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) + ' '; var msg = ' ' + this.xValueLabel(x) + ' ';
this.drawAxisLabelX(ctx, point3d, msg, armAngle, textMargin); this.drawAxisLabelX(ctx, point3d, msg, armAngle, textMargin);
@ -1266,29 +1311,29 @@ Graph3d.prototype._redrawAxis = function() {
// draw y-grid lines // draw y-grid lines
ctx.lineWidth = 1; ctx.lineWidth = 1;
prettyStep = (this.defaultYStep === undefined); 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); step.start(true);
while (!step.end()) { while (!step.end()) {
var y = step.getCurrent(); var y = step.getCurrent();
if (this.showGrid) { 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); this._line3d(ctx, from, to, this.gridColor);
} }
else { 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); 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); 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) + ' '; var msg = ' ' + this.yValueLabel(y) + ' ';
this.drawAxisLabelY(ctx, point3d, msg, armAngle, textMargin); this.drawAxisLabelY(ctx, point3d, msg, armAngle, textMargin);
@ -1298,11 +1343,11 @@ Graph3d.prototype._redrawAxis = function() {
// draw z-grid lines and axis // draw z-grid lines and axis
ctx.lineWidth = 1; ctx.lineWidth = 1;
prettyStep = (this.defaultZStep === undefined); 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); 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()) { while (!step.end()) {
var z = step.getCurrent(); var z = step.getCurrent();
@ -1320,39 +1365,42 @@ Graph3d.prototype._redrawAxis = function() {
} }
ctx.lineWidth = 1; 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); this._line3d(ctx, from, to, this.axisColor);
// draw x-axis // draw x-axis
var xMin2d;
var xMax2d;
ctx.lineWidth = 1; ctx.lineWidth = 1;
// line at yMin // 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); this._line3d(ctx, xMin2d, xMax2d, this.axisColor);
// line at ymax // 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); this._line3d(ctx, xMin2d, xMax2d, this.axisColor);
// draw y-axis // draw y-axis
ctx.lineWidth = 1; ctx.lineWidth = 1;
// line at xMin // 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); this._line3d(ctx, from, to, this.axisColor);
// line at xMax // 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); this._line3d(ctx, from, to, this.axisColor);
// draw x-label // draw x-label
var xLabel = this.xLabel; var xLabel = this.xLabel;
if (xLabel.length > 0) { if (xLabel.length > 0) {
yOffset = 0.1 / this.scale.y; 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); this.drawAxisLabelX(ctx, text, xLabel, armAngle);
} }
@ -1360,9 +1408,9 @@ Graph3d.prototype._redrawAxis = function() {
var yLabel = this.yLabel; var yLabel = this.yLabel;
if (yLabel.length > 0) { if (yLabel.length > 0) {
xOffset = 0.1 / this.scale.x; 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); this.drawAxisLabelY(ctx, text, yLabel, armAngle);
} }
@ -1371,9 +1419,9 @@ Graph3d.prototype._redrawAxis = function() {
var zLabel = this.zLabel; var zLabel = this.zLabel;
if (zLabel.length > 0) { if (zLabel.length > 0) {
offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? 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); text = new Point3d(xText, yText, zText);
this.drawAxisLabelZ(ctx, text, zLabel, offset); this.drawAxisLabelZ(ctx, text, zLabel, offset);
@ -1436,6 +1484,7 @@ Graph3d.prototype._redrawBar = function(ctx, point, xWidth, yWidth, color, borde
// calculate all corner points // calculate all corner points
var me = this; var me = this;
var point3d = point.point; var point3d = point.point;
var zMin = this.zRange.min;
var top = [ 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)},
@ -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)} {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
]; ];
var bottom = [ 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 // calculate screen location of the points
@ -1555,7 +1604,7 @@ Graph3d.prototype._drawCircle = function(ctx, point, color, borderColor, size) {
*/ */
Graph3d.prototype._getColorsRegular = function(point) { Graph3d.prototype._getColorsRegular = function(point) {
// calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 // 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 color = this._hsv2rgb(hue, 1, 1);
var borderColor = this._hsv2rgb(hue, 1, 0.8); var borderColor = this._hsv2rgb(hue, 1, 0.8);
@ -1572,7 +1621,7 @@ Graph3d.prototype._getColorsRegular = function(point) {
*/ */
Graph3d.prototype._getColorsColor = function(point) { Graph3d.prototype._getColorsColor = function(point) {
// calculate the color based on the value // 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 color = this._hsv2rgb(hue, 1, 1);
var borderColor = this._hsv2rgb(hue, 1, 0.8); var borderColor = this._hsv2rgb(hue, 1, 0.8);
@ -1656,7 +1705,7 @@ Graph3d.prototype._redrawBarColorGraphPoint = function(ctx, point) {
*/ */
Graph3d.prototype._redrawBarSizeGraphPoint = function(ctx, point) { Graph3d.prototype._redrawBarSizeGraphPoint = function(ctx, point) {
// calculate size for the bar // 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 xWidth = (this.xBarWidth / 2) * (fraction * 0.8 + 0.2);
var yWidth = (this.yBarWidth / 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) { Graph3d.prototype._redrawDotSizeGraphPoint = function(ctx, point) {
var dotSize = this._dotSize(); 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 size = dotSize/2 + 2*dotSize * fraction;
var colors = this._getColorsSize(); 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 // 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 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 s = 1; // saturation
var v; 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 // 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 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.lineWidth = this._getStrokeWidth(from) * 2;
ctx.strokeStyle = this._hsv2rgb(h, 1, 1); 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 * 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 * @private
*/ */
Graph3d.prototype._insideTriangle = function (point, triangle) { 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) * 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 * @private
*/ */
Graph3d.prototype._dataPointFromXY = function (x, y) { Graph3d.prototype._dataPointFromXY = function (x, y) {
@ -2268,8 +2321,9 @@ Graph3d.prototype._hideTooltip = function () {
/** /**
* Get the horizontal mouse position from a mouse event * 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) { function getMouseX (event) {
if ('clientX' in event) return event.clientX; if ('clientX' in event) return event.clientX;
@ -2278,8 +2332,9 @@ function getMouseX (event) {
/** /**
* Get the vertical mouse position from a mouse 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) { function getMouseY (event) {
if ('clientY' in event) return event.clientY; if ('clientY' in event) return event.clientY;
@ -2293,19 +2348,16 @@ function getMouseY (event) {
/** /**
* Set the rotation and distance of the camera * 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) { Graph3d.prototype.setCameraPosition = function(pos) {
Settings.setCameraPosition(pos, this); Settings.setCameraPosition(pos, this);

+ 97
- 0
lib/graph3d/Range.js View File

@ -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;

Loading…
Cancel
Save