From: Evert Timberg Date: Tue, 17 Dec 2019 13:04:40 +0000 (-0500) Subject: Move all helpers to src/helpers (#6841) X-Git-Tag: v3.0.0-alpha~193 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c8bdca62e89b4d6d9f334cec7a8bfa673725014c;p=thirdparty%2FChart.js.git 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 --- diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index d76bd487b..9fa7610fa 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -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` diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 36199e7c2..7b00ea00f 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -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; diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 90ac98672..34d421f98 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -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, diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 689782707..c52f871de 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -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, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 320049212..f13433399 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -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 diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 1a82ff484..f8e39b482 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -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; } } diff --git a/src/core/core.element.js b/src/core/core.element.js index f4feb4bd6..159e4721c 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -1,7 +1,8 @@ 'use strict'; -const color = require('chartjs-color'); -const helpers = require('../helpers/index'); +import color from 'chartjs-color'; +import helpers from '../helpers/index'; +import {isNumber} from '../helpers/helpers.math'; function interpolate(start, view, model, ease) { var keys = Object.keys(model); @@ -110,10 +111,9 @@ class Element { } hasValue() { - return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y); + return isNumber(this._model.x) && isNumber(this._model.y); } } Element.extend = helpers.inherits; - -module.exports = Element; +export default Element; diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js deleted file mode 100644 index 9f86f2199..000000000 --- a/src/core/core.helpers.js +++ /dev/null @@ -1,571 +0,0 @@ -'use strict'; - -var color = require('chartjs-color'); -var defaults = require('./core.defaults'); -var helpers = require('../helpers/index'); - -module.exports = function() { - - // -- Basic js utility methods - - helpers.where = function(collection, filterCallback) { - if (helpers.isArray(collection) && Array.prototype.filter) { - return collection.filter(filterCallback); - } - var filtered = []; - - helpers.each(collection, function(item) { - if (filterCallback(item)) { - filtered.push(item); - } - }); - - return filtered; - }; - helpers.findIndex = Array.prototype.findIndex ? - function(array, callback, scope) { - return array.findIndex(callback, scope); - } : - function(array, callback, scope) { - scope = scope === undefined ? array : scope; - for (var i = 0, ilen = array.length; i < ilen; ++i) { - if (callback.call(scope, array[i], i, array)) { - return i; - } - } - return -1; - }; - helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) { - // Default to start of the array - if (helpers.isNullOrUndef(startIndex)) { - startIndex = -1; - } - for (var i = startIndex + 1; i < arrayToSearch.length; i++) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)) { - return currentItem; - } - } - }; - helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) { - // Default to end of the array - if (helpers.isNullOrUndef(startIndex)) { - startIndex = arrayToSearch.length; - } - for (var i = startIndex - 1; i >= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)) { - return currentItem; - } - } - }; - - // -- Math methods - helpers.isNumber = function(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - }; - helpers.almostEquals = function(x, y, epsilon) { - return Math.abs(x - y) < epsilon; - }; - helpers.almostWhole = function(x, epsilon) { - var rounded = Math.round(x); - return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); - }; - helpers._setMinAndMax = function(array, target) { - var i, ilen, value; - - for (i = 0, ilen = array.length; i < ilen; i++) { - value = array[i]; - if (!isNaN(value)) { - target.min = Math.min(target.min, value); - target.max = Math.max(target.max, value); - } - } - }; - helpers._setMinAndMaxByKey = function(array, target, property) { - var i, ilen, value; - - for (i = 0, ilen = array.length; i < ilen; i++) { - value = array[i][property]; - if (!isNaN(value)) { - target.min = Math.min(target.min, value); - target.max = Math.max(target.max, value); - } - } - }; - helpers.sign = Math.sign ? - function(x) { - return Math.sign(x); - } : - function(x) { - x = +x; // convert to a number - if (x === 0 || isNaN(x)) { - return x; - } - return x > 0 ? 1 : -1; - }; - helpers.toRadians = function(degrees) { - return degrees * (Math.PI / 180); - }; - helpers.toDegrees = function(radians) { - return radians * (180 / Math.PI); - }; - - /** - * Returns the number of decimal places - * i.e. the number of digits after the decimal point, of the value of this Number. - * @param {number} x - A number. - * @returns {number} The number of decimal places. - * @private - */ - helpers._decimalPlaces = function(x) { - if (!helpers.isFinite(x)) { - return; - } - var e = 1; - var p = 0; - while (Math.round(x * e) / e !== x) { - e *= 10; - p++; - } - return p; - }; - - // Gets the angle from vertical upright to the point about a centre. - helpers.getAngleFromPoint = function(centrePoint, anglePoint) { - var distanceFromXCenter = anglePoint.x - centrePoint.x; - var distanceFromYCenter = anglePoint.y - centrePoint.y; - var radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); - - if (angle < (-0.5 * Math.PI)) { - angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2] - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }; - helpers.distanceBetweenPoints = function(pt1, pt2) { - return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); - }; - - helpers.splineCurve = function(firstPoint, middlePoint, afterPoint, t) { - // Props to Rob Spencer at scaled innovation for his post on splining between points - // http://scaledinnovation.com/analytics/splines/aboutSplines.html - - // This function must also respect "skipped" points - - var previous = firstPoint.skip ? middlePoint : firstPoint; - var current = middlePoint; - var next = afterPoint.skip ? middlePoint : afterPoint; - - var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2)); - var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2)); - - var s01 = d01 / (d01 + d12); - var s12 = d12 / (d01 + d12); - - // If all points are the same, s01 & s02 will be inf - s01 = isNaN(s01) ? 0 : s01; - s12 = isNaN(s12) ? 0 : s12; - - var fa = t * s01; // scaling factor for triangle Ta - var fb = t * s12; - - return { - previous: { - x: current.x - fa * (next.x - previous.x), - y: current.y - fa * (next.y - previous.y) - }, - next: { - x: current.x + fb * (next.x - previous.x), - y: current.y + fb * (next.y - previous.y) - } - }; - }; - helpers.EPSILON = Number.EPSILON || 1e-14; - helpers.splineCurveMonotone = function(points) { - // This function calculates Bézier control points in a similar way than |splineCurve|, - // but preserves monotonicity of the provided data and ensures no local extremums are added - // between the dataset discrete points due to the interpolation. - // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation - - var pointsWithTangents = (points || []).map(function(point) { - return { - model: point._model, - deltaK: 0, - mK: 0 - }; - }); - - // Calculate slopes (deltaK) and initialize tangents (mK) - var pointsLen = pointsWithTangents.length; - var i, pointBefore, pointCurrent, pointAfter; - for (i = 0; i < pointsLen; ++i) { - pointCurrent = pointsWithTangents[i]; - if (pointCurrent.model.skip) { - continue; - } - - pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; - pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; - if (pointAfter && !pointAfter.model.skip) { - var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); - - // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 - pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; - } - - if (!pointBefore || pointBefore.model.skip) { - pointCurrent.mK = pointCurrent.deltaK; - } else if (!pointAfter || pointAfter.model.skip) { - pointCurrent.mK = pointBefore.deltaK; - } else if (this.sign(pointBefore.deltaK) !== this.sign(pointCurrent.deltaK)) { - pointCurrent.mK = 0; - } else { - pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2; - } - } - - // Adjust tangents to ensure monotonic properties - var alphaK, betaK, tauK, squaredMagnitude; - for (i = 0; i < pointsLen - 1; ++i) { - pointCurrent = pointsWithTangents[i]; - pointAfter = pointsWithTangents[i + 1]; - if (pointCurrent.model.skip || pointAfter.model.skip) { - continue; - } - - if (helpers.almostEquals(pointCurrent.deltaK, 0, this.EPSILON)) { - pointCurrent.mK = pointAfter.mK = 0; - continue; - } - - alphaK = pointCurrent.mK / pointCurrent.deltaK; - betaK = pointAfter.mK / pointCurrent.deltaK; - squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); - if (squaredMagnitude <= 9) { - continue; - } - - tauK = 3 / Math.sqrt(squaredMagnitude); - pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK; - pointAfter.mK = betaK * tauK * pointCurrent.deltaK; - } - - // Compute control points - var deltaX; - for (i = 0; i < pointsLen; ++i) { - pointCurrent = pointsWithTangents[i]; - if (pointCurrent.model.skip) { - continue; - } - - pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; - pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; - if (pointBefore && !pointBefore.model.skip) { - deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3; - pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX; - pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK; - } - if (pointAfter && !pointAfter.model.skip) { - deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3; - pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX; - pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK; - } - } - }; - // Implementation of the nice number algorithm used in determining where axis labels will go - helpers.niceNum = function(range, round) { - var exponent = Math.floor(helpers.math.log10(range)); - var fraction = range / Math.pow(10, exponent); - var niceFraction; - - if (round) { - if (fraction < 1.5) { - niceFraction = 1; - } else if (fraction < 3) { - niceFraction = 2; - } else if (fraction < 7) { - niceFraction = 5; - } else { - niceFraction = 10; - } - } else if (fraction <= 1.0) { - niceFraction = 1; - } else if (fraction <= 2) { - niceFraction = 2; - } else if (fraction <= 5) { - niceFraction = 5; - } else { - niceFraction = 10; - } - - return niceFraction * Math.pow(10, exponent); - }; - // Request animation polyfill - https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - helpers.requestAnimFrame = (function() { - if (typeof window === 'undefined') { - return function(callback) { - callback(); - }; - } - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - }()); - // -- DOM methods - helpers.getRelativePosition = function(evt, chart) { - var mouseX, mouseY; - var e = evt.originalEvent || evt; - var canvas = evt.target || evt.srcElement; - var boundingRect = canvas.getBoundingClientRect(); - - var touches = e.touches; - if (touches && touches.length > 0) { - mouseX = touches[0].clientX; - mouseY = touches[0].clientY; - - } else { - mouseX = e.clientX; - mouseY = e.clientY; - } - - // Scale mouse coordinates into canvas coordinates - // by following the pattern laid out by 'jerryj' in the comments of - // https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ - var paddingLeft = parseFloat(helpers.getStyle(canvas, 'padding-left')); - var paddingTop = parseFloat(helpers.getStyle(canvas, 'padding-top')); - var paddingRight = parseFloat(helpers.getStyle(canvas, 'padding-right')); - var paddingBottom = parseFloat(helpers.getStyle(canvas, 'padding-bottom')); - var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight; - var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom; - - // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However - // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here - mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvas.width / chart.currentDevicePixelRatio); - mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvas.height / chart.currentDevicePixelRatio); - - return { - x: mouseX, - y: mouseY - }; - - }; - - // Private helper function to convert max-width/max-height values that may be percentages into a number - function parseMaxStyle(styleValue, node, parentProperty) { - var valueInPixels; - if (typeof styleValue === 'string') { - valueInPixels = parseInt(styleValue, 10); - - if (styleValue.indexOf('%') !== -1) { - // percentage * size in dimension - valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty]; - } - } else { - valueInPixels = styleValue; - } - - return valueInPixels; - } - - /** - * Returns if the given value contains an effective constraint. - * @private - */ - function isConstrainedValue(value) { - return value !== undefined && value !== null && value !== 'none'; - } - - /** - * Returns the max width or height of the given DOM node in a cross-browser compatible fashion - * @param {HTMLElement} domNode - the node to check the constraint on - * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height') - * @param {string} percentageProperty - property of parent to use when calculating width as a percentage - * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser} - */ - function getConstraintDimension(domNode, maxStyle, percentageProperty) { - var view = document.defaultView; - var parentNode = helpers._getParentNode(domNode); - var constrainedNode = view.getComputedStyle(domNode)[maxStyle]; - var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle]; - var hasCNode = isConstrainedValue(constrainedNode); - var hasCContainer = isConstrainedValue(constrainedContainer); - var infinity = Number.POSITIVE_INFINITY; - - if (hasCNode || hasCContainer) { - return Math.min( - hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity, - hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity); - } - - return 'none'; - } - // returns Number or undefined if no constraint - helpers.getConstraintWidth = function(domNode) { - return getConstraintDimension(domNode, 'max-width', 'clientWidth'); - }; - // returns Number or undefined if no constraint - helpers.getConstraintHeight = function(domNode) { - return getConstraintDimension(domNode, 'max-height', 'clientHeight'); - }; - /** - * @private - */ - helpers._calculatePadding = function(container, padding, parentDimension) { - padding = helpers.getStyle(container, padding); - - return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10); - }; - /** - * @private - */ - helpers._getParentNode = function(domNode) { - var parent = domNode.parentNode; - if (parent && parent.toString() === '[object ShadowRoot]') { - parent = parent.host; - } - return parent; - }; - helpers.getMaximumWidth = function(domNode) { - var container = helpers._getParentNode(domNode); - if (!container) { - return domNode.clientWidth; - } - - var clientWidth = container.clientWidth; - var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth); - var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth); - - var w = clientWidth - paddingLeft - paddingRight; - var cw = helpers.getConstraintWidth(domNode); - return isNaN(cw) ? w : Math.min(w, cw); - }; - helpers.getMaximumHeight = function(domNode) { - var container = helpers._getParentNode(domNode); - if (!container) { - return domNode.clientHeight; - } - - var clientHeight = container.clientHeight; - var paddingTop = helpers._calculatePadding(container, 'padding-top', clientHeight); - var paddingBottom = helpers._calculatePadding(container, 'padding-bottom', clientHeight); - - var h = clientHeight - paddingTop - paddingBottom; - var ch = helpers.getConstraintHeight(domNode); - return isNaN(ch) ? h : Math.min(h, ch); - }; - helpers.getStyle = function(el, property) { - return el.currentStyle ? - el.currentStyle[property] : - document.defaultView.getComputedStyle(el, null).getPropertyValue(property); - }; - helpers.retinaScale = function(chart, forceRatio) { - var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1; - if (pixelRatio === 1) { - return; - } - - var canvas = chart.canvas; - var height = chart.height; - var width = chart.width; - - canvas.height = height * pixelRatio; - canvas.width = width * pixelRatio; - chart.ctx.scale(pixelRatio, pixelRatio); - - // If no style has been set on the canvas, the render size is used as display size, - // making the chart visually bigger, so let's enforce it to the "correct" values. - // See https://github.com/chartjs/Chart.js/issues/3575 - if (!canvas.style.height && !canvas.style.width) { - canvas.style.height = height + 'px'; - canvas.style.width = width + 'px'; - } - }; - // -- Canvas methods - helpers.fontString = function(pixelSize, fontStyle, fontFamily) { - return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; - }; - helpers.longestText = function(ctx, font, arrayOfThings, cache) { - cache = cache || {}; - var data = cache.data = cache.data || {}; - var gc = cache.garbageCollect = cache.garbageCollect || []; - - if (cache.font !== font) { - data = cache.data = {}; - gc = cache.garbageCollect = []; - cache.font = font; - } - - ctx.font = font; - var longest = 0; - var ilen = arrayOfThings.length; - var i, j, jlen, thing, nestedThing; - for (i = 0; i < ilen; i++) { - thing = arrayOfThings[i]; - - // Undefined strings and arrays should not be measured - if (thing !== undefined && thing !== null && helpers.isArray(thing) !== true) { - longest = helpers.measureText(ctx, data, gc, longest, thing); - } else if (helpers.isArray(thing)) { - // if it is an array lets measure each element - // to do maybe simplify this function a bit so we can do this more recursively? - for (j = 0, jlen = thing.length; j < jlen; j++) { - nestedThing = thing[j]; - // Undefined strings and arrays should not be measured - if (nestedThing !== undefined && nestedThing !== null && !helpers.isArray(nestedThing)) { - longest = helpers.measureText(ctx, data, gc, longest, nestedThing); - } - } - } - } - - var gcLen = gc.length / 2; - if (gcLen > arrayOfThings.length) { - for (i = 0; i < gcLen; i++) { - delete data[gc[i]]; - } - gc.splice(0, gcLen); - } - return longest; - }; - helpers.measureText = function(ctx, data, gc, longest, string) { - var textWidth = data[string]; - if (!textWidth) { - textWidth = data[string] = ctx.measureText(string).width; - gc.push(string); - } - if (textWidth > longest) { - longest = textWidth; - } - return longest; - }; - - helpers.color = !color ? - function(value) { - console.error('Color.js not found!'); - return value; - } : - function(value) { - if (value instanceof CanvasGradient) { - value = defaults.global.defaultColor; - } - - return color(value); - }; - - helpers.getHoverColor = function(colorValue) { - return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ? - colorValue : - helpers.color(colorValue).saturate(0.5).darken(0.1).rgbString(); - }; -}; diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index d829b006b..f1975cd1e 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -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: { /** diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 6adce4a89..49432e228 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -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; diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 2a53cce6d..000c98bc3 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -2,7 +2,7 @@ import defaults from '../core/core.defaults'; import Element from '../core/core.element'; -import helpers from '../helpers'; +import {getAngleFromPoint} from '../helpers/helpers.math'; const TAU = Math.PI * 2; defaults._set('global', { @@ -101,7 +101,7 @@ class Arc extends Element { var vm = this._view; if (vm) { - var pointRelativePosition = helpers.getAngleFromPoint(vm, {x: chartX, y: chartY}); + var pointRelativePosition = getAngleFromPoint(vm, {x: chartX, y: chartY}); var angle = pointRelativePosition.angle; var distance = pointRelativePosition.distance; diff --git a/src/helpers/helpers.curve.js b/src/helpers/helpers.curve.js new file mode 100644 index 000000000..292e27740 --- /dev/null +++ b/src/helpers/helpers.curve.js @@ -0,0 +1,130 @@ +import {almostEquals, sign} from './helpers.math'; + +const EPSILON = Number.EPSILON || 1e-14; + +export function splineCurve(firstPoint, middlePoint, afterPoint, t) { + // Props to Rob Spencer at scaled innovation for his post on splining between points + // http://scaledinnovation.com/analytics/splines/aboutSplines.html + + // This function must also respect "skipped" points + + var previous = firstPoint.skip ? middlePoint : firstPoint; + var current = middlePoint; + var next = afterPoint.skip ? middlePoint : afterPoint; + + var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2)); + var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2)); + + var s01 = d01 / (d01 + d12); + var s12 = d12 / (d01 + d12); + + // If all points are the same, s01 & s02 will be inf + s01 = isNaN(s01) ? 0 : s01; + s12 = isNaN(s12) ? 0 : s12; + + var fa = t * s01; // scaling factor for triangle Ta + var fb = t * s12; + + return { + previous: { + x: current.x - fa * (next.x - previous.x), + y: current.y - fa * (next.y - previous.y) + }, + next: { + x: current.x + fb * (next.x - previous.x), + y: current.y + fb * (next.y - previous.y) + } + }; +} + +export function splineCurveMonotone(points) { + // This function calculates Bézier control points in a similar way than |splineCurve|, + // but preserves monotonicity of the provided data and ensures no local extremums are added + // between the dataset discrete points due to the interpolation. + // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + + var pointsWithTangents = (points || []).map(function(point) { + return { + model: point._model, + deltaK: 0, + mK: 0 + }; + }); + + // Calculate slopes (deltaK) and initialize tangents (mK) + var pointsLen = pointsWithTangents.length; + var i, pointBefore, pointCurrent, pointAfter; + for (i = 0; i < pointsLen; ++i) { + pointCurrent = pointsWithTangents[i]; + if (pointCurrent.model.skip) { + continue; + } + + pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; + pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; + if (pointAfter && !pointAfter.model.skip) { + var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; + } + + if (!pointBefore || pointBefore.model.skip) { + pointCurrent.mK = pointCurrent.deltaK; + } else if (!pointAfter || pointAfter.model.skip) { + pointCurrent.mK = pointBefore.deltaK; + } else if (sign(pointBefore.deltaK) !== sign(pointCurrent.deltaK)) { + pointCurrent.mK = 0; + } else { + pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2; + } + } + + // Adjust tangents to ensure monotonic properties + var alphaK, betaK, tauK, squaredMagnitude; + for (i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointsWithTangents[i]; + pointAfter = pointsWithTangents[i + 1]; + if (pointCurrent.model.skip || pointAfter.model.skip) { + continue; + } + + if (almostEquals(pointCurrent.deltaK, 0, EPSILON)) { + pointCurrent.mK = pointAfter.mK = 0; + continue; + } + + alphaK = pointCurrent.mK / pointCurrent.deltaK; + betaK = pointAfter.mK / pointCurrent.deltaK; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; + } + + tauK = 3 / Math.sqrt(squaredMagnitude); + pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK; + pointAfter.mK = betaK * tauK * pointCurrent.deltaK; + } + + // Compute control points + var deltaX; + for (i = 0; i < pointsLen; ++i) { + pointCurrent = pointsWithTangents[i]; + if (pointCurrent.model.skip) { + continue; + } + + pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; + pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; + if (pointBefore && !pointBefore.model.skip) { + deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3; + pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX; + pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK; + } + if (pointAfter && !pointAfter.model.skip) { + deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3; + pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX; + pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK; + } + } +} diff --git a/src/helpers/helpers.dom.js b/src/helpers/helpers.dom.js new file mode 100644 index 000000000..939e4bbe8 --- /dev/null +++ b/src/helpers/helpers.dom.js @@ -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'; + } +} diff --git a/src/helpers/helpers.math.js b/src/helpers/helpers.math.js index f08ff2c1e..3b830ef6b 100644 --- a/src/helpers/helpers.math.js +++ b/src/helpers/helpers.math.js @@ -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)); +} diff --git a/src/helpers/index.js b/src/helpers/index.js index fa7efa3b0..6bd61b40b 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -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(); + } }; diff --git a/src/index.js b/src/index.js index 998cf09af..c8a88d7d4 100644 --- a/src/index.js +++ b/src/index.js @@ -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'); diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 5271860e5..7b0ed1624 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -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); } diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 60b6ad638..4423e346e 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -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(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index d48ce5b64..f42187396 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -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; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 34776ea00..b2c0b9757 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -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 { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 9e39e6a08..f1e553465 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -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); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 690f6643b..eecdc69a9 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -20,26 +20,6 @@ describe('Core helper tests', function() { expect(helpers.findPreviousWhere(data, callback, 0)).toBe(undefined); }); - it('should get the correct sign', function() { - expect(helpers.sign(0)).toBe(0); - expect(helpers.sign(10)).toBe(1); - expect(helpers.sign(-5)).toBe(-1); - }); - - it('should correctly determine if two numbers are essentially equal', function() { - expect(helpers.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true); - expect(helpers.almostEquals(1, 1.1, 0.0001)).toBe(false); - expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false); - expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); - }); - - it('should correctly determine if a numbers are essentially whole', function() { - expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true); - expect(helpers.almostWhole(0.9, 0.0001)).toBe(false); - expect(helpers.almostWhole(1234567890123, 0.0001)).toBe(true); - expect(helpers.almostWhole(1234567890123.001, 0.0001)).toBe(false); - }); - it('should generate integer ids', function() { var uid = helpers.uid(); expect(uid).toEqual(jasmine.any(Number)); @@ -48,270 +28,6 @@ describe('Core helper tests', function() { expect(helpers.uid()).toBe(uid + 3); }); - it('should detect a number', function() { - expect(helpers.isNumber(123)).toBe(true); - expect(helpers.isNumber('123')).toBe(true); - expect(helpers.isNumber(null)).toBe(false); - expect(helpers.isNumber(NaN)).toBe(false); - expect(helpers.isNumber(undefined)).toBe(false); - expect(helpers.isNumber('cbc')).toBe(false); - }); - - it('should convert between radians and degrees', function() { - expect(helpers.toRadians(180)).toBe(Math.PI); - expect(helpers.toRadians(90)).toBe(0.5 * Math.PI); - expect(helpers.toDegrees(Math.PI)).toBe(180); - expect(helpers.toDegrees(Math.PI * 3 / 2)).toBe(270); - }); - - it('should get the correct number of decimal places', function() { - expect(helpers._decimalPlaces(100)).toBe(0); - expect(helpers._decimalPlaces(1)).toBe(0); - expect(helpers._decimalPlaces(0)).toBe(0); - expect(helpers._decimalPlaces(0.01)).toBe(2); - expect(helpers._decimalPlaces(-0.01)).toBe(2); - expect(helpers._decimalPlaces('1')).toBe(undefined); - expect(helpers._decimalPlaces('')).toBe(undefined); - expect(helpers._decimalPlaces(undefined)).toBe(undefined); - expect(helpers._decimalPlaces(12345678.1234)).toBe(4); - expect(helpers._decimalPlaces(1234567890.1234567)).toBe(7); - }); - - it('should get an angle from a point', function() { - var center = { - x: 0, - y: 0 - }; - - expect(helpers.getAngleFromPoint(center, { - x: 0, - y: 10 - })).toEqual({ - angle: Math.PI / 2, - distance: 10, - }); - - expect(helpers.getAngleFromPoint(center, { - x: Math.sqrt(2), - y: Math.sqrt(2) - })).toEqual({ - angle: Math.PI / 4, - distance: 2 - }); - - expect(helpers.getAngleFromPoint(center, { - x: -1.0 * Math.sqrt(2), - y: -1.0 * Math.sqrt(2) - })).toEqual({ - angle: Math.PI * 1.25, - distance: 2 - }); - }); - - it('should spline curves', function() { - expect(helpers.splineCurve({ - x: 0, - y: 0 - }, { - x: 1, - y: 1 - }, { - x: 2, - y: 0 - }, 0)).toEqual({ - previous: { - x: 1, - y: 1, - }, - next: { - x: 1, - y: 1, - } - }); - - expect(helpers.splineCurve({ - x: 0, - y: 0 - }, { - x: 1, - y: 1 - }, { - x: 2, - y: 0 - }, 1)).toEqual({ - previous: { - x: 0, - y: 1, - }, - next: { - x: 2, - y: 1, - } - }); - }); - - it('should spline curves with monotone cubic interpolation', function() { - var dataPoints = [ - {_model: {x: 0, y: 0, skip: false}}, - {_model: {x: 3, y: 6, skip: false}}, - {_model: {x: 9, y: 6, skip: false}}, - {_model: {x: 12, y: 60, skip: false}}, - {_model: {x: 15, y: 60, skip: false}}, - {_model: {x: 18, y: 120, skip: false}}, - {_model: {x: null, y: null, skip: true}}, - {_model: {x: 21, y: 180, skip: false}}, - {_model: {x: 24, y: 120, skip: false}}, - {_model: {x: 27, y: 125, skip: false}}, - {_model: {x: 30, y: 105, skip: false}}, - {_model: {x: 33, y: 110, skip: false}}, - {_model: {x: 33, y: 110, skip: false}}, - {_model: {x: 36, y: 170, skip: false}} - ]; - helpers.splineCurveMonotone(dataPoints); - expect(dataPoints).toEqual([{ - _model: { - x: 0, - y: 0, - skip: false, - controlPointNextX: 1, - controlPointNextY: 2 - } - }, - { - _model: { - x: 3, - y: 6, - skip: false, - controlPointPreviousX: 2, - controlPointPreviousY: 6, - controlPointNextX: 5, - controlPointNextY: 6 - } - }, - { - _model: { - x: 9, - y: 6, - skip: false, - controlPointPreviousX: 7, - controlPointPreviousY: 6, - controlPointNextX: 10, - controlPointNextY: 6 - } - }, - { - _model: { - x: 12, - y: 60, - skip: false, - controlPointPreviousX: 11, - controlPointPreviousY: 60, - controlPointNextX: 13, - controlPointNextY: 60 - } - }, - { - _model: { - x: 15, - y: 60, - skip: false, - controlPointPreviousX: 14, - controlPointPreviousY: 60, - controlPointNextX: 16, - controlPointNextY: 60 - } - }, - { - _model: { - x: 18, - y: 120, - skip: false, - controlPointPreviousX: 17, - controlPointPreviousY: 100 - } - }, - { - _model: { - x: null, - y: null, - skip: true - } - }, - { - _model: { - x: 21, - y: 180, - skip: false, - controlPointNextX: 22, - controlPointNextY: 160 - } - }, - { - _model: { - x: 24, - y: 120, - skip: false, - controlPointPreviousX: 23, - controlPointPreviousY: 120, - controlPointNextX: 25, - controlPointNextY: 120 - } - }, - { - _model: { - x: 27, - y: 125, - skip: false, - controlPointPreviousX: 26, - controlPointPreviousY: 125, - controlPointNextX: 28, - controlPointNextY: 125 - } - }, - { - _model: { - x: 30, - y: 105, - skip: false, - controlPointPreviousX: 29, - controlPointPreviousY: 105, - controlPointNextX: 31, - controlPointNextY: 105 - } - }, - { - _model: { - x: 33, - y: 110, - skip: false, - controlPointPreviousX: 32, - controlPointPreviousY: 110, - controlPointNextX: 33, - controlPointNextY: 110 - } - }, - { - _model: { - x: 33, - y: 110, - skip: false, - controlPointPreviousX: 33, - controlPointPreviousY: 110, - controlPointNextX: 34, - controlPointNextY: 110 - } - }, - { - _model: { - x: 36, - y: 170, - skip: false, - controlPointPreviousX: 35, - controlPointPreviousY: 150 - } - }]); - }); - it('should return the width of the longest text in an Array and 2D Array', function() { var context = window.createMockContext(); var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; @@ -363,257 +79,6 @@ describe('Core helper tests', function() { }]); }); - it ('should get the maximum width and height for a node', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create the div we want to get the max size for - var innerDiv = document.createElement('div'); - div.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(200); - expect(helpers.getMaximumHeight(innerDiv)).toBe(300); - - document.body.removeChild(div); - }); - - it ('should get the maximum width and height for a node in a ShadowRoot', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - if (!div.attachShadow) { - // Shadow DOM is not natively supported - return; - } - - var shadow = div.attachShadow({mode: 'closed'}); - - // Create the div we want to get the max size for - var innerDiv = document.createElement('div'); - shadow.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(200); - expect(helpers.getMaximumHeight(innerDiv)).toBe(300); - - document.body.removeChild(div); - }); - - it ('should get the maximum width of a node that has a max-width style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create the div we want to get the max size for and set a max-width style - var innerDiv = document.createElement('div'); - innerDiv.style.maxWidth = '150px'; - div.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should get the maximum height of a node that has a max-height style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create the div we want to get the max size for and set a max-height style - var innerDiv = document.createElement('div'); - innerDiv.style.maxHeight = '150px'; - div.appendChild(innerDiv); - - expect(helpers.getMaximumHeight(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should get the maximum width of a node when the parent has a max-width style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create an inner wrapper around our div we want to size and give that a max-width style - var parentDiv = document.createElement('div'); - parentDiv.style.maxWidth = '150px'; - div.appendChild(parentDiv); - - // Create the div we want to get the max size for - var innerDiv = document.createElement('div'); - parentDiv.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should get the maximum height of a node when the parent has a max-height style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create an inner wrapper around our div we want to size and give that a max-height style - var parentDiv = document.createElement('div'); - parentDiv.style.maxHeight = '150px'; - div.appendChild(parentDiv); - - // Create the div we want to get the max size for - var innerDiv = document.createElement('div'); - innerDiv.style.height = '300px'; // make it large - parentDiv.appendChild(innerDiv); - - expect(helpers.getMaximumHeight(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should get the maximum width of a node that has a percentage max-width style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create the div we want to get the max size for and set a max-width style - var innerDiv = document.createElement('div'); - innerDiv.style.maxWidth = '50%'; - div.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(100); - - document.body.removeChild(div); - }); - - it ('should get the maximum height of a node that has a percentage max-height style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create the div we want to get the max size for and set a max-height style - var innerDiv = document.createElement('div'); - innerDiv.style.maxHeight = '50%'; - div.appendChild(innerDiv); - - expect(helpers.getMaximumHeight(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should get the maximum width of a node when the parent has a percentage max-width style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create an inner wrapper around our div we want to size and give that a max-width style - var parentDiv = document.createElement('div'); - parentDiv.style.maxWidth = '50%'; - div.appendChild(parentDiv); - - // Create the div we want to get the max size for - var innerDiv = document.createElement('div'); - parentDiv.appendChild(innerDiv); - - expect(helpers.getMaximumWidth(innerDiv)).toBe(100); - - document.body.removeChild(div); - }); - - it ('should get the maximum height of a node when the parent has a percentage max-height style', function() { - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.height = '300px'; - - document.body.appendChild(div); - - // Create an inner wrapper around our div we want to size and give that a max-height style - var parentDiv = document.createElement('div'); - parentDiv.style.maxHeight = '50%'; - div.appendChild(parentDiv); - - var innerDiv = document.createElement('div'); - innerDiv.style.height = '300px'; // make it large - parentDiv.appendChild(innerDiv); - - expect(helpers.getMaximumHeight(innerDiv)).toBe(150); - - document.body.removeChild(div); - }); - - it ('should leave styled height and width on canvas if explicitly set', function() { - var chart = window.acquireChart({}, { - canvas: { - height: 200, - width: 200, - style: 'height: 400px; width: 400px;' - } - }); - - helpers.retinaScale(chart, true); - - var canvas = chart.canvas; - - expect(canvas.style.height).toBe('400px'); - expect(canvas.style.width).toBe('400px'); - }); - - it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() { - - // Create div with fixed size as a test bed - var div = document.createElement('div'); - div.style.width = '300px'; - div.style.height = '300px'; - document.body.appendChild(div); - - // Inner DIV to have 5% padding of parent - var innerDiv = document.createElement('div'); - - div.appendChild(innerDiv); - - var canvas = document.createElement('canvas'); - innerDiv.appendChild(canvas); - - // No padding - expect(helpers.getMaximumWidth(canvas)).toBe(300); - - // test with percentage - innerDiv.style.padding = '5%'; - expect(helpers.getMaximumWidth(canvas)).toBe(270); - - // test with pixels - innerDiv.style.padding = '10px'; - expect(helpers.getMaximumWidth(canvas)).toBe(280); - - document.body.removeChild(div); - }); - describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, 'values') && Object.prototype.hasOwnProperty.call(obj.values, 'rgb'); @@ -622,13 +87,6 @@ describe('Core helper tests', function() { it('should return a color when called with a color', function() { expect(isColorInstance(helpers.color('rgb(1, 2, 3)'))).toBe(true); }); - - it('should return a color when called with a CanvasGradient instance', function() { - var context = document.createElement('canvas').getContext('2d'); - var gradient = context.createLinearGradient(0, 1, 2, 3); - - expect(isColorInstance(helpers.color(gradient))).toBe(true); - }); }); describe('Background hover color helper', function() { diff --git a/test/specs/helpers.curve.tests.js b/test/specs/helpers.curve.tests.js new file mode 100644 index 000000000..fea8adccc --- /dev/null +++ b/test/specs/helpers.curve.tests.js @@ -0,0 +1,211 @@ +describe('Curve helper tests', function() { + let helpers; + + beforeAll(function() { + helpers = window.Chart.helpers.curve; + }); + + it('should spline curves', function() { + expect(helpers.splineCurve({ + x: 0, + y: 0 + }, { + x: 1, + y: 1 + }, { + x: 2, + y: 0 + }, 0)).toEqual({ + previous: { + x: 1, + y: 1, + }, + next: { + x: 1, + y: 1, + } + }); + + expect(helpers.splineCurve({ + x: 0, + y: 0 + }, { + x: 1, + y: 1 + }, { + x: 2, + y: 0 + }, 1)).toEqual({ + previous: { + x: 0, + y: 1, + }, + next: { + x: 2, + y: 1, + } + }); + }); + + it('should spline curves with monotone cubic interpolation', function() { + var dataPoints = [ + {_model: {x: 0, y: 0, skip: false}}, + {_model: {x: 3, y: 6, skip: false}}, + {_model: {x: 9, y: 6, skip: false}}, + {_model: {x: 12, y: 60, skip: false}}, + {_model: {x: 15, y: 60, skip: false}}, + {_model: {x: 18, y: 120, skip: false}}, + {_model: {x: null, y: null, skip: true}}, + {_model: {x: 21, y: 180, skip: false}}, + {_model: {x: 24, y: 120, skip: false}}, + {_model: {x: 27, y: 125, skip: false}}, + {_model: {x: 30, y: 105, skip: false}}, + {_model: {x: 33, y: 110, skip: false}}, + {_model: {x: 33, y: 110, skip: false}}, + {_model: {x: 36, y: 170, skip: false}} + ]; + helpers.splineCurveMonotone(dataPoints); + expect(dataPoints).toEqual([{ + _model: { + x: 0, + y: 0, + skip: false, + controlPointNextX: 1, + controlPointNextY: 2 + } + }, + { + _model: { + x: 3, + y: 6, + skip: false, + controlPointPreviousX: 2, + controlPointPreviousY: 6, + controlPointNextX: 5, + controlPointNextY: 6 + } + }, + { + _model: { + x: 9, + y: 6, + skip: false, + controlPointPreviousX: 7, + controlPointPreviousY: 6, + controlPointNextX: 10, + controlPointNextY: 6 + } + }, + { + _model: { + x: 12, + y: 60, + skip: false, + controlPointPreviousX: 11, + controlPointPreviousY: 60, + controlPointNextX: 13, + controlPointNextY: 60 + } + }, + { + _model: { + x: 15, + y: 60, + skip: false, + controlPointPreviousX: 14, + controlPointPreviousY: 60, + controlPointNextX: 16, + controlPointNextY: 60 + } + }, + { + _model: { + x: 18, + y: 120, + skip: false, + controlPointPreviousX: 17, + controlPointPreviousY: 100 + } + }, + { + _model: { + x: null, + y: null, + skip: true + } + }, + { + _model: { + x: 21, + y: 180, + skip: false, + controlPointNextX: 22, + controlPointNextY: 160 + } + }, + { + _model: { + x: 24, + y: 120, + skip: false, + controlPointPreviousX: 23, + controlPointPreviousY: 120, + controlPointNextX: 25, + controlPointNextY: 120 + } + }, + { + _model: { + x: 27, + y: 125, + skip: false, + controlPointPreviousX: 26, + controlPointPreviousY: 125, + controlPointNextX: 28, + controlPointNextY: 125 + } + }, + { + _model: { + x: 30, + y: 105, + skip: false, + controlPointPreviousX: 29, + controlPointPreviousY: 105, + controlPointNextX: 31, + controlPointNextY: 105 + } + }, + { + _model: { + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 32, + controlPointPreviousY: 110, + controlPointNextX: 33, + controlPointNextY: 110 + } + }, + { + _model: { + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 33, + controlPointPreviousY: 110, + controlPointNextX: 34, + controlPointNextY: 110 + } + }, + { + _model: { + x: 36, + y: 170, + skip: false, + controlPointPreviousX: 35, + controlPointPreviousY: 150 + } + }]); + }); +}); diff --git a/test/specs/helpers.dom.tests.js b/test/specs/helpers.dom.tests.js new file mode 100644 index 000000000..ab614deee --- /dev/null +++ b/test/specs/helpers.dom.tests.js @@ -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'); + }); + +}); diff --git a/test/specs/helpers.math.tests.js b/test/specs/helpers.math.tests.js index 0006a3dd1..87ff63b16 100644 --- a/test/specs/helpers.math.tests.js +++ b/test/specs/helpers.math.tests.js @@ -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); + }); });