diff --git a/.gitignore b/.gitignore index 0853b7efd..53ce8fedb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /dist /docs/index.md /gh-pages +/jsdoc /node_modules .DS_Store .idea diff --git a/src/chart.js b/src/chart.js index f36d240c2..517224c93 100644 --- a/src/chart.js +++ b/src/chart.js @@ -3,11 +3,12 @@ */ var Chart = require('./core/core.js')(); +require('./helpers/helpers.core')(Chart); require('./core/core.helpers')(Chart); require('./helpers/helpers.time')(Chart); +require('./helpers/helpers.canvas')(Chart); require('./platforms/platform.js')(Chart); -require('./core/core.canvasHelpers')(Chart); require('./core/core.element')(Chart); require('./core/core.plugin.js')(Chart); require('./core/core.animation')(Chart); diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 1a26a04b7..169510954 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -285,13 +285,13 @@ module.exports = function(Chart) { var ilen = points.length; var i = 0; - Chart.canvasHelpers.clipArea(chart.ctx, area); + Chart.helpers.canvas.clipArea(chart.ctx, area); if (lineEnabled(me.getDataset(), chart.options)) { meta.dataset.draw(); } - Chart.canvasHelpers.unclipArea(chart.ctx); + Chart.helpers.canvas.unclipArea(chart.ctx); // Draw the points for (; i= 0; i--) { - callback.call(self, loopable[i], i); - } - } else { - for (i = 0; i < len; i++) { - callback.call(self, loopable[i], i); - } - } - } else if (typeof loopable === 'object') { - var keys = Object.keys(loopable); - len = keys.length; - for (i = 0; i < len; i++) { - callback.call(self, loopable[keys[i]], keys[i]); - } - } - }; helpers.clone = function(obj) { var objClone = {}; helpers.each(obj, function(value, key) { @@ -123,32 +100,7 @@ module.exports = function(Chart) { return base; }; - helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) { - if (value === undefined || value === null) { - return defaultValue; - } - if (helpers.isArray(value)) { - return index < value.length ? value[index] : defaultValue; - } - - return value; - }; - helpers.getValueOrDefault = function(value, defaultValue) { - return value === undefined ? defaultValue : value; - }; - helpers.indexOf = Array.prototype.indexOf? - function(array, item) { - return array.indexOf(item); - }: - function(array, item) { - for (var i = 0, ilen = array.length; i < ilen; ++i) { - if (array[i] === item) { - return i; - } - } - return -1; - }; helpers.where = function(collection, filterCallback) { if (helpers.isArray(collection) && Array.prototype.filter) { return collection.filter(filterCallback); @@ -178,7 +130,7 @@ module.exports = function(Chart) { }; helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) { // Default to start of the array - if (startIndex === undefined || startIndex === null) { + if (helpers.isNullOrUndef(startIndex)) { startIndex = -1; } for (var i = startIndex + 1; i < arrayToSearch.length; i++) { @@ -190,7 +142,7 @@ module.exports = function(Chart) { }; helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) { // Default to end of the array - if (startIndex === undefined || startIndex === null) { + if (helpers.isNullOrUndef(startIndex)) { startIndex = arrayToSearch.length; } for (var i = startIndex - 1; i >= 0; i--) { @@ -223,13 +175,6 @@ module.exports = function(Chart) { return ChartElement; }; - helpers.noop = function() {}; - helpers.uid = (function() { - var id = 0; - return function() { - return id++; - }; - }()); // -- Math methods helpers.isNumber = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); @@ -837,9 +782,6 @@ module.exports = function(Chart) { canvas.style.width = width + 'px'; }; // -- Canvas methods - helpers.clear = function(chart) { - chart.ctx.clearRect(0, 0, chart.width, chart.height); - }; helpers.fontString = function(pixelSize, fontStyle, fontFamily) { return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; }; @@ -903,19 +845,6 @@ module.exports = function(Chart) { }); return numberOfLines; }; - helpers.drawRoundedRectangle = function(ctx, x, y, width, height, radius) { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }; helpers.color = !color? function(value) { @@ -931,54 +860,10 @@ module.exports = function(Chart) { return color(value); }; - helpers.isArray = Array.isArray? - function(obj) { - return Array.isArray(obj); - } : - function(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; - }; - // ! @see http://stackoverflow.com/a/14853974 - helpers.arrayEquals = function(a0, a1) { - var i, ilen, v0, v1; - - if (!a0 || !a1 || a0.length !== a1.length) { - return false; - } - - for (i = 0, ilen=a0.length; i < ilen; ++i) { - v0 = a0[i]; - v1 = a1[i]; - - if (v0 instanceof Array && v1 instanceof Array) { - if (!helpers.arrayEquals(v0, v1)) { - return false; - } - } else if (v0 !== v1) { - // NOTE: two different object instances will never be equal: {x:20} != {x:20} - return false; - } - } - - return true; - }; - helpers.callback = function(fn, args, thisArg) { - if (fn && typeof fn.call === 'function') { - return fn.apply(thisArg, args); - } - }; helpers.getHoverColor = function(colorValue) { /* global CanvasPattern */ return (colorValue instanceof CanvasPattern) ? colorValue : helpers.color(colorValue).saturate(0.5).darken(0.1).rgbString(); }; - - /** - * Provided for backward compatibility, use Chart.helpers#callback instead. - * @function Chart.helpers#callCallback - * @deprecated since version 2.6.0 - * @todo remove at version 3 - */ - helpers.callCallback = helpers.callback; }; diff --git a/src/core/core.scale.js b/src/core/core.scale.js index be69ed908..48c99bc6c 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -422,7 +422,7 @@ module.exports = function(Chart) { // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not getRightValue: function(rawValue) { // Null and undefined values first - if (rawValue === null || typeof(rawValue) === 'undefined') { + if (helpers.isNullOrUndef(rawValue)) { return NaN; } // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values @@ -575,7 +575,7 @@ module.exports = function(Chart) { helpers.each(me.ticks, function(tick, index) { var label = (tick && tick.value) || tick; // If the callback returned a null or undefined value, do not draw this line - if (label === undefined || label === null) { + if (helpers.isNullOrUndef(label)) { return; } @@ -583,7 +583,7 @@ module.exports = function(Chart) { // Since we always show the last tick,we need may need to hide the last shown one before var shouldSkip = (skipRatio > 1 && index % skipRatio > 0) || (index % skipRatio === 0 && index + skipRatio >= me.ticks.length); - if (shouldSkip && !isLastTick || (label === undefined || label === null)) { + if (shouldSkip && !isLastTick || helpers.isNullOrUndef(label)) { return; } diff --git a/src/elements/element.point.js b/src/elements/element.point.js index c68edc786..b07581953 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -94,7 +94,7 @@ module.exports = function(Chart) { ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); } - Chart.canvasHelpers.drawPoint(ctx, pointStyle, radius, x, y); + Chart.helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } }); }; diff --git a/src/core/core.canvasHelpers.js b/src/helpers/helpers.canvas.js similarity index 55% rename from src/core/core.canvasHelpers.js rename to src/helpers/helpers.canvas.js index 610095122..917b29615 100644 --- a/src/core/core.canvasHelpers.js +++ b/src/helpers/helpers.canvas.js @@ -1,10 +1,50 @@ 'use strict'; module.exports = function(Chart) { - // Global Chart canvas helpers object for drawing items to canvas - var helpers = Chart.canvasHelpers = {}; + var helpers = Chart.helpers; - helpers.drawPoint = function(ctx, pointStyle, radius, x, y) { + /** + * @namespace Chart.helpers.canvas + */ + helpers.canvas = { + /** + * Clears the entire canvas associated to the given `chart`. + * @param {Chart} chart - The chart for which to clear the canvas. + */ + clear: function(chart) { + chart.ctx.clearRect(0, 0, chart.width, chart.height); + }, + + /** + * Creates a "path" for a rectangle with rounded corners at position (x, y) with a + * given size (width, height) and the same `radius` for all corners. + * @param {CanvasRenderingContext2D} ctx - The canvas 2D Context. + * @param {Number} x - The x axis of the coordinate for the rectangle starting point. + * @param {Number} y - The y axis of the coordinate for the rectangle starting point. + * @param {Number} width - The rectangle's width. + * @param {Number} height - The rectangle's height. + * @param {Number} radius - The rounded amount (in pixels) for the four corners. + * @todo handler `radius` as top-left, top-right, bottom-right, bottom-left array/object? + * @todo clamp `radius` to the maximum "correct" value. + */ + roundedRect: function(ctx, x, y, width, height, radius) { + if (radius) { + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + } else { + ctx.rect(x, y, width, height); + } + } + }; + + helpers.canvas.drawPoint = function(ctx, pointStyle, radius, x, y) { var type, edgeLength, xOffset, yOffset, height, size; if (typeof pointStyle === 'object') { @@ -48,7 +88,9 @@ module.exports = function(Chart) { var leftX = x - offset; var topY = y - offset; var sideSize = Math.SQRT2 * radius; - Chart.helpers.drawRoundedRectangle(ctx, leftX, topY, sideSize, sideSize, radius / 2); + ctx.beginPath(); + this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius / 2); + ctx.closePath(); ctx.fill(); break; case 'rectRot': @@ -110,18 +152,18 @@ module.exports = function(Chart) { ctx.stroke(); }; - helpers.clipArea = function(ctx, clipArea) { + helpers.canvas.clipArea = function(ctx, clipArea) { ctx.save(); ctx.beginPath(); ctx.rect(clipArea.left, clipArea.top, clipArea.right - clipArea.left, clipArea.bottom - clipArea.top); ctx.clip(); }; - helpers.unclipArea = function(ctx) { + helpers.canvas.unclipArea = function(ctx) { ctx.restore(); }; - helpers.lineTo = function(ctx, previous, target, flip) { + helpers.canvas.lineTo = function(ctx, previous, target, flip) { if (target.steppedLine) { if (target.steppedLine === 'after') { ctx.lineTo(previous.x, target.y); @@ -146,5 +188,34 @@ module.exports = function(Chart) { target.y); }; - Chart.helpers.canvas = helpers; + /** + * Provided for backward compatibility, use Chart.helpers.canvas instead. + * @namespace Chart.canvasHelpers + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + Chart.canvasHelpers = helpers.canvas; + + /** + * Provided for backward compatibility, use Chart.helpers.canvas.clear instead. + * @namespace Chart.helpers.clear + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.clear = helpers.canvas.clear; + + /** + * Provided for backward compatibility, use Chart.helpers.canvas.roundedRect instead. + * @namespace Chart.helpers.drawRoundedRectangle + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.drawRoundedRectangle = function(ctx) { + ctx.beginPath(); + helpers.canvas.roundedRect.apply(this, arguments); + ctx.closePath(); + }; }; diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js new file mode 100644 index 000000000..3d2a7c86b --- /dev/null +++ b/src/helpers/helpers.core.js @@ -0,0 +1,182 @@ +'use strict'; + +module.exports = function(Chart) { + /** + * @namespace Chart.helpers + */ + var helpers = Chart.helpers = { + /** + * An empty function that can be used, for example, for optional callback. + */ + noop: function() {}, + + /** + * Returns a unique id, sequentially generated from a global variable. + * @returns {Number} + * @function + */ + uid: (function() { + var id = 0; + return function() { + return id++; + }; + }()), + + /** + * Returns true if `value` is neither null nor undefined, else returns false. + * @param {*} value - The value to test. + * @returns {Boolean} + * @since 2.7.0 + */ + isNullOrUndef: function(value) { + return value === null || typeof value === 'undefined'; + }, + + /** + * Returns true if `value` is an array, else returns false. + * @param {*} value - The value to test. + * @returns {Boolean} + * @function + */ + isArray: Array.isArray? Array.isArray : function(value) { + return Object.prototype.toString.call(value) === '[object Array]'; + }, + + /** + * Returns true if `value` is an object (excluding null), else returns false. + * @param {*} value - The value to test. + * @returns {Boolean} + * @since 2.7.0 + */ + isObject: function(value) { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; + }, + + /** + * Returns `value` if defined, else returns `defaultValue`. + * @param {*} value - The value to return if defined. + * @param {*} defaultValue - The value to return if `value` is undefined. + * @returns {*} + */ + getValueOrDefault: function(value, defaultValue) { + return typeof value === 'undefined'? defaultValue : value; + }, + + /** + * Returns value at the given `index` in array if defined, else returns `defaultValue`. + * @param {Array} value - The array to lookup for value at `index`. + * @param {Number} index - The index in `value` to lookup for value. + * @param {*} defaultValue - The value to return if `value[index]` is undefined. + * @returns {*} + */ + getValueAtIndexOrDefault: function(value, index, defaultValue) { + if (helpers.isNullOrUndef(value)) { + return defaultValue; + } + + if (helpers.isArray(value)) { + value = value[index]; + return typeof value === 'undefined'? defaultValue : value; + } + + return value; + }, + + /** + * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the + * value returned by `fn`. If `fn` is not a function, this method returns undefined. + * @param {Function} fn - The function to call. + * @param {Array|undefined|null} args - The arguments with which `fn` should be called. + * @param {Object} [thisArg] - The value of `this` provided for the call to `fn`. + * @returns {*} + */ + callback: function(fn, args, thisArg) { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } + }, + + /** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param {Object|Array} loopable - The object or array to be iterated. + * @param {Function} fn - The function to call for each item. + * @param {Object} [thisArg] - The value of `this` provided for the call to `fn`. + * @param {Boolean} [reverse] - If true, iterates backward on the loopable. + */ + each: function(loopable, fn, thisArg, reverse) { + var i, len, keys; + if (helpers.isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (helpers.isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } + }, + + /** + * Returns true if the `a0` and `a1` arrays have the same content, else returns false. + * @see http://stackoverflow.com/a/14853974 + * @param {Array} a0 - The array to compare + * @param {Array} a1 - The array to compare + * @returns {Boolean} + */ + arrayEquals: function(a0, a1) { + var i, ilen, v0, v1; + + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + + for (i = 0, ilen=a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + + if (v0 instanceof Array && v1 instanceof Array) { + if (!helpers.arrayEquals(v0, v1)) { + return false; + } + } else if (v0 !== v1) { + // NOTE: two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + + return true; + } + }; + + /** + * Provided for backward compatibility, use Chart.helpers.callback instead. + * @function Chart.helpers.callCallback + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + helpers.callCallback = helpers.callback; + + /** + * Provided for backward compatibility, use Array.prototype.indexOf instead. + * Array.prototype.indexOf compatibility: Chrome, Opera, Safari, FF1.5+, IE9+ + * @function Chart.helpers.indexOf + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.indexOf = function(array, item, fromIndex) { + return Array.prototype.indexOf.call(array, item, fromIndex); + }; +}; diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index d498bc854..09c23820b 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -229,7 +229,7 @@ module.exports = function(Chart) { var initial = canvas._chartjs.initial; ['height', 'width'].forEach(function(prop) { var value = initial[prop]; - if (value === undefined || value === null) { + if (helpers.isNullOrUndef(value)) { canvas.removeAttribute(prop); } else { canvas.setAttribute(prop, value); diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 588e20600..0d6344835 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -360,7 +360,7 @@ module.exports = function(Chart) { var centerY = y + offSet; // Draw pointStyle as legend symbol - Chart.canvasHelpers.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + Chart.helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); } else { // Draw box as legend symbol if (!isLineWidthZero) { diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index cc5478adf..6b1532c17 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -2,7 +2,6 @@ module.exports = function(Chart) { - var helpers = Chart.helpers; // Default config for a category scale var defaultConfig = { position: 'bottom' @@ -28,13 +27,13 @@ module.exports = function(Chart) { if (me.options.ticks.min !== undefined) { // user specified min value - findIndex = helpers.indexOf(labels, me.options.ticks.min); + findIndex = labels.indexOf(me.options.ticks.min); me.minIndex = findIndex !== -1 ? findIndex : me.minIndex; } if (me.options.ticks.max !== undefined) { // user specified max value - findIndex = helpers.indexOf(labels, me.options.ticks.max); + findIndex = labels.indexOf(me.options.ticks.max); me.maxIndex = findIndex !== -1 ? findIndex : me.maxIndex; } diff --git a/test/jasmine.context.js b/test/jasmine.context.js index 814c24622..8f4171fee 100644 --- a/test/jasmine.context.js +++ b/test/jasmine.context.js @@ -89,6 +89,7 @@ Context.prototype._initMethods = function() { }, moveTo: function() {}, quadraticCurveTo: function() {}, + rect: function() {}, restore: function() {}, rotate: function() {}, save: function() {}, diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 7777c9b16..c1d1e8710 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -6,59 +6,6 @@ describe('Core helper tests', function() { helpers = window.Chart.helpers; }); - it('should iterate over an array and pass the extra data to that function', function() { - var testData = [0, 9, 'abc']; - var scope = {}; // fake out the scope and ensure that 'this' is the correct thing - - helpers.each(testData, function(item, index) { - expect(item).not.toBe(undefined); - expect(index).not.toBe(undefined); - - expect(testData[index]).toBe(item); - expect(this).toBe(scope); - }, scope); - - // Reverse iteration - var iterated = []; - helpers.each(testData, function(item, index) { - expect(item).not.toBe(undefined); - expect(index).not.toBe(undefined); - - expect(testData[index]).toBe(item); - expect(this).toBe(scope); - - iterated.push(item); - }, scope, true); - - expect(iterated.slice().reverse()).toEqual(testData); - }); - - it('should iterate over properties in an object', function() { - var testData = { - myProp1: 'abc', - myProp2: 276, - myProp3: ['a', 'b'] - }; - - helpers.each(testData, function(value, key) { - if (key === 'myProp1') { - expect(value).toBe('abc'); - } else if (key === 'myProp2') { - expect(value).toBe(276); - } else if (key === 'myProp3') { - expect(value).toEqual(['a', 'b']); - } else { - expect(false).toBe(true); - } - }); - }); - - it('should not error when iterating over a null object', function() { - expect(function() { - helpers.each(undefined); - }).not.toThrow(); - }); - it('should clone an object', function() { var testData = { myProp1: 'abc', @@ -268,14 +215,6 @@ describe('Core helper tests', function() { expect(merged.scales.yAxes[2].ticks.callback).toEqual(jasmine.any(Function)); }); - it('should get value or default', function() { - expect(helpers.getValueAtIndexOrDefault(98, 0, 56)).toBe(98); - expect(helpers.getValueAtIndexOrDefault(0, 0, 56)).toBe(0); - expect(helpers.getValueAtIndexOrDefault(undefined, undefined, 56)).toBe(56); - expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 1, 100)).toBe(2); - expect(helpers.getValueAtIndexOrDefault([1, 2, 3], 3, 100)).toBe(100); - }); - it('should filter an array', function() { var data = [-10, 0, 6, 0, 7]; var callback = function(item) { @@ -588,20 +527,6 @@ describe('Core helper tests', function() { expect(helpers.previousItem(testData, 1, true)).toEqual(0); }); - it('should clear a canvas', function() { - var context = window.createMockContext(); - helpers.clear({ - width: 100, - height: 150, - ctx: context - }); - - expect(context.getCalls()).toEqual([{ - name: 'clearRect', - args: [0, 0, 100, 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"; @@ -664,46 +589,6 @@ describe('Core helper tests', function() { expect(helpers.numberOfLabelLines(arrayOfThings3)).toEqual(3); }); - it('should draw a rounded rectangle', function() { - var context = window.createMockContext(); - helpers.drawRoundedRectangle(context, 10, 20, 30, 40, 5); - - expect(context.getCalls()).toEqual([{ - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [15, 20] - }, { - name: 'lineTo', - args: [35, 20] - }, { - name: 'quadraticCurveTo', - args: [40, 20, 40, 25] - }, { - name: 'lineTo', - args: [40, 55] - }, { - name: 'quadraticCurveTo', - args: [40, 60, 35, 60] - }, { - name: 'lineTo', - args: [15, 60] - }, { - name: 'quadraticCurveTo', - args: [10, 60, 10, 55] - }, { - name: 'lineTo', - args: [10, 25] - }, { - name: 'quadraticCurveTo', - args: [10, 20, 15, 20] - }, { - name: 'closePath', - args: [] - }]); - }); - 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'); diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 01eba3046..f09b912d3 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -209,9 +209,9 @@ describe('Point element tests', function() { }]); var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); - var drawRoundedRectangle = Chart.helpers.drawRoundedRectangle; + var drawRoundedRectangle = Chart.helpers.canvas.roundedRect; var offset = point._view.radius / Math.SQRT2; - Chart.helpers.drawRoundedRectangle = drawRoundedRectangleSpy; + Chart.helpers.canvas.roundedRect = drawRoundedRectangleSpy; mockContext.resetCalls(); point._view.pointStyle = 'rectRounded'; point.draw(); @@ -231,7 +231,7 @@ describe('Point element tests', function() { }) ); - Chart.helpers.drawRoundedRectangle = drawRoundedRectangle; + Chart.helpers.canvas.roundedRect = drawRoundedRectangle; mockContext.resetCalls(); point._view.pointStyle = 'rectRot'; point.draw(); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 917c1529a..50a5cc053 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,8 +1,8 @@ describe('Deprecations', function() { describe('Version 2.7.0', function() { describe('Chart.Controller.update(duration, lazy)', function() { - beforeEach(function() { - this.chart = acquireChart({ + it('should add an animation with the provided options', function() { + var chart = acquireChart({ type: 'doughnut', options: { animation: { @@ -12,14 +12,12 @@ describe('Deprecations', function() { } }); - this.addAnimationSpy = spyOn(Chart.animationService, 'addAnimation'); - }); + spyOn(Chart.animationService, 'addAnimation'); - it('should add an animation with the provided options', function() { - this.chart.update(800, false); + chart.update(800, false); - expect(this.addAnimationSpy).toHaveBeenCalledWith( - this.chart, + expect(Chart.animationService.addAnimation).toHaveBeenCalledWith( + chart, jasmine.objectContaining({easing: 'linear'}), 800, false @@ -28,8 +26,8 @@ describe('Deprecations', function() { }); describe('Chart.Controller.render(duration, lazy)', function() { - beforeEach(function() { - this.chart = acquireChart({ + it('should add an animation with the provided options', function() { + var chart = acquireChart({ type: 'doughnut', options: { animation: { @@ -39,20 +37,56 @@ describe('Deprecations', function() { } }); - this.addAnimationSpy = spyOn(Chart.animationService, 'addAnimation'); - }); + spyOn(Chart.animationService, 'addAnimation'); - it('should add an animation with the provided options', function() { - this.chart.render(800, true); + chart.render(800, true); - expect(this.addAnimationSpy).toHaveBeenCalledWith( - this.chart, + expect(Chart.animationService.addAnimation).toHaveBeenCalledWith( + chart, jasmine.objectContaining({easing: 'linear'}), 800, true ); }); }); + + describe('Chart.helpers.indexOf', function() { + it('should be defined and a function', function() { + expect(Chart.helpers.indexOf).toBeDefined(); + expect(typeof Chart.helpers.indexOf).toBe('function'); + }); + it('should returns the correct index', function() { + expect(Chart.helpers.indexOf([1, 2, 42], 42)).toBe(2); + expect(Chart.helpers.indexOf([1, 2, 42], 3)).toBe(-1); + expect(Chart.helpers.indexOf([1, 42, 2, 42], 42, 2)).toBe(3); + expect(Chart.helpers.indexOf([1, 42, 2, 42], 3, 2)).toBe(-1); + }); + }); + + describe('Chart.helpers.clear', function() { + it('should be defined and an alias of Chart.helpers.canvas.clear', function() { + expect(Chart.helpers.clear).toBeDefined(); + expect(Chart.helpers.clear).toBe(Chart.helpers.canvas.clear); + }); + }); + + describe('Chart.helpers.drawRoundedRectangle', function() { + it('should be defined and a function', function() { + expect(Chart.helpers.drawRoundedRectangle).toBeDefined(); + expect(typeof Chart.helpers.drawRoundedRectangle).toBe('function'); + }); + it('should call Chart.helpers.canvas.roundedRect', function() { + var ctx = window.createMockContext(); + spyOn(Chart.helpers.canvas, 'roundedRect'); + + Chart.helpers.drawRoundedRectangle(ctx, 10, 20, 30, 40, 5); + + var calls = ctx.getCalls(); + expect(calls[0]).toEqual({name: 'beginPath', args: []}); + expect(calls[calls.length-1]).toEqual({name: 'closePath', args: []}); + expect(Chart.helpers.canvas.roundedRect).toHaveBeenCalledWith(ctx, 10, 20, 30, 40, 5); + }); + }); }); describe('Version 2.6.0', function() { @@ -163,6 +197,13 @@ describe('Deprecations', function() { }); }); }); + + describe('Chart.helpers.callCallback', function() { + it('should be defined and an alias of Chart.helpers.callback', function() { + expect(Chart.helpers.callCallback).toBeDefined(); + expect(Chart.helpers.callCallback).toBe(Chart.helpers.callback); + }); + }); }); describe('Version 2.5.0', function() { diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js new file mode 100644 index 000000000..81326529b --- /dev/null +++ b/test/specs/helpers.canvas.tests.js @@ -0,0 +1,50 @@ +'use strict'; + +describe('Chart.helpers.canvas', function() { + var helpers = Chart.helpers; + + describe('clear', function() { + it('should clear the chart canvas', function() { + var chart = acquireChart({}, { + canvas: { + style: 'width: 150px; height: 245px' + } + }); + + spyOn(chart.ctx, 'clearRect'); + + helpers.canvas.clear(chart); + + expect(chart.ctx.clearRect.calls.count()).toBe(1); + expect(chart.ctx.clearRect.calls.first().object).toBe(chart.ctx); + expect(chart.ctx.clearRect.calls.first().args).toEqual([0, 0, 150, 245]); + }); + }); + + describe('roundedRect', function() { + it('should create a rounded rectangle path', function() { + var context = window.createMockContext(); + + helpers.canvas.roundedRect(context, 10, 20, 30, 40, 5); + + expect(context.getCalls()).toEqual([ + {name: 'moveTo', args: [15, 20]}, + {name: 'lineTo', args: [35, 20]}, + {name: 'quadraticCurveTo', args: [40, 20, 40, 25]}, + {name: 'lineTo', args: [40, 55]}, + {name: 'quadraticCurveTo', args: [40, 60, 35, 60]}, + {name: 'lineTo', args: [15, 60]}, + {name: 'quadraticCurveTo', args: [10, 60, 10, 55]}, + {name: 'lineTo', args: [10, 25]}, + {name: 'quadraticCurveTo', args: [10, 20, 15, 20]} + ]); + }); + it('should optimize path if radius is 0', function() { + var context = window.createMockContext(); + + helpers.canvas.roundedRect(context, 10, 20, 30, 40, 0); + + expect(context.getCalls()).toEqual([{name: 'rect', args: [10, 20, 30, 40]}]); + }); + }); +}); diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js new file mode 100644 index 000000000..9bb5f5f29 --- /dev/null +++ b/test/specs/helpers.core.tests.js @@ -0,0 +1,239 @@ +'use strict'; + +describe('Chart.helpers.core', function() { + var helpers = Chart.helpers; + + describe('noop', function() { + it('should be callable', function() { + expect(helpers.noop).toBeDefined(); + expect(typeof helpers.noop).toBe('function'); + expect(typeof helpers.noop.call).toBe('function'); + }); + it('should returns "undefined"', function() { + expect(helpers.noop(42)).not.toBeDefined(); + expect(helpers.noop.call(this, 42)).not.toBeDefined(); + }); + }); + + describe('isArray', function() { + it('should return true if value is an array', function() { + expect(helpers.isArray([])).toBeTruthy(); + expect(helpers.isArray([42])).toBeTruthy(); + expect(helpers.isArray(new Array())).toBeTruthy(); + expect(helpers.isArray(Array.prototype)).toBeTruthy(); + }); + it('should return false if value is not an array', function() { + expect(helpers.isArray()).toBeFalsy(); + expect(helpers.isArray({})).toBeFalsy(); + expect(helpers.isArray(undefined)).toBeFalsy(); + expect(helpers.isArray(null)).toBeFalsy(); + expect(helpers.isArray(true)).toBeFalsy(); + expect(helpers.isArray(false)).toBeFalsy(); + expect(helpers.isArray(42)).toBeFalsy(); + expect(helpers.isArray('Array')).toBeFalsy(); + expect(helpers.isArray({__proto__: Array.prototype})).toBeFalsy(); + }); + }); + + describe('isObject', function() { + it('should return true if value is an object', function() { + expect(helpers.isObject({})).toBeTruthy(); + expect(helpers.isObject({a: 42})).toBeTruthy(); + expect(helpers.isObject(new Object())).toBeTruthy(); + }); + it('should return false if value is not an object', function() { + expect(helpers.isObject()).toBeFalsy(); + expect(helpers.isObject(undefined)).toBeFalsy(); + expect(helpers.isObject(null)).toBeFalsy(); + expect(helpers.isObject(true)).toBeFalsy(); + expect(helpers.isObject(false)).toBeFalsy(); + expect(helpers.isObject(42)).toBeFalsy(); + expect(helpers.isObject('Object')).toBeFalsy(); + expect(helpers.isObject([])).toBeFalsy(); + expect(helpers.isObject([42])).toBeFalsy(); + expect(helpers.isObject(new Array())).toBeFalsy(); + expect(helpers.isObject(new Date())).toBeFalsy(); + }); + }); + + describe('isNullOrUndef', function() { + it('should return true if value is null/undefined', function() { + expect(helpers.isNullOrUndef(null)).toBeTruthy(); + expect(helpers.isNullOrUndef(undefined)).toBeTruthy(); + }); + it('should return false if value is not null/undefined', function() { + expect(helpers.isNullOrUndef(true)).toBeFalsy(); + expect(helpers.isNullOrUndef(false)).toBeFalsy(); + expect(helpers.isNullOrUndef('')).toBeFalsy(); + expect(helpers.isNullOrUndef('String')).toBeFalsy(); + expect(helpers.isNullOrUndef(0)).toBeFalsy(); + expect(helpers.isNullOrUndef([])).toBeFalsy(); + expect(helpers.isNullOrUndef({})).toBeFalsy(); + expect(helpers.isNullOrUndef([42])).toBeFalsy(); + expect(helpers.isNullOrUndef(new Date())).toBeFalsy(); + }); + }); + + describe('getValueOrDefault', function() { + it('should return value if defined', function() { + var object = {}; + var array = []; + + expect(helpers.getValueOrDefault(null, 42)).toBe(null); + expect(helpers.getValueOrDefault(false, 42)).toBe(false); + expect(helpers.getValueOrDefault(object, 42)).toBe(object); + expect(helpers.getValueOrDefault(array, 42)).toBe(array); + expect(helpers.getValueOrDefault('', 42)).toBe(''); + expect(helpers.getValueOrDefault(0, 42)).toBe(0); + }); + it('should return default if undefined', function() { + expect(helpers.getValueOrDefault(undefined, 42)).toBe(42); + expect(helpers.getValueOrDefault({}.foo, 42)).toBe(42); + }); + }); + + describe('getValueAtIndexOrDefault', function() { + it('should return the passed value if not an array', function() { + expect(helpers.getValueAtIndexOrDefault(0, 0, 42)).toBe(0); + expect(helpers.getValueAtIndexOrDefault('', 0, 42)).toBe(''); + expect(helpers.getValueAtIndexOrDefault(false, 0, 42)).toBe(false); + expect(helpers.getValueAtIndexOrDefault(98, 0, 42)).toBe(98); + }); + it('should return the default value if the passed value is null or undefined', function() { + expect(helpers.getValueAtIndexOrDefault(null, 0, 42)).toBe(42); + expect(helpers.getValueAtIndexOrDefault(undefined, 0, 42)).toBe(42); + }); + it('should return the value at index if defined', function() { + expect(helpers.getValueAtIndexOrDefault([1, false, 'foo'], 1, 42)).toBe(false); + expect(helpers.getValueAtIndexOrDefault([1, false, 'foo'], 2, 42)).toBe('foo'); + }); + it('should return the default value if value at index is undefined', function() { + expect(helpers.getValueAtIndexOrDefault([1, false, 'foo'], 3, 42)).toBe(42); + expect(helpers.getValueAtIndexOrDefault([1, undefined, 'foo'], 1, 42)).toBe(42); + }); + }); + + describe('callback', function() { + it('should return undefined if fn is not a function', function() { + expect(helpers.callback()).not.toBeDefined(); + expect(helpers.callback(null)).not.toBeDefined(); + expect(helpers.callback(42)).not.toBeDefined(); + expect(helpers.callback([])).not.toBeDefined(); + expect(helpers.callback({})).not.toBeDefined(); + }); + it('should call fn with the given args', function() { + var spy = jasmine.createSpy('spy'); + helpers.callback(spy); + helpers.callback(spy, []); + helpers.callback(spy, ['foo']); + helpers.callback(spy, [42, 'bar']); + + expect(spy.calls.argsFor(0)).toEqual([]); + expect(spy.calls.argsFor(1)).toEqual([]); + expect(spy.calls.argsFor(2)).toEqual(['foo']); + expect(spy.calls.argsFor(3)).toEqual([42, 'bar']); + }); + it('should call fn with the given scope', function() { + var spy = jasmine.createSpy('spy'); + var scope = {}; + + helpers.callback(spy); + helpers.callback(spy, [], null); + helpers.callback(spy, [], undefined); + helpers.callback(spy, [], scope); + + expect(spy.calls.all()[0].object).toBe(window); + expect(spy.calls.all()[1].object).toBe(window); + expect(spy.calls.all()[2].object).toBe(window); + expect(spy.calls.all()[3].object).toBe(scope); + }); + it('should return the value returned by fn', function() { + expect(helpers.callback(helpers.noop, [41])).toBe(undefined); + expect(helpers.callback(function(i) { + return i+1; + }, [41])).toBe(42); + }); + }); + + describe('each', function() { + it('should iterate over an array forward if reverse === false', function() { + var scope = {}; + var scopes = []; + var items = []; + var keys = []; + + helpers.each(['foo', 'bar', 42], function(item, key) { + scopes.push(this); + items.push(item); + keys.push(key); + }, scope); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual(['foo', 'bar', 42]); + expect(keys).toEqual([0, 1, 2]); + }); + it('should iterate over an array backward if reverse === true', function() { + var scope = {}; + var scopes = []; + var items = []; + var keys = []; + + helpers.each(['foo', 'bar', 42], function(item, key) { + scopes.push(this); + items.push(item); + keys.push(key); + }, scope, true); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual([42, 'bar', 'foo']); + expect(keys).toEqual([2, 1, 0]); + }); + it('should iterate over object properties', function() { + var scope = {}; + var scopes = []; + var items = []; + + helpers.each({a: 'foo', b: 'bar', c: 42}, function(item, key) { + scopes.push(this); + items[key] = item; + }, scope); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual(jasmine.objectContaining({a: 'foo', b: 'bar', c: 42})); + }); + it('should not throw when called with a non iterable object', function() { + expect(function() { + helpers.each(undefined); + }).not.toThrow(); + expect(function() { + helpers.each(null); + }).not.toThrow(); + expect(function() { + helpers.each(42); + }).not.toThrow(); + }); + }); + + describe('arrayEquals', function() { + it('should return false if arrays are not the same', function() { + expect(helpers.arrayEquals([], [42])).toBeFalsy(); + expect(helpers.arrayEquals([42], ['42'])).toBeFalsy(); + expect(helpers.arrayEquals([1, 2, 3], [1, 2, 3, 4])).toBeFalsy(); + expect(helpers.arrayEquals(['foo', 'bar'], ['bar', 'foo'])).toBeFalsy(); + expect(helpers.arrayEquals([1, 2, 3], [1, 2, 'foo'])).toBeFalsy(); + expect(helpers.arrayEquals([1, 2, [3, 4]], [1, 2, [3, 'foo']])).toBeFalsy(); + expect(helpers.arrayEquals([{a: 42}], [{a: 42}])).toBeFalsy(); + }); + it('should return false if arrays are not the same', function() { + var o0 = {}; + var o1 = {}; + var o2 = {}; + + expect(helpers.arrayEquals([], [])).toBeTruthy(); + expect(helpers.arrayEquals([1, 2, 3], [1, 2, 3])).toBeTruthy(); + expect(helpers.arrayEquals(['foo', 'bar'], ['foo', 'bar'])).toBeTruthy(); + expect(helpers.arrayEquals([true, false, true], [true, false, true])).toBeTruthy(); + expect(helpers.arrayEquals([o0, o1, o2], [o0, o1, o2])).toBeTruthy(); + }); + }); +});