]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Move all helpers to src/helpers (#6841)
authorEvert Timberg <evert.timberg+github@gmail.com>
Tue, 17 Dec 2019 13:04:40 +0000 (08:04 -0500)
committerGitHub <noreply@github.com>
Tue, 17 Dec 2019 13:04:40 +0000 (08:04 -0500)
* Move all helpers into src/helpers

* Move curve helpers to their own file

* DOM helpers moved to their own file

* Update migration docs

* Remove migration docs on new functions

25 files changed:
docs/getting-started/v3-migration.md
src/controllers/controller.bar.js
src/controllers/controller.line.js
src/controllers/controller.radar.js
src/core/core.controller.js
src/core/core.datasetController.js
src/core/core.element.js
src/core/core.helpers.js [deleted file]
src/core/core.interaction.js
src/core/core.scale.js
src/elements/element.arc.js
src/helpers/helpers.curve.js [new file with mode: 0644]
src/helpers/helpers.dom.js [new file with mode: 0644]
src/helpers/helpers.math.js
src/helpers/index.js
src/index.js
src/platforms/platform.dom.js
src/scales/scale.linearbase.js
src/scales/scale.logarithmic.js
src/scales/scale.radialLinear.js
src/scales/scale.time.js
test/specs/core.helpers.tests.js
test/specs/helpers.curve.tests.js [new file with mode: 0644]
test/specs/helpers.dom.tests.js [new file with mode: 0644]
test/specs/helpers.math.tests.js

index d76bd487bb7dd4347d810078d483e1a47e8b25dd..9fa7610fac07d2bbaecd3db6fca5add8e8970525 100644 (file)
@@ -105,6 +105,22 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released
 * `helpers.getValueAtIndexOrDefault` was renamed to `helpers.valueAtIndexOrDefault`
 * `helpers.easingEffects` was renamed to `helpers.easing.effects`
 * `helpers.log10` was renamed to `helpers.math.log10`
+* `helpers.almostEquals` was renamed to `helpers.math.almostEquals`
+* `helpers.almostWhole` was renamed to `helpers.math.almostWhole`
+* `helpers._decimalPlaces` was renamed to `helpers.math._decimalPlaces`
+* `helpers.distanceBetweenPoints` was renamed to `helpers.math.distanceBetweenPoints`
+* `helpers.isNumber` was renamed to `helpers.math.isNumber`
+* `helpers.sign` was renamed to `helpers.math.sign`
+* `helpers.toDegrees` was renamed to `helpers.math.toDegrees`
+* `helpers.toRadians` was renamed to `helpers.math.toRadians`
+* `helpers.getAngleFromPoint` was renamed to `helpers.math.getAngleFromPoint`
+* `helpers.splineCurveMonotone` was renamed to `helpers.curve.splineCurveMonotone`
+* `helpers.splineCurve` was renamed to `helpers.curve.splineCurve`
+* `helpers.retinaScale` was renamed to `helpers.dom.retinaScale`
+* `helpers.getMaximumWidth` was renamed to `helpers.dom.getMaximumWidth`
+* `helpers.getMaximumHeight` was renamed to `helpers.dom.getMaximumHeight`
+* `helpers.getRelativePosition` was renamed to `helpers.dom.getRelativePosition`
+* `helpers.getStyle` was renamed to `helpers.dom.getStyle`
 * `Chart.Animation.animationObject` was renamed to `Chart.Animation`
 * `Chart.Animation.chartInstance` was renamed to `Chart.Animation.chart`
 * `DatasetController.updateElement` was renamed to `DatasetController.updateElements`
index 36199e7c258f0d6f5b77754933d099dd79fd2dec..7b00ea00f890fb9899224c010a101c9ae219665c 100644 (file)
@@ -422,7 +422,7 @@ module.exports = DatasetController.extend({
                        value = custom.barStart;
                        length = custom.barEnd - custom.barStart;
                        // bars crossing origin are not stacked
-                       if (value !== 0 && helpers.sign(value) !== helpers.sign(custom.barEnd)) {
+                       if (value !== 0 && helpers.math.sign(value) !== helpers.math.sign(custom.barEnd)) {
                                start = 0;
                        }
                        start += value;
index 90ac98672f076790569b87367ba8656cbecbc6b6..34d421f98591657927c7052ba37da430381e6e86 100644 (file)
@@ -188,11 +188,11 @@ module.exports = DatasetController.extend({
                }
 
                if (lineModel.cubicInterpolationMode === 'monotone') {
-                       helpers.splineCurveMonotone(points);
+                       helpers.curve.splineCurveMonotone(points);
                } else {
                        for (i = 0, ilen = points.length; i < ilen; ++i) {
                                const model = points[i]._model;
-                               const controlPoints = helpers.splineCurve(
+                               const controlPoints = helpers.curve.splineCurve(
                                        points[Math.max(0, i - 1)]._model,
                                        model,
                                        points[Math.min(i + 1, ilen - 1)]._model,
index 6897827075b1fcd332d35c8feb50d408334e5928..c52f871de599987420cfea7c003b93767862beb7 100644 (file)
@@ -192,7 +192,7 @@ module.exports = DatasetController.extend({
 
                for (i = 0, ilen = points.length; i < ilen; ++i) {
                        model = points[i]._model;
-                       controlPoints = helpers.splineCurve(
+                       controlPoints = helpers.curve.splineCurve(
                                previousItem(points, i)._model,
                                model,
                                nextItem(points, i)._model,
index 320049212115a0b440d5bb5c1a0f4c9a936ac300..f13433399b416fa2e705754d66f7f0c399a1fce7 100644 (file)
@@ -225,7 +225,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ {
                // Before init plugin notification
                plugins.notify(me, 'beforeInit');
 
-               helpers.retinaScale(me, me.options.devicePixelRatio);
+               helpers.dom.retinaScale(me, me.options.devicePixelRatio);
 
                me.bindEvents();
 
@@ -263,8 +263,8 @@ helpers.extend(Chart.prototype, /** @lends Chart */ {
                // the canvas display style uses the same integer values to avoid blurring effect.
 
                // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed
-               var newWidth = Math.max(0, Math.floor(helpers.getMaximumWidth(canvas)));
-               var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)));
+               var newWidth = Math.max(0, Math.floor(helpers.dom.getMaximumWidth(canvas)));
+               var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.dom.getMaximumHeight(canvas)));
 
                if (me.width === newWidth && me.height === newHeight) {
                        return;
@@ -275,7 +275,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ {
                canvas.style.width = newWidth + 'px';
                canvas.style.height = newHeight + 'px';
 
-               helpers.retinaScale(me, options.devicePixelRatio);
+               helpers.dom.retinaScale(me, options.devicePixelRatio);
 
                if (!silent) {
                        // Notify any plugins about the resize
index 1a82ff48461b2f70e133c74f9b94f394f28fa5f6..f8e39b482c002e99defe5c92d9ce8c5346a2ec65 100644 (file)
@@ -145,7 +145,7 @@ function applyStack(stack, value, dsIndex, allOther) {
                        break;
                }
                otherValue = stack.values[datasetIndex];
-               if (!isNaN(otherValue) && (value === 0 || helpers.sign(value) === helpers.sign(otherValue))) {
+               if (!isNaN(otherValue) && (value === 0 || helpers.math.sign(value) === helpers.math.sign(otherValue))) {
                        value += otherValue;
                }
        }
index f4feb4bd6ec0f138dd867c08e4b209f9d6192f6b..159e4721cd0291aec33548cba9965b613c704644 100644 (file)
@@ -1,7 +1,8 @@
 'use strict';
 
-const color = require('chartjs-color');
-const helpers = require('../helpers/index');
+import color from 'chartjs-color';
+import helpers from '../helpers/index';
+import {isNumber} from '../helpers/helpers.math';
 
 function interpolate(start, view, model, ease) {
        var keys = Object.keys(model);
@@ -110,10 +111,9 @@ class Element {
        }
 
        hasValue() {
-               return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y);
+               return isNumber(this._model.x) && isNumber(this._model.y);
        }
 }
 
 Element.extend = helpers.inherits;
-
-module.exports = Element;
+export default Element;
diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js
deleted file mode 100644 (file)
index 9f86f21..0000000
+++ /dev/null
@@ -1,571 +0,0 @@
-'use strict';
-
-var color = require('chartjs-color');
-var defaults = require('./core.defaults');
-var helpers = require('../helpers/index');
-
-module.exports = function() {
-
-       // -- Basic js utility methods
-
-       helpers.where = function(collection, filterCallback) {
-               if (helpers.isArray(collection) && Array.prototype.filter) {
-                       return collection.filter(filterCallback);
-               }
-               var filtered = [];
-
-               helpers.each(collection, function(item) {
-                       if (filterCallback(item)) {
-                               filtered.push(item);
-                       }
-               });
-
-               return filtered;
-       };
-       helpers.findIndex = Array.prototype.findIndex ?
-               function(array, callback, scope) {
-                       return array.findIndex(callback, scope);
-               } :
-               function(array, callback, scope) {
-                       scope = scope === undefined ? array : scope;
-                       for (var i = 0, ilen = array.length; i < ilen; ++i) {
-                               if (callback.call(scope, array[i], i, array)) {
-                                       return i;
-                               }
-                       }
-                       return -1;
-               };
-       helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) {
-               // Default to start of the array
-               if (helpers.isNullOrUndef(startIndex)) {
-                       startIndex = -1;
-               }
-               for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
-                       var currentItem = arrayToSearch[i];
-                       if (filterCallback(currentItem)) {
-                               return currentItem;
-                       }
-               }
-       };
-       helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) {
-               // Default to end of the array
-               if (helpers.isNullOrUndef(startIndex)) {
-                       startIndex = arrayToSearch.length;
-               }
-               for (var i = startIndex - 1; i >= 0; i--) {
-                       var currentItem = arrayToSearch[i];
-                       if (filterCallback(currentItem)) {
-                               return currentItem;
-                       }
-               }
-       };
-
-       // -- Math methods
-       helpers.isNumber = function(n) {
-               return !isNaN(parseFloat(n)) && isFinite(n);
-       };
-       helpers.almostEquals = function(x, y, epsilon) {
-               return Math.abs(x - y) < epsilon;
-       };
-       helpers.almostWhole = function(x, epsilon) {
-               var rounded = Math.round(x);
-               return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x);
-       };
-       helpers._setMinAndMax = function(array, target) {
-               var i, ilen, value;
-
-               for (i = 0, ilen = array.length; i < ilen; i++) {
-                       value = array[i];
-                       if (!isNaN(value)) {
-                               target.min = Math.min(target.min, value);
-                               target.max = Math.max(target.max, value);
-                       }
-               }
-       };
-       helpers._setMinAndMaxByKey = function(array, target, property) {
-               var i, ilen, value;
-
-               for (i = 0, ilen = array.length; i < ilen; i++) {
-                       value = array[i][property];
-                       if (!isNaN(value)) {
-                               target.min = Math.min(target.min, value);
-                               target.max = Math.max(target.max, value);
-                       }
-               }
-       };
-       helpers.sign = Math.sign ?
-               function(x) {
-                       return Math.sign(x);
-               } :
-               function(x) {
-                       x = +x; // convert to a number
-                       if (x === 0 || isNaN(x)) {
-                               return x;
-                       }
-                       return x > 0 ? 1 : -1;
-               };
-       helpers.toRadians = function(degrees) {
-               return degrees * (Math.PI / 180);
-       };
-       helpers.toDegrees = function(radians) {
-               return radians * (180 / Math.PI);
-       };
-
-       /**
-        * Returns the number of decimal places
-        * i.e. the number of digits after the decimal point, of the value of this Number.
-        * @param {number} x - A number.
-        * @returns {number} The number of decimal places.
-        * @private
-        */
-       helpers._decimalPlaces = function(x) {
-               if (!helpers.isFinite(x)) {
-                       return;
-               }
-               var e = 1;
-               var p = 0;
-               while (Math.round(x * e) / e !== x) {
-                       e *= 10;
-                       p++;
-               }
-               return p;
-       };
-
-       // Gets the angle from vertical upright to the point about a centre.
-       helpers.getAngleFromPoint = function(centrePoint, anglePoint) {
-               var distanceFromXCenter = anglePoint.x - centrePoint.x;
-               var distanceFromYCenter = anglePoint.y - centrePoint.y;
-               var radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
-
-               var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter);
-
-               if (angle < (-0.5 * Math.PI)) {
-                       angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2]
-               }
-
-               return {
-                       angle: angle,
-                       distance: radialDistanceFromCenter
-               };
-       };
-       helpers.distanceBetweenPoints = function(pt1, pt2) {
-               return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
-       };
-
-       helpers.splineCurve = function(firstPoint, middlePoint, afterPoint, t) {
-               // Props to Rob Spencer at scaled innovation for his post on splining between points
-               // http://scaledinnovation.com/analytics/splines/aboutSplines.html
-
-               // This function must also respect "skipped" points
-
-               var previous = firstPoint.skip ? middlePoint : firstPoint;
-               var current = middlePoint;
-               var next = afterPoint.skip ? middlePoint : afterPoint;
-
-               var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2));
-               var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2));
-
-               var s01 = d01 / (d01 + d12);
-               var s12 = d12 / (d01 + d12);
-
-               // If all points are the same, s01 & s02 will be inf
-               s01 = isNaN(s01) ? 0 : s01;
-               s12 = isNaN(s12) ? 0 : s12;
-
-               var fa = t * s01; // scaling factor for triangle Ta
-               var fb = t * s12;
-
-               return {
-                       previous: {
-                               x: current.x - fa * (next.x - previous.x),
-                               y: current.y - fa * (next.y - previous.y)
-                       },
-                       next: {
-                               x: current.x + fb * (next.x - previous.x),
-                               y: current.y + fb * (next.y - previous.y)
-                       }
-               };
-       };
-       helpers.EPSILON = Number.EPSILON || 1e-14;
-       helpers.splineCurveMonotone = function(points) {
-               // This function calculates Bézier control points in a similar way than |splineCurve|,
-               // but preserves monotonicity of the provided data and ensures no local extremums are added
-               // between the dataset discrete points due to the interpolation.
-               // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
-
-               var pointsWithTangents = (points || []).map(function(point) {
-                       return {
-                               model: point._model,
-                               deltaK: 0,
-                               mK: 0
-                       };
-               });
-
-               // Calculate slopes (deltaK) and initialize tangents (mK)
-               var pointsLen = pointsWithTangents.length;
-               var i, pointBefore, pointCurrent, pointAfter;
-               for (i = 0; i < pointsLen; ++i) {
-                       pointCurrent = pointsWithTangents[i];
-                       if (pointCurrent.model.skip) {
-                               continue;
-                       }
-
-                       pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
-                       pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
-                       if (pointAfter && !pointAfter.model.skip) {
-                               var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x);
-
-                               // In the case of two points that appear at the same x pixel, slopeDeltaX is 0
-                               pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0;
-                       }
-
-                       if (!pointBefore || pointBefore.model.skip) {
-                               pointCurrent.mK = pointCurrent.deltaK;
-                       } else if (!pointAfter || pointAfter.model.skip) {
-                               pointCurrent.mK = pointBefore.deltaK;
-                       } else if (this.sign(pointBefore.deltaK) !== this.sign(pointCurrent.deltaK)) {
-                               pointCurrent.mK = 0;
-                       } else {
-                               pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2;
-                       }
-               }
-
-               // Adjust tangents to ensure monotonic properties
-               var alphaK, betaK, tauK, squaredMagnitude;
-               for (i = 0; i < pointsLen - 1; ++i) {
-                       pointCurrent = pointsWithTangents[i];
-                       pointAfter = pointsWithTangents[i + 1];
-                       if (pointCurrent.model.skip || pointAfter.model.skip) {
-                               continue;
-                       }
-
-                       if (helpers.almostEquals(pointCurrent.deltaK, 0, this.EPSILON)) {
-                               pointCurrent.mK = pointAfter.mK = 0;
-                               continue;
-                       }
-
-                       alphaK = pointCurrent.mK / pointCurrent.deltaK;
-                       betaK = pointAfter.mK / pointCurrent.deltaK;
-                       squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
-                       if (squaredMagnitude <= 9) {
-                               continue;
-                       }
-
-                       tauK = 3 / Math.sqrt(squaredMagnitude);
-                       pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK;
-                       pointAfter.mK = betaK * tauK * pointCurrent.deltaK;
-               }
-
-               // Compute control points
-               var deltaX;
-               for (i = 0; i < pointsLen; ++i) {
-                       pointCurrent = pointsWithTangents[i];
-                       if (pointCurrent.model.skip) {
-                               continue;
-                       }
-
-                       pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
-                       pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
-                       if (pointBefore && !pointBefore.model.skip) {
-                               deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3;
-                               pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX;
-                               pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK;
-                       }
-                       if (pointAfter && !pointAfter.model.skip) {
-                               deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3;
-                               pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX;
-                               pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK;
-                       }
-               }
-       };
-       // Implementation of the nice number algorithm used in determining where axis labels will go
-       helpers.niceNum = function(range, round) {
-               var exponent = Math.floor(helpers.math.log10(range));
-               var fraction = range / Math.pow(10, exponent);
-               var niceFraction;
-
-               if (round) {
-                       if (fraction < 1.5) {
-                               niceFraction = 1;
-                       } else if (fraction < 3) {
-                               niceFraction = 2;
-                       } else if (fraction < 7) {
-                               niceFraction = 5;
-                       } else {
-                               niceFraction = 10;
-                       }
-               } else if (fraction <= 1.0) {
-                       niceFraction = 1;
-               } else if (fraction <= 2) {
-                       niceFraction = 2;
-               } else if (fraction <= 5) {
-                       niceFraction = 5;
-               } else {
-                       niceFraction = 10;
-               }
-
-               return niceFraction * Math.pow(10, exponent);
-       };
-       // Request animation polyfill - https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
-       helpers.requestAnimFrame = (function() {
-               if (typeof window === 'undefined') {
-                       return function(callback) {
-                               callback();
-                       };
-               }
-               return window.requestAnimationFrame ||
-                       window.webkitRequestAnimationFrame ||
-                       window.mozRequestAnimationFrame ||
-                       window.oRequestAnimationFrame ||
-                       window.msRequestAnimationFrame ||
-                       function(callback) {
-                               return window.setTimeout(callback, 1000 / 60);
-                       };
-       }());
-       // -- DOM methods
-       helpers.getRelativePosition = function(evt, chart) {
-               var mouseX, mouseY;
-               var e = evt.originalEvent || evt;
-               var canvas = evt.target || evt.srcElement;
-               var boundingRect = canvas.getBoundingClientRect();
-
-               var touches = e.touches;
-               if (touches && touches.length > 0) {
-                       mouseX = touches[0].clientX;
-                       mouseY = touches[0].clientY;
-
-               } else {
-                       mouseX = e.clientX;
-                       mouseY = e.clientY;
-               }
-
-               // Scale mouse coordinates into canvas coordinates
-               // by following the pattern laid out by 'jerryj' in the comments of
-               // https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
-               var paddingLeft = parseFloat(helpers.getStyle(canvas, 'padding-left'));
-               var paddingTop = parseFloat(helpers.getStyle(canvas, 'padding-top'));
-               var paddingRight = parseFloat(helpers.getStyle(canvas, 'padding-right'));
-               var paddingBottom = parseFloat(helpers.getStyle(canvas, 'padding-bottom'));
-               var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight;
-               var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom;
-
-               // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However
-               // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here
-               mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvas.width / chart.currentDevicePixelRatio);
-               mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvas.height / chart.currentDevicePixelRatio);
-
-               return {
-                       x: mouseX,
-                       y: mouseY
-               };
-
-       };
-
-       // Private helper function to convert max-width/max-height values that may be percentages into a number
-       function parseMaxStyle(styleValue, node, parentProperty) {
-               var valueInPixels;
-               if (typeof styleValue === 'string') {
-                       valueInPixels = parseInt(styleValue, 10);
-
-                       if (styleValue.indexOf('%') !== -1) {
-                               // percentage * size in dimension
-                               valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty];
-                       }
-               } else {
-                       valueInPixels = styleValue;
-               }
-
-               return valueInPixels;
-       }
-
-       /**
-        * Returns if the given value contains an effective constraint.
-        * @private
-        */
-       function isConstrainedValue(value) {
-               return value !== undefined && value !== null && value !== 'none';
-       }
-
-       /**
-        * Returns the max width or height of the given DOM node in a cross-browser compatible fashion
-        * @param {HTMLElement} domNode - the node to check the constraint on
-        * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height')
-        * @param {string} percentageProperty - property of parent to use when calculating width as a percentage
-        * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser}
-        */
-       function getConstraintDimension(domNode, maxStyle, percentageProperty) {
-               var view = document.defaultView;
-               var parentNode = helpers._getParentNode(domNode);
-               var constrainedNode = view.getComputedStyle(domNode)[maxStyle];
-               var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle];
-               var hasCNode = isConstrainedValue(constrainedNode);
-               var hasCContainer = isConstrainedValue(constrainedContainer);
-               var infinity = Number.POSITIVE_INFINITY;
-
-               if (hasCNode || hasCContainer) {
-                       return Math.min(
-                               hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity,
-                               hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity);
-               }
-
-               return 'none';
-       }
-       // returns Number or undefined if no constraint
-       helpers.getConstraintWidth = function(domNode) {
-               return getConstraintDimension(domNode, 'max-width', 'clientWidth');
-       };
-       // returns Number or undefined if no constraint
-       helpers.getConstraintHeight = function(domNode) {
-               return getConstraintDimension(domNode, 'max-height', 'clientHeight');
-       };
-       /**
-        * @private
-        */
-       helpers._calculatePadding = function(container, padding, parentDimension) {
-               padding = helpers.getStyle(container, padding);
-
-               return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10);
-       };
-       /**
-        * @private
-        */
-       helpers._getParentNode = function(domNode) {
-               var parent = domNode.parentNode;
-               if (parent && parent.toString() === '[object ShadowRoot]') {
-                       parent = parent.host;
-               }
-               return parent;
-       };
-       helpers.getMaximumWidth = function(domNode) {
-               var container = helpers._getParentNode(domNode);
-               if (!container) {
-                       return domNode.clientWidth;
-               }
-
-               var clientWidth = container.clientWidth;
-               var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth);
-               var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth);
-
-               var w = clientWidth - paddingLeft - paddingRight;
-               var cw = helpers.getConstraintWidth(domNode);
-               return isNaN(cw) ? w : Math.min(w, cw);
-       };
-       helpers.getMaximumHeight = function(domNode) {
-               var container = helpers._getParentNode(domNode);
-               if (!container) {
-                       return domNode.clientHeight;
-               }
-
-               var clientHeight = container.clientHeight;
-               var paddingTop = helpers._calculatePadding(container, 'padding-top', clientHeight);
-               var paddingBottom = helpers._calculatePadding(container, 'padding-bottom', clientHeight);
-
-               var h = clientHeight - paddingTop - paddingBottom;
-               var ch = helpers.getConstraintHeight(domNode);
-               return isNaN(ch) ? h : Math.min(h, ch);
-       };
-       helpers.getStyle = function(el, property) {
-               return el.currentStyle ?
-                       el.currentStyle[property] :
-                       document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
-       };
-       helpers.retinaScale = function(chart, forceRatio) {
-               var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
-               if (pixelRatio === 1) {
-                       return;
-               }
-
-               var canvas = chart.canvas;
-               var height = chart.height;
-               var width = chart.width;
-
-               canvas.height = height * pixelRatio;
-               canvas.width = width * pixelRatio;
-               chart.ctx.scale(pixelRatio, pixelRatio);
-
-               // If no style has been set on the canvas, the render size is used as display size,
-               // making the chart visually bigger, so let's enforce it to the "correct" values.
-               // See https://github.com/chartjs/Chart.js/issues/3575
-               if (!canvas.style.height && !canvas.style.width) {
-                       canvas.style.height = height + 'px';
-                       canvas.style.width = width + 'px';
-               }
-       };
-       // -- Canvas methods
-       helpers.fontString = function(pixelSize, fontStyle, fontFamily) {
-               return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;
-       };
-       helpers.longestText = function(ctx, font, arrayOfThings, cache) {
-               cache = cache || {};
-               var data = cache.data = cache.data || {};
-               var gc = cache.garbageCollect = cache.garbageCollect || [];
-
-               if (cache.font !== font) {
-                       data = cache.data = {};
-                       gc = cache.garbageCollect = [];
-                       cache.font = font;
-               }
-
-               ctx.font = font;
-               var longest = 0;
-               var ilen = arrayOfThings.length;
-               var i, j, jlen, thing, nestedThing;
-               for (i = 0; i < ilen; i++) {
-                       thing = arrayOfThings[i];
-
-                       // Undefined strings and arrays should not be measured
-                       if (thing !== undefined && thing !== null && helpers.isArray(thing) !== true) {
-                               longest = helpers.measureText(ctx, data, gc, longest, thing);
-                       } else if (helpers.isArray(thing)) {
-                               // if it is an array lets measure each element
-                               // to do maybe simplify this function a bit so we can do this more recursively?
-                               for (j = 0, jlen = thing.length; j < jlen; j++) {
-                                       nestedThing = thing[j];
-                                       // Undefined strings and arrays should not be measured
-                                       if (nestedThing !== undefined && nestedThing !== null && !helpers.isArray(nestedThing)) {
-                                               longest = helpers.measureText(ctx, data, gc, longest, nestedThing);
-                                       }
-                               }
-                       }
-               }
-
-               var gcLen = gc.length / 2;
-               if (gcLen > arrayOfThings.length) {
-                       for (i = 0; i < gcLen; i++) {
-                               delete data[gc[i]];
-                       }
-                       gc.splice(0, gcLen);
-               }
-               return longest;
-       };
-       helpers.measureText = function(ctx, data, gc, longest, string) {
-               var textWidth = data[string];
-               if (!textWidth) {
-                       textWidth = data[string] = ctx.measureText(string).width;
-                       gc.push(string);
-               }
-               if (textWidth > longest) {
-                       longest = textWidth;
-               }
-               return longest;
-       };
-
-       helpers.color = !color ?
-               function(value) {
-                       console.error('Color.js not found!');
-                       return value;
-               } :
-               function(value) {
-                       if (value instanceof CanvasGradient) {
-                               value = defaults.global.defaultColor;
-                       }
-
-                       return color(value);
-               };
-
-       helpers.getHoverColor = function(colorValue) {
-               return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ?
-                       colorValue :
-                       helpers.color(colorValue).saturate(0.5).darken(0.1).rgbString();
-       };
-};
index d829b006b07c44e049edb571ab5d699a3365868d..f1975cd1e380bd34725cc77ffa31df6dbdf5b211 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
-var helpers = require('../helpers/index');
+import helpers from '../helpers/index';
+import {isNumber} from '../helpers/helpers.math';
 
 /**
  * Helper function to get relative position for an event
@@ -16,7 +17,7 @@ function getRelativePosition(e, chart) {
                };
        }
 
-       return helpers.getRelativePosition(e, chart);
+       return helpers.dom.getRelativePosition(e, chart);
 }
 
 /**
@@ -55,7 +56,7 @@ function evaluateItemsAtIndex(chart, axis, position, handler) {
                        return false;
                }
                const index = iScale.getIndexForPixel(position[axis]);
-               if (!helpers.isNumber(index)) {
+               if (!isNumber(index)) {
                        return false;
                }
                indices.push(index);
@@ -163,7 +164,7 @@ function getNearestItems(chart, position, axis, intersect) {
  * Contains interaction related functions
  * @namespace Chart.Interaction
  */
-module.exports = {
+export default {
        // Helper function for different modes
        modes: {
                /**
index 6adce4a89ef99f7534751d8ab66beb1224a50279..49432e2284a25d9dad104b1d56910c4a8fe35e7b 100644 (file)
@@ -636,7 +636,7 @@ class Scale extends Element {
                        maxHeight = me.maxHeight - getTickMarkLength(options.gridLines)
                                - tickOpts.padding - getScaleLabelHeight(options.scaleLabel);
                        maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight);
-                       labelRotation = helpers.toDegrees(Math.min(
+                       labelRotation = helpers.math.toDegrees(Math.min(
                                Math.asin(Math.min((labelSizes.highest.height + 6) / tickWidth, 1)),
                                Math.asin(Math.min(maxHeight / maxLabelDiagonal, 1)) - Math.asin(maxLabelHeight / maxLabelDiagonal)
                        ));
@@ -699,7 +699,7 @@ class Scale extends Element {
                        if (isHorizontal) {
                                // A horizontal axis is more constrained by the height.
                                var isRotated = me.labelRotation !== 0;
-                               var angleRadians = helpers.toRadians(me.labelRotation);
+                               var angleRadians = helpers.math.toRadians(me.labelRotation);
                                var cosRotation = Math.cos(angleRadians);
                                var sinRotation = Math.sin(angleRadians);
 
@@ -931,7 +931,7 @@ class Scale extends Element {
                var optionTicks = me.options.ticks;
 
                // Calculate space needed by label in axis direction.
-               var rot = helpers.toRadians(me.labelRotation);
+               var rot = helpers.math.toRadians(me.labelRotation);
                var cos = Math.abs(Math.cos(rot));
                var sin = Math.abs(Math.sin(rot));
 
@@ -1103,7 +1103,7 @@ class Scale extends Element {
                const fonts = parseTickFontOptions(optionTicks);
                const tickPadding = optionTicks.padding;
                const tl = getTickMarkLength(options.gridLines);
-               const rotation = -helpers.toRadians(me.labelRotation);
+               const rotation = -helpers.math.toRadians(me.labelRotation);
                const items = [];
                let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;
 
index 2a53cce6d023b08b226fc75f4f783aab04d765cd..000c98bc311c51a152ff3d1c7b6d55a9c4f8ce90 100644 (file)
@@ -2,7 +2,7 @@
 
 import defaults from '../core/core.defaults';
 import Element from '../core/core.element';
-import helpers from '../helpers';
+import {getAngleFromPoint} from '../helpers/helpers.math';
 const TAU = Math.PI * 2;
 
 defaults._set('global', {
@@ -101,7 +101,7 @@ class Arc extends Element {
                var vm = this._view;
 
                if (vm) {
-                       var pointRelativePosition = helpers.getAngleFromPoint(vm, {x: chartX, y: chartY});
+                       var pointRelativePosition = getAngleFromPoint(vm, {x: chartX, y: chartY});
                        var angle = pointRelativePosition.angle;
                        var distance = pointRelativePosition.distance;
 
diff --git a/src/helpers/helpers.curve.js b/src/helpers/helpers.curve.js
new file mode 100644 (file)
index 0000000..292e277
--- /dev/null
@@ -0,0 +1,130 @@
+import {almostEquals, sign} from './helpers.math';
+
+const EPSILON = Number.EPSILON || 1e-14;
+
+export function splineCurve(firstPoint, middlePoint, afterPoint, t) {
+       // Props to Rob Spencer at scaled innovation for his post on splining between points
+       // http://scaledinnovation.com/analytics/splines/aboutSplines.html
+
+       // This function must also respect "skipped" points
+
+       var previous = firstPoint.skip ? middlePoint : firstPoint;
+       var current = middlePoint;
+       var next = afterPoint.skip ? middlePoint : afterPoint;
+
+       var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2));
+       var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2));
+
+       var s01 = d01 / (d01 + d12);
+       var s12 = d12 / (d01 + d12);
+
+       // If all points are the same, s01 & s02 will be inf
+       s01 = isNaN(s01) ? 0 : s01;
+       s12 = isNaN(s12) ? 0 : s12;
+
+       var fa = t * s01; // scaling factor for triangle Ta
+       var fb = t * s12;
+
+       return {
+               previous: {
+                       x: current.x - fa * (next.x - previous.x),
+                       y: current.y - fa * (next.y - previous.y)
+               },
+               next: {
+                       x: current.x + fb * (next.x - previous.x),
+                       y: current.y + fb * (next.y - previous.y)
+               }
+       };
+}
+
+export function splineCurveMonotone(points) {
+       // This function calculates Bézier control points in a similar way than |splineCurve|,
+       // but preserves monotonicity of the provided data and ensures no local extremums are added
+       // between the dataset discrete points due to the interpolation.
+       // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
+
+       var pointsWithTangents = (points || []).map(function(point) {
+               return {
+                       model: point._model,
+                       deltaK: 0,
+                       mK: 0
+               };
+       });
+
+       // Calculate slopes (deltaK) and initialize tangents (mK)
+       var pointsLen = pointsWithTangents.length;
+       var i, pointBefore, pointCurrent, pointAfter;
+       for (i = 0; i < pointsLen; ++i) {
+               pointCurrent = pointsWithTangents[i];
+               if (pointCurrent.model.skip) {
+                       continue;
+               }
+
+               pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
+               pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
+               if (pointAfter && !pointAfter.model.skip) {
+                       var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x);
+
+                       // In the case of two points that appear at the same x pixel, slopeDeltaX is 0
+                       pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0;
+               }
+
+               if (!pointBefore || pointBefore.model.skip) {
+                       pointCurrent.mK = pointCurrent.deltaK;
+               } else if (!pointAfter || pointAfter.model.skip) {
+                       pointCurrent.mK = pointBefore.deltaK;
+               } else if (sign(pointBefore.deltaK) !== sign(pointCurrent.deltaK)) {
+                       pointCurrent.mK = 0;
+               } else {
+                       pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2;
+               }
+       }
+
+       // Adjust tangents to ensure monotonic properties
+       var alphaK, betaK, tauK, squaredMagnitude;
+       for (i = 0; i < pointsLen - 1; ++i) {
+               pointCurrent = pointsWithTangents[i];
+               pointAfter = pointsWithTangents[i + 1];
+               if (pointCurrent.model.skip || pointAfter.model.skip) {
+                       continue;
+               }
+
+               if (almostEquals(pointCurrent.deltaK, 0, EPSILON)) {
+                       pointCurrent.mK = pointAfter.mK = 0;
+                       continue;
+               }
+
+               alphaK = pointCurrent.mK / pointCurrent.deltaK;
+               betaK = pointAfter.mK / pointCurrent.deltaK;
+               squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
+               if (squaredMagnitude <= 9) {
+                       continue;
+               }
+
+               tauK = 3 / Math.sqrt(squaredMagnitude);
+               pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK;
+               pointAfter.mK = betaK * tauK * pointCurrent.deltaK;
+       }
+
+       // Compute control points
+       var deltaX;
+       for (i = 0; i < pointsLen; ++i) {
+               pointCurrent = pointsWithTangents[i];
+               if (pointCurrent.model.skip) {
+                       continue;
+               }
+
+               pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
+               pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
+               if (pointBefore && !pointBefore.model.skip) {
+                       deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3;
+                       pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX;
+                       pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK;
+               }
+               if (pointAfter && !pointAfter.model.skip) {
+                       deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3;
+                       pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX;
+                       pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK;
+               }
+       }
+}
diff --git a/src/helpers/helpers.dom.js b/src/helpers/helpers.dom.js
new file mode 100644 (file)
index 0000000..939e4bb
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * Returns if the given value contains an effective constraint.
+ * @private
+ */
+function isConstrainedValue(value) {
+       return value !== undefined && value !== null && value !== 'none';
+}
+
+/**
+ * @private
+ */
+function _getParentNode(domNode) {
+       var parent = domNode.parentNode;
+       if (parent && parent.toString() === '[object ShadowRoot]') {
+               parent = parent.host;
+       }
+       return parent;
+}
+
+// Private helper function to convert max-width/max-height values that may be percentages into a number
+function parseMaxStyle(styleValue, node, parentProperty) {
+       var valueInPixels;
+       if (typeof styleValue === 'string') {
+               valueInPixels = parseInt(styleValue, 10);
+
+               if (styleValue.indexOf('%') !== -1) {
+                       // percentage * size in dimension
+                       valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty];
+               }
+       } else {
+               valueInPixels = styleValue;
+       }
+
+       return valueInPixels;
+}
+
+/**
+ * Returns the max width or height of the given DOM node in a cross-browser compatible fashion
+ * @param {HTMLElement} domNode - the node to check the constraint on
+ * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height')
+ * @param {string} percentageProperty - property of parent to use when calculating width as a percentage
+ * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser}
+ */
+function getConstraintDimension(domNode, maxStyle, percentageProperty) {
+       var view = document.defaultView;
+       var parentNode = _getParentNode(domNode);
+       var constrainedNode = view.getComputedStyle(domNode)[maxStyle];
+       var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle];
+       var hasCNode = isConstrainedValue(constrainedNode);
+       var hasCContainer = isConstrainedValue(constrainedContainer);
+       var infinity = Number.POSITIVE_INFINITY;
+
+       if (hasCNode || hasCContainer) {
+               return Math.min(
+                       hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity,
+                       hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity);
+       }
+
+       return 'none';
+}
+
+export function getStyle(el, property) {
+       return el.currentStyle ?
+               el.currentStyle[property] :
+               document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
+}
+
+// returns Number or undefined if no constraint
+function getConstraintWidth(domNode) {
+       return getConstraintDimension(domNode, 'max-width', 'clientWidth');
+}
+
+// returns Number or undefined if no constraint
+function getConstraintHeight(domNode) {
+       return getConstraintDimension(domNode, 'max-height', 'clientHeight');
+}
+
+/**
+ * @private
+ */
+function _calculatePadding(container, padding, parentDimension) {
+       padding = getStyle(container, padding);
+
+       return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10);
+}
+
+export function getRelativePosition(evt, chart) {
+       var mouseX, mouseY;
+       var e = evt.originalEvent || evt;
+       var canvasElement = evt.target || evt.srcElement;
+       var boundingRect = canvasElement.getBoundingClientRect();
+
+       var touches = e.touches;
+       if (touches && touches.length > 0) {
+               mouseX = touches[0].clientX;
+               mouseY = touches[0].clientY;
+
+       } else {
+               mouseX = e.clientX;
+               mouseY = e.clientY;
+       }
+
+       // Scale mouse coordinates into canvas coordinates
+       // by following the pattern laid out by 'jerryj' in the comments of
+       // https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
+       var paddingLeft = parseFloat(getStyle(canvasElement, 'padding-left'));
+       var paddingTop = parseFloat(getStyle(canvasElement, 'padding-top'));
+       var paddingRight = parseFloat(getStyle(canvasElement, 'padding-right'));
+       var paddingBottom = parseFloat(getStyle(canvasElement, 'padding-bottom'));
+       var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight;
+       var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom;
+
+       // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However
+       // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here
+       mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvasElement.width / chart.currentDevicePixelRatio);
+       mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvasElement.height / chart.currentDevicePixelRatio);
+
+       return {
+               x: mouseX,
+               y: mouseY
+       };
+}
+
+export function getMaximumWidth(domNode) {
+       var container = _getParentNode(domNode);
+       if (!container) {
+               return domNode.clientWidth;
+       }
+
+       var clientWidth = container.clientWidth;
+       var paddingLeft = _calculatePadding(container, 'padding-left', clientWidth);
+       var paddingRight = _calculatePadding(container, 'padding-right', clientWidth);
+
+       var w = clientWidth - paddingLeft - paddingRight;
+       var cw = getConstraintWidth(domNode);
+       return isNaN(cw) ? w : Math.min(w, cw);
+}
+
+export function getMaximumHeight(domNode) {
+       var container = _getParentNode(domNode);
+       if (!container) {
+               return domNode.clientHeight;
+       }
+
+       var clientHeight = container.clientHeight;
+       var paddingTop = _calculatePadding(container, 'padding-top', clientHeight);
+       var paddingBottom = _calculatePadding(container, 'padding-bottom', clientHeight);
+
+       var h = clientHeight - paddingTop - paddingBottom;
+       var ch = getConstraintHeight(domNode);
+       return isNaN(ch) ? h : Math.min(h, ch);
+}
+
+export function retinaScale(chart, forceRatio) {
+       var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
+       if (pixelRatio === 1) {
+               return;
+       }
+
+       var canvasElement = chart.canvas;
+       var height = chart.height;
+       var width = chart.width;
+
+       canvasElement.height = height * pixelRatio;
+       canvasElement.width = width * pixelRatio;
+       chart.ctx.scale(pixelRatio, pixelRatio);
+
+       // If no style has been set on the canvas, the render size is used as display size,
+       // making the chart visually bigger, so let's enforce it to the "correct" values.
+       // See https://github.com/chartjs/Chart.js/issues/3575
+       if (!canvasElement.style.height && !canvasElement.style.width) {
+               canvasElement.style.height = height + 'px';
+               canvasElement.style.width = width + 'px';
+       }
+}
index f08ff2c1e12f0e4d388caad828375daf3b998433..3b830ef6b8f672552ae159173bb65f59f1258b8e 100644 (file)
@@ -1,5 +1,7 @@
 'use strict';
 
+import {isFinite as isFiniteNumber} from './helpers.core';
+
 /**
  * @alias Chart.helpers.math
  * @namespace
@@ -38,3 +40,103 @@ export const log10 = Math.log10 || function(x) {
 
        return isPowerOf10 ? powerOf10 : exponent;
 };
+
+
+export function isNumber(n) {
+       return !isNaN(parseFloat(n)) && isFinite(n);
+}
+
+export function almostEquals(x, y, epsilon) {
+       return Math.abs(x - y) < epsilon;
+}
+
+export function almostWhole(x, epsilon) {
+       var rounded = Math.round(x);
+       return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x);
+}
+
+export function _setMinAndMax(array, target) {
+       var i, ilen, value;
+
+       for (i = 0, ilen = array.length; i < ilen; i++) {
+               value = array[i];
+               if (!isNaN(value)) {
+                       target.min = Math.min(target.min, value);
+                       target.max = Math.max(target.max, value);
+               }
+       }
+}
+
+export function _setMinAndMaxByKey(array, target, property) {
+       var i, ilen, value;
+
+       for (i = 0, ilen = array.length; i < ilen; i++) {
+               value = array[i][property];
+               if (!isNaN(value)) {
+                       target.min = Math.min(target.min, value);
+                       target.max = Math.max(target.max, value);
+               }
+       }
+}
+
+export const sign = Math.sign ?
+       function(x) {
+               return Math.sign(x);
+       } :
+       function(x) {
+               x = +x; // convert to a number
+               if (x === 0 || isNaN(x)) {
+                       return x;
+               }
+               return x > 0 ? 1 : -1;
+       };
+
+export function toRadians(degrees) {
+       return degrees * (Math.PI / 180);
+}
+
+export function toDegrees(radians) {
+       return radians * (180 / Math.PI);
+}
+
+/**
+ * Returns the number of decimal places
+ * i.e. the number of digits after the decimal point, of the value of this Number.
+ * @param {number} x - A number.
+ * @returns {number} The number of decimal places.
+ * @private
+ */
+export function _decimalPlaces(x) {
+       if (!isFiniteNumber(x)) {
+               return;
+       }
+       var e = 1;
+       var p = 0;
+       while (Math.round(x * e) / e !== x) {
+               e *= 10;
+               p++;
+       }
+       return p;
+}
+
+// Gets the angle from vertical upright to the point about a centre.
+export function getAngleFromPoint(centrePoint, anglePoint) {
+       var distanceFromXCenter = anglePoint.x - centrePoint.x;
+       var distanceFromYCenter = anglePoint.y - centrePoint.y;
+       var radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
+
+       var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter);
+
+       if (angle < (-0.5 * Math.PI)) {
+               angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2]
+       }
+
+       return {
+               angle: angle,
+               distance: radialDistanceFromCenter
+       };
+}
+
+export function distanceBetweenPoints(pt1, pt2) {
+       return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
+}
index fa7efa3b0ae42c5de91642f656424adf70532796..6bd61b40be5d7f78c2b32be76f82a5428a3c4332 100644 (file)
 'use strict';
 
+import color from 'chartjs-color';
+
 import * as coreHelpers from './helpers.core';
 import * as canvas from './helpers.canvas';
+import * as curve from './helpers.curve';
+import * as dom from './helpers.dom';
 import * as easing from './helpers.easing';
 import * as options from './helpers.options';
 import * as math from './helpers.math';
 import * as rtl from './helpers.rtl';
 
+const colorHelper = !color ?
+       function(value) {
+               console.error('Color.js not found!');
+               return value;
+       } :
+       function(value) {
+               if (value instanceof CanvasGradient || value instanceof CanvasPattern) {
+                       // TODO: figure out what this should be. Previously returned
+                       // the default color
+                       return value;
+               }
+
+               return color(value);
+       };
+
+function measureText(ctx, data, gc, longest, string) {
+       var textWidth = data[string];
+       if (!textWidth) {
+               textWidth = data[string] = ctx.measureText(string).width;
+               gc.push(string);
+       }
+       if (textWidth > longest) {
+               longest = textWidth;
+       }
+       return longest;
+}
+
 export default {
        ...coreHelpers,
        canvas,
+       curve,
+       dom,
        easing,
        options,
        math,
        rtl,
+
+       where: function(collection, filterCallback) {
+               if (coreHelpers.isArray(collection) && Array.prototype.filter) {
+                       return collection.filter(filterCallback);
+               }
+               var filtered = [];
+
+               coreHelpers.each(collection, function(item) {
+                       if (filterCallback(item)) {
+                               filtered.push(item);
+                       }
+               });
+
+               return filtered;
+       },
+       findIndex: Array.prototype.findIndex ?
+               function(array, callback, scope) {
+                       return array.findIndex(callback, scope);
+               } :
+               function(array, callback, scope) {
+                       scope = scope === undefined ? array : scope;
+                       for (var i = 0, ilen = array.length; i < ilen; ++i) {
+                               if (callback.call(scope, array[i], i, array)) {
+                                       return i;
+                               }
+                       }
+                       return -1;
+               },
+       findNextWhere: function(arrayToSearch, filterCallback, startIndex) {
+               // Default to start of the array
+               if (coreHelpers.isNullOrUndef(startIndex)) {
+                       startIndex = -1;
+               }
+               for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
+                       var currentItem = arrayToSearch[i];
+                       if (filterCallback(currentItem)) {
+                               return currentItem;
+                       }
+               }
+       },
+       findPreviousWhere: function(arrayToSearch, filterCallback, startIndex) {
+               // Default to end of the array
+               if (coreHelpers.isNullOrUndef(startIndex)) {
+                       startIndex = arrayToSearch.length;
+               }
+               for (var i = startIndex - 1; i >= 0; i--) {
+                       var currentItem = arrayToSearch[i];
+                       if (filterCallback(currentItem)) {
+                               return currentItem;
+                       }
+               }
+       },
+       // Implementation of the nice number algorithm used in determining where axis labels will go
+       niceNum: function(range, round) {
+               var exponent = Math.floor(math.log10(range));
+               var fraction = range / Math.pow(10, exponent);
+               var niceFraction;
+
+               if (round) {
+                       if (fraction < 1.5) {
+                               niceFraction = 1;
+                       } else if (fraction < 3) {
+                               niceFraction = 2;
+                       } else if (fraction < 7) {
+                               niceFraction = 5;
+                       } else {
+                               niceFraction = 10;
+                       }
+               } else if (fraction <= 1.0) {
+                       niceFraction = 1;
+               } else if (fraction <= 2) {
+                       niceFraction = 2;
+               } else if (fraction <= 5) {
+                       niceFraction = 5;
+               } else {
+                       niceFraction = 10;
+               }
+
+               return niceFraction * Math.pow(10, exponent);
+       },
+       // Request animation polyfill - https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
+       requestAnimFrame: (function() {
+               if (typeof window === 'undefined') {
+                       return function(callback) {
+                               callback();
+                       };
+               }
+               return window.requestAnimationFrame ||
+                       window.webkitRequestAnimationFrame ||
+                       window.mozRequestAnimationFrame ||
+                       window.oRequestAnimationFrame ||
+                       window.msRequestAnimationFrame ||
+                       function(callback) {
+                               return window.setTimeout(callback, 1000 / 60);
+                       };
+       }()),
+       // -- Canvas methods
+       fontString: function(pixelSize, fontStyle, fontFamily) {
+               return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;
+       },
+       longestText: function(ctx, font, arrayOfThings, cache) {
+               cache = cache || {};
+               var data = cache.data = cache.data || {};
+               var gc = cache.garbageCollect = cache.garbageCollect || [];
+
+               if (cache.font !== font) {
+                       data = cache.data = {};
+                       gc = cache.garbageCollect = [];
+                       cache.font = font;
+               }
+
+               ctx.font = font;
+               var longest = 0;
+               var ilen = arrayOfThings.length;
+               var i, j, jlen, thing, nestedThing;
+               for (i = 0; i < ilen; i++) {
+                       thing = arrayOfThings[i];
+
+                       // Undefined strings and arrays should not be measured
+                       if (thing !== undefined && thing !== null && coreHelpers.isArray(thing) !== true) {
+                               longest = measureText(ctx, data, gc, longest, thing);
+                       } else if (coreHelpers.isArray(thing)) {
+                               // if it is an array lets measure each element
+                               // to do maybe simplify this function a bit so we can do this more recursively?
+                               for (j = 0, jlen = thing.length; j < jlen; j++) {
+                                       nestedThing = thing[j];
+                                       // Undefined strings and arrays should not be measured
+                                       if (nestedThing !== undefined && nestedThing !== null && !coreHelpers.isArray(nestedThing)) {
+                                               longest = measureText(ctx, data, gc, longest, nestedThing);
+                                       }
+                               }
+                       }
+               }
+
+               var gcLen = gc.length / 2;
+               if (gcLen > arrayOfThings.length) {
+                       for (i = 0; i < gcLen; i++) {
+                               delete data[gc[i]];
+                       }
+                       gc.splice(0, gcLen);
+               }
+               return longest;
+       },
+       measureText,
+       color: colorHelper,
+       getHoverColor: function(colorValue) {
+               return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ?
+                       colorValue :
+                       colorHelper(colorValue).saturate(0.5).darken(0.1).rgbString();
+       }
 };
index 998cf09af852ff33fb4884f33450cc95375f5fb4..c8a88d7d41aa4abe12b46fb975aaca1af05fe502 100644 (file)
@@ -4,10 +4,6 @@
 var Chart = require('./core/core.controller');
 
 Chart.helpers = require('./helpers/index');
-
-// @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests!
-require('./core/core.helpers')(Chart);
-
 Chart._adapters = require('./core/core.adapters');
 Chart.Animation = require('./core/core.animation');
 Chart.animationService = require('./core/core.animations');
index 5271860e59b7bed9114b218a01e926e6b1777cd3..7b0ed16245da4df21a0bdca2604a7c11b803d7f8 100644 (file)
@@ -41,7 +41,7 @@ var EVENT_TYPES = {
  * @returns {number} Size in pixels or undefined if unknown.
  */
 function readUsedSize(element, property) {
-       var value = helpers.getStyle(element, property);
+       var value = helpers.dom.getStyle(element, property);
        var matches = value && value.match(/^(\d+)(\.\d+)?px$/);
        return matches ? Number(matches[1]) : undefined;
 }
@@ -146,7 +146,7 @@ function createEvent(type, chart, x, y, nativeEvent) {
 
 function fromNativeEvent(event, chart) {
        var type = EVENT_TYPES[event.type] || event.type;
-       var pos = helpers.getRelativePosition(event, chart);
+       var pos = helpers.dom.getRelativePosition(event, chart);
        return createEvent(type, chart, pos.x, pos.y, event);
 }
 
index 60b6ad638669d60fc1567fdb9c25edb6854e968b..4423e346e50b31e312a7efd980653563a9484065 100644 (file)
@@ -1,6 +1,7 @@
 'use strict';
 
 import helpers from '../helpers/index';
+import {almostEquals, almostWhole, _decimalPlaces, _setMinAndMaxByKey, sign} from '../helpers/helpers.math';
 import Scale from '../core/core.scale';
 
 const isNullOrUndef = helpers.isNullOrUndef;
@@ -43,7 +44,7 @@ function generateTicks(generationOptions, dataRange) {
 
        if (stepSize || isNullOrUndef(precision)) {
                // If a precision is not specified, calculate factor based on spacing
-               factor = Math.pow(10, helpers._decimalPlaces(spacing));
+               factor = Math.pow(10, _decimalPlaces(spacing));
        } else {
                // If the user specified a precision, round to that number of decimal places
                factor = Math.pow(10, precision);
@@ -56,17 +57,17 @@ function generateTicks(generationOptions, dataRange) {
        // If min, max and stepSize is set and they make an evenly spaced scale use it.
        if (stepSize) {
                // If very close to our whole number, use it.
-               if (!isNullOrUndef(min) && helpers.almostWhole(min / spacing, spacing / 1000)) {
+               if (!isNullOrUndef(min) && almostWhole(min / spacing, spacing / 1000)) {
                        niceMin = min;
                }
-               if (!isNullOrUndef(max) && helpers.almostWhole(max / spacing, spacing / 1000)) {
+               if (!isNullOrUndef(max) && almostWhole(max / spacing, spacing / 1000)) {
                        niceMax = max;
                }
        }
 
        numSpaces = (niceMax - niceMin) / spacing;
        // If very close to our rounded value, use it.
-       if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
+       if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
                numSpaces = Math.round(numSpaces);
        } else {
                numSpaces = Math.ceil(numSpaces);
@@ -103,8 +104,8 @@ class LinearScaleBase extends Scale {
                // do nothing since that would make the chart weird. If the user really wants a weird chart
                // axis, they can manually override it
                if (opts.beginAtZero) {
-                       var minSign = helpers.sign(me.min);
-                       var maxSign = helpers.sign(me.max);
+                       var minSign = sign(me.min);
+                       var maxSign = sign(me.max);
 
                        if (minSign < 0 && maxSign < 0) {
                                // move the top up to 0
@@ -215,7 +216,7 @@ class LinearScaleBase extends Scale {
 
                // At this point, we need to update our max and min given the tick values since we have expanded the
                // range of the scale
-               helpers._setMinAndMaxByKey(ticks, me, 'value');
+               _setMinAndMaxByKey(ticks, me, 'value');
 
                if (opts.reverse) {
                        ticks.reverse();
index d48ce5b64ae49e5909b9905f82b82cf79af279ee..f421873966f12c418963b2aaa1f05a929dd45f20 100644 (file)
@@ -2,6 +2,7 @@
 
 import defaults from '../core/core.defaults';
 import helpers from '../helpers/index';
+import {_setMinAndMaxByKey} from '../helpers/helpers.math';
 import Scale from '../core/core.scale';
 import LinearScaleBase from './scale.linearbase';
 import Ticks from '../core/core.ticks';
@@ -132,7 +133,7 @@ class LogarithmicScale extends Scale {
 
                // At this point, we need to update our max and min given the tick values since we have expanded the
                // range of the scale
-               helpers._setMinAndMaxByKey(ticks, me, 'value');
+               _setMinAndMaxByKey(ticks, me, 'value');
 
                if (opts.reverse) {
                        reverse = !reverse;
index 34776ea007ec0d3930852be89ef05d1463318bbb..b2c0b97573979830ff64ed0259a8e7584e894d9c 100644 (file)
@@ -2,6 +2,7 @@
 
 import defaults from '../core/core.defaults';
 import helpers from '../helpers/index';
+import {isNumber, toDegrees} from '../helpers/helpers.math';
 import LinearScaleBase from './scale.linearbase';
 import Ticks from '../core/core.ticks';
 
@@ -156,7 +157,7 @@ function fitWithPointLabels(scale) {
 
                // Add quarter circle to make degree 0 mean top of circle
                var angleRadians = scale.getIndexAngle(i);
-               var angle = helpers.toDegrees(angleRadians) % 360;
+               var angle = toDegrees(angleRadians) % 360;
                var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180);
                var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270);
 
@@ -239,7 +240,7 @@ function drawPointLabels(scale) {
                ctx.fillStyle = pointLabelFontColor;
 
                var angleRadians = scale.getIndexAngle(i);
-               var angle = helpers.toDegrees(angleRadians);
+               var angle = toDegrees(angleRadians);
                ctx.textAlign = getTextAlignForAngle(angle);
                adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition);
                fillText(ctx, scale.pointLabels[i], pointLabelPosition, plFont.lineHeight);
@@ -287,7 +288,7 @@ function drawRadiusLine(scale, gridLineOpts, radius, index) {
 }
 
 function numberOrZero(param) {
-       return helpers.isNumber(param) ? param : 0;
+       return isNumber(param) ? param : 0;
 }
 
 class RadialLinearScale extends LinearScaleBase {
index 9e39e6a082c5f8bc36e379c5b8b8f4d1b6a776e8..f1e553465b00d794083513bdd647139678f5ca48 100644 (file)
@@ -3,6 +3,7 @@
 import adapters from '../core/core.adapters';
 import defaults from '../core/core.defaults';
 import helpers from '../helpers/index';
+import {toRadians} from '../helpers/helpers.math';
 import Scale from '../core/core.scale';
 
 const resolve = helpers.options.resolve;
@@ -731,7 +732,7 @@ class TimeScale extends Scale {
                const me = this;
                const ticksOpts = me.options.ticks;
                const tickLabelWidth = me.ctx.measureText(label).width;
-               const angle = helpers.toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation);
+               const angle = toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation);
                const cosRotation = Math.cos(angle);
                const sinRotation = Math.sin(angle);
                const tickFontSize = valueOrDefault(ticksOpts.fontSize, defaults.global.defaultFontSize);
index 690f6643bdae319771a5940be22cc4f7bbacc68e..eecdc69a9e3b46cc9b9b29bfbb89b9560949c8dd 100644 (file)
@@ -20,26 +20,6 @@ describe('Core helper tests', function() {
                expect(helpers.findPreviousWhere(data, callback, 0)).toBe(undefined);
        });
 
-       it('should get the correct sign', function() {
-               expect(helpers.sign(0)).toBe(0);
-               expect(helpers.sign(10)).toBe(1);
-               expect(helpers.sign(-5)).toBe(-1);
-       });
-
-       it('should correctly determine if two numbers are essentially equal', function() {
-               expect(helpers.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
-               expect(helpers.almostEquals(1, 1.1, 0.0001)).toBe(false);
-               expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false);
-               expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
-       });
-
-       it('should correctly determine if a numbers are essentially whole', function() {
-               expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true);
-               expect(helpers.almostWhole(0.9, 0.0001)).toBe(false);
-               expect(helpers.almostWhole(1234567890123, 0.0001)).toBe(true);
-               expect(helpers.almostWhole(1234567890123.001, 0.0001)).toBe(false);
-       });
-
        it('should generate integer ids', function() {
                var uid = helpers.uid();
                expect(uid).toEqual(jasmine.any(Number));
@@ -48,270 +28,6 @@ describe('Core helper tests', function() {
                expect(helpers.uid()).toBe(uid + 3);
        });
 
-       it('should detect a number', function() {
-               expect(helpers.isNumber(123)).toBe(true);
-               expect(helpers.isNumber('123')).toBe(true);
-               expect(helpers.isNumber(null)).toBe(false);
-               expect(helpers.isNumber(NaN)).toBe(false);
-               expect(helpers.isNumber(undefined)).toBe(false);
-               expect(helpers.isNumber('cbc')).toBe(false);
-       });
-
-       it('should convert between radians and degrees', function() {
-               expect(helpers.toRadians(180)).toBe(Math.PI);
-               expect(helpers.toRadians(90)).toBe(0.5 * Math.PI);
-               expect(helpers.toDegrees(Math.PI)).toBe(180);
-               expect(helpers.toDegrees(Math.PI * 3 / 2)).toBe(270);
-       });
-
-       it('should get the correct number of decimal places', function() {
-               expect(helpers._decimalPlaces(100)).toBe(0);
-               expect(helpers._decimalPlaces(1)).toBe(0);
-               expect(helpers._decimalPlaces(0)).toBe(0);
-               expect(helpers._decimalPlaces(0.01)).toBe(2);
-               expect(helpers._decimalPlaces(-0.01)).toBe(2);
-               expect(helpers._decimalPlaces('1')).toBe(undefined);
-               expect(helpers._decimalPlaces('')).toBe(undefined);
-               expect(helpers._decimalPlaces(undefined)).toBe(undefined);
-               expect(helpers._decimalPlaces(12345678.1234)).toBe(4);
-               expect(helpers._decimalPlaces(1234567890.1234567)).toBe(7);
-       });
-
-       it('should get an angle from a point', function() {
-               var center = {
-                       x: 0,
-                       y: 0
-               };
-
-               expect(helpers.getAngleFromPoint(center, {
-                       x: 0,
-                       y: 10
-               })).toEqual({
-                       angle: Math.PI / 2,
-                       distance: 10,
-               });
-
-               expect(helpers.getAngleFromPoint(center, {
-                       x: Math.sqrt(2),
-                       y: Math.sqrt(2)
-               })).toEqual({
-                       angle: Math.PI / 4,
-                       distance: 2
-               });
-
-               expect(helpers.getAngleFromPoint(center, {
-                       x: -1.0 * Math.sqrt(2),
-                       y: -1.0 * Math.sqrt(2)
-               })).toEqual({
-                       angle: Math.PI * 1.25,
-                       distance: 2
-               });
-       });
-
-       it('should spline curves', function() {
-               expect(helpers.splineCurve({
-                       x: 0,
-                       y: 0
-               }, {
-                       x: 1,
-                       y: 1
-               }, {
-                       x: 2,
-                       y: 0
-               }, 0)).toEqual({
-                       previous: {
-                               x: 1,
-                               y: 1,
-                       },
-                       next: {
-                               x: 1,
-                               y: 1,
-                       }
-               });
-
-               expect(helpers.splineCurve({
-                       x: 0,
-                       y: 0
-               }, {
-                       x: 1,
-                       y: 1
-               }, {
-                       x: 2,
-                       y: 0
-               }, 1)).toEqual({
-                       previous: {
-                               x: 0,
-                               y: 1,
-                       },
-                       next: {
-                               x: 2,
-                               y: 1,
-                       }
-               });
-       });
-
-       it('should spline curves with monotone cubic interpolation', function() {
-               var dataPoints = [
-                       {_model: {x: 0, y: 0, skip: false}},
-                       {_model: {x: 3, y: 6, skip: false}},
-                       {_model: {x: 9, y: 6, skip: false}},
-                       {_model: {x: 12, y: 60, skip: false}},
-                       {_model: {x: 15, y: 60, skip: false}},
-                       {_model: {x: 18, y: 120, skip: false}},
-                       {_model: {x: null, y: null, skip: true}},
-                       {_model: {x: 21, y: 180, skip: false}},
-                       {_model: {x: 24, y: 120, skip: false}},
-                       {_model: {x: 27, y: 125, skip: false}},
-                       {_model: {x: 30, y: 105, skip: false}},
-                       {_model: {x: 33, y: 110, skip: false}},
-                       {_model: {x: 33, y: 110, skip: false}},
-                       {_model: {x: 36, y: 170, skip: false}}
-               ];
-               helpers.splineCurveMonotone(dataPoints);
-               expect(dataPoints).toEqual([{
-                       _model: {
-                               x: 0,
-                               y: 0,
-                               skip: false,
-                               controlPointNextX: 1,
-                               controlPointNextY: 2
-                       }
-               },
-               {
-                       _model: {
-                               x: 3,
-                               y: 6,
-                               skip: false,
-                               controlPointPreviousX: 2,
-                               controlPointPreviousY: 6,
-                               controlPointNextX: 5,
-                               controlPointNextY: 6
-                       }
-               },
-               {
-                       _model: {
-                               x: 9,
-                               y: 6,
-                               skip: false,
-                               controlPointPreviousX: 7,
-                               controlPointPreviousY: 6,
-                               controlPointNextX: 10,
-                               controlPointNextY: 6
-                       }
-               },
-               {
-                       _model: {
-                               x: 12,
-                               y: 60,
-                               skip: false,
-                               controlPointPreviousX: 11,
-                               controlPointPreviousY: 60,
-                               controlPointNextX: 13,
-                               controlPointNextY: 60
-                       }
-               },
-               {
-                       _model: {
-                               x: 15,
-                               y: 60,
-                               skip: false,
-                               controlPointPreviousX: 14,
-                               controlPointPreviousY: 60,
-                               controlPointNextX: 16,
-                               controlPointNextY: 60
-                       }
-               },
-               {
-                       _model: {
-                               x: 18,
-                               y: 120,
-                               skip: false,
-                               controlPointPreviousX: 17,
-                               controlPointPreviousY: 100
-                       }
-               },
-               {
-                       _model: {
-                               x: null,
-                               y: null,
-                               skip: true
-                       }
-               },
-               {
-                       _model: {
-                               x: 21,
-                               y: 180,
-                               skip: false,
-                               controlPointNextX: 22,
-                               controlPointNextY: 160
-                       }
-               },
-               {
-                       _model: {
-                               x: 24,
-                               y: 120,
-                               skip: false,
-                               controlPointPreviousX: 23,
-                               controlPointPreviousY: 120,
-                               controlPointNextX: 25,
-                               controlPointNextY: 120
-                       }
-               },
-               {
-                       _model: {
-                               x: 27,
-                               y: 125,
-                               skip: false,
-                               controlPointPreviousX: 26,
-                               controlPointPreviousY: 125,
-                               controlPointNextX: 28,
-                               controlPointNextY: 125
-                       }
-               },
-               {
-                       _model: {
-                               x: 30,
-                               y: 105,
-                               skip: false,
-                               controlPointPreviousX: 29,
-                               controlPointPreviousY: 105,
-                               controlPointNextX: 31,
-                               controlPointNextY: 105
-                       }
-               },
-               {
-                       _model: {
-                               x: 33,
-                               y: 110,
-                               skip: false,
-                               controlPointPreviousX: 32,
-                               controlPointPreviousY: 110,
-                               controlPointNextX: 33,
-                               controlPointNextY: 110
-                       }
-               },
-               {
-                       _model: {
-                               x: 33,
-                               y: 110,
-                               skip: false,
-                               controlPointPreviousX: 33,
-                               controlPointPreviousY: 110,
-                               controlPointNextX: 34,
-                               controlPointNextY: 110
-                       }
-               },
-               {
-                       _model: {
-                               x: 36,
-                               y: 170,
-                               skip: false,
-                               controlPointPreviousX: 35,
-                               controlPointPreviousY: 150
-                       }
-               }]);
-       });
-
        it('should return the width of the longest text in an Array and 2D Array', function() {
                var context = window.createMockContext();
                var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif";
@@ -363,257 +79,6 @@ describe('Core helper tests', function() {
                }]);
        });
 
-       it ('should get the maximum width and height for a node', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create the div we want to get the max size for
-               var innerDiv = document.createElement('div');
-               div.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum width and height for a node in a ShadowRoot', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               if (!div.attachShadow) {
-                       // Shadow DOM is not natively supported
-                       return;
-               }
-
-               var shadow = div.attachShadow({mode: 'closed'});
-
-               // Create the div we want to get the max size for
-               var innerDiv = document.createElement('div');
-               shadow.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum width of a node that has a max-width style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create the div we want to get the max size for and set a max-width style
-               var innerDiv = document.createElement('div');
-               innerDiv.style.maxWidth = '150px';
-               div.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum height of a node that has a max-height style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create the div we want to get the max size for and set a max-height style
-               var innerDiv = document.createElement('div');
-               innerDiv.style.maxHeight = '150px';
-               div.appendChild(innerDiv);
-
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum width of a node when the parent has a max-width style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create an inner wrapper around our div we want to size and give that a max-width style
-               var parentDiv = document.createElement('div');
-               parentDiv.style.maxWidth = '150px';
-               div.appendChild(parentDiv);
-
-               // Create the div we want to get the max size for
-               var innerDiv = document.createElement('div');
-               parentDiv.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum height of a node when the parent has a max-height style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create an inner wrapper around our div we want to size and give that a max-height style
-               var parentDiv = document.createElement('div');
-               parentDiv.style.maxHeight = '150px';
-               div.appendChild(parentDiv);
-
-               // Create the div we want to get the max size for
-               var innerDiv = document.createElement('div');
-               innerDiv.style.height = '300px'; // make it large
-               parentDiv.appendChild(innerDiv);
-
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum width of a node that has a percentage max-width style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create the div we want to get the max size for and set a max-width style
-               var innerDiv = document.createElement('div');
-               innerDiv.style.maxWidth = '50%';
-               div.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum height of a node that has a percentage max-height style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create the div we want to get the max size for and set a max-height style
-               var innerDiv = document.createElement('div');
-               innerDiv.style.maxHeight = '50%';
-               div.appendChild(innerDiv);
-
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum width of a node when the parent has a percentage max-width style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create an inner wrapper around our div we want to size and give that a max-width style
-               var parentDiv = document.createElement('div');
-               parentDiv.style.maxWidth = '50%';
-               div.appendChild(parentDiv);
-
-               // Create the div we want to get the max size for
-               var innerDiv = document.createElement('div');
-               parentDiv.appendChild(innerDiv);
-
-               expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should get the maximum height of a node when the parent has a percentage max-height style', function() {
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '200px';
-               div.style.height = '300px';
-
-               document.body.appendChild(div);
-
-               // Create an inner wrapper around our div we want to size and give that a max-height style
-               var parentDiv = document.createElement('div');
-               parentDiv.style.maxHeight = '50%';
-               div.appendChild(parentDiv);
-
-               var innerDiv = document.createElement('div');
-               innerDiv.style.height = '300px'; // make it large
-               parentDiv.appendChild(innerDiv);
-
-               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
-
-               document.body.removeChild(div);
-       });
-
-       it ('should leave styled height and width on canvas if explicitly set', function() {
-               var chart = window.acquireChart({}, {
-                       canvas: {
-                               height: 200,
-                               width: 200,
-                               style: 'height: 400px; width: 400px;'
-                       }
-               });
-
-               helpers.retinaScale(chart, true);
-
-               var canvas = chart.canvas;
-
-               expect(canvas.style.height).toBe('400px');
-               expect(canvas.style.width).toBe('400px');
-       });
-
-       it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() {
-
-               // Create div with fixed size as a test bed
-               var div = document.createElement('div');
-               div.style.width = '300px';
-               div.style.height = '300px';
-               document.body.appendChild(div);
-
-               // Inner DIV to have 5% padding of parent
-               var innerDiv = document.createElement('div');
-
-               div.appendChild(innerDiv);
-
-               var canvas = document.createElement('canvas');
-               innerDiv.appendChild(canvas);
-
-               // No padding
-               expect(helpers.getMaximumWidth(canvas)).toBe(300);
-
-               // test with percentage
-               innerDiv.style.padding = '5%';
-               expect(helpers.getMaximumWidth(canvas)).toBe(270);
-
-               // test with pixels
-               innerDiv.style.padding = '10px';
-               expect(helpers.getMaximumWidth(canvas)).toBe(280);
-
-               document.body.removeChild(div);
-       });
-
        describe('Color helper', function() {
                function isColorInstance(obj) {
                        return typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, 'values') && Object.prototype.hasOwnProperty.call(obj.values, 'rgb');
@@ -622,13 +87,6 @@ describe('Core helper tests', function() {
                it('should return a color when called with a color', function() {
                        expect(isColorInstance(helpers.color('rgb(1, 2, 3)'))).toBe(true);
                });
-
-               it('should return a color when called with a CanvasGradient instance', function() {
-                       var context = document.createElement('canvas').getContext('2d');
-                       var gradient = context.createLinearGradient(0, 1, 2, 3);
-
-                       expect(isColorInstance(helpers.color(gradient))).toBe(true);
-               });
        });
 
        describe('Background hover color helper', function() {
diff --git a/test/specs/helpers.curve.tests.js b/test/specs/helpers.curve.tests.js
new file mode 100644 (file)
index 0000000..fea8adc
--- /dev/null
@@ -0,0 +1,211 @@
+describe('Curve helper tests', function() {
+       let helpers;
+
+       beforeAll(function() {
+               helpers = window.Chart.helpers.curve;
+       });
+
+       it('should spline curves', function() {
+               expect(helpers.splineCurve({
+                       x: 0,
+                       y: 0
+               }, {
+                       x: 1,
+                       y: 1
+               }, {
+                       x: 2,
+                       y: 0
+               }, 0)).toEqual({
+                       previous: {
+                               x: 1,
+                               y: 1,
+                       },
+                       next: {
+                               x: 1,
+                               y: 1,
+                       }
+               });
+
+               expect(helpers.splineCurve({
+                       x: 0,
+                       y: 0
+               }, {
+                       x: 1,
+                       y: 1
+               }, {
+                       x: 2,
+                       y: 0
+               }, 1)).toEqual({
+                       previous: {
+                               x: 0,
+                               y: 1,
+                       },
+                       next: {
+                               x: 2,
+                               y: 1,
+                       }
+               });
+       });
+
+       it('should spline curves with monotone cubic interpolation', function() {
+               var dataPoints = [
+                       {_model: {x: 0, y: 0, skip: false}},
+                       {_model: {x: 3, y: 6, skip: false}},
+                       {_model: {x: 9, y: 6, skip: false}},
+                       {_model: {x: 12, y: 60, skip: false}},
+                       {_model: {x: 15, y: 60, skip: false}},
+                       {_model: {x: 18, y: 120, skip: false}},
+                       {_model: {x: null, y: null, skip: true}},
+                       {_model: {x: 21, y: 180, skip: false}},
+                       {_model: {x: 24, y: 120, skip: false}},
+                       {_model: {x: 27, y: 125, skip: false}},
+                       {_model: {x: 30, y: 105, skip: false}},
+                       {_model: {x: 33, y: 110, skip: false}},
+                       {_model: {x: 33, y: 110, skip: false}},
+                       {_model: {x: 36, y: 170, skip: false}}
+               ];
+               helpers.splineCurveMonotone(dataPoints);
+               expect(dataPoints).toEqual([{
+                       _model: {
+                               x: 0,
+                               y: 0,
+                               skip: false,
+                               controlPointNextX: 1,
+                               controlPointNextY: 2
+                       }
+               },
+               {
+                       _model: {
+                               x: 3,
+                               y: 6,
+                               skip: false,
+                               controlPointPreviousX: 2,
+                               controlPointPreviousY: 6,
+                               controlPointNextX: 5,
+                               controlPointNextY: 6
+                       }
+               },
+               {
+                       _model: {
+                               x: 9,
+                               y: 6,
+                               skip: false,
+                               controlPointPreviousX: 7,
+                               controlPointPreviousY: 6,
+                               controlPointNextX: 10,
+                               controlPointNextY: 6
+                       }
+               },
+               {
+                       _model: {
+                               x: 12,
+                               y: 60,
+                               skip: false,
+                               controlPointPreviousX: 11,
+                               controlPointPreviousY: 60,
+                               controlPointNextX: 13,
+                               controlPointNextY: 60
+                       }
+               },
+               {
+                       _model: {
+                               x: 15,
+                               y: 60,
+                               skip: false,
+                               controlPointPreviousX: 14,
+                               controlPointPreviousY: 60,
+                               controlPointNextX: 16,
+                               controlPointNextY: 60
+                       }
+               },
+               {
+                       _model: {
+                               x: 18,
+                               y: 120,
+                               skip: false,
+                               controlPointPreviousX: 17,
+                               controlPointPreviousY: 100
+                       }
+               },
+               {
+                       _model: {
+                               x: null,
+                               y: null,
+                               skip: true
+                       }
+               },
+               {
+                       _model: {
+                               x: 21,
+                               y: 180,
+                               skip: false,
+                               controlPointNextX: 22,
+                               controlPointNextY: 160
+                       }
+               },
+               {
+                       _model: {
+                               x: 24,
+                               y: 120,
+                               skip: false,
+                               controlPointPreviousX: 23,
+                               controlPointPreviousY: 120,
+                               controlPointNextX: 25,
+                               controlPointNextY: 120
+                       }
+               },
+               {
+                       _model: {
+                               x: 27,
+                               y: 125,
+                               skip: false,
+                               controlPointPreviousX: 26,
+                               controlPointPreviousY: 125,
+                               controlPointNextX: 28,
+                               controlPointNextY: 125
+                       }
+               },
+               {
+                       _model: {
+                               x: 30,
+                               y: 105,
+                               skip: false,
+                               controlPointPreviousX: 29,
+                               controlPointPreviousY: 105,
+                               controlPointNextX: 31,
+                               controlPointNextY: 105
+                       }
+               },
+               {
+                       _model: {
+                               x: 33,
+                               y: 110,
+                               skip: false,
+                               controlPointPreviousX: 32,
+                               controlPointPreviousY: 110,
+                               controlPointNextX: 33,
+                               controlPointNextY: 110
+                       }
+               },
+               {
+                       _model: {
+                               x: 33,
+                               y: 110,
+                               skip: false,
+                               controlPointPreviousX: 33,
+                               controlPointPreviousY: 110,
+                               controlPointNextX: 34,
+                               controlPointNextY: 110
+                       }
+               },
+               {
+                       _model: {
+                               x: 36,
+                               y: 170,
+                               skip: false,
+                               controlPointPreviousX: 35,
+                               controlPointPreviousY: 150
+                       }
+               }]);
+       });
+});
diff --git a/test/specs/helpers.dom.tests.js b/test/specs/helpers.dom.tests.js
new file mode 100644 (file)
index 0000000..ab614de
--- /dev/null
@@ -0,0 +1,259 @@
+describe('DOM helpers tests', function() {
+       let helpers;
+
+       beforeAll(function() {
+               helpers = window.Chart.helpers.dom;
+       });
+
+       it ('should get the maximum width and height for a node', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create the div we want to get the max size for
+               var innerDiv = document.createElement('div');
+               div.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum width and height for a node in a ShadowRoot', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               if (!div.attachShadow) {
+                       // Shadow DOM is not natively supported
+                       return;
+               }
+
+               var shadow = div.attachShadow({mode: 'closed'});
+
+               // Create the div we want to get the max size for
+               var innerDiv = document.createElement('div');
+               shadow.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(300);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum width of a node that has a max-width style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create the div we want to get the max size for and set a max-width style
+               var innerDiv = document.createElement('div');
+               innerDiv.style.maxWidth = '150px';
+               div.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum height of a node that has a max-height style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create the div we want to get the max size for and set a max-height style
+               var innerDiv = document.createElement('div');
+               innerDiv.style.maxHeight = '150px';
+               div.appendChild(innerDiv);
+
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum width of a node when the parent has a max-width style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create an inner wrapper around our div we want to size and give that a max-width style
+               var parentDiv = document.createElement('div');
+               parentDiv.style.maxWidth = '150px';
+               div.appendChild(parentDiv);
+
+               // Create the div we want to get the max size for
+               var innerDiv = document.createElement('div');
+               parentDiv.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum height of a node when the parent has a max-height style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create an inner wrapper around our div we want to size and give that a max-height style
+               var parentDiv = document.createElement('div');
+               parentDiv.style.maxHeight = '150px';
+               div.appendChild(parentDiv);
+
+               // Create the div we want to get the max size for
+               var innerDiv = document.createElement('div');
+               innerDiv.style.height = '300px'; // make it large
+               parentDiv.appendChild(innerDiv);
+
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum width of a node that has a percentage max-width style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create the div we want to get the max size for and set a max-width style
+               var innerDiv = document.createElement('div');
+               innerDiv.style.maxWidth = '50%';
+               div.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum height of a node that has a percentage max-height style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create the div we want to get the max size for and set a max-height style
+               var innerDiv = document.createElement('div');
+               innerDiv.style.maxHeight = '50%';
+               div.appendChild(innerDiv);
+
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum width of a node when the parent has a percentage max-width style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create an inner wrapper around our div we want to size and give that a max-width style
+               var parentDiv = document.createElement('div');
+               parentDiv.style.maxWidth = '50%';
+               div.appendChild(parentDiv);
+
+               // Create the div we want to get the max size for
+               var innerDiv = document.createElement('div');
+               parentDiv.appendChild(innerDiv);
+
+               expect(helpers.getMaximumWidth(innerDiv)).toBe(100);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should get the maximum height of a node when the parent has a percentage max-height style', function() {
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '200px';
+               div.style.height = '300px';
+
+               document.body.appendChild(div);
+
+               // Create an inner wrapper around our div we want to size and give that a max-height style
+               var parentDiv = document.createElement('div');
+               parentDiv.style.maxHeight = '50%';
+               div.appendChild(parentDiv);
+
+               var innerDiv = document.createElement('div');
+               innerDiv.style.height = '300px'; // make it large
+               parentDiv.appendChild(innerDiv);
+
+               expect(helpers.getMaximumHeight(innerDiv)).toBe(150);
+
+               document.body.removeChild(div);
+       });
+
+       it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() {
+
+               // Create div with fixed size as a test bed
+               var div = document.createElement('div');
+               div.style.width = '300px';
+               div.style.height = '300px';
+               document.body.appendChild(div);
+
+               // Inner DIV to have 5% padding of parent
+               var innerDiv = document.createElement('div');
+
+               div.appendChild(innerDiv);
+
+               var canvas = document.createElement('canvas');
+               innerDiv.appendChild(canvas);
+
+               // No padding
+               expect(helpers.getMaximumWidth(canvas)).toBe(300);
+
+               // test with percentage
+               innerDiv.style.padding = '5%';
+               expect(helpers.getMaximumWidth(canvas)).toBe(270);
+
+               // test with pixels
+               innerDiv.style.padding = '10px';
+               expect(helpers.getMaximumWidth(canvas)).toBe(280);
+
+               document.body.removeChild(div);
+       });
+
+       it ('should leave styled height and width on canvas if explicitly set', function() {
+               var chart = window.acquireChart({}, {
+                       canvas: {
+                               height: 200,
+                               width: 200,
+                               style: 'height: 400px; width: 400px;'
+                       }
+               });
+
+               helpers.retinaScale(chart, true);
+
+               var canvas = chart.canvas;
+
+               expect(canvas.style.height).toBe('400px');
+               expect(canvas.style.width).toBe('400px');
+       });
+
+});
index 0006a3dd16432cd7e8f498d827f87971ca0b0a0b..87ff63b163a4b6c199de3803c8b2df55e125242f 100644 (file)
@@ -4,6 +4,7 @@
 describe('Chart.helpers.math', function() {
        var math = Chart.helpers.math;
        var factorize = math._factorize;
+       var decimalPlaces = math._decimalPlaces;
 
        it('should factorize', function() {
                expect(factorize(1000)).toEqual([1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 100, 125, 200, 250, 500]);
@@ -25,4 +26,84 @@ describe('Chart.helpers.math', function() {
                        expect(math.log10(Math.pow(10, i))).toBe(i);
                }
        });
+
+       it('should get the correct number of decimal places', function() {
+               expect(decimalPlaces(100)).toBe(0);
+               expect(decimalPlaces(1)).toBe(0);
+               expect(decimalPlaces(0)).toBe(0);
+               expect(decimalPlaces(0.01)).toBe(2);
+               expect(decimalPlaces(-0.01)).toBe(2);
+               expect(decimalPlaces('1')).toBe(undefined);
+               expect(decimalPlaces('')).toBe(undefined);
+               expect(decimalPlaces(undefined)).toBe(undefined);
+               expect(decimalPlaces(12345678.1234)).toBe(4);
+               expect(decimalPlaces(1234567890.1234567)).toBe(7);
+       });
+
+       it('should get an angle from a point', function() {
+               var center = {
+                       x: 0,
+                       y: 0
+               };
+
+               expect(math.getAngleFromPoint(center, {
+                       x: 0,
+                       y: 10
+               })).toEqual({
+                       angle: Math.PI / 2,
+                       distance: 10,
+               });
+
+               expect(math.getAngleFromPoint(center, {
+                       x: Math.sqrt(2),
+                       y: Math.sqrt(2)
+               })).toEqual({
+                       angle: Math.PI / 4,
+                       distance: 2
+               });
+
+               expect(math.getAngleFromPoint(center, {
+                       x: -1.0 * Math.sqrt(2),
+                       y: -1.0 * Math.sqrt(2)
+               })).toEqual({
+                       angle: Math.PI * 1.25,
+                       distance: 2
+               });
+       });
+
+       it('should convert between radians and degrees', function() {
+               expect(math.toRadians(180)).toBe(Math.PI);
+               expect(math.toRadians(90)).toBe(0.5 * Math.PI);
+               expect(math.toDegrees(Math.PI)).toBe(180);
+               expect(math.toDegrees(Math.PI * 3 / 2)).toBe(270);
+       });
+
+       it('should correctly determine if two numbers are essentially equal', function() {
+               expect(math.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
+               expect(math.almostEquals(1, 1.1, 0.0001)).toBe(false);
+               expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false);
+               expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
+       });
+
+       it('should get the correct sign', function() {
+               expect(math.sign(0)).toBe(0);
+               expect(math.sign(10)).toBe(1);
+               expect(math.sign(-5)).toBe(-1);
+       });
+
+       it('should correctly determine if a numbers are essentially whole', function() {
+               expect(math.almostWhole(0.99999, 0.0001)).toBe(true);
+               expect(math.almostWhole(0.9, 0.0001)).toBe(false);
+               expect(math.almostWhole(1234567890123, 0.0001)).toBe(true);
+               expect(math.almostWhole(1234567890123.001, 0.0001)).toBe(false);
+       });
+
+       it('should detect a number', function() {
+               expect(math.isNumber(123)).toBe(true);
+               expect(math.isNumber('123')).toBe(true);
+               expect(math.isNumber(null)).toBe(false);
+               expect(math.isNumber(NaN)).toBe(false);
+               expect(math.isNumber(undefined)).toBe(false);
+               expect(math.isNumber('cbc')).toBe(false);
+       });
 });