mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Move all helpers to src/helpers (#6841)
* 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
This commit is contained in:
parent
374b7491a3
commit
c8bdca62e8
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
};
|
||||
};
|
||||
@ -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: {
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
130
src/helpers/helpers.curve.js
Normal file
130
src/helpers/helpers.curve.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/helpers/helpers.dom.js
Normal file
175
src/helpers/helpers.dom.js
Normal file
@ -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';
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -1,17 +1,200 @@
|
||||
'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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
211
test/specs/helpers.curve.tests.js
Normal file
211
test/specs/helpers.curve.tests.js
Normal file
@ -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
|
||||
}
|
||||
}]);
|
||||
});
|
||||
});
|
||||
259
test/specs/helpers.dom.tests.js
Normal file
259
test/specs/helpers.dom.tests.js
Normal file
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user