From 88c69f5f8c842052c27347ca1a0e7ef00a69402c Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Fri, 23 Sep 2016 00:12:40 +0000 Subject: [PATCH 01/58] Move reverse option for labels to correct section in docs Addresses comment in #3102 --- docs/01-Chart-Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 43e762ba4..8d81d06a6 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -130,6 +130,7 @@ fullWidth | Boolean | true | Marks that this box should take the full width of t onClick | Function | `function(event, legendItem) {}` | A callback that is called when a 'click' event is registered on top of a label item onHover | Function | `function(event, legendItem) {}` | A callback that is called when a 'mousemove' event is registered on top of a label item labels |Object|-| See the [Legend Label Configuration](#chart-configuration-legend-label-configuration) section below. +reverse | Boolean | false | Legend will show datasets in reverse order #### Legend Label Configuration @@ -145,7 +146,6 @@ fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Fon padding | Number | 10 | Padding between rows of colored boxes generateLabels: | Function | `function(chart) { }` | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#chart-configuration-legend-item-interface) for details. usePointStyle | Boolean | false | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). -reverse | Boolean | false | Legend will show datasets in reverse order #### Legend Item Interface From 84da2e0a1056ab7959028685dc12616ac8b8fc56 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:46:17 +0200 Subject: [PATCH 02/58] Make charts vertically responsive (#3105) (#3326) Ensure that the hidden iframe is stretched vertically in order to detect height changes. Remove the classlist check/call since it was incorrectly spelled (should be classList), but also useless since the iframe has just been generated. Also remove the callback check: addResizeListener should never be called w/o a valid callback. --- src/core/core.helpers.js | 48 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 5364d191f..8609acdd0 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -958,38 +958,32 @@ module.exports = function(Chart) { }; helpers.addResizeListener = function(node, callback) { // Hide an iframe before the node - var hiddenIframe = document.createElement('iframe'); - var hiddenIframeClass = 'chartjs-hidden-iframe'; + var iframe = document.createElement('iframe'); - if (hiddenIframe.classlist) { - // can use classlist - hiddenIframe.classlist.add(hiddenIframeClass); - } else { - hiddenIframe.setAttribute('class', hiddenIframeClass); - } + iframe.className = 'chartjs-hidden-iframe'; + iframe.style.cssText = + 'display:block;'+ + 'overflow:hidden;'+ + 'border:0;'+ + 'margin:0;'+ + 'top:0;'+ + 'left:0;'+ + 'bottom:0;'+ + 'right:0;'+ + 'height:100%;'+ + 'width:100%;'+ + 'position:absolute;'+ + 'pointer-events:none;'+ + 'z-index:-1;'; - // Set the style - hiddenIframe.tabIndex = -1; - var style = hiddenIframe.style; - style.width = '100%'; - style.display = 'block'; - style.border = 0; - style.height = 0; - style.margin = 0; - style.position = 'absolute'; - style.left = 0; - style.right = 0; - style.top = 0; - style.bottom = 0; + // Prevent the iframe to gain focus on tab. + // https://github.com/chartjs/Chart.js/issues/3090 + iframe.tabIndex = -1; // Insert the iframe so that contentWindow is available - node.insertBefore(hiddenIframe, node.firstChild); + node.insertBefore(iframe, node.firstChild); - (hiddenIframe.contentWindow || hiddenIframe).onresize = function() { - if (callback) { - return callback(); - } - }; + this.addEvent(iframe.contentWindow || iframe, 'resize', callback); }; helpers.removeResizeListener = function(node) { var hiddenIframe = node.querySelector('.chartjs-hidden-iframe'); From 8ec7ce2f93a731d53c9330b7ef900a72367ac7aa Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:42:56 +0200 Subject: [PATCH 03/58] Gulp command switch to run specific test files Add the --inputs command switch to the unittest and unittestWatch tasks, to be able to run unit tests from the specified files only (e.g. gulp unittest --inputs=test/core.element.tests.js;test/core.helpers.tests.js). --- gulpfile.js | 15 +++++++++------ package.json | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b3dab98d1..f4a25cf2f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,6 +17,7 @@ var browserify = require('browserify'); var source = require('vinyl-source-stream'); var merge = require('merge-stream'); var collapse = require('bundle-collapser/plugin'); +var argv = require('yargs').argv var package = require('./package.json'); var srcDir = './src/'; @@ -33,11 +34,10 @@ var header = "/*!\n" + " */\n"; var preTestFiles = [ - './node_modules/moment/min/moment.min.js', + './node_modules/moment/min/moment.min.js' ]; var testFiles = [ - './test/mockContext.js', './test/*.js', // Disable tests which need to be rewritten based on changes introduced by @@ -157,10 +157,13 @@ function validHTMLTask() { } function startTest() { - var files = ['./src/**/*.js']; - Array.prototype.unshift.apply(files, preTestFiles); - Array.prototype.push.apply(files, testFiles); - return files; + return [].concat(preTestFiles).concat([ + './src/**/*.js', + './test/mockContext.js' + ]).concat( + argv.inputs? + argv.inputs.split(';'): + testFiles); } function unittestTask() { diff --git a/package.json b/package.json index e901b26c3..4d2bb8042 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "karma-jasmine": "^0.3.6", "karma-jasmine-html-reporter": "^0.1.8", "merge-stream": "^1.0.0", - "vinyl-source-stream": "^1.1.0" + "vinyl-source-stream": "^1.1.0", + "yargs": "^5.0.0" }, "spm": { "main": "Chart.js" From 16bcd6adc579cb3deae16ea915680bc219924cdc Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:43:52 +0200 Subject: [PATCH 04/58] Fix initial aspect ratio when not responsive When responsive is false and no canvas height explicitly set, the aspectRatio option wasn't applied because of the canvas default height. Prevent the retinaScale method to change the canvas display size since this method is called for none responsive charts, but instead make the resize() responsible of these changes. Also, as discussed some time ago, moved most of the core.js logic into core.controller.js. Clean up the destroy process and make sure that initial canvas values are properly saved and restored. --- src/core/core.controller.js | 200 ++++++++++++++++++++++++++++++------ src/core/core.helpers.js | 3 - src/core/core.js | 46 +-------- 3 files changed, 171 insertions(+), 78 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 30ba13418..1d169adea 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -13,40 +14,176 @@ module.exports = function(Chart) { // Controllers available for dataset visualization eg. bar, line, slice, etc. Chart.controllers = {}; + /** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * TODO(SB) Move this method in the upcoming core.platform class. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {Number} Size in pixels or undefined if unknown. + */ + function readUsedSize(element, property) { + var value = helpers.getStyle(element, property); + var matches = value && value.match(/(\d+)px/); + return matches? Number(matches[1]) : undefined; + } + + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas._chartjs = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + /** + * Restores the canvas initial state, such as render/display sizes and style. + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function releaseCanvas(canvas) { + if (!canvas._chartjs) { + return; + } + + var initial = canvas._chartjs.initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (value === undefined || value === null) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + helpers.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); + + delete canvas._chartjs; + } + + /** + * Initializes the given config with global and chart default values. + */ + function initConfig(config) { + config = config || {}; + return helpers.configMerge({ + options: helpers.configMerge( + Chart.defaults.global, + Chart.defaults[config.type], + config.options || {}), + data: { + datasets: [], + labels: [] + } + }, config); + } + /** * @class Chart.Controller * The main controller of a chart. */ - Chart.Controller = function(instance) { + Chart.Controller = function(context, config, instance) { + var me = this; + var canvas; - this.chart = instance; - this.config = instance.config; - this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); - this.id = helpers.uid(); + config = initConfig(config); + canvas = initCanvas(context.canvas, config); - Object.defineProperty(this, 'data', { + instance.ctx = context; + instance.canvas = canvas; + instance.config = config; + instance.width = canvas.width; + instance.height = canvas.height; + instance.aspectRatio = canvas.width / canvas.height; + + helpers.retinaScale(instance); + + me.id = helpers.uid(); + me.chart = instance; + me.config = instance.config; + me.options = me.config.options; + + Object.defineProperty(me, 'data', { get: function() { - return this.config.data; + return me.config.data; + } + }); + + // Always bind this so that if the responsive state changes we still work + helpers.addResizeListener(canvas.parentNode, function() { + if (me.config.options.responsive) { + me.resize(); } }); // Add the chart instance to the global namespace - Chart.instances[this.id] = this; + Chart.instances[me.id] = me; - if (this.options.responsive) { + if (me.options.responsive) { // Silent resize before chart draws - this.resize(true); + me.resize(true); } - this.initialize(); + me.initialize(); - return this; + return me; }; helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { - initialize: function() { var me = this; + // Before init plugin notification Chart.plugins.notify('beforeInit', [me]); @@ -82,15 +219,17 @@ module.exports = function(Chart) { resize: function(silent) { var me = this; var chart = me.chart; + var options = me.options; var canvas = chart.canvas; - var newWidth = helpers.getMaximumWidth(canvas); - var aspectRatio = chart.aspectRatio; - var newHeight = (me.options.maintainAspectRatio && isNaN(aspectRatio) === false && isFinite(aspectRatio) && aspectRatio !== 0) ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas); + var aspectRatio = (options.maintainAspectRatio && chart.aspectRatio) || null; - var sizeChanged = chart.width !== newWidth || chart.height !== newHeight; + // the canvas render width and height will be casted to integers so make sure that + // the canvas display style uses the same integer values to avoid blurring effect. + var newWidth = Math.floor(helpers.getMaximumWidth(canvas)); + var newHeight = Math.floor(aspectRatio? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)); - if (!sizeChanged) { - return me; + if (chart.width === newWidth && chart.height === newHeight) { + return; } canvas.width = chart.width = newWidth; @@ -98,6 +237,9 @@ module.exports = function(Chart) { helpers.retinaScale(chart); + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; + // Notify any plugins about the resize var newSize = {width: newWidth, height: newHeight}; Chart.plugins.notify('resize', [me, newSize]); @@ -111,8 +253,6 @@ module.exports = function(Chart) { me.stop(); me.update(me.options.responsiveAnimationDuration); } - - return me; }, ensureScalesHaveIDs: function() { @@ -539,25 +679,23 @@ module.exports = function(Chart) { destroy: function() { var me = this; + var canvas = me.chart.canvas; + me.stop(); me.clear(); - helpers.unbindEvents(me, me.events); - helpers.removeResizeListener(me.chart.canvas.parentNode); - // Reset canvas height/width attributes - var canvas = me.chart.canvas; - canvas.width = me.chart.width; - canvas.height = me.chart.height; + helpers.unbindEvents(me, me.events); + + if (canvas) { + helpers.removeResizeListener(canvas.parentNode); + releaseCanvas(canvas); + } // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here if (me.chart.originalDevicePixelRatio !== undefined) { me.chart.ctx.scale(1 / me.chart.originalDevicePixelRatio, 1 / me.chart.originalDevicePixelRatio); } - // Reset to the old style since it may have been changed by the device pixel ratio changes - canvas.style.width = me.chart.originalCanvasStyleWidth; - canvas.style.height = me.chart.originalCanvasStyleHeight; - Chart.plugins.notify('destroy', [me]); delete Chart.instances[me.id]; diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 8609acdd0..e404b61b1 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -859,9 +859,6 @@ module.exports = function(Chart) { // when destroy is called chart.originalDevicePixelRatio = chart.originalDevicePixelRatio || pixelRatio; } - - canvas.style.width = width + 'px'; - canvas.style.height = height + 'px'; }; // -- Canvas methods helpers.clear = function(chart) { diff --git a/src/core/core.js b/src/core/core.js index 577db878e..d28997838 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -5,12 +5,6 @@ module.exports = function() { // Occupy the global variable of Chart, and create a simple base class var Chart = function(context, config) { var me = this; - var helpers = Chart.helpers; - me.config = config || { - data: { - datasets: [] - } - }; // Support a jQuery'd canvas element if (context.length && context[0].getContext) { @@ -22,44 +16,9 @@ module.exports = function() { context = context.getContext('2d'); } - me.ctx = context; - me.canvas = context.canvas; - - context.canvas.style.display = context.canvas.style.display || 'block'; - - // Figure out what the size of the chart will be. - // If the canvas has a specified width and height, we use those else - // we look to see if the canvas node has a CSS width and height. - // If there is still no height, fill the parent container - me.width = context.canvas.width || parseInt(helpers.getStyle(context.canvas, 'width'), 10) || helpers.getMaximumWidth(context.canvas); - me.height = context.canvas.height || parseInt(helpers.getStyle(context.canvas, 'height'), 10) || helpers.getMaximumHeight(context.canvas); - - me.aspectRatio = me.width / me.height; - - if (isNaN(me.aspectRatio) || isFinite(me.aspectRatio) === false) { - // If the canvas has no size, try and figure out what the aspect ratio will be. - // Some charts prefer square canvases (pie, radar, etc). If that is specified, use that - // else use the canvas default ratio of 2 - me.aspectRatio = config.aspectRatio !== undefined ? config.aspectRatio : 2; - } - - // Store the original style of the element so we can set it back - me.originalCanvasStyleWidth = context.canvas.style.width; - me.originalCanvasStyleHeight = context.canvas.style.height; - - // High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(me); - me.controller = new Chart.Controller(me); - - // Always bind this so that if the responsive state changes we still work - helpers.addResizeListener(context.canvas.parentNode, function() { - if (me.controller && me.controller.config.options.responsive) { - me.controller.resize(); - } - }); - - return me.controller ? me.controller : me; + me.controller = new Chart.Controller(context, config, me); + return me.controller; }; // Globally expose the defaults to allow for user updating/changing @@ -106,5 +65,4 @@ module.exports = function() { Chart.Chart = Chart; return Chart; - }; From d610987cfbc2ed37d5b2d508b1378f9cb401edd2 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:44:06 +0200 Subject: [PATCH 05/58] Fix radar default aspect ratio and samples Now that the aspect ratio is correctly handled, fix samples for charts with aspect ratio of 1 which was vertically too large. Also fix the default aspect ratio for radar charts which wasn't applied when creating a chart directly using new Chart(ctx, { type: 'radar' }). --- samples/doughnut.html | 2 +- samples/polar-area.html | 2 +- samples/radar-skip-points.html | 2 +- samples/radar.html | 2 +- src/charts/Chart.Radar.js | 1 - src/controllers/controller.radar.js | 1 + 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/doughnut.html b/samples/doughnut.html index 73c96ad80..38a9d5782 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/polar-area.html b/samples/polar-area.html index ae21e1d44..16572dd45 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/radar-skip-points.html b/samples/radar-skip-points.html index 9d0816d4b..c1680afd0 100644 --- a/samples/radar-skip-points.html +++ b/samples/radar-skip-points.html @@ -15,7 +15,7 @@ -
+
diff --git a/samples/radar.html b/samples/radar.html index 4203cff15..eadcd9eca 100644 --- a/samples/radar.html +++ b/samples/radar.html @@ -15,7 +15,7 @@ -
+
diff --git a/src/charts/Chart.Radar.js b/src/charts/Chart.Radar.js index 1648e9fac..d17bd5d81 100644 --- a/src/charts/Chart.Radar.js +++ b/src/charts/Chart.Radar.js @@ -3,7 +3,6 @@ module.exports = function(Chart) { Chart.Radar = function(context, config) { - config.options = Chart.helpers.configMerge({aspectRatio: 1}, config.options); config.type = 'radar'; return new Chart(context, config); diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 3c050595a..6232165f3 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -5,6 +5,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; Chart.defaults.radar = { + aspectRatio: 1, scale: { type: 'radialLinear' }, From 503e6f8291da89f04b935f5054cd18779920f236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Fri, 23 Sep 2016 16:00:34 -0300 Subject: [PATCH 06/58] Minor fixes (proposal) --- docs/01-Chart-Configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 8d81d06a6..dd16e6d2c 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -83,7 +83,7 @@ maintainAspectRatio | Boolean | true | Maintain the original canvas aspect ratio events | Array[String] | `["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]` | Events that the chart should listen to for tooltips and hovering onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed an array of active elements legendCallback | Function | ` function (chart) { }` | Function to generate a legend. Receives the chart object to generate a legend from. Default implementation returns an HTML string. -onResize | Function | null | Called when a resize occurs. Gets passed two arguemnts: the chart instance and the new size. +onResize | Function | null | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. ### Title Configuration @@ -345,7 +345,7 @@ The animation object passed to the callbacks is of type `Chart.Animation`. The o The global options for elements are defined in `Chart.defaults.global.elements`. -Options can be configured for four different types of elements; arc, lines, points, and rectangles. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. +Options can be configured for four different types of elements: arc, lines, points, and rectangles. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. #### Arc Configuration @@ -451,7 +451,7 @@ var chartData = { ### Mixed Chart Types -When creating a chart, you have the option to overlay different chart types on top of eachother as separate datasets. +When creating a chart, you have the option to overlay different chart types on top of each other as separate datasets. To do this, you must set a `type` for each dataset individually. You can create mixed chart types with bar and line chart types. From fb302d5f0074b5d367386319e696d5718b78a88d Mon Sep 17 00:00:00 2001 From: dylan-kerr Date: Fri, 23 Sep 2016 22:05:54 +0100 Subject: [PATCH 07/58] Set maxWidth during title draw to avoid overflow CanvasRenderingContext2D.fillText() accepts a fourth parameter called maxWidth that sets the maximum width of the drawn text, enforced by scaling the entire line. This commit uses the title element's layout dimensions to set maxWidth and avoid overflow outside of the canvas. --- src/core/core.title.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/core.title.js b/src/core/core.title.js index 0a8c1a5aa..eb9c4fc04 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -158,7 +158,8 @@ module.exports = function(Chart) { top = me.top, left = me.left, bottom = me.bottom, - right = me.right; + right = me.right, + maxWidth; ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour ctx.font = titleFont; @@ -167,9 +168,11 @@ module.exports = function(Chart) { if (me.isHorizontal()) { titleX = left + ((right - left) / 2); // midpoint of the width titleY = top + ((bottom - top) / 2); // midpoint of the height + maxWidth = right - left; } else { titleX = opts.position === 'left' ? left + (fontSize / 2) : right - (fontSize / 2); titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); } @@ -178,7 +181,7 @@ module.exports = function(Chart) { ctx.rotate(rotation); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText(opts.text, 0, 0); + ctx.fillText(opts.text, 0, 0, maxWidth); ctx.restore(); } } From 4f668c3ed94c57bc5176bd8c89c2bb743139087c Mon Sep 17 00:00:00 2001 From: dylan-kerr Date: Fri, 23 Sep 2016 22:36:58 +0100 Subject: [PATCH 08/58] Adjust expected values in core.title.tests.js --- test/core.title.tests.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/core.title.tests.js b/test/core.title.tests.js index ce1c437ea..b8ecd18e5 100644 --- a/test/core.title.tests.js +++ b/test/core.title.tests.js @@ -118,7 +118,7 @@ describe('Title block tests', function() { args: [0] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] @@ -168,7 +168,7 @@ describe('Title block tests', function() { args: [-0.5 * Math.PI] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] @@ -201,10 +201,10 @@ describe('Title block tests', function() { args: [0.5 * Math.PI] }, { name: 'fillText', - args: ['My title', 0, 0] + args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] }]); }); -}); \ No newline at end of file +}); From 7d65bd3f52fc0f24c247f605cf7b0b6a3c7d0469 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 23 Sep 2016 17:44:17 +0200 Subject: [PATCH 09/58] Initial core.controller.js unit tests --- test/core.controller.tests.js | 488 ++++++++++++++++++++++++++++++++++ test/mockContext.js | 48 +++- 2 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 test/core.controller.tests.js diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js new file mode 100644 index 000000000..83eba3baf --- /dev/null +++ b/test/core.controller.tests.js @@ -0,0 +1,488 @@ +describe('Chart.Controller', function() { + + function processResizeEvent(chart, callback) { + setTimeout(callback, 100); + } + + describe('config.options.aspectRatio', function() { + it('should use default "global" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 620px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 620, dh: 310, + rw: 620, rh: 310, + }); + }); + + it('should use default "chart" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + type: 'doughnut', + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 425px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 425, dh: 425, + rw: 425, rh: 425, + }); + }); + + it('should use "user" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 405px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 405, dh: 135, + rw: 405, rh: 135, + }); + }); + + it('should NOT apply aspect ratio when height specified', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 400px; height: 410px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 410, + rw: 400, rh: 410, + }); + }); + }); + + describe('config.options.responsive: false', function() { + it('should use default canvas size for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + }); + + it('should use canvas attributes for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '', + width: 305, + height: 245, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 305, dh: 245, + rw: 305, rh: 245, + }); + }); + + it('should use canvas style for render and display sizes (if no attributes)', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + + it('should use attributes for the render size and style for the display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px;', + width: 165, + height: 85, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 165, rh: 85, + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: false)', function() { + it('should fill parent width and height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + }); + + it('should resize the canvas when parent width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + wrapper.style.width = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 350, + rw: 150, rh: 350, + }); + + done(); + }); + }); + }); + + it('should resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 455, + rw: 300, rh: 455, + }); + + wrapper.style.height = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + done(); + }); + }); + }); + + it('should NOT include parent padding when resizing the canvas', function(done) { + var chart = acquireChart({ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'padding: 50px; width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '355px'; + wrapper.style.width = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + }); + + it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'display: none;' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var canvas = chart.chart.canvas; + canvas.style.display = 'block'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + done(); + }); + }); + + it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'display: none; width: 460px; height: 380px' + } + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.display = 'block'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 460, dh: 380, + rw: 460, rh: 380, + }); + + done(); + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should fill parent width and use aspect ratio to calculate height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 490, + rw: 300, rh: 490, + }); + }); + + it('should resize the canvas with correct apect ratio when parent width changes', function(done) { + var chart = acquireChart({ + type: 'line', // AR == 2 + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.width = '450px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 450, dh: 225, + rw: 450, rh: 225, + }); + + wrapper.style.width = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 75, + rw: 150, rh: 75, + }); + + done(); + }); + }); + }); + + it('should NOT resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + var wrapper = chart.chart.canvas.parentNode; + wrapper.style.height = '455px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + wrapper.style.height = '150px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + done(); + }); + }); + }); + }); + + describe('controller.destroy', function() { + it('should restore canvas (and context) initial values', function(done) { + var chart = acquireChart({ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + width: 180, + style: 'width: 512px; height: 480px' + }, + wrapper: { + style: 'width: 450px; height: 450px; position: relative' + } + }); + + var canvas = chart.chart.canvas; + var wrapper = canvas.parentNode; + wrapper.style.width = '475px'; + processResizeEvent(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 475, dh: 450, + rw: 475, rh: 450, + }); + + chart.destroy(); + + expect(canvas.getAttribute('width')).toBe('180'); + expect(canvas.getAttribute('height')).toBe(null); + expect(canvas.style.width).toBe('512px'); + expect(canvas.style.height).toBe('480px'); + expect(canvas.style.display).toBe(''); + + done(); + }); + }); + }); +}); diff --git a/test/mockContext.js b/test/mockContext.js index 9590b5360..48e5ce304 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -158,10 +158,55 @@ }; } + function toBeChartOfSize() { + return { + compare: function(actual, expected) { + var message = null; + var chart, canvas, style, dh, dw, rh, rw; + + if (!actual || !(actual instanceof Chart.Controller)) { + message = 'Expected ' + actual + ' to be an instance of Chart.Controller.'; + } else { + chart = actual.chart; + canvas = chart.ctx.canvas; + style = getComputedStyle(canvas); + dh = parseInt(style.height); + dw = parseInt(style.width); + rh = canvas.height; + rw = canvas.width; + + // sanity checks + if (chart.height !== rh) { + message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh; + } else if (chart.width !== rw) { + message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw; + } + + // validity checks + if (dh !== expected.dh) { + message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh; + } else if (dw !== expected.dw) { + message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw; + } else if (rh !== expected.rh) { + message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh; + } else if (rw !== expected.rw) { + message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw; + } + } + + return { + message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected, + pass: !message + } + } + } + } + beforeEach(function() { jasmine.addMatchers({ toBeCloseToPixel: toBeCloseToPixel, - toEqualOneOf: toEqualOneOf + toEqualOneOf: toEqualOneOf, + toBeChartOfSize: toBeChartOfSize }); }); @@ -214,6 +259,7 @@ } function releaseChart(chart) { + chart.destroy(); chart.chart.canvas.parentNode.remove(); delete charts[chart.id]; delete chart; From 9015e72ae1ddebe38ba90a538ee2556a46ee3daa Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 24 Sep 2016 04:57:33 -0400 Subject: [PATCH 10/58] Fix the legend drawing when `labels.usePointStyle` is true (#3323) --- src/core/core.legend.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/core/core.legend.js b/src/core/core.legend.js index e82c06ea3..3afbabcf3 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -64,6 +64,18 @@ module.exports = function(Chart) { } }; + /** + * Helper function to get the box width based on the usePointStyle option + * @param labelopts {Object} the label options on the legend + * @param fontSize {Number} the label font size + * @return {Number} width of the color box area + */ + function getBoxWidth(labelOpts, fontSize) { + return labelOpts.usePointStyle ? + fontSize * Math.SQRT2 : + labelOpts.boxWidth; + } + Chart.Legend = Chart.Element.extend({ initialize: function(config) { @@ -204,11 +216,9 @@ module.exports = function(Chart) { ctx.textBaseline = 'top'; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = labelOpts.usePointStyle ? - fontSize * Math.sqrt(2) : - labelOpts.boxWidth; - + var boxWidth = getBoxWidth(labelOpts, fontSize); var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { totalHeight += fontSize + (labelOpts.padding); lineWidths[lineWidths.length] = me.left; @@ -236,10 +246,7 @@ module.exports = function(Chart) { var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { - // If usePointStyle is set, multiple boxWidth by 2 since it represents - // the radius and not truly the width - var boxWidth = labelOpts.usePointStyle ? 2 * labelOpts.boxWidth : labelOpts.boxWidth; - + var boxWidth = getBoxWidth(labelOpts, fontSize); var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column @@ -308,7 +315,7 @@ module.exports = function(Chart) { ctx.fillStyle = fontColor; // render in correct colour ctx.font = labelFont; - var boxWidth = labelOpts.boxWidth, + var boxWidth = getBoxWidth(labelOpts, fontSize), hitboxes = me.legendHitBoxes; // current position @@ -385,9 +392,7 @@ module.exports = function(Chart) { var itemHeight = fontSize + labelOpts.padding; helpers.each(me.legendItems, function(legendItem, i) { var textWidth = ctx.measureText(legendItem.text).width, - width = labelOpts.usePointStyle ? - fontSize + (fontSize / 2) + textWidth : - boxWidth + (fontSize / 2) + textWidth, + width = boxWidth + (fontSize / 2) + textWidth, x = cursor.x, y = cursor.y; From 11e8c50f722c446b973f1cf8f412b72116ccf979 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 24 Sep 2016 04:58:31 -0400 Subject: [PATCH 11/58] Compute correct tooltip size when there is no title present (#3324) --- src/core/core.tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 5edfd0075..d0068ee67 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -354,7 +354,7 @@ module.exports = function(Chart) { footerFontSize = vm.footerFontSize; size.height += titleLineCount * titleFontSize; // Title Lines - size.height += (titleLineCount - 1) * vm.titleSpacing; // Title Line Spacing + size.height += titleLineCount ? (titleLineCount - 1) * vm.titleSpacing : 0; // Title Line Spacing size.height += titleLineCount ? vm.titleMarginBottom : 0; // Title's bottom Margin size.height += combinedBodyLength * bodyFontSize; // Body Lines size.height += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing From f6ac8279cc5b69359c05f69613ea2ad8ef51fec3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 24 Sep 2016 11:54:51 -0400 Subject: [PATCH 12/58] Fix 2 minor documentation issues in the scale documentation. #3341 (#3360) --- docs/02-Scales.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/02-Scales.md b/docs/02-Scales.md index c776355eb..313cec15a 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -59,7 +59,7 @@ offsetGridLines | Boolean | false | If true, labels are shifted to be between gr #### Scale Title Configuration -The grid line configuration is nested under the scale configuration in the `scaleLabel` key. It defines options for the scale title. +The scale label configuration is nested under the scale configuration in the `scaleLabel` key. It defines options for the scale title. Name | Type | Default | Description --- | --- | --- | --- @@ -72,7 +72,7 @@ fontStyle | String | "normal" | Font style for the scale title, follows CSS font #### Tick Configuration -The grid line configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. +The tick configuration is nested under the scale configuration in the `ticks` key. It defines options for the tick marks that are generated by the axis. Name | Type | Default | Description --- | --- | --- | --- From 62ef40ebf6f029eb0528998b69a755874863cbbe Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 24 Sep 2016 12:19:49 -0400 Subject: [PATCH 13/58] skip non finite data points when determining scale sizes. Fixes #3125 --- src/core/core.scale.js | 4 ++-- test/scale.linear.tests.js | 4 ++-- test/scale.logarithmic.tests.js | 4 ++-- test/scale.radialLinear.tests.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 123da3184..808e18e50 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -398,8 +398,8 @@ module.exports = function(Chart) { if (rawValue === null || typeof(rawValue) === 'undefined') { return NaN; } - // isNaN(object) returns true, so make sure NaN is checking for a number - if (typeof(rawValue) === 'number' && isNaN(rawValue)) { + // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values + if (typeof(rawValue) === 'number' && !isFinite(rawValue)) { return NaN; } // If it is in fact an object, dive in one more level diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 659ccc727..3df773258 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -156,9 +156,9 @@ describe('Linear Scale', function() { data: { datasets: [{ yAxisID: 'yScale0', - data: [null, 90, NaN, undefined, 45, 30] + data: [null, 90, NaN, undefined, 45, 30, Infinity, -Infinity] }], - labels: ['a', 'b', 'c', 'd', 'e', 'f'] + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] }, options: { scales: { diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index be6350341..221e61c4b 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -224,7 +224,7 @@ describe('Logarithmic Scale tests', function() { data: [undefined, 10, null, 5, 5000, NaN, 78, 450] }, { yAxisID: 'yScale0', - data: [undefined, 28, null, 1000, 500, NaN, 50, 42] + data: [undefined, 28, null, 1000, 500, NaN, 50, 42, Infinity, -Infinity] }, { yAxisID: 'yScale1', data: [undefined, 30, null, 9400, 0, NaN, 54, 836] @@ -232,7 +232,7 @@ describe('Logarithmic Scale tests', function() { yAxisID: 'yScale1', data: [undefined, 0, null, 800, 9, NaN, 894, 21] }], - labels: ['a', 'b', 'c', 'd', 'e', 'f' ,'g'] + labels: ['a', 'b', 'c', 'd', 'e', 'f' ,'g', 'h', 'i'] }, options: { scales: { diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 61b6d9689..dea9b1b03 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -132,9 +132,9 @@ describe('Test the radial linear scale', function() { type: 'radar', data: { datasets: [{ - data: [50, 60, NaN, 70, null, undefined] + data: [50, 60, NaN, 70, null, undefined, Infinity, -Infinity] }], - labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6'] + labels: ['lablel1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'] }, options: { scales: {} From c3d7a3328d759fd55cf044205b88c0e8fbc488d7 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 17 Sep 2016 17:06:26 +0200 Subject: [PATCH 14/58] In the doughnut chart, specifically handle multiline strings. --- src/controllers/controller.doughnut.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index e0f378dae..3dcd33e9f 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -102,7 +102,19 @@ module.exports = function(Chart) { return ''; }, label: function(tooltipItem, data) { - return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + var dataLabel = data.labels[tooltipItem.index]; + var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (helpers.isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; } } } From 9041b5a2b94cb5836687739d5280b28182810d09 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 17 Sep 2016 15:57:33 -0400 Subject: [PATCH 15/58] Update default config tests and re-enable --- gulpfile.js | 1 - test/defaultConfig.tests.js | 533 ++++++++++++++++++------------------ 2 files changed, 268 insertions(+), 266 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b3dab98d1..26ee0bd04 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -43,7 +43,6 @@ var testFiles = [ // Disable tests which need to be rewritten based on changes introduced by // the following changes: https://github.com/chartjs/Chart.js/pull/2346 '!./test/core.layoutService.tests.js', - '!./test/defaultConfig.tests.js' ]; gulp.task('bower', bowerTask); diff --git a/test/defaultConfig.tests.js b/test/defaultConfig.tests.js index 0123f420f..fb34d33ec 100644 --- a/test/defaultConfig.tests.js +++ b/test/defaultConfig.tests.js @@ -1,293 +1,296 @@ // Test the bubble chart default config -describe("Test the bubble chart default config", function() { - it('should reutrn correct tooltip strings', function() { - var config = Chart.defaults.bubble; +describe("Default Configs", function() { + describe("Bubble Chart", function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.bubble; + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + label: 'My dataset', + data: [{ + x: 10, + y: 12, + r: 5 + }] + }] + }, + options: config + }); - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[0]]; + chart.tooltip.update(); - // Item label - var data = { - datasets: [{ - label: 'My dataset', - data: [{ - x: 10, - y: 12, - r: 5 - }] - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 0 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('My dataset: (10, 12, 5)'); - }); -}); - -describe('Test the doughnut chart default config', function() { - it('should return correct tooltip strings', function() { - var config = Chart.defaults.doughnut; - - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); - - // Item label - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, 30], - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 1 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('label2: 20'); + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['My dataset: (10, 12, 5)'], + after: [] + }]); + }); }); - it('should return the correct html legend', function() { - var config = Chart.defaults.doughnut; + describe('Doughnut Chart', function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var chart = { - id: 'mychart', - data: { - labels: ['label1', 'label2'], - datasets: [{ - data: [10, 20], - backgroundColor: ['red', 'green'] - }] - } - }; - var expectedLegend = '
  • label1
  • label2
'; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - expect(config.legendCallback(chart)).toBe(expectedLegend); - }); + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['label2: 20'], + after: [] + }]); + }); - it('should return correct legend label objects', function() { - var config = Chart.defaults.doughnut; - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'], - metaData: [{}, {}, {}] - }] - }; + it('should return correct tooltip string for a multiline label', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', ['row1', 'row2', 'row3'], 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var expected = [{ - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label2', - fillStyle: 'green', - hidden: false, - index: 1, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label3', - fillStyle: 'blue', - hidden: true, - index: 2, - strokeStyle: '#000', - lineWidth: 2 - }]; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - var chart = { - data: data, - options: { - elements: { - arc: { + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: [ + 'row1: 20', + 'row2', + 'row3' + ], + after: [] + }]); + }); + + it('should return the correct html legend', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2'], + datasets: [{ + data: [10, 20], + backgroundColor: ['red', 'green'] + }] + }, + options: config + }); + + var expectedLegend = '
  • label1
  • label2
'; + expect(chart.generateLegend()).toBe(expectedLegend); + }); + + it('should return correct legend label objects', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' - } - } - } - }; - expect(config.legend.labels.generateLabels.call({ chart: chart }, data)).toEqual(expected); - }); + }] + }, + options: config + }); - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.doughnut; + var expected = [{ + text: 'label1', + fillStyle: 'red', + hidden: false, + index: 0, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label2', + fillStyle: 'green', + hidden: false, + index: 1, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label3', + fillStyle: 'blue', + hidden: true, + index: 2, + strokeStyle: '#000', + lineWidth: 2 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); - var legendItem = { - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0 - }; - - var chart = { - data: { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'] - }] - }, - update: function() {} - }; - - spyOn(chart, 'update'); - var scope = { - chart: chart - }; - - config.legend.onClick.call(scope, null, legendItem); - - expect(chart.data.datasets[0].metaHiddenData).toEqual([10]); - expect(chart.data.datasets[0].data).toEqual([NaN, 20, NaN]); - - expect(chart.update).toHaveBeenCalled(); - - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); - - // Should not toggle index 2 since there was never data for it - legendItem.index = 2; - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); - }); -}); - -describe('Test the polar area chart default config', function() { - it('should return correct tooltip strings', function() { - var config = Chart.defaults.polarArea; - - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); - - // Item label - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, 30], - }] - }; - - var tooltipItem = { - datasetIndex: 0, - index: 1, - yLabel: 20 - }; - - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('label2: 20'); - }); - - it('should return the correct html legend', function() { - var config = Chart.defaults.polarArea; - - var chart = { - id: 'mychart', - data: { - labels: ['label1', 'label2'], - datasets: [{ - data: [10, 20], - backgroundColor: ['red', 'green'] - }] - } - }; - var expectedLegend = '
  • label1
  • label2
'; - - expect(config.legendCallback(chart)).toBe(expectedLegend); - }); - - it('should return correct legend label objects', function() { - var config = Chart.defaults.polarArea; - var data = { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'], - metaData: [{}, {}, {}] - }] - }; - - var expected = [{ - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label2', - fillStyle: 'green', - hidden: false, - index: 1, - strokeStyle: '#000', - lineWidth: 2 - }, { - text: 'label3', - fillStyle: 'blue', - hidden: true, - index: 2, - strokeStyle: '#000', - lineWidth: 2 - }]; - - var chart = { - data: data, - options: { - elements: { - arc: { + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.defaults.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' - } - } - } - }; - expect(config.legend.labels.generateLabels.call({ chart: chart }, data)).toEqual(expected); + }] + }, + options: config + }); + var meta = chart.getDatasetMeta(0); + + spyOn(chart, 'update').and.callThrough(); + + var legendItem = chart.legend.legendItems[0]; + config.legend.onClick.call(chart.legend, null, legendItem); + + expect(meta.data[0].hidden).toBe(true); + expect(chart.update).toHaveBeenCalled(); + + config.legend.onClick.call(chart.legend, null, legendItem); + expect(meta.data[0].hidden).toBe(false); + }); }); - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.polarArea; + describe('Polar Area Chart', function() { + it('should return correct tooltip strings', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, 30], + }] + }, + options: config + }); - var legendItem = { - text: 'label1', - fillStyle: 'red', - hidden: false, - index: 0 - }; + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); - var chart = { - data: { - labels: ['label1', 'label2', 'label3'], - datasets: [{ - data: [10, 20, NaN], - backgroundColor: ['red', 'green', 'blue'] - }] - }, - update: function() {} - }; + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['label2: 20'], + after: [] + }]); + }); - spyOn(chart, 'update'); - var scope = { - chart: chart - }; + it('should return the correct html legend', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2'], + datasets: [{ + data: [10, 20], + backgroundColor: ['red', 'green'] + }] + }, + options: config + }); - config.legend.onClick.call(scope, null, legendItem); + var expectedLegend = '
  • label1
  • label2
'; + expect(chart.generateLegend()).toBe(expectedLegend); + }); - expect(chart.data.datasets[0].metaHiddenData).toEqual([10]); - expect(chart.data.datasets[0].data).toEqual([NaN, 20, NaN]); + it('should return correct legend label objects', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + options: config + }); - expect(chart.update).toHaveBeenCalled(); + var expected = [{ + text: 'label1', + fillStyle: 'red', + hidden: false, + index: 0, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label2', + fillStyle: 'green', + hidden: false, + index: 1, + strokeStyle: '#000', + lineWidth: 2 + }, { + text: 'label3', + fillStyle: 'blue', + hidden: true, + index: 2, + strokeStyle: '#000', + lineWidth: 2 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.defaults.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + options: config + }); + var meta = chart.getDatasetMeta(0); + + spyOn(chart, 'update').and.callThrough(); - // Should not toggle index 2 since there was never data for it - legendItem.index = 2; - config.legend.onClick.call(scope, null, legendItem); - expect(chart.data.datasets[0].data).toEqual([10, 20, NaN]); + var legendItem = chart.legend.legendItems[0]; + config.legend.onClick.call(chart.legend, null, legendItem); + + expect(meta.data[0].hidden).toBe(true); + expect(chart.update).toHaveBeenCalled(); + + config.legend.onClick.call(chart.legend, null, legendItem); + expect(meta.data[0].hidden).toBe(false); + }); }); }); From d09a17a2b163df7280491ad6c5999d81e892f537 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 24 Sep 2016 16:56:16 -0400 Subject: [PATCH 16/58] Better number -> string callback for the radial linear scale (#3281) Also create a new Chart.Ticks namespace to host common tick generators and formatters. --- src/chart.js | 1 + src/core/core.scale.js | 4 +- src/core/core.ticks.js | 193 +++++++++++++++++++++++++++++++ src/scales/scale.linear.js | 26 +---- src/scales/scale.linearbase.js | 43 ++----- src/scales/scale.logarithmic.js | 52 +-------- src/scales/scale.radialLinear.js | 4 +- 7 files changed, 213 insertions(+), 110 deletions(-) create mode 100644 src/core/core.ticks.js diff --git a/src/chart.js b/src/chart.js index a12890aa0..aa0d46c45 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,6 +12,7 @@ require('./core/core.datasetController')(Chart); require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.plugin.js')(Chart); +require('./core/core.ticks.js')(Chart); require('./core/core.scale')(Chart); require('./core/core.title')(Chart); require('./core/core.legend')(Chart); diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 808e18e50..890e04c42 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -46,9 +46,7 @@ module.exports = function(Chart) { autoSkipPadding: 0, labelOffset: 0, // We pass through arrays to be rendered as multiline labels, we convert Others to strings here. - callback: function(value) { - return helpers.isArray(value) ? value : '' + value; - } + callback: Chart.Ticks.formatters.values } }; diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js new file mode 100644 index 000000000..f55b8d40e --- /dev/null +++ b/src/core/core.ticks.js @@ -0,0 +1,193 @@ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + /** + * Namespace to hold static tick generation functions + * @namespace Chart.Ticks + */ + Chart.Ticks = { + /** + * Namespace to hold generators for different types of ticks + * @namespace Chart.Ticks.generators + */ + generators: { + /** + * Interface for the options provided to the numeric tick generator + * @interface INumericTickGenerationOptions + */ + /** + * The maximum number of ticks to display + * @name INumericTickGenerationOptions#maxTicks + * @type Number + */ + /** + * The distance between each tick. + * @name INumericTickGenerationOptions#stepSize + * @type Number + * @optional + */ + /** + * Forced minimum for the ticks. If not specified, the minimum of the data range is used to calculate the tick minimum + * @name INumericTickGenerationOptions#min + * @type Number + * @optional + */ + /** + * The maximum value of the ticks. If not specified, the maximum of the data range is used to calculate the tick maximum + * @name INumericTickGenerationOptions#max + * @type Number + * @optional + */ + + /** + * Generate a set of linear ticks + * @method Chart.Ticks.generators.linear + * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks + * @param dataRange {IRange} the range of the data + * @returns {Array} array of tick values + */ + linear: function(generationOptions, dataRange) { + var ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + var spacing; + if (generationOptions.stepSize && generationOptions.stepSize > 0) { + spacing = generationOptions.stepSize; + } else { + var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); + spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); + } + var niceMin = Math.floor(dataRange.min / spacing) * spacing; + var niceMax = Math.ceil(dataRange.max / spacing) * spacing; + var numSpaces = (niceMax - niceMin) / spacing; + + // If very close to our rounded value, use it. + if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + // Put the values into the ticks array + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(niceMin + (j * spacing)); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; + }, + + /** + * Generate a set of logarithmic ticks + * @method Chart.Ticks.generators.logarithmic + * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks + * @param dataRange {IRange} the range of the data + * @returns {Array} array of tick values + */ + logarithmic: function(generationOptions, dataRange) { + var ticks = []; + var getValueOrDefault = helpers.getValueOrDefault; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + var tickVal = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + while (tickVal < dataRange.max) { + ticks.push(tickVal); + + var exp; + var significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.round(dataRange.minNotZero / Math.pow(10, exp)); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; + } + + if (significand === 10) { + significand = 1; + ++exp; + } + + tickVal = significand * Math.pow(10, exp); + } + + var lastTick = getValueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; + } + }, + + /** + * Namespace to hold formatters for different types of ticks + * @namespace Chart.Ticks.formatters + */ + formatters: { + /** + * Formatter for value labels + * @method Chart.Ticks.formatters.values + * @param value the value to display + * @return {String|Array} the label to display + */ + values: function(value) { + return helpers.isArray(value) ? value : '' + value; + }, + + /** + * Formatter for linear numeric ticks + * @method Chart.Ticks.formatters.linear + * @param tickValue {Number} the value to be formatted + * @param index {Number} the position of the tickValue parameter in the ticks array + * @param ticks {Array} the list of ticks being converted + * @return {String} string representation of the tickValue parameter + */ + linear: function(tickValue, index, ticks) { + // If we have lots of ticks, don't use the ones + var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0]; + + // If we have a number like 2.5 as the delta, figure out how many decimal places we need + if (Math.abs(delta) > 1) { + if (tickValue !== Math.floor(tickValue)) { + // not an integer + delta = tickValue - Math.floor(tickValue); + } + } + + var logDelta = helpers.log10(Math.abs(delta)); + var tickString = ''; + + if (tickValue !== 0) { + var numDecimal = -1 * Math.floor(logDelta); + numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places + tickString = tickValue.toFixed(numDecimal); + } else { + tickString = '0'; // never show decimal places for 0 + } + + return tickString; + }, + + logarithmic: function(tickValue, index, ticks) { + var remain = tickValue / (Math.pow(10, Math.floor(helpers.log10(tickValue)))); + + if (tickValue === 0) { + return '0'; + } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === ticks.length - 1) { + return tickValue.toExponential(); + } + return ''; + } + } + }; +}; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index f2700a3a4..e8afa84f8 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -7,31 +7,7 @@ module.exports = function(Chart) { var defaultConfig = { position: 'left', ticks: { - callback: function(tickValue, index, ticks) { - // If we have lots of ticks, don't use the ones - var delta = ticks.length > 3 ? ticks[2] - ticks[1] : ticks[1] - ticks[0]; - - // If we have a number like 2.5 as the delta, figure out how many decimal places we need - if (Math.abs(delta) > 1) { - if (tickValue !== Math.floor(tickValue)) { - // not an integer - delta = tickValue - Math.floor(tickValue); - } - } - - var logDelta = helpers.log10(Math.abs(delta)); - var tickString = ''; - - if (tickValue !== 0) { - var numDecimal = -1 * Math.floor(logDelta); - numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places - tickString = tickValue.toFixed(numDecimal); - } else { - tickString = '0'; // never show decimal places for 0 - } - - return tickString; - } + callback: Chart.Ticks.formatters.linear } }; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 37caa0359..b8bc5c979 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -53,49 +53,22 @@ module.exports = function(Chart) { buildTicks: function() { var me = this; var opts = me.options; - var ticks = me.ticks = []; var tickOpts = opts.ticks; - var getValueOrDefault = helpers.getValueOrDefault; // Figure out what the max number of ticks we can support it is based on the size of // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - + // the graph. Make sure we always have at least 2 ticks var maxTicks = me.getTickLimit(); - - // Make sure we always have at least 2 ticks maxTicks = Math.max(2, maxTicks); - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var spacing; - var fixedStepSizeSet = (tickOpts.fixedStepSize && tickOpts.fixedStepSize > 0) || (tickOpts.stepSize && tickOpts.stepSize > 0); - if (fixedStepSizeSet) { - spacing = getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize); - } else { - var niceRange = helpers.niceNum(me.max - me.min, false); - spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - } - var niceMin = Math.floor(me.min / spacing) * spacing; - var niceMax = Math.ceil(me.max / spacing) * spacing; - var numSpaces = (niceMax - niceMin) / spacing; - - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); - } - - // Put the values into the ticks array - ticks.push(tickOpts.min !== undefined ? tickOpts.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); - } - ticks.push(tickOpts.max !== undefined ? tickOpts.max : niceMax); + var numericGeneratorOptions = { + maxTicks: maxTicks, + min: tickOpts.min, + max: tickOpts.max, + stepSize: helpers.getValueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) + }; + var ticks = me.ticks = Chart.Ticks.generators.linear(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 50417528a..a23f2eed4 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -9,16 +9,7 @@ module.exports = function(Chart) { // label settings ticks: { - callback: function(value, index, arr) { - var remain = value / (Math.pow(10, Math.floor(helpers.log10(value)))); - - if (value === 0) { - return '0'; - } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) { - return value.toExponential(); - } - return ''; - } + callback: Chart.Ticks.formatters.logarithmic } }; @@ -124,43 +115,12 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; - var getValueOrDefault = helpers.getValueOrDefault; - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - var ticks = me.ticks = []; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - - var tickVal = getValueOrDefault(tickOpts.min, Math.pow(10, Math.floor(helpers.log10(me.min)))); - - while (tickVal < me.max) { - ticks.push(tickVal); - - var exp; - var significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(me.minNotZero)); - significand = Math.round(me.minNotZero / Math.pow(10, exp)); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - } - - if (significand === 10) { - significand = 1; - ++exp; - } - - tickVal = significand * Math.pow(10, exp); - } - - var lastTick = getValueOrDefault(tickOpts.max, tickVal); - ticks.push(lastTick); + var generationOptions = { + min: tickOpts.min, + max: tickOpts.max + }; + var ticks = me.ticks = Chart.Ticks.generators.logarithmic(generationOptions, me); if (!me.isHorizontal()) { // We are in a vertical orientation. The top value is the highest. So reverse the array diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 361ad3cba..cc7c6557d 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -31,7 +31,9 @@ module.exports = function(Chart) { backdropPaddingY: 2, // Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2 + backdropPaddingX: 2, + + callback: Chart.Ticks.formatters.linear }, pointLabels: { From d3d9573af5e8410cb456c86857cd58983e4b1ba8 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 25 Sep 2016 08:30:39 -0400 Subject: [PATCH 17/58] Fixes HTML legend string for polar area charts to match doughnut charts. (#3361) Fixes HTML legend string for polar area charts to match doughnut charts --- src/controllers/controller.polarArea.js | 4 ++-- test/defaultConfig.tests.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index b27daf417..78234ea59 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -32,11 +32,11 @@ module.exports = function(Chart) { if (datasets.length) { for (var i = 0; i < datasets[0].data.length; ++i) { - text.push('
  • '); + text.push('
  • '); if (labels[i]) { text.push(labels[i]); } - text.push('
  • '); + text.push(''); } } diff --git a/test/defaultConfig.tests.js b/test/defaultConfig.tests.js index fb34d33ec..84c9655e6 100644 --- a/test/defaultConfig.tests.js +++ b/test/defaultConfig.tests.js @@ -219,7 +219,7 @@ describe("Default Configs", function() { options: config }); - var expectedLegend = '
    • label1
    • label2
    '; + var expectedLegend = '
    • label1
    • label2
    '; expect(chart.generateLegend()).toBe(expectedLegend); }); From 806a3832ef3f169547edb913130b8c394104e356 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 24 Sep 2016 21:51:12 +0200 Subject: [PATCH 18/58] Inject iframe for responsive charts only Responsiveness is currently based on the use of an iframe, however this method causes performance issues and could be troublesome when used with ad blockers. So make sure that the user is still able to create a chart without iframe when responsive is false. --- src/core/core.controller.js | 31 ++++++++++++++++--------------- src/core/core.helpers.js | 19 ++++++++++++++----- test/core.controller.tests.js | 32 +++++++++++++++++++++++++++++++- test/mockContext.js | 8 ++++---- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 1d169adea..e335c3a3a 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -151,8 +151,11 @@ module.exports = function(Chart) { me.id = helpers.uid(); me.chart = instance; - me.config = instance.config; - me.options = me.config.options; + me.config = config; + me.options = config.options; + + // Add the chart instance to the global namespace + Chart.instances[me.id] = me; Object.defineProperty(me, 'data', { get: function() { @@ -160,18 +163,16 @@ module.exports = function(Chart) { } }); - // Always bind this so that if the responsive state changes we still work - helpers.addResizeListener(canvas.parentNode, function() { - if (me.config.options.responsive) { - me.resize(); - } - }); - - // Add the chart instance to the global namespace - Chart.instances[me.id] = me; - + // Responsiveness is currently based on the use of an iframe, however this method causes + // performance issues and could be troublesome when used with ad blockers. So make sure + // that the user is still able to create a chart without iframe when responsive is false. + // See https://github.com/chartjs/Chart.js/issues/2210 if (me.options.responsive) { - // Silent resize before chart draws + helpers.addResizeListener(canvas.parentNode, function() { + me.resize(); + }); + + // Initial resize before chart draws (must be silent to preserve initial animations). me.resize(true); } @@ -684,11 +685,11 @@ module.exports = function(Chart) { me.stop(); me.clear(); - helpers.unbindEvents(me, me.events); - if (canvas) { + helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); releaseCanvas(canvas); + me.chart.canvas = null; } // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index e404b61b1..6d63284a7 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -980,15 +980,24 @@ module.exports = function(Chart) { // Insert the iframe so that contentWindow is available node.insertBefore(iframe, node.firstChild); + // Let's keep track of this added iframe and thus avoid DOM query when removing it. + node._chartjs = { + resizer: iframe + }; + this.addEvent(iframe.contentWindow || iframe, 'resize', callback); }; helpers.removeResizeListener = function(node) { - var hiddenIframe = node.querySelector('.chartjs-hidden-iframe'); - - // Remove the resize detect iframe - if (hiddenIframe) { - hiddenIframe.parentNode.removeChild(hiddenIframe); + if (!node || !node._chartjs) { + return; } + + var iframe = node._chartjs.resizer; + if (iframe) { + iframe.parentNode.removeChild(iframe); + } + + delete node._chartjs; }; helpers.isArray = Array.isArray? function(obj) { diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 83eba3baf..37661b707 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -149,6 +149,18 @@ describe('Chart.Controller', function() { rw: 165, rh: 85, }); }); + + it('should NOT inject the resizer element', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }); + + var wrapper = chart.chart.canvas.parentNode; + expect(wrapper.childNodes.length).toBe(1); + expect(wrapper.firstChild.tagName).toBe('CANVAS'); + }); }); describe('config.options.responsive: true (maintainAspectRatio: false)', function() { @@ -449,7 +461,6 @@ describe('Chart.Controller', function() { describe('controller.destroy', function() { it('should restore canvas (and context) initial values', function(done) { var chart = acquireChart({ - type: 'line', options: { responsive: true, maintainAspectRatio: false @@ -484,5 +495,24 @@ describe('Chart.Controller', function() { done(); }); }); + + it('should remove the resizer element when responsive: true', function() { + var chart = acquireChart({ + options: { + responsive: true + } + }); + + var wrapper = chart.chart.canvas.parentNode; + var resizer = wrapper.firstChild; + + expect(wrapper.childNodes.length).toBe(2); + expect(resizer.tagName).toBe('IFRAME'); + + chart.destroy(); + + expect(wrapper.childNodes.length).toBe(1); + expect(wrapper.firstChild.tagName).toBe('CANVAS'); + }); }); }); diff --git a/test/mockContext.js b/test/mockContext.js index 48e5ce304..83b73e315 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -253,23 +253,23 @@ window.document.body.appendChild(wrapper); chart = new Chart(canvas.getContext("2d"), config); - chart.__test_persistent = options.persistent; + chart._test_persistent = options.persistent; + chart._test_wrapper = wrapper; charts[chart.id] = chart; return chart; } function releaseChart(chart) { chart.destroy(); - chart.chart.canvas.parentNode.remove(); + chart._test_wrapper.remove(); delete charts[chart.id]; - delete chart; } afterEach(function() { // Auto releasing acquired charts for (var id in charts) { var chart = charts[id]; - if (!chart.__test_persistent) { + if (!chart._test_persistent) { releaseChart(chart); } } From 9deebf837188164078b092a19bf3b7add660406f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 29 Sep 2016 22:18:34 +0200 Subject: [PATCH 19/58] Fix config initialization and add unit tests --- src/core/core.controller.js | 23 +++++----- test/core.controller.tests.js | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e335c3a3a..52f24d2ce 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -117,16 +117,19 @@ module.exports = function(Chart) { */ function initConfig(config) { config = config || {}; - return helpers.configMerge({ - options: helpers.configMerge( - Chart.defaults.global, - Chart.defaults[config.type], - config.options || {}), - data: { - datasets: [], - labels: [] - } - }, config); + + // Do NOT use configMerge() for the data object because this method merges arrays + // and so would change references to labels and datasets, preventing data updates. + var data = config.data = config.data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + + config.options = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[config.type], + config.options || {}); + + return config; } /** diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 37661b707..d7548d4ce 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -4,6 +4,89 @@ describe('Chart.Controller', function() { setTimeout(callback, 100); } + describe('config', function() { + it('should create missing config.data properties', function() { + var chart = acquireChart({}); + var data = chart.data; + + expect(data instanceof Object).toBeTruthy(); + expect(data.labels instanceof Array).toBeTruthy(); + expect(data.labels.length).toBe(0); + expect(data.datasets instanceof Array).toBeTruthy(); + expect(data.datasets.length).toBe(0); + }); + + it('should NOT alter config.data references', function() { + var ds0 = {data: [10, 11, 12, 13]}; + var ds1 = {data: [20, 21, 22, 23]}; + var datasets = [ds0, ds1]; + var labels = [0, 1, 2, 3]; + var data = {labels: labels, datasets: datasets}; + + var chart = acquireChart({ + type: 'line', + data: data + }); + + expect(chart.data).toBe(data); + expect(chart.data.labels).toBe(labels); + expect(chart.data.datasets).toBe(datasets); + expect(chart.data.datasets[0]).toBe(ds0); + expect(chart.data.datasets[1]).toBe(ds1); + expect(chart.data.datasets[0].data).toBe(ds0.data); + expect(chart.data.datasets[1].data).toBe(ds1.data); + }); + + it('should initialize config with default options', function() { + var callback = function() {}; + + var defaults = Chart.defaults; + defaults.global.responsiveAnimationDuration = 42; + defaults.global.hover.onHover = callback; + defaults.line.hover.mode = 'x-axis'; + defaults.line.spanGaps = true; + + var chart = acquireChart({ + type: 'line' + }); + + var options = chart.options; + expect(options.defaultFontSize).toBe(defaults.global.defaultFontSize); + expect(options.showLines).toBe(defaults.line.showLines); + expect(options.spanGaps).toBe(true); + expect(options.responsiveAnimationDuration).toBe(42); + expect(options.hover.onHover).toBe(callback); + expect(options.hover.mode).toBe('x-axis'); + }); + + it('should override default options', function() { + var defaults = Chart.defaults; + defaults.global.responsiveAnimationDuration = 42; + defaults.line.hover.mode = 'x-axis'; + defaults.line.spanGaps = true; + + var chart = acquireChart({ + type: 'line', + options: { + responsiveAnimationDuration: 4242, + spanGaps: false, + hover: { + mode: 'dataset', + }, + title: { + position: 'bottom' + } + } + }); + + var options = chart.options; + expect(options.responsiveAnimationDuration).toBe(4242); + expect(options.spanGaps).toBe(false); + expect(options.hover.mode).toBe('dataset'); + expect(options.title.position).toBe('bottom'); + }); + }); + describe('config.options.aspectRatio', function() { it('should use default "global" aspect ratio for render and display sizes', function() { var chart = acquireChart({ From 1484520692293902c9d64d39dc3a014abc98be0f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 1 Oct 2016 15:38:19 +0200 Subject: [PATCH 20/58] Better animation when adding or removing data In order to simulate real-time chart updates (i.e. horizontal animation), it's necessary to distinguish a removed or added value from a simple update. The dataset controller now hooks array methods that alter the data array length to synchronize metadata accordingly. Also remove the duplicate calls of updateBezierControlPoints() for line and radar charts. --- src/controllers/controller.line.js | 13 -- src/controllers/controller.radar.js | 8 -- src/core/core.controller.js | 10 ++ src/core/core.datasetController.js | 185 +++++++++++++++++++++++--- test/core.datasetController.tests.js | 188 +++++++++++++++++++++++++++ 5 files changed, 362 insertions(+), 42 deletions(-) create mode 100644 test/core.datasetController.tests.js diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index bfc061af2..7c36396bb 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -34,19 +34,6 @@ module.exports = function(Chart) { dataElementType: Chart.elements.Point, - addElementAndReset: function(index) { - var me = this; - var options = me.chart.options; - var meta = me.getMeta(); - - Chart.DatasetController.prototype.addElementAndReset.call(me, index); - - // Make sure bezier control points are updated - if (lineEnabled(me.getDataset(), options) && meta.dataset._model.tension !== 0) { - me.updateBezierControlPoints(); - } - }, - update: function(reset) { var me = this; var meta = me.getMeta(); diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 6232165f3..cd62da476 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -24,13 +24,6 @@ module.exports = function(Chart) { linkScales: helpers.noop, - addElementAndReset: function(index) { - Chart.DatasetController.prototype.addElementAndReset.call(this, index); - - // Make sure bezier control points are updated - this.updateBezierControlPoints(); - }, - update: function(reset) { var me = this; var meta = me.getMeta(); @@ -79,7 +72,6 @@ module.exports = function(Chart) { me.updateElement(point, index, reset); }, me); - // Update bezier control points me.updateBezierControlPoints(); }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 52f24d2ce..b2928c6b8 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -684,10 +684,20 @@ module.exports = function(Chart) { destroy: function() { var me = this; var canvas = me.chart.canvas; + var meta, i, ilen; me.stop(); me.clear(); + // dataset controllers need to cleanup associated data + for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + meta = me.getDatasetMeta(i); + if (meta.controller) { + meta.controller.destroy(); + meta.controller = null; + } + } + if (canvas) { helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 29b1aacea..2faff1b6e 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -3,7 +3,77 @@ module.exports = function(Chart) { var helpers = Chart.helpers; - var noop = helpers.noop; + + var arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; + + /** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the 'onData*' callbacks (e.g. onDataPush, etc.) with same arguments. + */ + function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + + arrayEvents.forEach(function(key) { + var method = 'onData' + key.charAt(0).toUpperCase() + key.slice(1); + var base = array[key]; + + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value: function() { + var args = Array.prototype.slice.call(arguments); + var res = base.apply(this, args); + + helpers.each(array._chartjs.listeners, function(object) { + if (typeof object[method] === 'function') { + object[method].apply(object, args); + } + }); + + return res; + } + }); + }); + } + + /** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ + function unlistenArrayEvents(array, listener) { + var stub = array._chartjs; + if (!stub) { + return; + } + + var listeners = stub.listeners; + var index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + + if (listeners.length > 0) { + return; + } + + arrayEvents.forEach(function(key) { + delete array[key]; + }); + + delete array._chartjs; + } // Base class for all dataset controllers (line, bar, etc) Chart.DatasetController = function(chart, datasetIndex) { @@ -65,6 +135,15 @@ module.exports = function(Chart) { this.update(true); }, + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + createMetaDataset: function() { var me = this; var type = me.datasetElementType; @@ -92,39 +171,42 @@ module.exports = function(Chart) { var i, ilen; for (i=0, ilen=data.length; i numMetaData) { - // Add new elements - for (var index = numMetaData; index < numData; ++index) { - this.addElementAndReset(index); + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + if (me._data !== data) { + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); } + + listenArrayEvents(data, me); + me._data = data; } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + me.resyncElements(); }, - update: noop, + update: helpers.noop, draw: function(ease) { var easingDecimal = ease || 1; @@ -156,8 +238,69 @@ module.exports = function(Chart) { model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : valueOrDefault(dataset.hoverBackgroundColor, index, getHoverColor(model.backgroundColor)); model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : valueOrDefault(dataset.hoverBorderColor, index, getHoverColor(model.borderColor)); model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : valueOrDefault(dataset.hoverBorderWidth, index, model.borderWidth); - } + }, + /** + * @private + */ + resyncElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data; + var numMeta = meta.data.length; + var numData = data.length; + + if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + } else if (numData > numMeta) { + me.insertElements(numMeta, numData - numMeta); + } + }, + + /** + * @private + */ + insertElements: function(start, count) { + for (var i=0; i Date: Mon, 3 Oct 2016 16:05:21 -0400 Subject: [PATCH 21/58] Improve Tooltip and Hover Interaction (#3400) Refactored interaction modes to use lookup functions in Chart.Interaction.modes and added new modes for 'point', 'index', 'nearest', 'x', and 'y' --- docs/01-Chart-Configuration.md | 35 +- samples/AnimationCallbacks/progress-bar.html | 2 +- samples/bar-multi-axis.html | 2 +- samples/bar-stacked.html | 2 +- samples/different-point-sizes.html | 2 +- samples/line-cubicInterpolationMode.html | 2 +- samples/line-legend.html | 2 +- samples/line-multi-axis.html | 2 +- samples/line-multiline-labels.html | 2 +- samples/line-skip-points.html | 4 +- samples/line-stacked-area.html | 4 +- samples/line-stepped.html | 2 +- samples/line.html | 6 +- samples/scatter-multi-axis.html | 3 +- samples/tooltip-hooks.html | 4 +- src/chart.js | 1 + src/controllers/controller.bar.js | 15 - src/core/core.controller.js | 121 +--- src/core/core.helpers.js | 3 + src/core/core.interaction.js | 263 +++++++++ src/core/core.js | 3 +- src/core/core.tooltip.js | 3 +- src/elements/element.arc.js | 13 + src/elements/element.point.js | 25 +- src/elements/element.rectangle.js | 94 +++- test/core.interaction.tests.js | 562 +++++++++++++++++++ test/element.arc.tests.js | 40 ++ test/element.point.tests.js | 30 + test/element.rectangle.tests.js | 34 ++ 29 files changed, 1119 insertions(+), 162 deletions(-) create mode 100644 src/core/core.interaction.js create mode 100644 test/core.interaction.tests.js diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index dd16e6d2c..5ed712a97 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -36,13 +36,13 @@ This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https:// Chart.js merges the options object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults.global`. The defaults for each chart type are discussed in the documentation for that chart type. -The following example would set the hover mode to 'single' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. +The following example would set the hover mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. ```javascript -Chart.defaults.global.hover.mode = 'single'; +Chart.defaults.global.hover.mode = 'nearest'; -// Hover mode is set to single because it was not overridden here -var chartInstanceHoverModeSingle = new Chart(ctx, { +// Hover mode is set to nearest because it was not overridden here +var chartInstanceHoverModeNearest = new Chart(ctx, { type: 'line', data: data, }); @@ -54,7 +54,7 @@ var chartInstanceDifferentHoverMode = new Chart(ctx, { options: { hover: { // Overrides the global setting - mode: 'label' + mode: 'index' } } }) @@ -200,7 +200,7 @@ var chartInstance = new Chart(ctx, { fontColor: 'rgb(255, 99, 132)' } } - } +} }); ``` @@ -212,7 +212,8 @@ Name | Type | Default | Description --- | --- | --- | --- enabled | Boolean | true | Are tooltips enabled custom | Function | null | See [section](#advanced-usage-external-tooltips) below -mode | String | 'single' | Sets which elements appear in the tooltip. Acceptable options are `'single'`, `'label'` or `'x-axis'`.
     
    `single` highlights the closest element.
     
    `label` highlights elements in all datasets at the same `X` value.
     
    `'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value. +mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details +intersect | Boolean | true | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. itemSort | Function | undefined | Allows sorting of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. backgroundColor | Color | 'rgba(0,0,0,0.8)' | Background color of the tooltip titleFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip title inherited from global font family @@ -286,10 +287,28 @@ The hover configuration is passed into the `options.hover` namespace. The global Name | Type | Default | Description --- | --- | --- | --- -mode | String | 'single' | Sets which elements hover. Acceptable options are `'single'`, `'label'`, `'x-axis'`, or `'dataset'`.
     
    `single` highlights the closest element.
     
    `label` highlights elements in all datasets at the same `X` value.
     
    `'x-axis'` also highlights elements in all datasets at the same `X` value, but activates when hovering anywhere within the vertical slice of the x-axis representing that `X` value.
     
    `dataset` highlights the closest dataset. +mode | String | 'naerest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details +intersect | Boolean | true | if true, the hover mode only applies when the mouse position intersects an item on the chart animationDuration | Number | 400 | Duration in milliseconds it takes to animate hover style changes onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed an array of active elements (bars, points, etc) +### Interaction Modes +When configuring interaction with the graph via hover or tooltips, a number of different modes are available. + +The following table details the modes and how they behave in conjunction with the `intersect` setting + +Mode | Behaviour +--- | --- +point | Finds all of the items that intersect the point +nearest | Gets the item that is nearest to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). If 2 or more items are at the same distance, the one with the smallest area is used. If `intersect` is true, this is only triggered when the mouse position intersects an item in the graph. This is very useful for combo charts where points are hidden behind bars. +single (deprecated) | Finds the first item that intersects the point and returns it. Behaves like 'nearest' mode with intersect = true. +label (deprecated) | See `'index'` mode +index | Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. +x-axis (deprecated) | Behaves like `'index'` mode with `intersect = true` +dataset | Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. +x | Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts +y | Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. + ### Animation Configuration The following animation options are available. The global options for are defined in `Chart.defaults.global.animation`. diff --git a/samples/AnimationCallbacks/progress-bar.html b/samples/AnimationCallbacks/progress-bar.html index 7d79d6c1d..fad7bd489 100644 --- a/samples/AnimationCallbacks/progress-bar.html +++ b/samples/AnimationCallbacks/progress-bar.html @@ -75,7 +75,7 @@ } }, tooltips: { - mode: 'label', + mode: 'index', }, scales: { xAxes: [{ diff --git a/samples/bar-multi-axis.html b/samples/bar-multi-axis.html index 9c8ec3642..ee027aaf8 100644 --- a/samples/bar-multi-axis.html +++ b/samples/bar-multi-axis.html @@ -56,7 +56,7 @@ data: barChartData, options: { responsive: true, - hoverMode: 'label', + hoverMode: 'index', hoverAnimationDuration: 400, stacked: false, title:{ diff --git a/samples/bar-stacked.html b/samples/bar-stacked.html index e85a72b26..685af83db 100644 --- a/samples/bar-stacked.html +++ b/samples/bar-stacked.html @@ -55,7 +55,7 @@ text:"Chart.js Bar Chart - Stacked" }, tooltips: { - mode: 'label' + mode: 'index' }, responsive: true, scales: { diff --git a/samples/different-point-sizes.html b/samples/different-point-sizes.html index 926eecfba..0fb231f52 100644 --- a/samples/different-point-sizes.html +++ b/samples/different-point-sizes.html @@ -70,7 +70,7 @@ position: 'bottom', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-cubicInterpolationMode.html b/samples/line-cubicInterpolationMode.html index 97dac46ca..2d85c95c9 100644 --- a/samples/line-cubicInterpolationMode.html +++ b/samples/line-cubicInterpolationMode.html @@ -68,7 +68,7 @@ text:'Chart.js Line Chart - Cubic interpolation mode' }, tooltips: { - mode: 'label' + mode: 'index' }, hover: { mode: 'dataset' diff --git a/samples/line-legend.html b/samples/line-legend.html index 92e5e5b5a..201d4c07e 100644 --- a/samples/line-legend.html +++ b/samples/line-legend.html @@ -68,7 +68,7 @@ position: 'bottom', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-multi-axis.html b/samples/line-multi-axis.html index 03da24bda..0b296ff69 100644 --- a/samples/line-multi-axis.html +++ b/samples/line-multi-axis.html @@ -54,7 +54,7 @@ data: lineChartData, options: { responsive: true, - hoverMode: 'label', + hoverMode: 'index', stacked: false, title:{ display:true, diff --git a/samples/line-multiline-labels.html b/samples/line-multiline-labels.html index 3fd0de5e1..b9d08aee7 100644 --- a/samples/line-multiline-labels.html +++ b/samples/line-multiline-labels.html @@ -65,7 +65,7 @@ text:'Chart.js Line Chart' }, tooltips: { - mode: 'label', + mode: 'index', callbacks: { // beforeTitle: function() { // return '...beforeTitle'; diff --git a/samples/line-skip-points.html b/samples/line-skip-points.html index 2d760d2e1..3ef275587 100644 --- a/samples/line-skip-points.html +++ b/samples/line-skip-points.html @@ -64,10 +64,10 @@ text:'Chart.js Line Chart - Skip Points' }, tooltips: { - mode: 'label', + mode: 'index', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-stacked-area.html b/samples/line-stacked-area.html index 88f14e2bd..26616f5de 100644 --- a/samples/line-stacked-area.html +++ b/samples/line-stacked-area.html @@ -63,10 +63,10 @@ text:"Chart.js Line Chart - Stacked Area" }, tooltips: { - mode: 'label', + mode: 'index', }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/samples/line-stepped.html b/samples/line-stepped.html index e618a698c..f6cebe262 100644 --- a/samples/line-stepped.html +++ b/samples/line-stepped.html @@ -68,7 +68,7 @@ text:'Chart.js Line Chart' }, tooltips: { - mode: 'label', + mode: 'index', callbacks: { // beforeTitle: function() { // return '...beforeTitle'; diff --git a/samples/line.html b/samples/line.html index ffca9df3b..464072ba1 100644 --- a/samples/line.html +++ b/samples/line.html @@ -65,7 +65,8 @@ text:'Chart.js Line Chart' }, tooltips: { - mode: 'label', + mode: 'index', + intersect: false, callbacks: { // beforeTitle: function() { // return '...beforeTitle'; @@ -91,7 +92,8 @@ } }, hover: { - mode: 'dataset' + mode: 'nearest', + intersect: true }, scales: { xAxes: [{ diff --git a/samples/scatter-multi-axis.html b/samples/scatter-multi-axis.html index 43d27e502..842f8891a 100644 --- a/samples/scatter-multi-axis.html +++ b/samples/scatter-multi-axis.html @@ -99,7 +99,8 @@ data: scatterChartData, options: { responsive: true, - hoverMode: 'single', + hoverMode: 'nearest', + intersect: true, title: { display: true, text: 'Chart.js Scatter Chart - Multi Axis' diff --git a/samples/tooltip-hooks.html b/samples/tooltip-hooks.html index 88a660515..08055ece8 100644 --- a/samples/tooltip-hooks.html +++ b/samples/tooltip-hooks.html @@ -59,7 +59,7 @@ text:"Chart.js Line Chart - Tooltip Hooks" }, tooltips: { - mode: 'label', + mode: 'index', callbacks: { beforeTitle: function() { return '...beforeTitle'; @@ -91,7 +91,7 @@ } }, hover: { - mode: 'label' + mode: 'index' }, scales: { xAxes: [{ diff --git a/src/chart.js b/src/chart.js index aa0d46c45..2c5e62826 100644 --- a/src/chart.js +++ b/src/chart.js @@ -16,6 +16,7 @@ require('./core/core.ticks.js')(Chart); require('./core/core.scale')(Chart); require('./core/core.title')(Chart); require('./core/core.legend')(Chart); +require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); require('./elements/element.arc')(Chart); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index d9fa7bfeb..f828eb79e 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -422,21 +422,6 @@ module.exports = function(Chart) { if (vm.borderWidth) { ctx.stroke(); } - }, - - inRange: function(mouseX, mouseY) { - var vm = this._view; - var inRange = false; - - if (vm) { - if (vm.x < vm.base) { - inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base); - } else { - inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x); - } - } - - return inRange; } }); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index b2928c6b8..9712d8fb6 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -515,125 +515,28 @@ module.exports = function(Chart) { // Get the single element that was clicked on // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw getElementAtEvent: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex); - helpers.each(meta.data, function(element) { - if (element.inRange(eventPosition.x, eventPosition.y)) { - elementsArray.push(element); - return elementsArray; - } - }); - } - }); - - return elementsArray.slice(0, 1); + return Chart.Interaction.modes.single(this, e); }, getElementsAtEvent: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - var found = function() { - if (me.data.datasets) { - for (var i = 0; i < me.data.datasets.length; i++) { - var meta = me.getDatasetMeta(i); - if (me.isDatasetVisible(i)) { - for (var j = 0; j < meta.data.length; j++) { - if (meta.data[j].inRange(eventPosition.x, eventPosition.y)) { - return meta.data[j]; - } - } - } - } - } - }.call(me); - - if (!found) { - return elementsArray; - } - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex), - element = meta.data[found._index]; - if (element && !element._view.skip) { - elementsArray.push(element); - } - } - }, me); - - return elementsArray; + return Chart.Interaction.modes.label(this, e, {intersect: true}); }, getElementsAtXAxis: function(e) { - var me = this; - var eventPosition = helpers.getRelativePosition(e, me.chart); - var elementsArray = []; - - var found = function() { - if (me.data.datasets) { - for (var i = 0; i < me.data.datasets.length; i++) { - var meta = me.getDatasetMeta(i); - if (me.isDatasetVisible(i)) { - for (var j = 0; j < meta.data.length; j++) { - if (meta.data[j].inLabelRange(eventPosition.x, eventPosition.y)) { - return meta.data[j]; - } - } - } - } - } - }.call(me); - - if (!found) { - return elementsArray; - } - - helpers.each(me.data.datasets, function(dataset, datasetIndex) { - if (me.isDatasetVisible(datasetIndex)) { - var meta = me.getDatasetMeta(datasetIndex); - var index = helpers.findIndex(meta.data, function(it) { - return found._model.x === it._model.x; - }); - if (index !== -1 && !meta.data[index]._view.skip) { - elementsArray.push(meta.data[index]); - } - } - }, me); - - return elementsArray; + return Chart.Interaction.modes['x-axis'](this, e, {intersect: true}); }, - getElementsAtEventForMode: function(e, mode) { - var me = this; - switch (mode) { - case 'single': - return me.getElementAtEvent(e); - case 'label': - return me.getElementsAtEvent(e); - case 'dataset': - return me.getDatasetAtEvent(e); - case 'x-axis': - return me.getElementsAtXAxis(e); - default: - return e; + getElementsAtEventForMode: function(e, mode, options) { + var method = Chart.Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options); } + + return []; }, getDatasetAtEvent: function(e) { - var elementsArray = this.getElementAtEvent(e); - - if (elementsArray.length > 0) { - elementsArray = this.getDatasetMeta(elementsArray[0]._datasetIndex).data; - } - - return elementsArray; + return Chart.Interaction.modes.dataset(this, e); }, getDatasetMeta: function(datasetIndex) { @@ -777,8 +680,8 @@ module.exports = function(Chart) { me.active = []; me.tooltipActive = []; } else { - me.active = me.getElementsAtEventForMode(e, hoverOptions.mode); - me.tooltipActive = me.getElementsAtEventForMode(e, tooltipsOptions.mode); + me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions); + me.tooltipActive = me.getElementsAtEventForMode(e, tooltipsOptions.mode, tooltipsOptions); } // On Hover hook diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 6d63284a7..d3fd72741 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -308,6 +308,9 @@ module.exports = function(Chart) { 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.aliasPixel = function(pixelWidth) { return (pixelWidth % 2 === 0) ? 0 : 0.5; }; diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js new file mode 100644 index 000000000..571f3c3c0 --- /dev/null +++ b/src/core/core.interaction.js @@ -0,0 +1,263 @@ +'use strict'; + +module.exports = function(Chart) { + var helpers = Chart.helpers; + + /** + * Helper function to traverse all of the visible elements in the chart + * @param chart {chart} the chart + * @param handler {Function} the callback to execute for each visible item + */ + function parseVisibleItems(chart, handler) { + var datasets = chart.data.datasets; + var meta, i, j, ilen, jlen; + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + if (!chart.isDatasetVisible(i)) { + continue; + } + + meta = chart.getDatasetMeta(i); + for (j = 0, jlen = meta.data.length; j < jlen; ++j) { + var element = meta.data[j]; + if (!element._view.skip) { + handler(element); + } + } + } + } + + /** + * Helper function to get the items that intersect the event position + * @param items {ChartElement[]} elements to filter + * @param position {Point} the point to be nearest to + * @return {ChartElement[]} the nearest items + */ + function getIntersectItems(chart, position) { + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + } + }); + + return elements; + } + + /** + * Helper function to get the items nearest to the event position considering all visible items in teh chart + * @param chart {Chart} the chart to look at elements from + * @param position {Point} the point to be nearest to + * @param intersect {Boolean} if true, only consider items that intersect the position + * @return {ChartElement[]} the nearest items + */ + function getNearestItems(chart, position, intersect) { + var minDistance = Number.POSITIVE_INFINITY; + var nearestItems = []; + + parseVisibleItems(chart, function(element) { + if (intersect && !element.inRange(position.x, position.y)) { + return; + } + + var center = element.getCenterPoint(); + var distance = Math.round(helpers.distanceBetweenPoints(position, center)); + + if (distance < minDistance) { + nearestItems = [element]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + nearestItems.push(element); + } + }); + + return nearestItems; + } + + function indexMode(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); + var elements = []; + + if (!items.length) { + return []; + } + + chart.data.datasets.forEach(function(dataset, datasetIndex) { + if (chart.isDatasetVisible(datasetIndex)) { + var meta = chart.getDatasetMeta(datasetIndex), + element = meta.data[items[0]._index]; + + // don't count items that are skipped (null data) + if (element && !element._view.skip) { + elements.push(element); + } + } + }); + + return elements; + } + + /** + * @interface IInteractionOptions + */ + /** + * If true, only consider items that intersect the point + * @name IInterfaceOptions#boolean + * @type Boolean + */ + + /** + * @namespace Chart.Interaction + * Contains interaction related functions + */ + Chart.Interaction = { + // Helper function for different modes + modes: { + single: function(chart, e) { + var position = helpers.getRelativePosition(e, chart.chart); + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + return elements; + } + }); + + return elements.slice(0, 1); + }, + + /** + * @function Chart.Interaction.modes.label + * @deprecated since version 2.4.0 + */ + label: indexMode, + + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + * @function Chart.Interaction.modes.index + * @since v2.4.0 + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + index: indexMode, + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + * @function Chart.Interaction.modes.dataset + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + dataset: function(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); + + if (items.length > 0) { + items = chart.getDatasetMeta(items[0]._datasetIndex).data; + } + + return items; + }, + + /** + * @function Chart.Interaction.modes.x-axis + * @deprecated since version 2.4.0. Use index mode and intersect == true + */ + 'x-axis': function(chart, e) { + return indexMode(chart, e, true); + }, + + /** + * Point mode returns all elements that hit test based on the event position + * of the event + * @function Chart.Interaction.modes.intersect + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + point: function(chart, e) { + var position = helpers.getRelativePosition(e, chart.chart); + return getIntersectItems(chart, position); + }, + + /** + * nearest mode returns the element closest to the point + * @function Chart.Interaction.modes.intersect + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + nearest: function(chart, e, options) { + var position = helpers.getRelativePosition(e, chart.chart); + var nearestItems = getNearestItems(chart, position, options.intersect); + + // We have multiple items at the same distance from the event. Now sort by smallest + if (nearestItems.length > 1) { + nearestItems.sort(function(a, b) { + var sizeA = a.getArea(); + var sizeB = b.getArea(); + var ret = sizeA - sizeB; + + if (ret === 0) { + // if equal sort by dataset index + ret = a._datasetIndex - b._datasetIndex; + } + + return ret; + }); + } + + // Return only 1 item + return nearestItems.slice(0, 1); + }, + + /** + * x mode returns the elements that hit-test at the current x coordinate + * @function Chart.Interaction.modes.x + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + x: function(chart, e) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = []; + parseVisibleItems(chart, function(element) { + if (element.inXRange(position.x)) { + items.push(element); + } + }); + return items; + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @function Chart.Interaction.modes.y + * @param chart {chart} the chart we are returning items from + * @param e {Event} the event we are find things at + * @param options {IInteractionOptions} options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + y: function(chart, e) { + var position = helpers.getRelativePosition(e, chart.chart); + var items = []; + parseVisibleItems(chart, function(element) { + if (element.inYRange(position.x)) { + items.push(element); + } + }); + return items; + } + } + }; +}; diff --git a/src/core/core.js b/src/core/core.js index d28997838..319219f5d 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -30,7 +30,8 @@ module.exports = function() { events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], hover: { onHover: null, - mode: 'single', + mode: 'nearest', + intersect: true, animationDuration: 400 }, onClick: null, diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index d0068ee67..9210ed529 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -7,7 +7,8 @@ module.exports = function(Chart) { Chart.defaults.global.tooltips = { enabled: true, custom: null, - mode: 'single', + mode: 'nearest', + intersect: true, backgroundColor: 'rgba(0,0,0,0.8)', titleFontStyle: 'bold', titleSpacing: 2, diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index b114e6f04..a61d19a3a 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -52,6 +52,19 @@ module.exports = function(Chart) { } return false; }, + getCenterPoint: function() { + var vm = this._view; + var halfAngle = (vm.startAngle + vm.endAngle) / 2; + var halfRadius = (vm.innerRadius + vm.outerRadius) / 2; + return { + x: vm.x + Math.cos(halfAngle) * halfRadius, + y: vm.y + Math.sin(halfAngle) * halfRadius + }; + }, + getArea: function() { + var vm = this._view; + return Math.PI * ((vm.endAngle - vm.startAngle) / (2 * Math.PI)) * (Math.pow(vm.outerRadius, 2) - Math.pow(vm.innerRadius, 2)); + }, tooltipPosition: function() { var vm = this._view; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 474ef252f..fe3a6696b 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -18,14 +18,35 @@ module.exports = function(Chart) { hoverBorderWidth: 1 }; + function xRange(mouseX) { + var vm = this._view; + return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + } + + function yRange(mouseY) { + var vm = this._view; + return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + } + Chart.elements.Point = Chart.Element.extend({ inRange: function(mouseX, mouseY) { var vm = this._view; return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; }, - inLabelRange: function(mouseX) { + + inLabelRange: xRange, + inXRange: xRange, + inYRange: yRange, + + getCenterPoint: function() { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return { + x: vm.x, + y: vm.y + }; + }, + getArea: function() { + return Math.PI * Math.pow(this._view.radius, 2); }, tooltipPosition: function() { var vm = this._view; diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 6752228c8..3f4ea10f9 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -11,6 +11,44 @@ module.exports = function(Chart) { borderSkipped: 'bottom' }; + function isVertical(bar) { + return bar._view.width !== undefined; + } + + /** + * Helper function to get the bounds of the bar regardless of the orientation + * @private + * @param bar {Chart.Element.Rectangle} the bar + * @return {Bounds} bounds of the bar + */ + function getBarBounds(bar) { + var vm = bar._view; + var x1, x2, y1, y2; + + if (isVertical(bar)) { + // vertical + var halfWidth = vm.width / 2; + x1 = vm.x - halfWidth; + x2 = vm.x + halfWidth; + y1 = Math.min(vm.y, vm.base); + y2 = Math.max(vm.y, vm.base); + } else { + // horizontal bar + var halfHeight = vm.height / 2; + x1 = Math.min(vm.x, vm.base); + x2 = Math.max(vm.x, vm.base); + y1 = vm.y - halfHeight; + y2 = vm.y + halfHeight; + } + + return { + left: x1, + top: y1, + right: x2, + bottom: y2 + }; + } + Chart.elements.Rectangle = Chart.Element.extend({ draw: function() { var ctx = this._chart.ctx; @@ -72,16 +110,56 @@ module.exports = function(Chart) { return vm.base - vm.y; }, inRange: function(mouseX, mouseY) { - var vm = this._view; - return vm ? - (vm.y < vm.base ? - (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base) : - (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y)) : - false; + var inRange = false; + + if (this._view) { + var bounds = getBarBounds(this); + inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom; + } + + return inRange; }, - inLabelRange: function(mouseX) { + inLabelRange: function(mouseX, mouseY) { + var me = this; + if (!me._view) { + return false; + } + + var inRange = false; + var bounds = getBarBounds(me); + + if (isVertical(me)) { + inRange = mouseX >= bounds.left && mouseX <= bounds.right; + } else { + inRange = mouseY >= bounds.top && mouseY <= bounds.bottom; + } + + return inRange; + }, + inXRange: function(mouseX) { + var bounds = getBarBounds(this); + return mouseX >= bounds.left && mouseX <= bounds.right; + }, + inYRange: function(mouseY) { + var bounds = getBarBounds(this); + return mouseY >= bounds.top && mouseY <= bounds.bottom; + }, + getCenterPoint: function() { var vm = this._view; - return vm ? (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) : false; + var x, y; + if (isVertical(this)) { + x = vm.x; + y = (vm.y + vm.base) / 2; + } else { + x = (vm.x + vm.base) / 2; + y = vm.y; + } + + return {x: x, y: y}; + }, + getArea: function() { + var vm = this._view; + return vm.width * Math.abs(vm.y - vm.base); }, tooltipPosition: function() { var vm = this._view; diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js new file mode 100644 index 000000000..312ac8fe6 --- /dev/null +++ b/test/core.interaction.tests.js @@ -0,0 +1,562 @@ +// Tests of the interaction handlers in Core.Interaction + +// Test the rectangle element +describe('Core.Interaction', function() { + describe('point mode', function() { + it ('should return all items under the point', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 20, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + var point = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.point(chartInstance, evt); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('should return an empty array when no items are found', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 20, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event at (0, 0) + var node = chartInstance.chart.canvas; + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: 0, + clientY: 0, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.point(chartInstance, evt); + expect(elements).toEqual([]); + }); + }); + + describe('index mode', function() { + it ('should return all items at the same index', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + var point = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('should return all items at the same index when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: false }); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + }); + + describe('dataset mode', function() { + it ('should return all items in the dataset of the first item found', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: true }); + expect(elements).toEqual(meta.data); + }); + + it ('should return all items in the dataset of the first item found when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left, + clientY: rect.top, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: false }); + + var meta = chartInstance.getDatasetMeta(1); + expect(elements).toEqual(meta.data); + }); + }); + + describe('nearest mode', function() { + it ('should return the nearest item', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(1); + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: 0, + clientY: 0, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + expect(elements).toEqual([meta.data[0]]); + }); + + it ('should return the smallest item if more than 1 are at the same distance', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 5, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the lowest dataset index if size and area are the same', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var meta1 = chartInstance.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + expect(elements).toEqual([meta0.data[1]]); + }); + }); + + describe('nearest intersect mode', function() { + it ('should return the nearest item', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(1); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._view.x + 15, + clientY: rect.top + point._view.y, + currentTarget: node + }; + + // Nothing intersects so find nothing + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([]); + + evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._view.x, + clientY: rect.top + point._view.y, + currentTarget: node + }; + elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([point]); + }); + + it ('should return the nearest item even if 2 intersect', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 39, 30], + pointRadius: [5, 30, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the smallest item if more than 1 are at the same distance', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 5, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the item at the lowest dataset index if distance and area are the same', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + pt.x, + clientY: rect.top + pt.y, + currentTarget: node + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + expect(elements).toEqual([meta0.data[1]]); + }); + }); +}); diff --git a/test/element.arc.tests.js b/test/element.arc.tests.js index 4ba854575..7ce70517d 100644 --- a/test/element.arc.tests.js +++ b/test/element.arc.tests.js @@ -60,6 +60,46 @@ describe('Arc element tests', function() { expect(pos.y).toBeCloseTo(0.5); }); + it ('should get the area', function() { + var arc = new Chart.elements.Arc({ + _datasetIndex: 2, + _index: 1 + }); + + // Mock out the view as if the controller put it there + arc._view = { + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + }; + + expect(arc.getArea()).toBeCloseTo(0.5 * Math.PI, 6); + }); + + it ('should get the center', function() { + var arc = new Chart.elements.Arc({ + _datasetIndex: 2, + _index: 1 + }); + + // Mock out the view as if the controller put it there + arc._view = { + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + }; + + var center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(0.5, 6); + expect(center.y).toBeCloseTo(0.5, 6); + }); + it ('should draw correctly with no border', function() { var mockContext = window.createMockContext(); var arc = new Chart.elements.Arc({ diff --git a/test/element.point.tests.js b/test/element.point.tests.js index c257f7375..9cbac8ea5 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -64,6 +64,36 @@ describe('Point element tests', function() { }); }); + it('should get the correct area', function() { + var point = new Chart.elements.Point({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + point._view = { + radius: 2, + }; + + expect(point.getArea()).toEqual(Math.PI * 4); + }); + + it('should get the correct center point', function() { + var point = new Chart.elements.Point({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + point._view = { + radius: 2, + x: 10, + y: 10 + }; + + expect(point.getCenterPoint()).toEqual({ x: 10, y: 10 }); + }); + it ('should draw correctly', function() { var mockContext = window.createMockContext(); var point = new Chart.elements.Point({ diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index 8c28970b1..bd32ff3ac 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -132,6 +132,40 @@ describe('Rectangle element tests', function() { }); }); + it ('should get the correct area', function() { + var rectangle = new Chart.elements.Rectangle({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + rectangle._view = { + base: 0, + width: 4, + x: 10, + y: 15 + }; + + expect(rectangle.getArea()).toEqual(60); + }); + + it ('should get the center', function() { + var rectangle = new Chart.elements.Rectangle({ + _datasetIndex: 2, + _index: 1 + }); + + // Attach a view object as if we were the controller + rectangle._view = { + base: 0, + width: 4, + x: 10, + y: 15 + }; + + expect(rectangle.getCenterPoint()).toEqual({ x: 10, y: 7.5 }); + }); + it ('should draw correctly', function() { var mockContext = window.createMockContext(); var rectangle = new Chart.elements.Rectangle({ From c15fa9897847c5e6a44377785ffccdbbfdec9145 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 3 Oct 2016 19:10:54 -0400 Subject: [PATCH 22/58] Display tooltip color boxes for all tooltips. Added a new `displayColors` option to turn them off --- docs/01-Chart-Configuration.md | 1 + src/core/core.tooltip.js | 18 +++++++++--------- test/core.tooltip.tests.js | 7 ++++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 5ed712a97..1aeaab940 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -238,6 +238,7 @@ yPadding | Number | 6 | Padding to add on top and bottom of tooltip caretSize | Number | 5 | Size, in px, of the tooltip arrow cornerRadius | Number | 6 | Radius of tooltip corner curves multiKeyBackground | Color | "#fff" | Color to draw behind the colored boxes when multiple items are in the tooltip +displayColors | Boolean | true | if true, color boxes are shown in the tooltip callbacks | Object | | See the [callbacks section](#chart-configuration-tooltip-callbacks) below #### Tooltip Callbacks diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 9210ed529..1c9a1fdb1 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -30,6 +30,7 @@ module.exports = function(Chart) { caretSize: 5, cornerRadius: 6, multiKeyBackground: '#fff', + displayColors: true, callbacks: { // Args are: (tooltipItems, data) beforeTitle: helpers.noop, @@ -193,7 +194,8 @@ module.exports = function(Chart) { cornerRadius: tooltipOpts.cornerRadius, backgroundColor: tooltipOpts.backgroundColor, opacity: 0, - legendColorBackground: tooltipOpts.multiKeyBackground + legendColorBackground: tooltipOpts.multiKeyBackground, + displayColors: tooltipOpts.displayColors } }); }, @@ -298,12 +300,10 @@ module.exports = function(Chart) { }); } - // If there is more than one item, show color items - if (active.length > 1) { - helpers.each(tooltipItems, function(tooltipItem) { - labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance)); - }); - } + // Determine colors for boxes + helpers.each(tooltipItems, function(tooltipItem) { + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance)); + }); // Build the Text Lines helpers.extend(model, { @@ -377,7 +377,7 @@ module.exports = function(Chart) { helpers.each(vm.beforeBody.concat(vm.afterBody), maxLineWidth); // Body lines may include some extra width due to the color box - widthPadding = body.length > 1 ? (bodyFontSize + 2) : 0; + widthPadding = vm.displayColors ? (bodyFontSize + 2) : 0; helpers.each(body, function(bodyItem) { helpers.each(bodyItem.before, maxLineWidth); helpers.each(bodyItem.lines, maxLineWidth); @@ -613,7 +613,7 @@ module.exports = function(Chart) { // Before body lines helpers.each(vm.beforeBody, fillLineOfText); - var drawColorBoxes = body.length > 1; + var drawColorBoxes = vm.displayColors; xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; // Draw body lines now diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 21d25ebaa..9927debc0 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -85,6 +85,7 @@ describe('tooltip tests', function() { backgroundColor: 'rgba(0,0,0,0.8)', opacity: 1, legendColorBackground: '#fff', + displayColors: true, // Text title: ['Point 2'], @@ -199,6 +200,7 @@ describe('tooltip tests', function() { backgroundColor: 'rgba(0,0,0,0.8)', opacity: 1, legendColorBackground: '#fff', + displayColors: true, // Text title: ['Point 2'], @@ -211,7 +213,10 @@ describe('tooltip tests', function() { afterBody: [], footer: [], caretPadding: 2, - labelColors: [] + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }] })); expect(tooltip._view.x).toBeCloseToPixel(269); From 99b8d6740a27bdf8ecbf887bd2d29ffc32d0f5dc Mon Sep 17 00:00:00 2001 From: Aidan Fewster Date: Tue, 4 Oct 2016 11:48:18 +0100 Subject: [PATCH 23/58] Added the watchify NPM dependency to satisfy karma-browserify's peerDependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4d2bb8042..1e16185de 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "karma-jasmine-html-reporter": "^0.1.8", "merge-stream": "^1.0.0", "vinyl-source-stream": "^1.1.0", + "watchify": "^3.7.0", "yargs": "^5.0.0" }, "spm": { From 365bdbb3467eb675f4a0353c422f853058348d2e Mon Sep 17 00:00:00 2001 From: Aidan Fewster Date: Tue, 4 Oct 2016 11:53:55 +0100 Subject: [PATCH 24/58] If tick options have min, max and stepSize use them to generate evenly spaced ticks --- src/core/core.ticks.js | 11 ++++++++++- test/scale.linear.tests.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index f55b8d40e..78ec07c3b 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -64,8 +64,17 @@ module.exports = function(Chart) { } var niceMin = Math.floor(dataRange.min / spacing) * spacing; var niceMax = Math.ceil(dataRange.max / spacing) * spacing; - var numSpaces = (niceMax - niceMin) / spacing; + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + var minMaxDeltaDivisableByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; + if (minMaxDeltaDivisableByStepSize) { + niceMin = generationOptions.min; + niceMax = generationOptions.max; + } + } + + var numSpaces = (niceMax - niceMin) / spacing; // If very close to our rounded value, use it. if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { numSpaces = Math.round(numSpaces); diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 3df773258..4eb585a7b 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -486,6 +486,38 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.ticks[chart.scales.yScale0.ticks.length - 1]).toBe('-1010'); }); + it('Should use min, max and stepSize to create fixed spaced ticks', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [10, 3, 6, 8, 3, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + min: 1, + max: 11, + stepSize: 2 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(1); + expect(chart.scales.yScale0.max).toBe(11); + expect(chart.scales.yScale0.ticks).toEqual(['11', '9', '7', '5', '3', '1']); + }); + + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({ type: 'bar', From b64cab004669a010025e4641eb7f359c4035f6b9 Mon Sep 17 00:00:00 2001 From: Mickael Jeanroy Date: Tue, 4 Oct 2016 17:08:55 +0200 Subject: [PATCH 25/58] Refactor tooltip draw function to extract drawBackground method See issue #3416. --- src/core/core.tooltip.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 1c9a1fdb1..54f9f49f5 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -670,6 +670,12 @@ module.exports = function(Chart) { }); } }, + drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { + var bgColor = helpers.color(vm.backgroundColor); + ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); + helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); + ctx.fill(); + }, draw: function() { var ctx = this._chart.ctx; var vm = this._view; @@ -689,10 +695,7 @@ module.exports = function(Chart) { if (this._options.enabled) { // Draw Background - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); - helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); - ctx.fill(); + this.drawBackground(pt, vm, ctx, tooltipSize, opacity); // Draw Caret this.drawCaret(pt, tooltipSize, opacity); From 4d2772e313d007d8327aac2b51289301d7eb7264 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Thu, 6 Oct 2016 08:54:50 -0400 Subject: [PATCH 26/58] Fix bubble chart tooltip callback to use correct labels (#3421) Fix bubble chart tooltip callback to use correct label parsed from scales. Fixes #3029 --- src/controllers/controller.bubble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index b1c6474e0..abe807b3b 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -31,7 +31,7 @@ module.exports = function(Chart) { label: function(tooltipItem, data) { var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; var dataPoint = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - return datasetLabel + ': (' + dataPoint.x + ', ' + dataPoint.y + ', ' + dataPoint.r + ')'; + return datasetLabel + ': (' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ', ' + dataPoint.r + ')'; } } } From 65a06e473560a4d923ed52d4437c3666662d44a6 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 7 Oct 2016 20:39:24 -0400 Subject: [PATCH 27/58] Properly merge colors for the label colors in the tooltip. I added a private helper to simplify the code in the tooltip --- src/core/core.tooltip.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 54f9f49f5..8a0afe3dc 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -4,6 +4,14 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper method to merge the opacity into a color + */ + function mergeOpacity(colorString, opacity) { + var color = helpers.color(colorString); + return color.alpha(opacity * color.alpha()).rgbaString(); + } + Chart.defaults.global.tooltips = { enabled: true, custom: null, @@ -556,8 +564,7 @@ module.exports = function(Chart) { } } - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); @@ -575,8 +582,7 @@ module.exports = function(Chart) { var titleFontSize = vm.titleFontSize, titleSpacing = vm.titleSpacing; - var titleFontColor = helpers.color(vm.titleFontColor); - ctx.fillStyle = titleFontColor.alpha(opacity * titleFontColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); var i, len; @@ -598,8 +604,7 @@ module.exports = function(Chart) { ctx.textAlign = vm._bodyAlign; ctx.textBaseline = 'top'; - var bodyFontColor = helpers.color(vm.bodyFontColor); - var textColor = bodyFontColor.alpha(opacity * bodyFontColor.alpha()).rgbString(); + var textColor = mergeOpacity(vm.bodyFontColor, opacity); ctx.fillStyle = textColor; ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); @@ -624,15 +629,15 @@ module.exports = function(Chart) { // Draw Legend-like boxes if needed if (drawColorBoxes) { // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = helpers.color(vm.legendColorBackground).alpha(opacity).rgbaString(); + ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Border - ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString(); + ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Inner square - ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(opacity).rgbaString(); + ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); ctx.fillStyle = textColor; @@ -660,8 +665,7 @@ module.exports = function(Chart) { ctx.textAlign = vm._footerAlign; ctx.textBaseline = 'top'; - var footerFontColor = helpers.color(vm.footerFontColor); - ctx.fillStyle = footerFontColor.alpha(opacity * footerFontColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); helpers.each(footer, function(line) { @@ -671,8 +675,7 @@ module.exports = function(Chart) { } }, drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { - var bgColor = helpers.color(vm.backgroundColor); - ctx.fillStyle = bgColor.alpha(opacity * bgColor.alpha()).rgbString(); + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); ctx.fill(); }, From d21a853f30a87a4cb0fe6c6f2bb39320c0404c19 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 9 Oct 2016 12:26:59 -0400 Subject: [PATCH 28/58] Fix/3061 (#3446) Solve weird animation issues with the tooltip. The optimization in Chart.Element.transition when the animation finishes to set `_view = _model` caused problems during update because we were using `helpers.extend` all over the place. I changed to code so that we regenerate the model variable rather than continuously extending the old version. I also removed unnecessary tooltip reinitializations from the controller which should improve overall performance during interaction. --- src/core/core.controller.js | 6 +- src/core/core.tooltip.js | 526 ++++++++++++++++++++---------------- 2 files changed, 292 insertions(+), 240 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 9712d8fb6..6302dd11c 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -630,6 +630,7 @@ module.exports = function(Chart) { _data: me.data, _options: me.options.tooltips }, me); + me.tooltip.initialize(); }, bindEvents: function() { @@ -711,14 +712,10 @@ module.exports = function(Chart) { // Built in Tooltips if (tooltipsOptions.enabled || tooltipsOptions.custom) { - tooltip.initialize(); tooltip._active = me.tooltipActive; - tooltip.update(true); } // Hover animations - tooltip.pivot(); - if (!me.animating) { // If entering, leaving, or changing elements, animate the change via pivot if (!helpers.arrayEquals(me.active, me.lastActive) || @@ -728,6 +725,7 @@ module.exports = function(Chart) { if (tooltipsOptions.enabled || tooltipsOptions.custom) { tooltip.update(true); + tooltip.pivot(); } // We only need to render at this point. Updating will cause scales to be diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 8a0afe3dc..dcb9e89d3 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -33,8 +33,6 @@ module.exports = function(Chart) { footerAlign: 'left', yPadding: 6, xPadding: 6, - yAlign: 'center', - xAlign: 'center', caretSize: 5, cornerRadius: 6, multiKeyBackground: '#fff', @@ -156,56 +154,249 @@ module.exports = function(Chart) { }; } + /** + * Helper to get the reset model for the tooltip + * @param tooltipOpts {Object} the tooltip options + */ + function getBaseModel(tooltipOpts) { + var globalDefaults = Chart.defaults.global; + var getValueOrDefault = helpers.getValueOrDefault; + + return { + // Positioning + xPadding: tooltipOpts.xPadding, + yPadding: tooltipOpts.yPadding, + xAlign: tooltipOpts.xAlign, + yAlign: tooltipOpts.yAlign, + + // Body + bodyFontColor: tooltipOpts.bodyFontColor, + _bodyFontFamily: getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyAlign: tooltipOpts.bodyAlign, + bodyFontSize: getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodySpacing: tooltipOpts.bodySpacing, + + // Title + titleFontColor: tooltipOpts.titleFontColor, + _titleFontFamily: getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleAlign: tooltipOpts.titleAlign, + titleSpacing: tooltipOpts.titleSpacing, + titleMarginBottom: tooltipOpts.titleMarginBottom, + + // Footer + footerFontColor: tooltipOpts.footerFontColor, + _footerFontFamily: getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), + _footerAlign: tooltipOpts.footerAlign, + footerSpacing: tooltipOpts.footerSpacing, + footerMarginTop: tooltipOpts.footerMarginTop, + + // Appearance + caretSize: tooltipOpts.caretSize, + cornerRadius: tooltipOpts.cornerRadius, + backgroundColor: tooltipOpts.backgroundColor, + opacity: 0, + legendColorBackground: tooltipOpts.multiKeyBackground, + displayColors: tooltipOpts.displayColors + }; + } + + /** + * Get the size of the tooltip + */ + function getTooltipSize(tooltip, model) { + var ctx = tooltip._chart.ctx; + + var height = model.yPadding * 2; // Tooltip Padding + var width = 0; + + // Count of all lines in the body + var body = model.body; + var combinedBodyLength = body.reduce(function(count, bodyItem) { + return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; + }, 0); + combinedBodyLength += model.beforeBody.length + model.afterBody.length; + + var titleLineCount = model.title.length; + var footerLineCount = model.footer.length; + var titleFontSize = model.titleFontSize, + bodyFontSize = model.bodyFontSize, + footerFontSize = model.footerFontSize; + + height += titleLineCount * titleFontSize; // Title Lines + height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing + height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin + height += combinedBodyLength * bodyFontSize; // Body Lines + height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing + height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin + height += footerLineCount * (footerFontSize); // Footer Lines + height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing + + // Title width + var widthPadding = 0; + var maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + + ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); + helpers.each(model.title, maxLineWidth); + + // Body width + ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); + helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + + // Body lines may include some extra width due to the color box + widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; + helpers.each(body, function(bodyItem) { + helpers.each(bodyItem.before, maxLineWidth); + helpers.each(bodyItem.lines, maxLineWidth); + helpers.each(bodyItem.after, maxLineWidth); + }); + + // Reset back to 0 + widthPadding = 0; + + // Footer width + ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); + helpers.each(model.footer, maxLineWidth); + + // Add padding + width += 2 * model.xPadding; + + return { + width: width, + height: height + }; + } + + /** + * Helper to get the alignment of a tooltip given the size + */ + function determineAlignment(tooltip, size) { + var model = tooltip._model; + var chart = tooltip._chart; + var chartArea = tooltip._chartInstance.chartArea; + var xAlign = 'center'; + var yAlign = 'center'; + + if (model.y < size.height) { + yAlign = 'top'; + } else if (model.y > (chart.height - size.height)) { + yAlign = 'bottom'; + } + + var lf, rf; // functions to determine left, right alignment + var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart + var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges + var midX = (chartArea.left + chartArea.right) / 2; + var midY = (chartArea.top + chartArea.bottom) / 2; + + if (yAlign === 'center') { + lf = function(x) { + return x <= midX; + }; + rf = function(x) { + return x > midX; + }; + } else { + lf = function(x) { + return x <= (size.width / 2); + }; + rf = function(x) { + return x >= (chart.width - (size.width / 2)); + }; + } + + olf = function(x) { + return x + size.width > chart.width; + }; + orf = function(x) { + return x - size.width < 0; + }; + yf = function(y) { + return y <= midY ? 'top' : 'bottom'; + }; + + if (lf(model.x)) { + xAlign = 'left'; + + // Is tooltip too wide and goes over the right side of the chart.? + if (olf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } else if (rf(model.x)) { + xAlign = 'right'; + + // Is tooltip too wide and goes outside left edge of canvas? + if (orf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } + } + + var opts = tooltip._options; + return { + xAlign: opts.xAlign ? opts.xAlign : xAlign, + yAlign: opts.yAlign ? opts.yAlign : yAlign + }; + } + + /** + * @Helper to get the location a tooltiop needs to be placed at given the initial position (via the vm) and the size and alignment + */ + function getBackgroundPoint(vm, size, alignment) { + // Background Position + var x = vm.x; + var y = vm.y; + + var caretSize = vm.caretSize, + caretPadding = vm.caretPadding, + cornerRadius = vm.cornerRadius, + xAlign = alignment.xAlign, + yAlign = alignment.yAlign, + paddingAndSize = caretSize + caretPadding, + radiusAndPadding = cornerRadius + caretPadding; + + if (xAlign === 'right') { + x -= size.width; + } else if (xAlign === 'center') { + x -= (size.width / 2); + } + + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= size.height + paddingAndSize; + } else { + y -= (size.height / 2); + } + + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; + } + + return { + x: x, + y: y + }; + } + Chart.Tooltip = Chart.Element.extend({ initialize: function() { - var me = this; - var globalDefaults = Chart.defaults.global; - var tooltipOpts = me._options; - var getValueOrDefault = helpers.getValueOrDefault; - - helpers.extend(me, { - _model: { - // Positioning - xPadding: tooltipOpts.xPadding, - yPadding: tooltipOpts.yPadding, - xAlign: tooltipOpts.xAlign, - yAlign: tooltipOpts.yAlign, - - // Body - bodyFontColor: tooltipOpts.bodyFontColor, - _bodyFontFamily: getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), - _bodyFontStyle: getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), - _bodyAlign: tooltipOpts.bodyAlign, - bodyFontSize: getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), - bodySpacing: tooltipOpts.bodySpacing, - - // Title - titleFontColor: tooltipOpts.titleFontColor, - _titleFontFamily: getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), - _titleFontStyle: getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), - titleFontSize: getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), - _titleAlign: tooltipOpts.titleAlign, - titleSpacing: tooltipOpts.titleSpacing, - titleMarginBottom: tooltipOpts.titleMarginBottom, - - // Footer - footerFontColor: tooltipOpts.footerFontColor, - _footerFontFamily: getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), - _footerFontStyle: getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), - footerFontSize: getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize), - _footerAlign: tooltipOpts.footerAlign, - footerSpacing: tooltipOpts.footerSpacing, - footerMarginTop: tooltipOpts.footerMarginTop, - - // Appearance - caretSize: tooltipOpts.caretSize, - cornerRadius: tooltipOpts.cornerRadius, - backgroundColor: tooltipOpts.backgroundColor, - opacity: 0, - legendColorBackground: tooltipOpts.multiKeyBackground, - displayColors: tooltipOpts.displayColors - } - }); + this._model = getBaseModel(this._options); }, // Get the title @@ -282,12 +473,31 @@ module.exports = function(Chart) { update: function(changed) { var me = this; var opts = me._options; - var model = me._model; + + // Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition + // that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time + // which breaks any animations. + var existingModel = me._model; + var model = me._model = getBaseModel(opts); var active = me._active; var data = me._data; var chartInstance = me._chartInstance; + // In the case where active.length === 0 we need to keep these at existing values for good animations + var alignment = { + xAlign: existingModel.xAlign, + yAlign: existingModel.yAlign + }; + var backgroundPoint = { + x: existingModel.x, + y: existingModel.y + }; + var tooltipSize = { + width: existingModel.width, + height: existingModel.height + }; + var i, len; if (active.length) { @@ -314,201 +524,42 @@ module.exports = function(Chart) { }); // Build the Text Lines - helpers.extend(model, { - title: me.getTitle(tooltipItems, data), - beforeBody: me.getBeforeBody(tooltipItems, data), - body: me.getBody(tooltipItems, data), - afterBody: me.getAfterBody(tooltipItems, data), - footer: me.getFooter(tooltipItems, data), - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: helpers.getValueOrDefault(tooltipPosition.padding, 2), - labelColors: labelColors - }); + model.title = me.getTitle(tooltipItems, data); + model.beforeBody = me.getBeforeBody(tooltipItems, data); + model.body = me.getBody(tooltipItems, data); + model.afterBody = me.getAfterBody(tooltipItems, data); + model.footer = me.getFooter(tooltipItems, data); - // We need to determine alignment of - var tooltipSize = me.getTooltipSize(model); - me.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas + // Initial positioning and colors + model.x = Math.round(tooltipPosition.x); + model.y = Math.round(tooltipPosition.y); + model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2); + model.labelColors = labelColors; - helpers.extend(model, me.getBackgroundPoint(model, tooltipSize)); + // We need to determine alignment of the tooltip + tooltipSize = getTooltipSize(this, model); + alignment = determineAlignment(this, tooltipSize); + // Final Size and Position + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment); } else { - me._model.opacity = 0; + model.opacity = 0; } + model.xAlign = alignment.xAlign; + model.yAlign = alignment.yAlign; + model.x = backgroundPoint.x; + model.y = backgroundPoint.y; + model.width = tooltipSize.width; + model.height = tooltipSize.height; + + me._model = model; + if (changed && opts.custom) { opts.custom.call(me, model); } return me; }, - getTooltipSize: function(vm) { - var ctx = this._chart.ctx; - - var size = { - height: vm.yPadding * 2, // Tooltip Padding - width: 0 - }; - - // Count of all lines in the body - var body = vm.body; - var combinedBodyLength = body.reduce(function(count, bodyItem) { - return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length; - }, 0); - combinedBodyLength += vm.beforeBody.length + vm.afterBody.length; - - var titleLineCount = vm.title.length; - var footerLineCount = vm.footer.length; - var titleFontSize = vm.titleFontSize, - bodyFontSize = vm.bodyFontSize, - footerFontSize = vm.footerFontSize; - - size.height += titleLineCount * titleFontSize; // Title Lines - size.height += titleLineCount ? (titleLineCount - 1) * vm.titleSpacing : 0; // Title Line Spacing - size.height += titleLineCount ? vm.titleMarginBottom : 0; // Title's bottom Margin - size.height += combinedBodyLength * bodyFontSize; // Body Lines - size.height += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing - size.height += footerLineCount ? vm.footerMarginTop : 0; // Footer Margin - size.height += footerLineCount * (footerFontSize); // Footer Lines - size.height += footerLineCount ? (footerLineCount - 1) * vm.footerSpacing : 0; // Footer Line Spacing - - // Title width - var widthPadding = 0; - var maxLineWidth = function(line) { - size.width = Math.max(size.width, ctx.measureText(line).width + widthPadding); - }; - - ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - helpers.each(vm.title, maxLineWidth); - - // Body width - ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - helpers.each(vm.beforeBody.concat(vm.afterBody), maxLineWidth); - - // Body lines may include some extra width due to the color box - widthPadding = vm.displayColors ? (bodyFontSize + 2) : 0; - helpers.each(body, function(bodyItem) { - helpers.each(bodyItem.before, maxLineWidth); - helpers.each(bodyItem.lines, maxLineWidth); - helpers.each(bodyItem.after, maxLineWidth); - }); - - // Reset back to 0 - widthPadding = 0; - - // Footer width - ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - helpers.each(vm.footer, maxLineWidth); - - // Add padding - size.width += 2 * vm.xPadding; - - return size; - }, - determineAlignment: function(size) { - var me = this; - var model = me._model; - var chart = me._chart; - var chartArea = me._chartInstance.chartArea; - - if (model.y < size.height) { - model.yAlign = 'top'; - } else if (model.y > (chart.height - size.height)) { - model.yAlign = 'bottom'; - } - - var lf, rf; // functions to determine left, right alignment - var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart - var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges - var midX = (chartArea.left + chartArea.right) / 2; - var midY = (chartArea.top + chartArea.bottom) / 2; - - if (model.yAlign === 'center') { - lf = function(x) { - return x <= midX; - }; - rf = function(x) { - return x > midX; - }; - } else { - lf = function(x) { - return x <= (size.width / 2); - }; - rf = function(x) { - return x >= (chart.width - (size.width / 2)); - }; - } - - olf = function(x) { - return x + size.width > chart.width; - }; - orf = function(x) { - return x - size.width < 0; - }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; - }; - - if (lf(model.x)) { - model.xAlign = 'left'; - - // Is tooltip too wide and goes over the right side of the chart.? - if (olf(model.x)) { - model.xAlign = 'center'; - model.yAlign = yf(model.y); - } - } else if (rf(model.x)) { - model.xAlign = 'right'; - - // Is tooltip too wide and goes outside left edge of canvas? - if (orf(model.x)) { - model.xAlign = 'center'; - model.yAlign = yf(model.y); - } - } - }, - getBackgroundPoint: function(vm, size) { - // Background Position - var pt = { - x: vm.x, - y: vm.y - }; - - var caretSize = vm.caretSize, - caretPadding = vm.caretPadding, - cornerRadius = vm.cornerRadius, - xAlign = vm.xAlign, - yAlign = vm.yAlign, - paddingAndSize = caretSize + caretPadding, - radiusAndPadding = cornerRadius + caretPadding; - - if (xAlign === 'right') { - pt.x -= size.width; - } else if (xAlign === 'center') { - pt.x -= (size.width / 2); - } - - if (yAlign === 'top') { - pt.y += paddingAndSize; - } else if (yAlign === 'bottom') { - pt.y -= size.height + paddingAndSize; - } else { - pt.y -= (size.height / 2); - } - - if (yAlign === 'center') { - if (xAlign === 'left') { - pt.x += paddingAndSize; - } else if (xAlign === 'right') { - pt.x -= paddingAndSize; - } - } else if (xAlign === 'left') { - pt.x -= radiusAndPadding; - } else if (xAlign === 'right') { - pt.x += radiusAndPadding; - } - - return pt; - }, drawCaret: function(tooltipPoint, size, opacity) { var vm = this._view; var ctx = this._chart.ctx; @@ -687,7 +738,10 @@ module.exports = function(Chart) { return; } - var tooltipSize = this.getTooltipSize(vm); + var tooltipSize = { + width: vm.width, + height: vm.height + }; var pt = { x: vm.x, y: vm.y From c61ab012c42a4bda482bfc50c39daa9faaca8eea Mon Sep 17 00:00:00 2001 From: Tieson Trowbridge Date: Fri, 7 Oct 2016 22:14:25 -0400 Subject: [PATCH 29/58] Replaces Unicode character with HTML entity --- docs/10-Notes.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index 85cc6507a..fd2125e17 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -49,31 +49,31 @@ Library Features | Feature | Chart.js | D3 | HighCharts | Chartist | | ------- | -------- | --- | ---------- | -------- | -| Completely Free | ✓ | ✓ | | ✓ | -| Canvas | ✓ | | | | -| SVG | | ✓ | ✓ | ✓ | -| Built-in Charts | ✓ | | ✓ | ✓ | -| 8+ Chart Types | ✓ | ✓ | ✓ | | -| Extendable to Custom Charts | ✓ | ✓ | | | -| Supports Modern Browsers | ✓ | ✓ | ✓ | ✓ | -| Extensive Documentation | ✓ | ✓ | ✓ | ✓ | -| Open Source | ✓ | ✓ | ✓ | ✓ | +| Completely Free | ✓ | ✓ | | ✓ | +| Canvas | ✓ | | | | +| SVG | | ✓ | ✓ | ✓ | +| Built-in Charts | ✓ | | ✓ | ✓ | +| 8+ Chart Types | ✓ | ✓ | ✓ | | +| Extendable to Custom Charts | ✓ | ✓ | | | +| Supports Modern Browsers | ✓ | ✓ | ✓ | ✓ | +| Extensive Documentation | ✓ | ✓ | ✓ | ✓ | +| Open Source | ✓ | ✓ | ✓ | ✓ | Built in Chart Types | Type | Chart.js | HighCharts | Chartist | | ---- | -------- | ---------- | -------- | -| Combined Types | ✓ | ✓ | | -| Line | ✓ | ✓ | ✓ | -| Bar | ✓ | ✓ | ✓ | -| Horizontal Bar | ✓ | ✓ | ✓ | -| Pie/Doughnut | ✓ | ✓ | ✓ | -| Polar Area | ✓ | ✓ | | -| Radar | ✓ | | | -| Scatter | ✓ | ✓ | ✓ | -| Bubble | ✓ | | | -| Gauges | | ✓ | | -| Maps (Heat/Tree/etc.) | | ✓ | | +| Combined Types | ✓ | ✓ | | +| Line | ✓ | ✓ | ✓ | +| Bar | ✓ | ✓ | ✓ | +| Horizontal Bar | ✓ | ✓ | ✓ | +| Pie/Doughnut | ✓ | ✓ | ✓ | +| Polar Area | ✓ | ✓ | | +| Radar | ✓ | | | +| Scatter | ✓ | ✓ | ✓ | +| Bubble | ✓ | | | +| Gauges | | ✓ | | +| Maps (Heat/Tree/etc.) | | ✓ | | ### Popular Plugins From 0817199f457fae4c7a42c69574b4e20a5ef1a37c Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 9 Oct 2016 16:24:47 -0400 Subject: [PATCH 30/58] No longer merge arrays during the config merge. Simply replace the property --- src/core/core.helpers.js | 40 +++++++++++++------------------------- test/core.helpers.tests.js | 33 +------------------------------ 2 files changed, 14 insertions(+), 59 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index d3fd72741..2cafdcb6c 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -58,37 +58,23 @@ module.exports = function(Chart) { var base = helpers.clone(_base); helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) { helpers.each(extension, function(value, key) { - if (key === 'scales') { - // Scale config merging is complex. Add out own function here for that - base[key] = helpers.scaleMerge(base.hasOwnProperty(key) ? base[key] : {}, value); + var baseHasProperty = base.hasOwnProperty(key); + var baseVal = baseHasProperty ? base[key] : {}; + if (key === 'scales') { + // Scale config merging is complex. Add our own function here for that + base[key] = helpers.scaleMerge(baseVal, value); } else if (key === 'scale') { // Used in polar area & radar charts since there is only one scale - base[key] = helpers.configMerge(base.hasOwnProperty(key) ? base[key] : {}, Chart.scaleService.getScaleDefaults(value.type), value); - } else if (base.hasOwnProperty(key) && helpers.isArray(base[key]) && helpers.isArray(value)) { - // In this case we have an array of objects replacing another array. Rather than doing a strict replace, - // merge. This allows easy scale option merging - var baseArray = base[key]; - - helpers.each(value, function(valueObj, index) { - - if (index < baseArray.length) { - if (typeof baseArray[index] === 'object' && baseArray[index] !== null && typeof valueObj === 'object' && valueObj !== null) { - // Two objects are coming together. Do a merge of them. - baseArray[index] = helpers.configMerge(baseArray[index], valueObj); - } else { - // Just overwrite in this case since there is nothing to merge - baseArray[index] = valueObj; - } - } else { - baseArray.push(valueObj); // nothing to merge - } - }); - - } else if (base.hasOwnProperty(key) && typeof base[key] === 'object' && base[key] !== null && typeof value === 'object') { + base[key] = helpers.configMerge(baseVal, Chart.scaleService.getScaleDefaults(value.type), value); + } else if (baseHasProperty + && typeof baseVal === 'object' + && !helpers.isArray(baseVal) + && baseVal !== null + && typeof value === 'object' + && !helpers.isArray(value)) { // If we are overwriting an object with an object, do a merge of the properties. - base[key] = helpers.configMerge(base[key], value); - + base[key] = helpers.configMerge(baseVal, value); } else { // can just overwrite the value in this case base[key] = value; diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 8ca5bb59c..3d4316062 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -121,7 +121,7 @@ describe('Core helper tests', function() { expect(merged).toEqual({ valueProp: 5, valueProp2: null, - arrayProp: ['a', 'c', 3, 4, 5, 6], + arrayProp: ['a', 'c'], objectProp: { prop1: 'c', prop2: 56, @@ -130,37 +130,6 @@ describe('Core helper tests', function() { }); }); - it('should merge arrays containing objects', function() { - var baseConfig = { - arrayProp: [{ - prop1: 'abc', - prop2: 56 - }], - }; - - var toMerge = { - arrayProp: [{ - prop1: 'myProp1', - prop3: 'prop3' - }, 2, { - prop1: 'myProp1' - }], - }; - - var merged = helpers.configMerge(baseConfig, toMerge); - expect(merged).toEqual({ - arrayProp: [{ - prop1: 'myProp1', - prop2: 56, - prop3: 'prop3' - }, - 2, { - prop1: 'myProp1' - } - ], - }); - }); - it('should merge scale configs', function() { var baseConfig = { scales: { From f481746fe1e56a1b0b88eae150863471d3550478 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 9 Oct 2016 10:54:42 +0200 Subject: [PATCH 31/58] Update the GitHub issue template --- .github/ISSUE_TEMPLATE.md | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 426edad8d..7d57bbd0c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,40 @@ + -Example of issue on an interactive website such as the following: -- http://jsbin.com/ -- http://jsfiddle.net/ -- http://codepen.io/pen/ -- Premade template: http://codepen.io/pen?template=JXVYzq \ No newline at end of file + + +## Expected Behavior + + + +## Current Behavior + + + +## Possible Solution + + + +## Steps to Reproduce (for bugs) + + +1. +2. +3. +4. + +## Context + + + +## Environment + +* Chart.js version: +* Browser name and version: +* Link to your project: From f8e90b1c2ddfcbf3df378c0bcf81f50bcc714558 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 14 Oct 2016 06:19:47 -0500 Subject: [PATCH 32/58] New fill modes for lines (#3460) New fill modes for lines allowing the user to customize where the fill goes to --- docs/01-Chart-Configuration.md | 2 +- src/elements/element.line.js | 25 ++- test/element.line.tests.js | 312 +++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 9 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 1aeaab940..ef0a5b57f 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -392,7 +392,7 @@ borderDash | Array | `[]` | Default line dash. See [MDN](https://developer.mozil borderDashOffset | Number | 0.0 | Default line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) borderJoinStyle | String | 'miter' | Default line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) capBezierPoints | Boolean | true | If true, bezier control points are kept inside the chart. If false, no restriction is enforced. -fill | Boolean | true | If true, the line is filled. +fill | Boolean or String | true | If true, the fill is assumed to be to zero. String values are 'zero', 'top', and 'bottom' to fill to different locations. If `false`, no fill is added stepped | Boolean | false | If true, the line is shown as a stepped line and 'tension' will be ignored #### Point Configuration diff --git a/src/elements/element.line.js b/src/elements/element.line.js index d81e7861c..b63ece78c 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -15,7 +15,7 @@ module.exports = function(Chart) { borderDashOffset: 0.0, borderJoinStyle: 'miter', capBezierPoints: true, - fill: true // do we fill in the area between the line and its base axis + fill: true, // do we fill in the area between the line and its base axis }; Chart.elements.Line = Chart.Element.extend({ @@ -23,9 +23,18 @@ module.exports = function(Chart) { var me = this; var vm = me._view; var spanGaps = vm.spanGaps; - var scaleZero = vm.scaleZero; + var fillPoint = vm.scaleZero; var loop = me._loop; + // Handle different fill modes for cartesian lines + if (!loop) { + if (vm.fill === 'top') { + fillPoint = vm.scaleTop; + } else if (vm.fill === 'bottom') { + fillPoint = vm.scaleBottom; + } + } + var ctx = me._chart.ctx; ctx.save(); @@ -71,9 +80,9 @@ module.exports = function(Chart) { // First point moves to it's starting position no matter what if (index === 0) { if (loop) { - ctx.moveTo(scaleZero.x, scaleZero.y); + ctx.moveTo(fillPoint.x, fillPoint.y); } else { - ctx.moveTo(currentVM.x, scaleZero); + ctx.moveTo(currentVM.x, fillPoint); } if (!currentVM.skip) { @@ -87,9 +96,9 @@ module.exports = function(Chart) { // Only do this if this is the first point that is skipped if (!spanGaps && lastDrawnIndex === (index - 1)) { if (loop) { - ctx.lineTo(scaleZero.x, scaleZero.y); + ctx.lineTo(fillPoint.x, fillPoint.y); } else { - ctx.lineTo(previous._view.x, scaleZero); + ctx.lineTo(previous._view.x, fillPoint); } } } else { @@ -102,7 +111,7 @@ module.exports = function(Chart) { } else if (loop) { ctx.lineTo(currentVM.x, currentVM.y); } else { - ctx.lineTo(currentVM.x, scaleZero); + ctx.lineTo(currentVM.x, fillPoint); ctx.lineTo(currentVM.x, currentVM.y); } } else { @@ -115,7 +124,7 @@ module.exports = function(Chart) { } if (!loop && lastDrawnIndex !== -1) { - ctx.lineTo(points[lastDrawnIndex]._view.x, scaleZero); + ctx.lineTo(points[lastDrawnIndex]._view.x, fillPoint); } ctx.fillStyle = vm.backgroundColor || globalDefaults.defaultColor; diff --git a/test/element.line.tests.js b/test/element.line.tests.js index 38a56fe3e..0f495c9d1 100644 --- a/test/element.line.tests.js +++ b/test/element.line.tests.js @@ -527,6 +527,318 @@ describe('Line element tests', function() { expect(mockContext.getCalls()).toEqual(expected); }); + it('should draw with fillMode top', function() { + var mockContext = window.createMockContext(); + + // Create our points + var points = []; + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 0, + _view: { + x: 0, + y: 10, + controlPointNextX: 0, + controlPointNextY: 10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 1, + _view: { + x: 5, + y: 0, + controlPointPreviousX: 5, + controlPointPreviousY: 0, + controlPointNextX: 5, + controlPointNextY: 0 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 2, + _view: { + x: 15, + y: -10, + controlPointPreviousX: 15, + controlPointPreviousY: -10, + controlPointNextX: 15, + controlPointNextY: -10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 3, + _view: { + x: 19, + y: -5, + controlPointPreviousX: 19, + controlPointPreviousY: -5, + controlPointNextX: 19, + controlPointNextY: -5 + } + })); + + var line = new Chart.elements.Line({ + _datasetindex: 2, + _chart: { + ctx: mockContext, + }, + _children: points, + // Need to provide some settings + _view: { + fill: 'top', + scaleZero: 2, // for filling lines + scaleTop: -2, + scaleBottom: 10, + tension: 0.0, // no bezier curve for now + + borderCapStyle: 'round', + borderColor: 'rgb(255, 255, 0)', + borderDash: [2, 2], + borderDashOffset: 1.5, + borderJoinStyle: 'bevel', + borderWidth: 4, + backgroundColor: 'rgb(0, 0, 0)' + } + }); + + line.draw(); + + var expected = [{ + name: 'save', + args: [] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, -2] + }, { + name: 'lineTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'lineTo', + args: [19, -2] + }, { + name: 'setFillStyle', + args: ['rgb(0, 0, 0)'] + }, { + name: 'closePath', + args: [] + }, { + name: 'fill', + args: [] + }, { + name: 'setLineCap', + args: ['round'] + }, { + name: 'setLineDash', + args: [ + [2, 2] + ] + }, { + name: 'setLineDashOffset', + args: [1.5] + }, { + name: 'setLineJoin', + args: ['bevel'] + }, { + name: 'setLineWidth', + args: [4] + }, { + name: 'setStrokeStyle', + args: ['rgb(255, 255, 0)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]; + expect(mockContext.getCalls()).toEqual(expected); + }); + + it('should draw with fillMode bottom', function() { + var mockContext = window.createMockContext(); + + // Create our points + var points = []; + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 0, + _view: { + x: 0, + y: 10, + controlPointNextX: 0, + controlPointNextY: 10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 1, + _view: { + x: 5, + y: 0, + controlPointPreviousX: 5, + controlPointPreviousY: 0, + controlPointNextX: 5, + controlPointNextY: 0 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 2, + _view: { + x: 15, + y: -10, + controlPointPreviousX: 15, + controlPointPreviousY: -10, + controlPointNextX: 15, + controlPointNextY: -10 + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 3, + _view: { + x: 19, + y: -5, + controlPointPreviousX: 19, + controlPointPreviousY: -5, + controlPointNextX: 19, + controlPointNextY: -5 + } + })); + + var line = new Chart.elements.Line({ + _datasetindex: 2, + _chart: { + ctx: mockContext, + }, + _children: points, + // Need to provide some settings + _view: { + fill: 'bottom', + scaleZero: 2, // for filling lines + scaleTop: -2, + scaleBottom: 10, + tension: 0.0, // no bezier curve for now + + borderCapStyle: 'round', + borderColor: 'rgb(255, 255, 0)', + borderDash: [2, 2], + borderDashOffset: 1.5, + borderJoinStyle: 'bevel', + borderWidth: 4, + backgroundColor: 'rgb(0, 0, 0)' + } + }); + + line.draw(); + + var expected = [{ + name: 'save', + args: [] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'lineTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'lineTo', + args: [19, 10] + }, { + name: 'setFillStyle', + args: ['rgb(0, 0, 0)'] + }, { + name: 'closePath', + args: [] + }, { + name: 'fill', + args: [] + }, { + name: 'setLineCap', + args: ['round'] + }, { + name: 'setLineDash', + args: [ + [2, 2] + ] + }, { + name: 'setLineDashOffset', + args: [1.5] + }, { + name: 'setLineJoin', + args: ['bevel'] + }, { + name: 'setLineWidth', + args: [4] + }, { + name: 'setStrokeStyle', + args: ['rgb(255, 255, 0)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'bezierCurveTo', + args: [0, 10, 5, 0, 5, 0] + }, { + name: 'bezierCurveTo', + args: [5, 0, 15, -10, 15, -10] + }, { + name: 'bezierCurveTo', + args: [15, -10, 19, -5, 19, -5] + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]; + expect(mockContext.getCalls()).toEqual(expected); + }); + it('should skip points correctly', function() { var mockContext = window.createMockContext(); From 3365ba6d29e579e99db61dd65390e39536a0800d Mon Sep 17 00:00:00 2001 From: etimberg Date: Thu, 13 Oct 2016 20:43:11 -0400 Subject: [PATCH 33/58] Bar chart performance improvements --- src/controllers/controller.bar.js | 216 ++++++++++++++--------------- src/core/core.datasetController.js | 8 +- src/core/core.scale.js | 9 +- src/elements/element.rectangle.js | 7 +- 4 files changed, 116 insertions(+), 124 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index f828eb79e..e9a85aabd 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -69,31 +69,29 @@ module.exports = function(Chart) { var custom = rectangle.custom || {}; var dataset = me.getDataset(); - helpers.extend(rectangle, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: me.index, - _index: index, + rectangle._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = index; - // Desired view properties - _model: { - x: me.calculateBarX(index, me.index), - y: reset ? scaleBase : me.calculateBarY(index, me.index), + var ruler = me.getRuler(index); + rectangle._model = { + x: me.calculateBarX(index, me.index, ruler), + y: reset ? scaleBase : me.calculateBarY(index, me.index), - // Tooltip - label: me.chart.data.labels[index], - datasetLabel: dataset.label, + // Tooltip + label: me.chart.data.labels[index], + datasetLabel: dataset.label, + + // Appearance + base: reset ? scaleBase : me.calculateBarBase(me.index, index), + width: me.calculateBarWidth(ruler), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) + }; - // Appearance - base: reset ? scaleBase : me.calculateBarBase(me.index, index), - width: me.calculateBarWidth(index), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), - borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, - borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) - } - }); rectangle.pivot(); }, @@ -160,12 +158,11 @@ module.exports = function(Chart) { }; }, - calculateBarWidth: function(index) { + calculateBarWidth: function(ruler) { var xScale = this.getScaleForId(this.getMeta().xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - var ruler = this.getRuler(index); return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; }, @@ -184,13 +181,11 @@ module.exports = function(Chart) { return barIndex; }, - calculateBarX: function(index, datasetIndex) { + calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); var barIndex = me.getBarIndex(datasetIndex); - - var ruler = me.getRuler(index); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; @@ -242,12 +237,16 @@ module.exports = function(Chart) { draw: function(ease) { var me = this; var easingDecimal = ease || 1; - helpers.each(me.getMeta().data, function(rectangle, index) { - var d = me.getDataset().data[index]; + var metaData = me.getMeta().data; + var dataset = me.getDataset(); + var i, len; + + for (i = 0, len = metaData.length; i < len; ++i) { + var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { - rectangle.transition(easingDecimal).draw(); + metaData[i].transition(easingDecimal).draw(); } - }, me); + } }, setHoverStyle: function(rectangle) { @@ -342,88 +341,84 @@ module.exports = function(Chart) { var dataset = me.getDataset(); var rectangleElementOptions = me.chart.options.elements.rectangle; - helpers.extend(rectangle, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: me.index, - _index: index, + rectangle._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = index; - // Desired view properties - _model: { - x: reset ? scaleBase : me.calculateBarX(index, me.index), - y: me.calculateBarY(index, me.index), + var ruler = me.getRuler(index); + rectangle._model = { + x: reset ? scaleBase : me.calculateBarX(index, me.index), + y: me.calculateBarY(index, me.index, ruler), - // Tooltip - label: me.chart.data.labels[index], - datasetLabel: dataset.label, + // Tooltip + label: me.chart.data.labels[index], + datasetLabel: dataset.label, - // Appearance - base: reset ? scaleBase : me.calculateBarBase(me.index, index), - height: me.calculateBarHeight(index), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), - borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, - borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) - }, + // Appearance + base: reset ? scaleBase : me.calculateBarBase(me.index, index), + height: me.calculateBarHeight(ruler), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped, + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) + }; + rectangle.draw = function() { + var ctx = this._chart.ctx; + var vm = this._view; - draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; + var halfHeight = vm.height / 2, + topY = vm.y - halfHeight, + bottomY = vm.y + halfHeight, + right = vm.base - (vm.base - vm.x), + halfStroke = vm.borderWidth / 2; - var halfHeight = vm.height / 2, - topY = vm.y - halfHeight, - bottomY = vm.y + halfHeight, - right = vm.base - (vm.base - vm.x), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - topY += halfStroke; - bottomY -= halfStroke; - right += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [vm.base, bottomY], - [vm.base, topY], - [right, topY], - [right, bottomY] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(cornerIndex) { - return corners[(startCorner + cornerIndex) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); - } - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (vm.borderWidth) { + topY += halfStroke; + bottomY -= halfStroke; + right += halfStroke; } - }); + + ctx.beginPath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + + // Corner points, from bottom-left to bottom-right clockwise + // | 1 2 | + // | 0 3 | + var corners = [ + [vm.base, bottomY], + [vm.base, topY], + [right, topY], + [right, bottomY] + ]; + + // Find first (starting) corner with fallback to 'bottom' + var borders = ['bottom', 'left', 'top', 'right']; + var startCorner = borders.indexOf(vm.borderSkipped, 0); + if (startCorner === -1) { + startCorner = 0; + } + + function cornerAt(cornerIndex) { + return corners[(startCorner + cornerIndex) % 4]; + } + + // Draw rectangle from 'startCorner' + ctx.moveTo.apply(ctx, cornerAt(0)); + for (var i = 1; i < 4; i++) { + ctx.lineTo.apply(ctx, cornerAt(i)); + } + + ctx.fill(); + if (vm.borderWidth) { + ctx.stroke(); + } + }; rectangle.pivot(); }, @@ -490,13 +485,12 @@ module.exports = function(Chart) { }; }, - calculateBarHeight: function(index) { + calculateBarHeight: function(ruler) { var me = this; var yScale = me.getScaleForId(me.getMeta().yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - var ruler = me.getRuler(index); return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; }, @@ -533,13 +527,11 @@ module.exports = function(Chart) { return xScale.getPixelForValue(value); }, - calculateBarY: function(index, datasetIndex) { + calculateBarY: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); var barIndex = me.getBarIndex(datasetIndex); - - var ruler = me.getRuler(index); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 2faff1b6e..2e218dfe9 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -210,9 +210,11 @@ module.exports = function(Chart) { draw: function(ease) { var easingDecimal = ease || 1; - helpers.each(this.getMeta().data, function(element) { - element.transition(easingDecimal).draw(); - }); + var i, len; + var metaData = this.getMeta().data; + for (i = 0, len = metaData.length; i < len; ++i) { + metaData[i].transition(easingDecimal).draw(); + } }, removeHoverStyle: function(element, elementOpts) { diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 890e04c42..e2d5f56bc 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -167,13 +167,8 @@ module.exports = function(Chart) { convertTicksToLabels: function() { var me = this; // Convert ticks to strings - me.ticks = me.ticks.map(function(numericalTick, index, ticks) { - if (me.options.ticks.userCallback) { - return me.options.ticks.userCallback(numericalTick, index, ticks); - } - return me.options.ticks.callback(numericalTick, index, ticks); - }, - me); + var tickOpts = me.options.ticks; + me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback); }, afterTickToLabelConversion: function() { helpers.callCallback(this.options.afterTickToLabelConversion, [this]); diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 3f4ea10f9..427916791 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -95,9 +95,12 @@ module.exports = function(Chart) { } // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); + var corner = cornerAt(0); + ctx.moveTo(corner[0], corner[1]); + for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); + corner = cornerAt(i); + ctx.lineTo(corner[0], corner[1]); } ctx.fill(); From a86c47cf480e8d86ea03a121e9b6552a17aae41d Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 14 Oct 2016 16:36:49 -0500 Subject: [PATCH 34/58] Configurable Tooltip Position Modes (#3453) Adds new tooltip position option that allows configuring where a tooltip is displayed on the graph in relation to the elements that appear in it --- docs/01-Chart-Configuration.md | 1 + src/core/core.controller.js | 87 +++++++++---------- src/core/core.legend.js | 12 ++- src/core/core.tooltip.js | 150 +++++++++++++++++++++++++-------- 4 files changed, 166 insertions(+), 84 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index ef0a5b57f..c08e23d44 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -214,6 +214,7 @@ enabled | Boolean | true | Are tooltips enabled custom | Function | null | See [section](#advanced-usage-external-tooltips) below mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details intersect | Boolean | true | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. +position | String | 'average' | The mode for positioning the tooltip. 'average' mode will place the tooltip at the average position of the items displayed in the tooltip. 'nearest' will place the tooltip at the position of the element closest to the event position. New modes can be defined by adding functions to the Chart.Tooltip.positioners map. itemSort | Function | undefined | Allows sorting of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. backgroundColor | Color | 'rgba(0,0,0,0.8)' | Background color of the tooltip titleFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip title inherited from global font family diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 6302dd11c..5e3632e4f 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -156,6 +156,7 @@ module.exports = function(Chart) { me.chart = instance; me.config = config; me.options = config.options; + me._bufferedRender = false; // Add the chart instance to the global namespace Chart.instances[me.id] = me; @@ -403,7 +404,9 @@ module.exports = function(Chart) { // Do this before render so that any plugins that need final scale updates can use it Chart.plugins.notify('afterUpdate', [me]); - me.render(animationDuration, lazy); + if (!me._bufferedRender) { + me.render(animationDuration, lazy); + } }, /** @@ -644,20 +647,6 @@ module.exports = function(Chart) { var method = enabled? 'setHoverStyle' : 'removeHoverStyle'; var element, i, ilen; - switch (mode) { - case 'single': - elements = [elements[0]]; - break; - case 'label': - case 'dataset': - case 'x-axis': - // elements = elements; - break; - default: - // unsupported mode - return; - } - for (i=0, ilen=elements.length; i Date: Sat, 15 Oct 2016 16:49:35 -0500 Subject: [PATCH 35/58] Make index mode only work with the horizontal distance to an element (#3471) Make index mode only work with the horizontal distance to an element if intersect is off --- src/core/core.interaction.js | 14 +- test/core.tooltip.tests.js | 307 +++++++++++++++++++++++------------ 2 files changed, 210 insertions(+), 111 deletions(-) diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 571f3c3c0..2e680bea5 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -50,19 +50,24 @@ module.exports = function(Chart) { * @param chart {Chart} the chart to look at elements from * @param position {Point} the point to be nearest to * @param intersect {Boolean} if true, only consider items that intersect the position + * @param distanceMetric {Function} Optional function to provide the distance between * @return {ChartElement[]} the nearest items */ - function getNearestItems(chart, position, intersect) { + function getNearestItems(chart, position, intersect, distanceMetric) { var minDistance = Number.POSITIVE_INFINITY; var nearestItems = []; + if (!distanceMetric) { + distanceMetric = helpers.distanceBetweenPoints; + } + parseVisibleItems(chart, function(element) { if (intersect && !element.inRange(position.x, position.y)) { return; } var center = element.getCenterPoint(); - var distance = Math.round(helpers.distanceBetweenPoints(position, center)); + var distance = distanceMetric(position, center); if (distance < minDistance) { nearestItems = [element]; @@ -78,7 +83,10 @@ module.exports = function(Chart) { function indexMode(chart, e, options) { var position = helpers.getRelativePosition(e, chart.chart); - var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); + var distanceMetric = function(pt1, pt2) { + return Math.abs(pt1.x - pt2.x); + }; + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric); var elements = []; if (!items.length) { diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 9927debc0..dd6ee17f5 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -1,118 +1,209 @@ // Test the rectangle element -describe('tooltip tests', function() { - it('Should display in label mode', function() { - var chartInstance = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - label: 'Dataset 1', - data: [10, 20, 30], - pointHoverBorderColor: 'rgb(255, 0, 0)', - pointHoverBackgroundColor: 'rgb(0, 255, 0)' - }, { - label: 'Dataset 2', - data: [40, 40, 40], - pointHoverBorderColor: 'rgb(0, 0, 255)', - pointHoverBackgroundColor: 'rgb(0, 255, 255)' - }], - labels: ['Point 1', 'Point 2', 'Point 3'] - }, - options: { - tooltips: { - mode: 'label' +describe('Core.Tooltip', function() { + describe('index mode', function() { + it('Should only use x distance when intersect is false', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'index', + intersect: false + } } - } + }); + + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: 0 + }); + + // Manully trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + xAlign: 'left', + yAlign: 'center', + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: 'left', + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 1, + legendColorBackground: '#fff', + displayColors: true, + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 1: 20'], + after: [] + }, { + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + caretPadding: 2, + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + + expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.y).toBeCloseToPixel(155); }); - // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); - var point = meta.data[1]; + it('Should only display if intersecting if intersect is set', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'index', + intersect: true + } + } + }); - var node = chartInstance.chart.canvas; - var rect = node.getBoundingClientRect(); + // Trigger an event over top of the + var meta = chartInstance.getDatasetMeta(0); + var point = meta.data[1]; - var evt = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: 0 + }); + + // Manully trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: 'left', + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 0, + legendColorBackground: '#fff', + displayColors: true, + })); }); - - // Manully trigger rather than having an async test - node.dispatchEvent(evt); - - // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; - var globalDefaults = Chart.defaults.global; - - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'left', - yAlign: 'center', - - // Body - bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', - bodyFontSize: globalDefaults.defaultFontSize, - bodySpacing: 2, - - // Title - titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', - titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', - titleSpacing: 2, - titleMarginBottom: 6, - - // Footer - footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', - footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', - footerSpacing: 2, - footerMarginTop: 6, - - // Appearance - caretSize: 5, - cornerRadius: 6, - backgroundColor: 'rgba(0,0,0,0.8)', - opacity: 1, - legendColorBackground: '#fff', - displayColors: true, - - // Text - title: ['Point 2'], - beforeBody: [], - body: [{ - before: [], - lines: ['Dataset 1: 20'], - after: [] - }, { - before: [], - lines: ['Dataset 2: 40'], - after: [] - }], - afterBody: [], - footer: [], - caretPadding: 2, - labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' - }, { - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' - }] - })); - - expect(tooltip._view.x).toBeCloseToPixel(269); - expect(tooltip._view.y).toBeCloseToPixel(155); }); it('Should display in single mode', function() { From 4a5b5a0e7eba85ca44f658375cb0c78e6af93e5c Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 15 Oct 2016 23:40:22 +0200 Subject: [PATCH 36/58] Enhance context acquisition on chart creation Add support for creating a chart from the canvas id and prevent exceptions, at construction time, when the given item doesn't provide a valid CanvasRenderingContext2D or when the getContext API is not accessible (e.g. undefined by add-ons to prevent fingerprinting). New jasmine matcher to verify chart validity. --- .gitignore | 1 + docs/00-Getting-Started.md | 1 + src/core/core.controller.js | 61 ++++++++++++++++++++---- src/core/core.js | 19 ++------ test/core.controller.tests.js | 70 +++++++++++++++++++++++++++- test/mockContext.js | 88 +++++++++++++++++++++++------------ 6 files changed, 184 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 478edb5ec..8ef0139ea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ .DS_Store .idea +.vscode bower.json diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index 910ea5bea..b7eb09dfc 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -71,6 +71,7 @@ To create a chart, we need to instantiate the `Chart` class. To do this, we need var ctx = document.getElementById("myChart"); var ctx = document.getElementById("myChart").getContext("2d"); var ctx = $("#myChart"); +var ctx = "myChart"; ``` Once you have the element or context, you're ready to instantiate a pre-defined chart-type or create your own! diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 5e3632e4f..c08d01928 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -112,6 +112,36 @@ module.exports = function(Chart) { delete canvas._chartjs; } + /** + * TODO(SB) Move this method in the upcoming core.platform class. + */ + function acquireContext(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + if (item instanceof HTMLCanvasElement) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item.getContext && item.getContext('2d'); + if (context instanceof CanvasRenderingContext2D) { + initCanvas(item, config); + return context; + } + } + + return null; + } + /** * Initializes the given config with global and chart default values. */ @@ -136,21 +166,22 @@ module.exports = function(Chart) { * @class Chart.Controller * The main controller of a chart. */ - Chart.Controller = function(context, config, instance) { + Chart.Controller = function(item, config, instance) { var me = this; - var canvas; config = initConfig(config); - canvas = initCanvas(context.canvas, config); + + var context = acquireContext(item, config); + var canvas = context && context.canvas; + var height = canvas && canvas.height; + var width = canvas && canvas.width; instance.ctx = context; instance.canvas = canvas; instance.config = config; - instance.width = canvas.width; - instance.height = canvas.height; - instance.aspectRatio = canvas.width / canvas.height; - - helpers.retinaScale(instance); + instance.width = width; + instance.height = height; + instance.aspectRatio = height? width / height : null; me.id = helpers.uid(); me.chart = instance; @@ -167,6 +198,17 @@ module.exports = function(Chart) { } }); + if (!context || !canvas) { + // The given item is not a compatible context2d element, let's return before finalizing + // the chart initialization but after setting basic chart / controller properties that + // can help to figure out that the chart is not valid (e.g chart.canvas !== null); + // https://github.com/chartjs/Chart.js/issues/2807 + console.error("Failed to create chart: can't acquire context from the given item"); + return me; + } + + helpers.retinaScale(instance); + // Responsiveness is currently based on the use of an iframe, however this method causes // performance issues and could be troublesome when used with ad blockers. So make sure // that the user is still able to create a chart without iframe when responsive is false. @@ -593,7 +635,6 @@ module.exports = function(Chart) { var meta, i, ilen; me.stop(); - me.clear(); // dataset controllers need to cleanup associated data for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { @@ -607,8 +648,10 @@ module.exports = function(Chart) { if (canvas) { helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); + helpers.clear(me.chart); releaseCanvas(canvas); me.chart.canvas = null; + me.chart.ctx = null; } // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here diff --git a/src/core/core.js b/src/core/core.js index 319219f5d..cea3cf30b 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -3,22 +3,9 @@ module.exports = function() { // Occupy the global variable of Chart, and create a simple base class - var Chart = function(context, config) { - var me = this; - - // Support a jQuery'd canvas element - if (context.length && context[0].getContext) { - context = context[0]; - } - - // Support a canvas domnode - if (context.getContext) { - context = context.getContext('2d'); - } - - me.controller = new Chart.Controller(context, config, me); - - return me.controller; + var Chart = function(item, config) { + this.controller = new Chart.Controller(item, config, this); + return this.controller; }; // Globally expose the defaults to allow for user updating/changing diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index d7548d4ce..a7b504ecd 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -4,7 +4,75 @@ describe('Chart.Controller', function() { setTimeout(callback, 100); } - describe('config', function() { + describe('context acquisition', function() { + var canvasId = 'chartjs-canvas'; + + beforeEach(function() { + var canvas = document.createElement('canvas'); + canvas.setAttribute('id', canvasId); + window.document.body.appendChild(canvas); + }); + + afterEach(function() { + document.getElementById(canvasId).remove(); + }); + + // see https://github.com/chartjs/Chart.js/issues/2807 + it('should gracefully handle invalid item', function() { + var chart = new Chart('foobar'); + + expect(chart).not.toBeValidChart(); + + chart.destroy(); + }); + + it('should accept a DOM element id', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvasId); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas element', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas context2D', function() { + var canvas = document.getElementById(canvasId); + var context = canvas.getContext('2d'); + var chart = new Chart(context); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(context); + + chart.destroy(); + }); + + it('should accept an array containing canvas', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart([canvas]); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + }); + + describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); var data = chart.data; diff --git a/test/mockContext.js b/test/mockContext.js index 83b73e315..8904cf886 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -158,54 +158,82 @@ }; } + function toBeValidChart() { + return { + compare: function(actual) { + var chart = actual && actual.chart; + var message = null; + + if (!(actual instanceof Chart.Controller)) { + message = 'Expected ' + actual + ' to be an instance of Chart.Controller'; + } else if (!(chart instanceof Chart)) { + message = 'Expected chart to be an instance of Chart'; + } else if (!(chart.canvas instanceof HTMLCanvasElement)) { + message = 'Expected canvas to be an instance of HTMLCanvasElement'; + } else if (!(chart.ctx instanceof CanvasRenderingContext2D)) { + message = 'Expected context to be an instance of CanvasRenderingContext2D'; + } else if (typeof chart.height !== 'number' || !isFinite(chart.height)) { + message = 'Expected height to be a strict finite number'; + } else if (typeof chart.width !== 'number' || !isFinite(chart.width)) { + message = 'Expected width to be a strict finite number'; + } + + return { + message: message? message : 'Expected ' + actual + ' to be valid chart', + pass: !message + }; + } + }; + } + function toBeChartOfSize() { return { compare: function(actual, expected) { + var res = toBeValidChart().compare(actual); + if (!res.pass) { + return res; + } + var message = null; - var chart, canvas, style, dh, dw, rh, rw; + var chart = actual.chart; + var canvas = chart.ctx.canvas; + var style = getComputedStyle(canvas); + var dh = parseInt(style.height, 10); + var dw = parseInt(style.width, 10); + var rh = canvas.height; + var rw = canvas.width; - if (!actual || !(actual instanceof Chart.Controller)) { - message = 'Expected ' + actual + ' to be an instance of Chart.Controller.'; - } else { - chart = actual.chart; - canvas = chart.ctx.canvas; - style = getComputedStyle(canvas); - dh = parseInt(style.height); - dw = parseInt(style.width); - rh = canvas.height; - rw = canvas.width; + // sanity checks + if (chart.height !== rh) { + message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh; + } else if (chart.width !== rw) { + message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw; + } - // sanity checks - if (chart.height !== rh) { - message = 'Expected chart height ' + chart.height + ' to be equal to render height ' + rh; - } else if (chart.width !== rw) { - message = 'Expected chart width ' + chart.width + ' to be equal to render width ' + rw; - } - - // validity checks - if (dh !== expected.dh) { - message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh; - } else if (dw !== expected.dw) { - message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw; - } else if (rh !== expected.rh) { - message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh; - } else if (rw !== expected.rw) { - message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw; - } + // validity checks + if (dh !== expected.dh) { + message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh; + } else if (dw !== expected.dw) { + message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw; + } else if (rh !== expected.rh) { + message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh; + } else if (rw !== expected.rw) { + message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw; } return { message: message? message : 'Expected ' + actual + ' to be a chart of size ' + expected, pass: !message - } + }; } - } + }; } beforeEach(function() { jasmine.addMatchers({ toBeCloseToPixel: toBeCloseToPixel, toEqualOneOf: toEqualOneOf, + toBeValidChart: toBeValidChart, toBeChartOfSize: toBeChartOfSize }); }); From 766ca49cd094562f3a2e870e0c2b162ee16c8189 Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Sun, 16 Oct 2016 17:34:59 -0400 Subject: [PATCH 37/58] Extend eslint to test files (#3473) * Add eslint to test files * Fix mockContext for tests * Make formatting look better for nested objects --- gulpfile.js | 18 +++- test/controller.bar.tests.js | 69 +++++++------ test/controller.bubble.tests.js | 9 +- test/controller.doughnut.tests.js | 31 +++--- test/controller.line.tests.js | 151 +++++++++++++++-------------- test/controller.polarArea.tests.js | 56 +++++------ test/controller.radar.tests.js | 28 +++--- test/core.element.tests.js | 6 +- test/core.helpers.tests.js | 126 ++++++++++++------------ test/core.interaction.tests.js | 25 +++-- test/core.layoutService.tests.js | 6 +- test/core.legend.tests.js | 11 ++- test/core.plugin.tests.js | 46 +++++++-- test/core.scaleService.tests.js | 2 +- test/core.title.tests.js | 10 +- test/core.tooltip.tests.js | 7 +- test/defaultConfig.tests.js | 14 +-- test/element.arc.tests.js | 2 +- test/element.line.tests.js | 2 +- test/element.point.tests.js | 8 +- test/element.rectangle.tests.js | 46 ++++----- test/mockContext.js | 102 +++++++++++-------- test/scale.category.tests.js | 18 ++-- test/scale.linear.tests.js | 6 +- test/scale.logarithmic.tests.js | 62 ++++++------ test/scale.radialLinear.tests.js | 18 ++-- test/scale.time.tests.js | 86 ++++++++-------- 27 files changed, 520 insertions(+), 445 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 57cbd1cc6..58b8bf8fa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -22,6 +22,7 @@ var package = require('./package.json'); var srcDir = './src/'; var outDir = './dist/'; +var testDir = './test/'; var header = "/*!\n" + " * Chart.js\n" + @@ -132,6 +133,7 @@ function packageTask() { function lintTask() { var files = [ srcDir + '**/*.js', + testDir + '**/*.js' ]; // NOTE(SB) codeclimate has 'complexity' and 'max-statements' eslint rules way too strict @@ -141,7 +143,21 @@ function lintTask() { rules: { 'complexity': [1, 6], 'max-statements': [1, 30] - } + }, + globals: [ + 'Chart', + 'acquireChart', + 'afterAll', + 'afterEach', + 'beforeAll', + 'beforeEach', + 'describe', + 'expect', + 'it', + 'jasmine', + 'moment', + 'spyOn' + ] }; return gulp.src(files) diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 3ef9cabdf..a77eea22e 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -5,8 +5,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [] } + {data: []}, + {data: []} ], labels: [] } @@ -30,8 +30,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [] } + {data: []}, + {data: []} ], labels: [] }, @@ -57,10 +57,10 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [], type: 'line' }, - { data: [], hidden: true }, - { data: [] }, - { data: [] } + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} ], labels: [] } @@ -75,10 +75,10 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [], hidden: true }, - { data: [], type: 'line' }, - { data: [] } + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} ], labels: [] } @@ -94,8 +94,8 @@ describe('Bar controller tests', function() { type: 'bar', data: { datasets: [ - { data: [] }, - { data: [10, 15, 0, -4] } + {data: []}, + {data: [10, 15, 0, -4]} ], labels: [] } @@ -154,8 +154,9 @@ describe('Bar controller tests', function() { expect(meta.data.length).toBe(2); - [ { x: 122, y: 484 }, - { x: 234, y: 32 } + [ + {x: 122, y: 484}, + {x: 234, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(1); expect(meta.data[i]._index).toBe(i); @@ -251,10 +252,11 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { b: 290, w: 91, x: 95, y: 161 }, - { b: 290, w: 91, x: 209, y: 419 }, - { b: 290, w: 91, x: 322, y: 161 }, - { b: 290, w: 91, x: 436, y: 419 } + [ + {b: 290, w: 91, x: 95, y: 161}, + {b: 290, w: 91, x: 209, y: 419}, + {b: 290, w: 91, x: 322, y: 161}, + {b: 290, w: 91, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -264,10 +266,11 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); - [ { b: 161, w: 91, x: 95, y: 32 }, - { b: 290, w: 91, x: 209, y: 97 }, - { b: 161, w: 91, x: 322, y: 161 }, - { b: 419, w: 91, x: 436, y: 471 } + [ + {b: 161, w: 91, x: 95, y: 32}, + {b: 290, w: 91, x: 209, y: 97}, + {b: 161, w: 91, x: 322, y: 161}, + {b: 419, w: 91, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -305,10 +308,11 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { b: 290, w: 91, x: 95, y: 161 }, - { b: 290, w: 91, x: 209, y: 419 }, - { b: 290, w: 91, x: 322, y: 161 }, - { b: 290, w: 91, x: 436, y: 419 } + [ + {b: 290, w: 91, x: 95, y: 161}, + {b: 290, w: 91, x: 209, y: 419}, + {b: 290, w: 91, x: 322, y: 161}, + {b: 290, w: 91, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -318,10 +322,11 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); - [ { b: 161, w: 91, x: 95, y: 32 }, - { b: 290, w: 91, x: 209, y: 97 }, - { b: 161, w: 91, x: 322, y: 161 }, - { b: 419, w: 91, x: 436, y: 471 } + [ + {b: 161, w: 91, x: 95, y: 32}, + {b: 290, w: 91, x: 209, y: 97}, + {b: 161, w: 91, x: 322, y: 161}, + {b: 419, w: 91, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); diff --git a/test/controller.bubble.tests.js b/test/controller.bubble.tests.js index ec61a0b50..977b2d517 100644 --- a/test/controller.bubble.tests.js +++ b/test/controller.bubble.tests.js @@ -133,10 +133,11 @@ describe('Bubble controller tests', function() { var meta = chart.getDatasetMeta(0); - [ { r: 5, x: 38, y: 32 }, - { r: 1, x: 189, y: 484 }, - { r: 2, x: 341, y: 461 }, - { r: 1, x: 492, y: 32 } + [ + {r: 5, x: 38, y: 32}, + {r: 1, x: 189, y: 484}, + {r: 2, x: 341, y: 461}, + {r: 1, x: 492, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._model.radius).toBe(expected.r); expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); diff --git a/test/controller.doughnut.tests.js b/test/controller.doughnut.tests.js index 0d31653a7..d9319bb80 100644 --- a/test/controller.doughnut.tests.js +++ b/test/controller.doughnut.tests.js @@ -78,10 +78,11 @@ describe('Doughnut controller tests', function() { expect(meta.data.length).toBe(4); - [ { c: 0 }, - { c: 0 }, - { c: 0, }, - { c: 0 } + [ + {c: 0}, + {c: 0}, + {c: 0}, + {c: 0} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -96,14 +97,15 @@ describe('Doughnut controller tests', function() { borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); - }) + }); chart.update(); - [ { c: 1.7453292519, s: -1.5707963267, e: 0.1745329251 }, - { c: 2.0943951023, s: 0.1745329251, e: 2.2689280275 }, - { c: 0, s: 2.2689280275, e: 2.2689280275 }, - { c: 2.4434609527, s: 2.2689280275, e: 4.7123889803 } + [ + {c: 1.7453292519, s: -1.5707963267, e: 0.1745329251}, + {c: 2.0943951023, s: 0.1745329251, e: 2.2689280275}, + {c: 0, s: 2.2689280275, e: 2.2689280275}, + {c: 2.4434609527, s: 2.2689280275, e: 4.7123889803} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -118,7 +120,7 @@ describe('Doughnut controller tests', function() { borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); - }) + }); // Change the amount of data and ensure that arcs are updated accordingly chart.data.datasets[1].data = [1, 2]; // remove 2 elements from dataset 0 @@ -172,17 +174,18 @@ describe('Doughnut controller tests', function() { expect(meta.data.length).toBe(2); // Only startAngle, endAngle and circumference should be different. - [ { c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8 }, - { c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2 } + [ + {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, + {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(495); expect(meta.data[i]._model.y).toBeCloseToPixel(511); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(478); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(359); - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c,8); + expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); - }) + }); }); it ('should draw all arcs', function() { diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 9afc9cec0..3c66b268e 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -165,12 +165,12 @@ describe('Line controller tests', function() { var chart = window.acquireChart({ type: 'line', data: { - datasets: [{ - data: [10, 15, 0, -4], - label: 'dataset', - xAxisID: 'firstXScaleID', - yAxisID: 'firstYScaleID' - }], + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset', + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID' + }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { @@ -202,8 +202,9 @@ describe('Line controller tests', function() { expect(meta.data.length).toBe(2); - [ { x: 44, y: 484 }, - { x: 193, y: 32 } + [ + {x: 44, y: 484}, + {x: 193, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(0); expect(meta.data[i]._index).toBe(i); @@ -223,23 +224,23 @@ describe('Line controller tests', function() { expect(meta.data.length).toBe(3); // should add a new meta data item }); - it('should correctly calculate x scale for label and point', function(){ - var chart = window.acquireChart({ + it('should correctly calculate x scale for label and point', function() { + var chart = window.acquireChart({ type: 'line', - data: { - labels: ["One"], - datasets: [{ - data: [1], - }] + data: { + labels: ['One'], + datasets: [{ + data: [1], + }] + }, + options: { + hover: { + mode: 'single' }, - options: { - hover: { - mode: 'single' - }, scales: { yAxes: [{ ticks: { - beginAtZero:true + beginAtZero: true } }] } @@ -252,7 +253,7 @@ describe('Line controller tests', function() { expect(point._model.x).toBeCloseToPixel(267); // 2 points - chart.data.labels = ["One", "Two"]; + chart.data.labels = ['One', 'Two']; chart.data.datasets[0].data = [1, 2]; chart.update(); @@ -262,7 +263,7 @@ describe('Line controller tests', function() { expect(points[1]._model.x).toBeCloseToPixel(498); // 3 points - chart.data.labels = ["One", "Two", "Three"]; + chart.data.labels = ['One', 'Two', 'Three']; chart.data.datasets[0].data = [1, 2, 3]; chart.update(); @@ -273,7 +274,7 @@ describe('Line controller tests', function() { expect(points[2]._model.x).toBeCloseToPixel(493); // 4 points - chart.data.labels = ["One", "Two", "Three", "Four"]; + chart.data.labels = ['One', 'Two', 'Three', 'Four']; chart.data.datasets[0].data = [1, 2, 3, 4]; chart.update(); @@ -309,24 +310,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -362,24 +365,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 76, y: 161 }, - { x: 215, y: 419 }, - { x: 353, y: 161 }, - { x: 492, y: 419 } + [ + {x: 76, y: 161}, + {x: 215, y: 419}, + {x: 353, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 76, y: 32 }, - { x: 215, y: 97 }, - { x: 353, y: 161 }, - { x: 492, y: 471 } + [ + {x: 76, y: 32}, + {x: 215, y: 97}, + {x: 353, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -432,24 +437,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); @@ -478,24 +485,26 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); - [ { x: 38, y: 161 }, - { x: 189, y: 419 }, - { x: 341, y: 161 }, - { x: 492, y: 419 } + [ + {x: 38, y: 161}, + {x: 189, y: 419}, + {x: 341, y: 161}, + {x: 492, y: 419} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); - [ { x: 38, y: 32 }, - { x: 189, y: 97 }, - { x: 341, y: 161 }, - { x: 492, y: 471 } + [ + {x: 38, y: 32}, + {x: 189, y: 97}, + {x: 341, y: 161}, + {x: 492, y: 471} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); }); }); diff --git a/test/controller.polarArea.tests.js b/test/controller.polarArea.tests.js index 648117a4c..3bd95eb23 100644 --- a/test/controller.polarArea.tests.js +++ b/test/controller.polarArea.tests.js @@ -2,14 +2,14 @@ describe('Polar area controller tests', function() { it('should be constructed', function() { var chart = window.acquireChart({ - type: 'polarArea', - data: { - datasets: [ - { data: [] }, - { data: [] } - ], - labels: [] - } + type: 'polarArea', + data: { + datasets: [ + {data: []}, + {data: []} + ], + labels: [] + } }); var meta = chart.getDatasetMeta(1); @@ -28,8 +28,8 @@ describe('Polar area controller tests', function() { type: 'polarArea', data: { datasets: [ - { data: [] }, - { data: [10, 15, 0, -4] } + {data: []}, + {data: [10, 15, 0, -4]} ], labels: [] } @@ -45,14 +45,14 @@ describe('Polar area controller tests', function() { it('should draw all elements', function() { var chart = window.acquireChart({ - type: 'polarArea', - data: { - datasets: [{ - data: [10, 15, 0, -4], - label: 'dataset2' - }], - labels: ['label1', 'label2', 'label3', 'label4'] - } + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } }); var meta = chart.getDatasetMeta(0); @@ -95,10 +95,11 @@ describe('Polar area controller tests', function() { var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); - [ { o: 156, s: -0.5 * Math.PI, e: 0 }, - { o: 211, s: 0, e: 0.5 * Math.PI }, - { o: 45, s: 0.5 * Math.PI, e: Math.PI }, - { o: 0, s: Math.PI, e: 1.5 * Math.PI } + [ + {o: 156, s: -0.5 * Math.PI, e: 0}, + {o: 211, s: 0, e: 0.5 * Math.PI}, + {o: 45, s: 0.5 * Math.PI, e: Math.PI}, + {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -176,10 +177,11 @@ describe('Polar area controller tests', function() { var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); - [ { o: 156, s: 0, e: 0.5 * Math.PI }, - { o: 211, s: 0.5 * Math.PI, e: Math.PI }, - { o: 45, s: Math.PI, e: 1.5 * Math.PI }, - { o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI } + [ + {o: 156, s: 0, e: 0.5 * Math.PI}, + {o: 211, s: 0.5 * Math.PI, e: Math.PI}, + {o: 45, s: Math.PI, e: 1.5 * Math.PI}, + {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); expect(meta.data[i]._model.y).toBeCloseToPixel(272); @@ -230,7 +232,7 @@ describe('Polar area controller tests', function() { expect(meta.data[0] instanceof Chart.elements.Arc).toBe(true); expect(meta.data[1] instanceof Chart.elements.Arc).toBe(true); - // add 3 items + // add 3 items chart.data.labels = ['label1', 'label2', 'label3', 'label4', 'label5']; chart.data.datasets[0].data = [1, 2, 3, 4, 5]; chart.update(); diff --git a/test/controller.radar.tests.js b/test/controller.radar.tests.js index 421ff9eb3..449d295c9 100644 --- a/test/controller.radar.tests.js +++ b/test/controller.radar.tests.js @@ -32,8 +32,6 @@ describe('Radar controller tests', function() { } }); - var controller = new Chart.controllers.radar(chart, 0); - var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.Line).toBe(true); // line element expect(meta.data.length).toBe(4); // 4 points created @@ -130,10 +128,10 @@ describe('Radar controller tests', function() { })); [ - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, - { x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, + {x: 256, y: 272, cppx: 256, cppy: 272, cpnx: 256, cpny: 272}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -157,10 +155,10 @@ describe('Radar controller tests', function() { meta.controller.update(); [ - { x: 256, y: 133, cppx: 246, cppy: 133, cpnx: 272, cpny: 133 }, - { x: 464, y: 272, cppx: 464, cppy: 264, cpnx: 464, cpny: 278 }, - { x: 256, y: 272, cppx: 276.9, cppy: 272, cpnx: 250.4, cpny: 272 }, - { x: 200, y: 272, cppx: 200, cppy: 275, cpnx: 200, cpny: 261 }, + {x: 256, y: 133, cppx: 246, cppy: 133, cpnx: 272, cpny: 133}, + {x: 464, y: 272, cppx: 464, cppy: 264, cpnx: 464, cpny: 278}, + {x: 256, y: 272, cppx: 276.9, cppy: 272, cpnx: 250.4, cpny: 272}, + {x: 200, y: 272, cppx: 200, cppy: 275, cpnx: 200, cpny: 261}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -218,10 +216,10 @@ describe('Radar controller tests', function() { // Since tension is now 0, we don't care about the control points [ - { x: 256, y: 133 }, - { x: 464, y: 272 }, - { x: 256, y: 272 }, - { x: 200, y: 272 }, + {x: 256, y: 133}, + {x: 464, y: 272}, + {x: 256, y: 272}, + {x: 200, y: 272}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -454,4 +452,4 @@ describe('Radar controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); -}); \ No newline at end of file +}); diff --git a/test/core.element.tests.js b/test/core.element.tests.js index 6443de099..7d194562e 100644 --- a/test/core.element.tests.js +++ b/test/core.element.tests.js @@ -25,9 +25,9 @@ describe('Core element tests', function() { element._model.numberProp = 100; element._model.numberProp2 = 250; element._model._underscoreProp = 200; - element._model.stringProp = 'def' + element._model.stringProp = 'def'; element._model.newStringProp = 'newString'; - element._model.colorProp = 'rgb(255, 255, 0)' + element._model.colorProp = 'rgb(255, 255, 0)'; element.transition(0.25); expect(element._view).toEqual({ @@ -42,4 +42,4 @@ describe('Core element tests', function() { colorProp: 'rgb(64, 64, 0)', }); }); -}); \ No newline at end of file +}); diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 3d4316062..e9cbb8e43 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -7,7 +7,7 @@ describe('Core helper tests', function() { }); it('should iterate over an array and pass the extra data to that function', function() { - var testData = [0, 9, "abc"]; + 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) { @@ -183,7 +183,7 @@ describe('Core helper tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -191,12 +191,12 @@ describe('Core helper tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "right", + position: 'right', scaleLabel: { labelString: '', display: false, @@ -219,7 +219,7 @@ describe('Core helper tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label, @@ -227,12 +227,12 @@ describe('Core helper tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, @@ -271,7 +271,7 @@ describe('Core helper tests', function() { it('should filter an array', function() { var data = [-10, 0, 6, 0, 7]; var callback = function(item) { - return item > 2 + return item > 2; }; expect(helpers.where(data, callback)).toEqual([6, 7]); expect(helpers.findNextWhere(data, callback)).toEqual(6); @@ -400,19 +400,19 @@ describe('Core helper tests', function() { 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: 36, y: 170, skip: false } } + {_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: 36, y: 170, skip: false}} ]; helpers.splineCurveMonotone(dataPoints); expect(dataPoints).toEqual([{ @@ -580,13 +580,13 @@ describe('Core helper tests', function() { 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"; - var arrayOfThings_1D = ['FooBar','Bar']; - var arrayOfThings_2D = [['FooBar_1','Bar_2'],'Foo_1']; - + var arrayOfThings1D = ['FooBar', 'Bar']; + var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1']; + // Regardless 'FooBar' is the longest label it should return (charcters * 10) - expect(helpers.longestText(context, font, arrayOfThings_1D, {})).toEqual(60); - expect(helpers.longestText(context, font, arrayOfThings_2D, {})).toEqual(80); + expect(helpers.longestText(context, font, arrayOfThings1D, {})).toEqual(60); + expect(helpers.longestText(context, font, arrayOfThings2D, {})).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'measureText', @@ -629,14 +629,14 @@ describe('Core helper tests', function() { }); it('count look at all the labels and return maximum number of lines', function() { - var context = window.createMockContext(); - var arrayOfThings_1 = ['Foo','Bar']; - var arrayOfThings_2 = [['Foo','Bar'],'Foo']; - var arrayOfThings_3 = [['Foo','Bar','Boo'],['Foo','Bar'],'Foo']; + window.createMockContext(); + var arrayOfThings1 = ['Foo', 'Bar']; + var arrayOfThings2 = [['Foo', 'Bar'], 'Foo']; + var arrayOfThings3 = [['Foo', 'Bar', 'Boo'], ['Foo', 'Bar'], 'Foo']; - expect(helpers.numberOfLabelLines(arrayOfThings_1)).toEqual(1); - expect(helpers.numberOfLabelLines(arrayOfThings_2)).toEqual(2); - expect(helpers.numberOfLabelLines(arrayOfThings_3)).toEqual(3); + expect(helpers.numberOfLabelLines(arrayOfThings1)).toEqual(1); + expect(helpers.numberOfLabelLines(arrayOfThings2)).toEqual(2); + expect(helpers.numberOfLabelLines(arrayOfThings3)).toEqual(3); }); it('should draw a rounded rectangle', function() { @@ -676,14 +676,14 @@ describe('Core helper tests', function() { }, { 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'); - div.style.width = "200px"; - div.style.height = "300px"; + div.style.width = '200px'; + div.style.height = '300px'; document.body.appendChild(div); @@ -700,14 +700,14 @@ describe('Core helper tests', function() { 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"; + 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"; + innerDiv.style.maxWidth = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(150); @@ -718,14 +718,14 @@ describe('Core helper tests', function() { 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"; + 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"; + innerDiv.style.maxHeight = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -736,14 +736,14 @@ describe('Core helper tests', function() { 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"; + 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"; + parentDiv.style.maxWidth = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for @@ -758,19 +758,19 @@ describe('Core helper tests', function() { 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"; + 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"; + 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 + innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -781,14 +781,14 @@ describe('Core helper tests', function() { 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"; + 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%"; + innerDiv.style.maxWidth = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumWidth(innerDiv)).toBe(100); @@ -799,14 +799,14 @@ describe('Core helper tests', function() { 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"; + 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%"; + innerDiv.style.maxHeight = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -817,14 +817,14 @@ describe('Core helper tests', function() { 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"; + 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%"; + parentDiv.style.maxWidth = '50%'; div.appendChild(parentDiv); // Create the div we want to get the max size for @@ -839,18 +839,18 @@ describe('Core helper tests', function() { 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"; + 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%"; + parentDiv.style.maxHeight = '50%'; div.appendChild(parentDiv); var innerDiv = document.createElement('div'); - innerDiv.style.height = "300px"; // make it large + innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumHeight(innerDiv)).toBe(150); @@ -891,7 +891,7 @@ describe('Core helper tests', function() { expect(backgroundColor instanceof CanvasPattern).toBe(true); done(); - } + }; }); it('should return a modified version of color when called with a color', function() { diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js index 312ac8fe6..15189a477 100644 --- a/test/core.interaction.tests.js +++ b/test/core.interaction.tests.js @@ -115,7 +115,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: true}); expect(elements).toEqual([point, meta1.data[1]]); }); @@ -154,7 +154,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.index(chartInstance, evt, { intersect: false }); + var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: false}); expect(elements).toEqual([meta0.data[0], meta1.data[0]]); }); }); @@ -195,7 +195,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: true}); expect(elements).toEqual(meta.data); }); @@ -230,7 +230,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.dataset(chartInstance, evt, { intersect: false }); + var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: false}); var meta = chartInstance.getDatasetMeta(1); expect(elements).toEqual(meta.data); @@ -260,7 +260,6 @@ describe('Core.Interaction', function() { // Trigger an event over top of the var meta = chartInstance.getDatasetMeta(1); var node = chartInstance.chart.canvas; - var rect = node.getBoundingClientRect(); var evt = { view: window, bubbles: true, @@ -271,7 +270,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); expect(elements).toEqual([meta.data[0]]); }); @@ -318,7 +317,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); expect(elements).toEqual([meta0.data[1]]); }); @@ -365,7 +364,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: false }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: false}); expect(elements).toEqual([meta0.data[1]]); }); }); @@ -406,7 +405,7 @@ describe('Core.Interaction', function() { }; // Nothing intersects so find nothing - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); expect(elements).toEqual([]); evt = { @@ -417,7 +416,7 @@ describe('Core.Interaction', function() { clientY: rect.top + point._view.y, currentTarget: node }; - elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); expect(elements).toEqual([point]); }); @@ -463,7 +462,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1]]); }); @@ -509,7 +508,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1]]); }); @@ -555,7 +554,7 @@ describe('Core.Interaction', function() { }; // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chartInstance, evt, { intersect: true }); + var elements = Chart.Interaction.modes.nearest(chartInstance, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1]]); }); }); diff --git a/test/core.layoutService.tests.js b/test/core.layoutService.tests.js index 860b4b649..4fa1039d5 100644 --- a/test/core.layoutService.tests.js +++ b/test/core.layoutService.tests.js @@ -5,7 +5,7 @@ describe('Test the layout service', function() { type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -53,7 +53,7 @@ describe('Test the layout service', function() { type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -121,7 +121,7 @@ describe('Test the layout service', function() { expect(chart.scale.right).toBeCloseToPixel(512); expect(chart.scale.top).toBeCloseToPixel(32); expect(chart.scale.width).toBeCloseToPixel(512); - expect(chart.scale.height).toBeCloseToPixel(480) + expect(chart.scale.height).toBeCloseToPixel(480); }); it('should fit multiple axes in the same position', function() { diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index caa35883a..6d539fd1e 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -118,20 +118,21 @@ describe('Legend block tests', function() { expect(chart.legend.legendHitBoxes.length).toBe(3); - [ { h: 12, l: 101, t: 10, w: 93 }, - { h: 12, l: 205, t: 10, w: 93 }, - { h: 12, l: 308, t: 10, w: 93 } + [ + {h: 12, l: 101, t: 10, w: 93}, + {h: 12, l: 205, t: 10, w: 93}, + {h: 12, l: 308, t: 10, w: 93} ].forEach(function(expected, i) { expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); expect(chart.legend.legendHitBoxes[i].left).toBeCloseToPixel(expected.l); expect(chart.legend.legendHitBoxes[i].top).toBeCloseToPixel(expected.t); expect(chart.legend.legendHitBoxes[i].width).toBeCloseToPixel(expected.w); - }) + }); // NOTE(SB) We should get ride of the following tests and use image diff instead. // For now, as discussed with Evert Timberg, simply comment out. // See http://humblesoftware.github.io/js-imagediff/test.html - /*chart.legend.ctx = window.createMockContext(); + /* chart.legend.ctx = window.createMockContext(); chart.update(); expect(chart.legend.ctx .getCalls()).toEqual([{ diff --git a/test/core.plugin.tests.js b/test/core.plugin.tests.js index 0d4186af8..59a71b1c5 100644 --- a/test/core.plugin.tests.js +++ b/test/core.plugin.tests.js @@ -75,24 +75,52 @@ describe('Chart.plugins', function() { }; Chart.plugins.register(myplugin); - Chart.plugins.notify('trigger', [{ count: 10 }]); + Chart.plugins.notify('trigger', [{count: 10}]); expect(myplugin.count).toBe(10); }); it('should return TRUE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ check: function() {} }); - Chart.plugins.register({ check: function() { return; } }); - Chart.plugins.register({ check: function() { return null; } }); - Chart.plugins.register({ check: function() { return 42 } }); + Chart.plugins.register({ + check: function() {} + }); + Chart.plugins.register({ + check: function() { + return; + } + }); + Chart.plugins.register({ + check: function() { + return null; + } + }); + Chart.plugins.register({ + check: function() { + return 42; + } + }); var res = Chart.plugins.notify('check'); expect(res).toBeTruthy(); }); it('should return FALSE if no plugin explicitly returns FALSE', function() { - Chart.plugins.register({ check: function() {} }); - Chart.plugins.register({ check: function() { return; } }); - Chart.plugins.register({ check: function() { return false; } }); - Chart.plugins.register({ check: function() { return 42 } }); + Chart.plugins.register({ + check: function() {} + }); + Chart.plugins.register({ + check: function() { + return; + } + }); + Chart.plugins.register({ + check: function() { + return false; + } + }); + Chart.plugins.register({ + check: function() { + return 42; + } + }); var res = Chart.plugins.notify('check'); expect(res).toBeFalsy(); }); diff --git a/test/core.scaleService.tests.js b/test/core.scaleService.tests.js index ca0eaf20d..aacac9879 100644 --- a/test/core.scaleService.tests.js +++ b/test/core.scaleService.tests.js @@ -15,7 +15,7 @@ describe('Test the scale service', function() { expect(Chart.scaleService.getScaleDefaults(type)).toEqual(jasmine.objectContaining({ testProp: true })); - + Chart.scaleService.updateScaleDefaults(type, { testProp: 'red', newProp: 42 diff --git a/test/core.title.tests.js b/test/core.title.tests.js index b8ecd18e5..704605278 100644 --- a/test/core.title.tests.js +++ b/test/core.title.tests.js @@ -14,14 +14,14 @@ describe('Title block tests', function() { fontStyle: 'bold', padding: 10, text: '' - }) + }); }); it('should update correctly', function() { var chart = {}; var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; var title = new Chart.Title({ chart: chart, @@ -50,7 +50,7 @@ describe('Title block tests', function() { var chart = {}; var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; options.position = 'left'; var title = new Chart.Title({ @@ -81,7 +81,7 @@ describe('Title block tests', function() { var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; var title = new Chart.Title({ chart: chart, @@ -130,7 +130,7 @@ describe('Title block tests', function() { var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.global.title); - options.text = "My title"; + options.text = 'My title'; options.position = 'left'; var title = new Chart.Title({ diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index dd6ee17f5..29c6b721a 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -342,7 +342,7 @@ describe('Core.Tooltip', function() { return 'title'; }, afterTitle: function() { - return 'afterTitle' + return 'afterTitle'; }, beforeBody: function() { return 'beforeBody'; @@ -366,7 +366,7 @@ describe('Core.Tooltip', function() { return 'footer'; }, afterFooter: function() { - return 'afterFooter' + return 'afterFooter'; } } } @@ -494,9 +494,6 @@ describe('Core.Tooltip', function() { var meta0 = chartInstance.getDatasetMeta(0); var point0 = meta0.data[1]; - var meta1 = chartInstance.getDatasetMeta(1); - var point1 = meta1.data[1]; - var node = chartInstance.chart.canvas; var rect = node.getBoundingClientRect(); diff --git a/test/defaultConfig.tests.js b/test/defaultConfig.tests.js index 84c9655e6..0e8599014 100644 --- a/test/defaultConfig.tests.js +++ b/test/defaultConfig.tests.js @@ -1,6 +1,6 @@ // Test the bubble chart default config -describe("Default Configs", function() { - describe("Bubble Chart", function() { +describe('Default Configs', function() { + describe('Bubble Chart', function() { it('should return correct tooltip strings', function() { var config = Chart.defaults.bubble; var chart = window.acquireChart({ @@ -22,7 +22,7 @@ describe("Default Configs", function() { chart.tooltip._active = [chart.getDatasetMeta(0).data[0]]; chart.tooltip.update(); - // Title is always blank + // Title is always blank expect(chart.tooltip._model.title).toEqual([]); expect(chart.tooltip._model.body).toEqual([{ before: [], @@ -50,7 +50,7 @@ describe("Default Configs", function() { chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; chart.tooltip.update(); - // Title is always blank + // Title is always blank expect(chart.tooltip._model.title).toEqual([]); expect(chart.tooltip._model.body).toEqual([{ before: [], @@ -76,7 +76,7 @@ describe("Default Configs", function() { chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; chart.tooltip.update(); - // Title is always blank + // Title is always blank expect(chart.tooltip._model.title).toEqual([]); expect(chart.tooltip._model.body).toEqual([{ before: [], @@ -196,7 +196,7 @@ describe("Default Configs", function() { chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; chart.tooltip.update(); - // Title is always blank + // Title is always blank expect(chart.tooltip._model.title).toEqual([]); expect(chart.tooltip._model.body).toEqual([{ before: [], @@ -280,7 +280,7 @@ describe("Default Configs", function() { options: config }); var meta = chart.getDatasetMeta(0); - + spyOn(chart, 'update').and.callThrough(); var legendItem = chart.legend.legendItems[0]; diff --git a/test/element.arc.tests.js b/test/element.arc.tests.js index 7ce70517d..b2caaadaa 100644 --- a/test/element.arc.tests.js +++ b/test/element.arc.tests.js @@ -213,4 +213,4 @@ describe('Arc element tests', function() { args: [] }]); }); -}); \ No newline at end of file +}); diff --git a/test/element.line.tests.js b/test/element.line.tests.js index 0f495c9d1..0a2a6eced 100644 --- a/test/element.line.tests.js +++ b/test/element.line.tests.js @@ -2580,4 +2580,4 @@ describe('Line element tests', function() { args: [] }]); }); -}); \ No newline at end of file +}); diff --git a/test/element.point.tests.js b/test/element.point.tests.js index 9cbac8ea5..61fe3d09a 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -91,7 +91,7 @@ describe('Point element tests', function() { y: 10 }; - expect(point.getCenterPoint()).toEqual({ x: 10, y: 10 }); + expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); it ('should draw correctly', function() { @@ -275,7 +275,7 @@ describe('Point element tests', function() { }, { name: 'lineTo', args: [12, 15], - },{ + }, { name: 'closePath', args: [], }, { @@ -347,7 +347,7 @@ describe('Point element tests', function() { }, { name: 'lineTo', args: [12, 15], - },{ + }, { name: 'moveTo', args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] }, { @@ -501,4 +501,4 @@ describe('Point element tests', function() { expect(mockContext.getCalls()).toEqual([]); }); -}); \ No newline at end of file +}); diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index bd32ff3ac..b833862bd 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -163,7 +163,7 @@ describe('Rectangle element tests', function() { y: 15 }; - expect(rectangle.getCenterPoint()).toEqual({ x: 10, y: 7.5 }); + expect(rectangle.getCenterPoint()).toEqual({x: 10, y: 7.5}); }); it ('should draw correctly', function() { @@ -276,10 +276,10 @@ describe('Rectangle element tests', function() { }]); }); - function testBorderSkipped (borderSkipped, expectedDrawCalls) { + function testBorderSkipped(borderSkipped, expectedDrawCalls) { var mockContext = window.createMockContext(); var rectangle = new Chart.elements.Rectangle({ - _chart: { ctx: mockContext } + _chart: {ctx: mockContext} }); // Attach a view object as if we were the controller @@ -291,47 +291,47 @@ describe('Rectangle element tests', function() { x: 10, y: 15, }; - + rectangle.draw(); - var drawCalls = rectangle._view.ctx.getCalls().splice(4, 4); + var drawCalls = rectangle._view.ctx.getCalls().splice(4, 4); expect(drawCalls).toEqual(expectedDrawCalls); } - + it ('should draw correctly respecting "borderSkipped" == "bottom"', function() { testBorderSkipped ('bottom', [ - { name: 'moveTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, + {name: 'moveTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "left"', function() { testBorderSkipped ('left', [ - { name: 'moveTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, + {name: 'moveTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "top"', function() { testBorderSkipped ('top', [ - { name: 'moveTo', args: [12, 15] }, - { name: 'lineTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, + {name: 'moveTo', args: [12, 15]}, + {name: 'lineTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, ]); }); it ('should draw correctly respecting "borderSkipped" == "right"', function() { testBorderSkipped ('right', [ - { name: 'moveTo', args: [12, 0] }, - { name: 'lineTo', args: [8, 0] }, - { name: 'lineTo', args: [8, 15] }, - { name: 'lineTo', args: [12, 15] }, + {name: 'moveTo', args: [12, 0]}, + {name: 'lineTo', args: [8, 0]}, + {name: 'lineTo', args: [8, 15]}, + {name: 'lineTo', args: [12, 15]}, ]); }); -}); \ No newline at end of file +}); diff --git a/test/mockContext.js b/test/mockContext.js index 8904cf886..58a2ccf23 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -1,3 +1,5 @@ +/* eslint guard-for-in: 1 */ +/* eslint camelcase: 1 */ (function() { // Code from http://stackoverflow.com/questions/4406864/html-canvas-unit-testing var Context = function() { @@ -13,44 +15,56 @@ // Define properties here so that we can record each time they are set Object.defineProperties(this, { - "fillStyle": { - 'get': function() { return this._fillStyle; }, - 'set': function(style) { + fillStyle: { + get: function() { + return this._fillStyle; + }, + set: function(style) { this._fillStyle = style; this.record('setFillStyle', [style]); } }, - 'lineCap': { - 'get': function() { return this._lineCap; }, - 'set': function(cap) { + lineCap: { + get: function() { + return this._lineCap; + }, + set: function(cap) { this._lineCap = cap; this.record('setLineCap', [cap]); } }, - 'lineDashOffset': { - 'get': function() { return this._lineDashOffset; }, - 'set': function(offset) { + lineDashOffset: { + get: function() { + return this._lineDashOffset; + }, + set: function(offset) { this._lineDashOffset = offset; this.record('setLineDashOffset', [offset]); } }, - 'lineJoin': { - 'get': function() { return this._lineJoin; }, - 'set': function(join) { + lineJoin: { + get: function() { + return this._lineJoin; + }, + set: function(join) { this._lineJoin = join; this.record('setLineJoin', [join]); } }, - 'lineWidth': { - 'get': function() { return this._lineWidth; }, - 'set': function (width) { + lineWidth: { + get: function() { + return this._lineWidth; + }, + set: function(width) { this._lineWidth = width; this.record('setLineWidth', [width]); } }, - 'strokeStyle': { - 'get': function() { return this._strokeStyle; }, - 'set': function(style) { + strokeStyle: { + get: function() { + return this._strokeStyle; + }, + set: function(style) { this._strokeStyle = style; this.record('setStrokeStyle', [style]); } @@ -70,33 +84,35 @@ fill: function() {}, fillRect: function() {}, fillText: function() {}, - lineTo: function(x, y) {}, + lineTo: function() {}, measureText: function(text) { // return the number of characters * fixed size - return text ? { width: text.length * 10 } : {width: 0}; + return text ? {width: text.length * 10} : {width: 0}; }, - moveTo: function(x, y) {}, + moveTo: function() {}, quadraticCurveTo: function() {}, restore: function() {}, rotate: function() {}, save: function() {}, setLineDash: function() {}, stroke: function() {}, - strokeRect: function(x, y, w, h) {}, - setTransform: function(a, b, c, d, e, f) {}, - translate: function(x, y) {}, + strokeRect: function() {}, + setTransform: function() {}, + translate: function() {}, }; // attach methods to the class itself - var scope = this; - var addMethod = function(name, method) { - scope[methodName] = function() { - scope.record(name, arguments); - return method.apply(scope, arguments); - }; - } + var me = this; + var methodName; - for (var methodName in methods) { + var addMethod = function(name, method) { + me[methodName] = function() { + me.record(name, arguments); + return method.apply(me, arguments); + }; + }; + + for (methodName in methods) { var method = methods[methodName]; addMethod(methodName, method); @@ -108,11 +124,11 @@ name: methodName, args: Array.prototype.slice.call(args) }); - }, + }; Context.prototype.getCalls = function() { return this._calls; - } + }; Context.prototype.resetCalls = function() { this._calls = []; @@ -136,10 +152,10 @@ result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine } - return { pass: result }; + return {pass: result}; } - } - }; + }; + } function toEqualOneOf() { return { @@ -251,13 +267,13 @@ * @param {boolean} options.persistent - If true, the chart will not be released after the spec. */ function acquireChart(config, options) { - var wrapper = document.createElement("div"); - var canvas = document.createElement("canvas"); + var wrapper = document.createElement('div'); + var canvas = document.createElement('canvas'); var chart, key; options = options || {}; - options.canvas = options.canvas || { height: 512, width: 512 }; - options.wrapper = options.wrapper || { class: 'chartjs-wrapper' }; + options.canvas = options.canvas || {height: 512, width: 512}; + options.wrapper = options.wrapper || {class: 'chartjs-wrapper'}; for (key in options.canvas) { if (options.canvas.hasOwnProperty(key)) { @@ -280,7 +296,7 @@ wrapper.appendChild(canvas); window.document.body.appendChild(wrapper); - chart = new Chart(canvas.getContext("2d"), config); + chart = new Chart(canvas.getContext('2d'), config); chart._test_persistent = options.persistent; chart._test_wrapper = wrapper; charts[chart.id] = chart; @@ -329,4 +345,4 @@ '.chartjs-wrapper {' + 'position: absolute' + '}'); -})(); +}()); diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index 5307ba652..13955d35b 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -13,7 +13,7 @@ describe('Category scale tests', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -21,12 +21,12 @@ describe('Category scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "bottom", + position: 'bottom', scaleLabel: { labelString: '', display: false @@ -287,8 +287,8 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.ticks.min = "tick2"; - config.ticks.max = "tick4"; + config.ticks.min = 'tick2'; + config.ticks.max = 'tick4'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ @@ -349,7 +349,7 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.position = "left"; + config.position = 'left'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ ctx: mockContext, @@ -414,9 +414,9 @@ describe('Category scale tests', function() { var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); config.gridLines.offsetGridLines = true; - config.ticks.min = "tick2"; - config.ticks.max = "tick4"; - config.position = "left"; + config.ticks.min = 'tick2'; + config.ticks.max = 'tick4'; + config.position = 'left'; var Constructor = Chart.scaleService.getScaleConstructor('category'); var scale = new Constructor({ diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 4eb585a7b..7102a6f4a 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -11,7 +11,7 @@ describe('Linear Scale', function() { display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, // draw ticks extending towards the label @@ -19,12 +19,12 @@ describe('Linear Scale', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 221e61c4b..9636a0b37 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -10,7 +10,7 @@ describe('Logarithmic Scale tests', function() { expect(defaultConfig).toEqual({ display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -18,12 +18,12 @@ describe('Logarithmic Scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "left", + position: 'left', scaleLabel: { labelString: '', display: false, @@ -78,14 +78,14 @@ describe('Logarithmic Scale tests', function() { id: 'yScale1', type: 'logarithmic' }, - { - id: 'yScale2', - type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + { + id: 'yScale2', + type: 'logarithmic' + }, + { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -125,7 +125,7 @@ describe('Logarithmic Scale tests', function() { data: ['20', '0', '150', '1800', '3040'] }, { yAxisID: 'yScale3', - data: ['67', '0.0004', '0', '820', '0.001'] + data: ['67', '0.0004', '0', '820', '0.001'] }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -141,10 +141,10 @@ describe('Logarithmic Scale tests', function() { id: 'yScale2', type: 'logarithmic' }, - { - id: 'yScale3', - type: 'logarithmic' - }] + { + id: 'yScale3', + type: 'logarithmic' + }] } } }); @@ -185,8 +185,8 @@ describe('Logarithmic Scale tests', function() { data: [20, 0, 7400, 14, 291] }, { yAxisID: 'yScale2', - data: [6, 0.0007, 9, 890, 60000], - hidden: true + data: [6, 0.0007, 9, 890, 60000], + hidden: true }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -232,7 +232,7 @@ describe('Logarithmic Scale tests', function() { yAxisID: 'yScale1', data: [undefined, 0, null, 800, 9, NaN, 894, 21] }], - labels: ['a', 'b', 'c', 'd', 'e', 'f' ,'g', 'h', 'i'] + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] }, options: { scales: { @@ -269,10 +269,10 @@ describe('Logarithmic Scale tests', function() { data: { datasets: [{ data: [ - { x: 10, y: 100 }, - { x: 2, y: 6 }, - { x: 65, y: 121 }, - { x: 99, y: 7 } + {x: 10, y: 100}, + {x: 2, y: 6}, + {x: 65, y: 121}, + {x: 99, y: 7} ] }] }, @@ -304,10 +304,10 @@ describe('Logarithmic Scale tests', function() { data: { datasets: [{ data: [ - { x: 7, y: 950 }, - { x: 289, y: 0 }, - { x: 0, y: 8 }, - { x: 23, y: 0.04 } + {x: 7, y: 950}, + {x: 289, y: 0}, + {x: 0, y: 8}, + {x: 23, y: 0.04} ] }] }, @@ -718,9 +718,9 @@ describe('Logarithmic Scale tests', function() { var xScale = chart.scales.xScale; expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue( 1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft + expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(283); // halfway - expect(xScale.getPixelForValue( 0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. expect(xScale.getValueForPixel(495)).toBeCloseTo(80, 1e-4); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); @@ -728,9 +728,9 @@ describe('Logarithmic Scale tests', function() { var yScale = chart.scales.yScale; expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue( 1, 0, 0)).toBeCloseToPixel(456); // bottom - paddingBottom + expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(456); // bottom - paddingBottom expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(234); // halfway - expect(yScale.getPixelForValue( 0, 0, 0)).toBeCloseToPixel(32); // 0 is invalid. force it on top + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // 0 is invalid. force it on top expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); expect(yScale.getValueForPixel(456)).toBeCloseTo(1, 1e-4); diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index dea9b1b03..a47214f4f 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -11,13 +11,13 @@ describe('Test the radial linear scale', function() { expect(defaultConfig).toEqual({ angleLines: { display: true, - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', lineWidth: 1 }, animate: true, display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -25,7 +25,7 @@ describe('Test the radial linear scale', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 @@ -35,13 +35,13 @@ describe('Test the radial linear scale', function() { fontSize: 10, callback: defaultConfig.pointLabels.callback, // make this nicer, then check explicitly below }, - position: "chartArea", + position: 'chartArea', scaleLabel: { labelString: '', display: false, }, ticks: { - backdropColor: "rgba(255,255,255,0.75)", + backdropColor: 'rgba(255,255,255,0.75)', backdropPaddingY: 2, backdropPaddingX: 2, beginAtZero: false, @@ -426,19 +426,19 @@ describe('Test the radial linear scale', function() { var radToNearestDegree = function(rad) { return Math.round((360 * rad) / (2 * Math.PI)); - } + }; var slice = 72; // (360 / 5) - for(var i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i) - 90); } chart.options.startAngle = 0; chart.update(); - for(var i = 0; i < 5; i++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe((slice * i) - 90); + for (var x = 0; x < 5; x++) { + expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x) - 90); } }); }); diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index 5179191ba..896ff00d4 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -15,7 +15,7 @@ describe('Time scale tests', function() { pass: result }; } - } + }; } }); }); @@ -35,7 +35,7 @@ describe('Time scale tests', function() { expect(defaultConfig).toEqual({ display: true, gridLines: { - color: "rgba(0, 0, 0, 0.1)", + color: 'rgba(0, 0, 0, 0.1)', drawBorder: true, drawOnChartArea: true, drawTicks: true, @@ -43,12 +43,12 @@ describe('Time scale tests', function() { lineWidth: 1, offsetGridLines: false, display: true, - zeroLineColor: "rgba(0,0,0,0.25)", + zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, - position: "bottom", + position: 'bottom', scaleLabel: { labelString: '', display: false @@ -75,15 +75,15 @@ describe('Time scale tests', function() { displayFormat: false, minUnit: 'millisecond', displayFormats: { - 'millisecond': 'h:mm:ss.SSS a', // 11:20:01.123 AM - 'second': 'h:mm:ss a', // 11:20:01 AM - 'minute': 'h:mm:ss a', // 11:20:01 AM - 'hour': 'MMM D, hA', // Sept 4, 5PM - 'day': 'll', // Sep 4 2015 - 'week': 'll', // Week 46, or maybe "[W]WW - YYYY" ? - 'month': 'MMM YYYY', // Sept 2015 - 'quarter': '[Q]Q - YYYY', // Q3 - 'year': 'YYYY' // 2015 + millisecond: 'h:mm:ss.SSS a', // 11:20:01.123 AM + second: 'h:mm:ss a', // 11:20:01 AM + minute: 'h:mm:ss a', // 11:20:01 AM + hour: 'MMM D, hA', // Sept 4, 5PM + day: 'll', // Sep 4 2015 + week: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + month: 'MMM YYYY', // Sept 2015 + quarter: '[Q]Q - YYYY', // Q3 + year: 'YYYY' // 2015 } } }); @@ -96,7 +96,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }; var mockContext = window.createMockContext(); @@ -110,11 +110,11 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual([ 'Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015' ]); + expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); }); it('should build ticks using date objects', function() { @@ -142,7 +142,7 @@ describe('Time scale tests', function() { scale.update(400, 50); // Counts down because the lines are drawn top to bottom - expect(scale.ticks).toEqual([ 'Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015' ]); + expect(scale.ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015']); }); it('should build ticks when the data is xy points', function() { @@ -198,7 +198,7 @@ describe('Time scale tests', function() { // Counts down because the lines are drawn top to bottom var xScale = chart.scales.xScale0; - expect(xScale.ticks).toEqual([ 'Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015' ]); + expect(xScale.ticks).toEqual(['Jan 1, 2015', 'Jan 3, 2015', 'Jan 5, 2015', 'Jan 7, 2015', 'Jan 9, 2015', 'Jan 11, 2015']); }); it('should allow custom time parsers', function() { @@ -223,7 +223,7 @@ describe('Time scale tests', function() { time: { unit: 'day', round: true, - parser: function customTimeParser(label) { + parser: function(label) { return moment.unix(label); } } @@ -248,7 +248,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; var mockContext = window.createMockContext(); @@ -264,7 +264,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); expect(scale.ticks).toEqual(['Jan 1, 8PM', 'Jan 1, 9PM', 'Jan 1, 10PM', 'Jan 1, 11PM', 'Jan 2, 12AM', 'Jan 2, 1AM', 'Jan 2, 2AM', 'Jan 2, 3AM', 'Jan 2, 4AM', 'Jan 2, 5AM', 'Jan 2, 6AM', 'Jan 2, 7AM', 'Jan 2, 8AM', 'Jan 2, 9AM', 'Jan 2, 10AM', 'Jan 2, 11AM', 'Jan 2, 12PM', 'Jan 2, 1PM', 'Jan 2, 2PM', 'Jan 2, 3PM', 'Jan 2, 4PM', 'Jan 2, 5PM', 'Jan 2, 6PM', 'Jan 2, 7PM', 'Jan 2, 8PM', 'Jan 2, 9PM']); }); @@ -273,7 +273,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], // days }; var mockContext = window.createMockContext(); @@ -289,7 +289,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 2, 2015', 'Jan 3, 2015']); }); @@ -298,7 +298,7 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-02-02T21:00:00", "2015-02-21T01:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'], // days }; var mockContext = window.createMockContext(); @@ -315,7 +315,7 @@ describe('Time scale tests', function() { id: scaleID }); - //scale.buildTicks(); + // scale.buildTicks(); scale.update(400, 50); // last date is feb 15 because we round to start of week @@ -326,13 +326,13 @@ describe('Time scale tests', function() { var scaleID = 'myScale'; var mockData = { - labels: ["2015-01-01T20:00:00", "2015-01-02T20:00:00", "2015-01-03T20:00:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], // days }; var mockContext = window.createMockContext(); var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); - config.time.min = "2015-01-01T04:00:00"; - config.time.max = "2015-01-05T06:00:00" + config.time.min = '2015-01-01T04:00:00'; + config.time.max = '2015-01-05T06:00:00'; var Constructor = Chart.scaleService.getScaleConstructor('time'); var scale = new Constructor({ ctx: mockContext, @@ -344,7 +344,7 @@ describe('Time scale tests', function() { }); scale.update(400, 50); - expect(scale.ticks).toEqual([ 'Jan 1, 2015', 'Jan 5, 2015' ]); + expect(scale.ticks).toEqual(['Jan 1, 2015', 'Jan 5, 2015']); }); it('Should use the isoWeekday option', function() { @@ -352,9 +352,9 @@ describe('Time scale tests', function() { var mockData = { labels: [ - "2015-01-01T20:00:00", // Thursday - "2015-01-02T20:00:00", // Friday - "2015-01-03T20:00:00" // Saturday + '2015-01-01T20:00:00', // Thursday + '2015-01-02T20:00:00', // Friday + '2015-01-03T20:00:00' // Saturday ] }; @@ -374,7 +374,7 @@ describe('Time scale tests', function() { }); scale.update(400, 50); - expect(scale.ticks).toEqual([ 'Dec 31, 2014', 'Jan 7, 2015' ]); + expect(scale.ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); }); it('should get the correct pixel for a value', function() { @@ -386,7 +386,7 @@ describe('Time scale tests', function() { yAxisID: 'yScale0', data: [] }], - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { @@ -430,7 +430,7 @@ describe('Time scale tests', function() { yAxisID: 'yScale0', data: [] }], - labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { @@ -457,7 +457,7 @@ describe('Time scale tests', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-05-27"], + labels: ['2016-05-27'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -468,7 +468,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time" + type: 'time' }] } } @@ -485,11 +485,11 @@ describe('Time scale tests', function() { }); }); - it("should not throw an error if the datasetIndex is out of bounds", function() { + it('should not throw an error if the datasetIndex is out of bounds', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-06-26"], + labels: ['2016-06-26'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -500,7 +500,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time", + type: 'time', }] } } @@ -514,11 +514,11 @@ describe('Time scale tests', function() { expect(getOutOfBoundLabelMoment).not.toThrow(); }); - it("should not throw an error if the datasetIndex or index are null", function() { + it('should not throw an error if the datasetIndex or index are null', function() { var chart = window.acquireChart({ type: 'line', data: { - labels: ["2016-06-26"], + labels: ['2016-06-26'], datasets: [{ xAxisID: 'xScale0', data: [5] @@ -529,7 +529,7 @@ describe('Time scale tests', function() { xAxes: [{ id: 'xScale0', display: true, - type: "time", + type: 'time', }] } } From 16f23b2c44ebeccda1e144b3dd75e9c63f81307b Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 16 Oct 2016 09:28:27 -0400 Subject: [PATCH 38/58] Add reset method to chart prototype --- docs/09-Advanced.md | 8 ++++++++ src/core/core.controller.js | 14 ++++++++++++++ test/core.controller.tests.js | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 3a361bcff..5ea957179 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -34,6 +34,14 @@ myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's v myLineChart.update(); // Calling update now animates the position of March from 90 to 50. ``` +#### .reset() + +Reset the chart to it's state before the initial animation. A new animation can then be triggered using `update`. + +```javascript +myLineChart.reset(); +``` + #### .render(duration, lazy) Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use `.update()` in that case. diff --git a/src/core/core.controller.js b/src/core/core.controller.js index c08d01928..3fb4ff1c9 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -409,6 +409,11 @@ module.exports = function(Chart) { return newControllers; }, + /** + * Reset the elements of all datasets + * @method resetElements + * @private + */ resetElements: function() { var me = this; helpers.each(me.data.datasets, function(dataset, datasetIndex) { @@ -416,6 +421,15 @@ module.exports = function(Chart) { }, me); }, + /** + * Resets the chart back to it's state before the initial animation + * @method reset + */ + reset: function() { + this.resetElements(); + this.tooltip.initialize(); + }, + update: function(animationDuration, lazy) { var me = this; Chart.plugins.notify('beforeUpdate', [me]); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index a7b504ecd..170080559 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -666,4 +666,38 @@ describe('Chart.Controller', function() { expect(wrapper.firstChild.tagName).toBe('CANVAS'); }); }); + + describe('controller.reset', function() { + it('should reset the chart elements', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 0] + }] + }, + options: { + responsive: true + } + }); + + var meta = chart.getDatasetMeta(0); + + // Verify that points are at their initial correct location, + // then we will reset and see that they moved + expect(meta.data[0]._model.y).toBe(333); + expect(meta.data[1]._model.y).toBe(183); + expect(meta.data[2]._model.y).toBe(32); + expect(meta.data[3]._model.y).toBe(484); + + chart.reset(); + + // For a line chart, the animation state is the bottom + expect(meta.data[0]._model.y).toBe(484); + expect(meta.data[1]._model.y).toBe(484); + expect(meta.data[2]._model.y).toBe(484); + expect(meta.data[3]._model.y).toBe(484); + }); + }); }); From 596ff3718c59138b54f8f76f3792e5565fa18e0f Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 15 Oct 2016 21:16:35 -0400 Subject: [PATCH 39/58] Layout service now supports configurable padding on left, top, right and bottom. Re-enabled the layout service tests and then properly disabled the tests that fail on the CI. --- docs/01-Chart-Configuration.md | 9 ++ gulpfile.js | 6 +- src/core/core.layoutService.js | 50 ++++++---- test/core.layoutService.tests.js | 153 ++++++++++++++++++++++++++++++- 4 files changed, 193 insertions(+), 25 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index c08e23d44..c12a102af 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -85,6 +85,15 @@ onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. legendCallback | Function | ` function (chart) { }` | Function to generate a legend. Receives the chart object to generate a legend from. Default implementation returns an HTML string. onResize | Function | null | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. +### Layout Configuration + +The layout configuration is passed into the `options.layout` namespace. The global options for the chart layout is defined in `Chart.defaults.global.layout`. + +Name | Type | Default | Description +--- | --- | --- | --- +padding | Number or Object | 0 | The padding to add inside the chart. If this value is a number, it is applied to all sides of the chart (left, top, right, bottom). If this value is an object, the `left` property defines the left padding. Similarly the `right`, `top`, and `bottom` properties can also be specified. + + ### Title Configuration The title configuration is passed into the `options.title` namespace. The global options for the chart title is defined in `Chart.defaults.global.title`. diff --git a/gulpfile.js b/gulpfile.js index 58b8bf8fa..530ae2101 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,11 +39,7 @@ var preTestFiles = [ ]; var testFiles = [ - './test/*.js', - - // Disable tests which need to be rewritten based on changes introduced by - // the following changes: https://github.com/chartjs/Chart.js/pull/2346 - '!./test/core.layoutService.tests.js', + './test/*.js' ]; gulp.task('bower', bowerTask); diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 7336deac0..220a25344 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -32,8 +32,26 @@ module.exports = function(Chart) { return; } - var xPadding = 0; - var yPadding = 0; + var layoutOptions = chartInstance.options.layout; + var padding = layoutOptions ? layoutOptions.padding : null; + + var leftPadding = 0; + var rightPadding = 0; + var topPadding = 0; + var bottomPadding = 0; + + if (!isNaN(padding)) { + // options.layout.padding is a number. assign to all + leftPadding = padding; + rightPadding = padding; + topPadding = padding; + bottomPadding = padding; + } else { + leftPadding = padding.left || 0; + rightPadding = padding.right || 0; + topPadding = padding.top || 0; + bottomPadding = padding.bottom || 0; + } var leftBoxes = helpers.where(chartInstance.boxes, function(box) { return box.options.position === 'left'; @@ -99,8 +117,8 @@ module.exports = function(Chart) { // 9. Tell any axes that overlay the chart area the positions of the chart area // Step 1 - var chartWidth = width - (2 * xPadding); - var chartHeight = height - (2 * yPadding); + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; var chartAreaWidth = chartWidth / 2; // min 50% var chartAreaHeight = chartHeight / 2; // min 50% @@ -140,10 +158,10 @@ module.exports = function(Chart) { // be if the axes are drawn at their minimum sizes. // Steps 5 & 6 - var totalLeftBoxesWidth = xPadding; - var totalRightBoxesWidth = xPadding; - var totalTopBoxesHeight = yPadding; - var totalBottomBoxesHeight = yPadding; + var totalLeftBoxesWidth = leftPadding; + var totalRightBoxesWidth = rightPadding; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; // Function to fit a box function fitBox(box) { @@ -213,10 +231,10 @@ module.exports = function(Chart) { helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - totalLeftBoxesWidth = xPadding; - totalRightBoxesWidth = xPadding; - totalTopBoxesHeight = yPadding; - totalBottomBoxesHeight = yPadding; + totalLeftBoxesWidth = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; helpers.each(leftBoxes, function(box) { totalLeftBoxesWidth += box.width; @@ -265,13 +283,13 @@ module.exports = function(Chart) { } // Step 7 - Position the boxes - var left = xPadding; - var top = yPadding; + var left = leftPadding; + var top = topPadding; function placeBox(box) { if (box.isHorizontal()) { - box.left = box.options.fullWidth ? xPadding : totalLeftBoxesWidth; - box.right = box.options.fullWidth ? width - xPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.left = box.options.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.options.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; box.top = top; box.bottom = top + box.height; diff --git a/test/core.layoutService.tests.js b/test/core.layoutService.tests.js index 4fa1039d5..fcc2b7dcb 100644 --- a/test/core.layoutService.tests.js +++ b/test/core.layoutService.tests.js @@ -1,6 +1,9 @@ // Tests of the scale service describe('Test the layout service', function() { - it('should fit a simple chart with 2 scales', function() { + // Disable tests which need to be rewritten based on changes introduced by + // the following changes: https://github.com/chartjs/Chart.js/pull/2346 + // using xit marks the test as pending: http://jasmine.github.io/2.0/introduction.html#section-Pending_Specs + xit('should fit a simple chart with 2 scales', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -48,7 +51,7 @@ describe('Test the layout service', function() { expect(chart.scales.yScale.labelRotation).toBeCloseTo(0); }); - it('should fit scales that are in the top and right positions', function() { + xit('should fit scales that are in the top and right positions', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -124,7 +127,7 @@ describe('Test the layout service', function() { expect(chart.scale.height).toBeCloseToPixel(480); }); - it('should fit multiple axes in the same position', function() { + xit('should fit multiple axes in the same position', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -185,7 +188,7 @@ describe('Test the layout service', function() { expect(chart.scales.yScale2.labelRotation).toBeCloseTo(0); }); - it ('should fix a full width box correctly', function() { + xit ('should fix a full width box correctly', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -239,4 +242,146 @@ describe('Test the layout service', function() { expect(chart.scales.yScale.right).toBeCloseToPixel(45); expect(chart.scales.yScale.top).toBeCloseToPixel(60); }); + + describe('padding settings', function() { + it('should apply a single padding to all dimensions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { data: [10, 5, 0, 25, 78, -10] } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: 10 + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(140); + expect(chart.chartArea.left).toBeCloseToPixel(10); + expect(chart.chartArea.right).toBeCloseToPixel(240); + expect(chart.chartArea.top).toBeCloseToPixel(10); + }); + + it('should apply padding in all positions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { data: [10, 5, 0, 25, 78, -10] } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: { + left: 5, + right: 15, + top: 8, + bottom: 12 + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(138); + expect(chart.chartArea.left).toBeCloseToPixel(5); + expect(chart.chartArea.right).toBeCloseToPixel(235); + expect(chart.chartArea.top).toBeCloseToPixel(8); + }); + + it('should default to 0 padding if no dimensions specified', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { data: [10, 5, 0, 25, 78, -10] } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'category', + display: false + }], + yAxes: [{ + id: 'yScale', + type: 'linear', + display: false + }] + }, + legend: { + display: false + }, + title: { + display: false + }, + layout: { + padding: {} + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(150); + expect(chart.chartArea.left).toBeCloseToPixel(0); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(0); + }); + }); }); From a0ce74643f3d6f7ad6f2f4a8ac4343d6526b91a8 Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Tue, 18 Oct 2016 22:00:55 +0000 Subject: [PATCH 40/58] Fix eslint errors in layout service test --- gulpfile.js | 3 ++- test/core.layoutService.tests.js | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 530ae2101..1ce312d90 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -152,7 +152,8 @@ function lintTask() { 'it', 'jasmine', 'moment', - 'spyOn' + 'spyOn', + 'xit' ] }; diff --git a/test/core.layoutService.tests.js b/test/core.layoutService.tests.js index fcc2b7dcb..951ea234e 100644 --- a/test/core.layoutService.tests.js +++ b/test/core.layoutService.tests.js @@ -1,8 +1,8 @@ // Tests of the scale service describe('Test the layout service', function() { // Disable tests which need to be rewritten based on changes introduced by - // the following changes: https://github.com/chartjs/Chart.js/pull/2346 - // using xit marks the test as pending: http://jasmine.github.io/2.0/introduction.html#section-Pending_Specs + // the following changes: https://github.com/chartjs/Chart.js/pull/2346 + // using xit marks the test as pending: http://jasmine.github.io/2.0/introduction.html#section-Pending_Specs xit('should fit a simple chart with 2 scales', function() { var chart = window.acquireChart({ type: 'bar', @@ -249,7 +249,9 @@ describe('Test the layout service', function() { type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + { + data: [10, 5, 0, 25, 78, -10] + } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -294,7 +296,9 @@ describe('Test the layout service', function() { type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + { + data: [10, 5, 0, 25, 78, -10] + } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, @@ -344,7 +348,9 @@ describe('Test the layout service', function() { type: 'bar', data: { datasets: [ - { data: [10, 5, 0, 25, 78, -10] } + { + data: [10, 5, 0, 25, 78, -10] + } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, From a0388eff4cc38d69872190675bb17d49af841a93 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 15 Oct 2016 17:04:22 -0400 Subject: [PATCH 41/58] Add new properties for the caretX,caretY point of a tooltip. Useful for custom tooltips. The custom tooltip sample was updated as well to use the new properties. --- docs/09-Advanced.md | 2 + samples/line-customTooltips.html | 79 +++++++++++++++++--------------- src/core/core.tooltip.js | 12 ++++- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 5ea957179..db346bc94 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -173,6 +173,8 @@ var myPieChart = new Chart(ctx, { // tooltip.text // tooltip.x // tooltip.y + // tooltip.caretX + // tooltip.caretY // etc... } } diff --git a/samples/line-customTooltips.html b/samples/line-customTooltips.html index 6695cb663..9272caff8 100644 --- a/samples/line-customTooltips.html +++ b/samples/line-customTooltips.html @@ -4,7 +4,6 @@ Line Chart with Custom Tooltips - + + + +
    + +
    +
    +
    + + + + diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index af8d3197c..fca8a581e 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -118,7 +118,9 @@ module.exports = function(Chart) { xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', index: index, - datasetIndex: datasetIndex + datasetIndex: datasetIndex, + x: element._model.x, + y: element._model.y }; } @@ -508,6 +510,9 @@ module.exports = function(Chart) { model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2); model.labelColors = labelColors; + // data points + model.dataPoints = tooltipItems; + // We need to determine alignment of the tooltip tooltipSize = getTooltipSize(this, model); alignment = determineAlignment(this, tooltipSize); diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 29c6b721a..5890e79e0 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -542,4 +542,64 @@ describe('Core.Tooltip', function() { expect(tooltip._view.x).toBeCloseToPixel(269); expect(tooltip._view.y).toBeCloseToPixel(155); }); + + it('Should have dataPoints', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'single' + } + } + }); + + // Trigger an event over top of the + var pointIndex = 1; + var datasetIndex = 0; + var meta = chartInstance.getDatasetMeta(datasetIndex); + var point = meta.data[pointIndex]; + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y + }); + + // Manully trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + + expect(tooltip._view instanceof Object).toBe(true); + expect(tooltip._view.dataPoints instanceof Array).toBe(true); + expect(tooltip._view.dataPoints.length).toEqual(1); + expect(tooltip._view.dataPoints[0].index).toEqual(pointIndex); + expect(tooltip._view.dataPoints[0].datasetIndex).toEqual(datasetIndex); + expect(tooltip._view.dataPoints[0].xLabel).toEqual( + chartInstance.config.data.labels[pointIndex] + ); + expect(tooltip._view.dataPoints[0].yLabel).toEqual( + chartInstance.config.data.datasets[datasetIndex].data[pointIndex] + ); + expect(tooltip._view.dataPoints[0].x).toBeCloseToPixel(point._model.x); + expect(tooltip._view.dataPoints[0].y).toBeCloseToPixel(point._model.y); + }); }); From 5ae268e94236b921d6cc966c580cd4984d5f4f94 Mon Sep 17 00:00:00 2001 From: etimberg Date: Thu, 20 Oct 2016 21:49:13 -0400 Subject: [PATCH 43/58] Add a way to filter items in the tooltip --- docs/01-Chart-Configuration.md | 1 + src/core/core.tooltip.js | 7 ++++ test/core.tooltip.tests.js | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index bf9eb5baa..912d826eb 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -225,6 +225,7 @@ mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Inte intersect | Boolean | true | if true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. position | String | 'average' | The mode for positioning the tooltip. 'average' mode will place the tooltip at the average position of the items displayed in the tooltip. 'nearest' will place the tooltip at the position of the element closest to the event position. New modes can be defined by adding functions to the Chart.Tooltip.positioners map. itemSort | Function | undefined | Allows sorting of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. +filter | Function | undefined | Allows filtering of [tooltip items](#chart-configuration-tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.filter](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). This function can also accept a second parameter that is the data object passed to the chart. backgroundColor | Color | 'rgba(0,0,0,0.8)' | Background color of the tooltip titleFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip title inherited from global font family titleFontSize | Number | 12 | Font size for tooltip title inherited from global font size diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index fca8a581e..ecf7ce646 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -485,6 +485,13 @@ module.exports = function(Chart) { tooltipItems.push(createTooltipItem(active[i])); } + // If the user provided a filter function, use it to modify the tooltip items + if (opts.filter) { + tooltipItems = tooltipItems.filter(function(a) { + return opts.filter(a, data); + }); + } + // If the user provided a sorting function, use it to modify the tooltip items if (opts.itemSort) { tooltipItems = tooltipItems.sort(function(a, b) { diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 5890e79e0..0c13707ae 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -543,6 +543,78 @@ describe('Core.Tooltip', function() { expect(tooltip._view.y).toBeCloseToPixel(155); }); + it('should filter items from the tooltip using the callback', function() { + var chartInstance = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + tooltipHidden: true + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + tooltips: { + mode: 'label', + filter: function(tooltipItem, data) { + // For testing purposes remove the first dataset that has a tooltipHidden property + return !data.datasets[tooltipItem.datasetIndex].tooltipHidden; + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var point0 = meta0.data[1]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point0._model.x, + clientY: rect.top + point0._model.y + }); + + // Manully trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chartInstance.tooltip; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + }); + it('Should have dataPoints', function() { var chartInstance = window.acquireChart({ type: 'line', From 9ad9cd215466e3abb82731f6550d4df80bbc3a84 Mon Sep 17 00:00:00 2001 From: etimberg Date: Thu, 20 Oct 2016 23:12:10 -0400 Subject: [PATCH 44/58] Reorganized sample files into sub directories. Added a helper containing colours that should be used by all samples. I added new samples to explain behaviour and modified all samples to have consistent styling. In updating the samples, I removed the use of jQuery and instead use standard methods. For the custom tooltip samples, I updated the styling to show color boxes like the regular tooltips. --- samples/AnimationCallbacks/progress-bar.html | 128 +++------- samples/{ => bar}/bar-horizontal.html | 62 +++-- samples/{ => bar}/bar-multi-axis.html | 72 +++--- samples/{ => bar}/bar-stacked.html | 56 +++-- samples/{ => bar}/bar.html | 67 +++--- samples/bubble.html | 42 ++-- samples/chartColors.js | 9 + samples/combo-bar-line.html | 67 ++++-- samples/dataPoints-customTooltips.html | 104 -------- samples/data_label_combo-bar-line.html | 95 -------- samples/data_labelling.html | 136 +++++++++++ samples/different-point-sizes.html | 154 ------------ samples/doughnut.html | 88 ++----- samples/generalScales/display-settings.html | 124 ++++++++++ samples/generalScales/filtering-labels.html | 93 ++++++++ samples/generalScales/gridlines.html | 69 ++++++ .../line-non-numeric-y.html | 17 +- samples/generalScales/multiline-labels.html | 86 +++++++ samples/legend/pointstyle.html | 120 ++++++++++ samples/legend/positions.html | 125 ++++++++++ samples/line-customTooltips.html | 146 ------------ samples/line-legend.html | 172 -------------- samples/line-logarithmic.html | 155 ------------ samples/line-multiline-labels.html | 218 ----------------- samples/line-stacked-area.html | 163 ------------- samples/line-stepped.html | 224 ------------------ samples/line-x-axis-filter.html | 151 ------------ samples/line.html | 220 ----------------- samples/line/different-point-sizes.html | 134 +++++++++++ .../interpolation-modes.html} | 22 +- samples/{ => line}/line-multi-axis.html | 59 +++-- samples/line/line-skip-points.html | 99 ++++++++ samples/line/line-stacked-area.html | 188 +++++++++++++++ samples/line/line-stepped.html | 96 ++++++++ samples/line/line-styles.html | 115 +++++++++ .../{line-skip-points.html => line/line.html} | 88 +++---- samples/line/point-styles.html | 92 +++++++ samples/linearScale/min-max-settings.html | 64 +++++ samples/linearScale/step-size.html | 116 +++++++++ .../suggested-min-max-settings.html | 67 ++++++ .../logarithmicScale/line-logarithmic.html | 97 ++++++++ .../{ => logarithmicScale}/scatter-logX.html | 26 +- samples/pie-customTooltips.html | 156 ------------ samples/pie.html | 87 +++---- samples/polar-area.html | 56 ++--- samples/radar-skip-points.html | 143 ----------- samples/radar/radar-skip-points.html | 109 +++++++++ samples/{ => radar}/radar.html | 79 +++--- samples/scatter-multi-axis.html | 187 --------------- samples/scatter.html | 177 -------------- samples/scatter/scatter-multi-axis.html | 143 +++++++++++ samples/scatter/scatter.html | 111 +++++++++ samples/timeScale/combo-time-scale.html | 102 ++++---- samples/timeScale/line-time-point-data.html | 41 ++-- samples/timeScale/line-time-scale.html | 89 ++++--- samples/tooltip-hooks.html | 189 --------------- .../tooltips/dataPoints-customTooltips.html | 128 ++++++++++ samples/tooltips/interaction-modes.html | 119 ++++++++++ samples/tooltips/line-customTooltips.html | 168 +++++++++++++ samples/tooltips/pie-customTooltips.html | 146 ++++++++++++ samples/tooltips/position-modes.html | 86 +++++++ samples/tooltips/tooltip-callbacks.html | 111 +++++++++ 62 files changed, 3552 insertions(+), 3301 deletions(-) rename samples/{ => bar}/bar-horizontal.html (66%) rename samples/{ => bar}/bar-multi-axis.html (50%) rename samples/{ => bar}/bar-stacked.html (50%) rename samples/{ => bar}/bar.html (63%) create mode 100644 samples/chartColors.js delete mode 100644 samples/dataPoints-customTooltips.html delete mode 100644 samples/data_label_combo-bar-line.html create mode 100644 samples/data_labelling.html delete mode 100644 samples/different-point-sizes.html create mode 100644 samples/generalScales/display-settings.html create mode 100644 samples/generalScales/filtering-labels.html create mode 100644 samples/generalScales/gridlines.html rename samples/{ => generalScales}/line-non-numeric-y.html (81%) create mode 100644 samples/generalScales/multiline-labels.html create mode 100644 samples/legend/pointstyle.html create mode 100644 samples/legend/positions.html delete mode 100644 samples/line-customTooltips.html delete mode 100644 samples/line-legend.html delete mode 100644 samples/line-logarithmic.html delete mode 100644 samples/line-multiline-labels.html delete mode 100644 samples/line-stacked-area.html delete mode 100644 samples/line-stepped.html delete mode 100644 samples/line-x-axis-filter.html delete mode 100644 samples/line.html create mode 100644 samples/line/different-point-sizes.html rename samples/{line-cubicInterpolationMode.html => line/interpolation-modes.html} (78%) rename samples/{ => line}/line-multi-axis.html (56%) create mode 100644 samples/line/line-skip-points.html create mode 100644 samples/line/line-stacked-area.html create mode 100644 samples/line/line-stepped.html create mode 100644 samples/line/line-styles.html rename samples/{line-skip-points.html => line/line.html} (60%) create mode 100644 samples/line/point-styles.html create mode 100644 samples/linearScale/min-max-settings.html create mode 100644 samples/linearScale/step-size.html create mode 100644 samples/linearScale/suggested-min-max-settings.html create mode 100644 samples/logarithmicScale/line-logarithmic.html rename samples/{ => logarithmicScale}/scatter-logX.html (76%) delete mode 100644 samples/pie-customTooltips.html delete mode 100644 samples/radar-skip-points.html create mode 100644 samples/radar/radar-skip-points.html rename samples/{ => radar}/radar.html (53%) delete mode 100644 samples/scatter-multi-axis.html delete mode 100644 samples/scatter.html create mode 100644 samples/scatter/scatter-multi-axis.html create mode 100644 samples/scatter/scatter.html delete mode 100644 samples/tooltip-hooks.html create mode 100644 samples/tooltips/dataPoints-customTooltips.html create mode 100644 samples/tooltips/interaction-modes.html create mode 100644 samples/tooltips/line-customTooltips.html create mode 100644 samples/tooltips/pie-customTooltips.html create mode 100644 samples/tooltips/position-modes.html create mode 100644 samples/tooltips/tooltip-callbacks.html diff --git a/samples/AnimationCallbacks/progress-bar.html b/samples/AnimationCallbacks/progress-bar.html index fad7bd489..ddfc12e3c 100644 --- a/samples/AnimationCallbacks/progress-bar.html +++ b/samples/AnimationCallbacks/progress-bar.html @@ -3,7 +3,7 @@ Animation Callbacks - + - - - -
    - -
    -
    -
    - - - - diff --git a/samples/data_label_combo-bar-line.html b/samples/data_label_combo-bar-line.html deleted file mode 100644 index a5e1e8d37..000000000 --- a/samples/data_label_combo-bar-line.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - Combo Bar-Line Chart - - - - - - -
    - -
    - - - - - diff --git a/samples/data_labelling.html b/samples/data_labelling.html new file mode 100644 index 000000000..849de80d3 --- /dev/null +++ b/samples/data_labelling.html @@ -0,0 +1,136 @@ + + + + + + Labelling Data Points + + + + + + +
    + +
    + + + + + diff --git a/samples/different-point-sizes.html b/samples/different-point-sizes.html deleted file mode 100644 index 0fb231f52..000000000 --- a/samples/different-point-sizes.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - diff --git a/samples/doughnut.html b/samples/doughnut.html index 38a9d5782..e2fd662d8 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -4,7 +4,7 @@ Doughnut Chart - + + + + +
    + + + + diff --git a/samples/generalScales/filtering-labels.html b/samples/generalScales/filtering-labels.html new file mode 100644 index 000000000..21cb0cc89 --- /dev/null +++ b/samples/generalScales/filtering-labels.html @@ -0,0 +1,93 @@ + + + + + Chart with xAxis Filtering + + + + + + +
    + +
    + + + + diff --git a/samples/generalScales/gridlines.html b/samples/generalScales/gridlines.html new file mode 100644 index 000000000..e133e4e8d --- /dev/null +++ b/samples/generalScales/gridlines.html @@ -0,0 +1,69 @@ + + + + + Suggested Min/Max Settings + + + + + + +
    + +
    + + + + diff --git a/samples/line-non-numeric-y.html b/samples/generalScales/line-non-numeric-y.html similarity index 81% rename from samples/line-non-numeric-y.html rename to samples/generalScales/line-non-numeric-y.html index c0b74a150..3ec760e92 100644 --- a/samples/line-non-numeric-y.html +++ b/samples/generalScales/line-non-numeric-y.html @@ -3,8 +3,8 @@ Line Chart - - + + + + + +
    + +
    + + + + diff --git a/samples/legend/pointstyle.html b/samples/legend/pointstyle.html new file mode 100644 index 000000000..e90b63def --- /dev/null +++ b/samples/legend/pointstyle.html @@ -0,0 +1,120 @@ + + + + + Legend Point Style + + + + + + +
    +
    + +
    +
    + +
    +
    + + + + diff --git a/samples/legend/positions.html b/samples/legend/positions.html new file mode 100644 index 000000000..f1edd7c6f --- /dev/null +++ b/samples/legend/positions.html @@ -0,0 +1,125 @@ + + + + + Legend Positions + + + + + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + + + diff --git a/samples/line-customTooltips.html b/samples/line-customTooltips.html deleted file mode 100644 index 9272caff8..000000000 --- a/samples/line-customTooltips.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - Line Chart with Custom Tooltips - - - - - -
    - -
    - - - - diff --git a/samples/line-legend.html b/samples/line-legend.html deleted file mode 100644 index 201d4c07e..000000000 --- a/samples/line-legend.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - - - diff --git a/samples/line-logarithmic.html b/samples/line-logarithmic.html deleted file mode 100644 index 290ea43ea..000000000 --- a/samples/line-logarithmic.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    - - - - - - - - - diff --git a/samples/line-multiline-labels.html b/samples/line-multiline-labels.html deleted file mode 100644 index b9d08aee7..000000000 --- a/samples/line-multiline-labels.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - - - - diff --git a/samples/line-stacked-area.html b/samples/line-stacked-area.html deleted file mode 100644 index 26616f5de..000000000 --- a/samples/line-stacked-area.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - - - diff --git a/samples/line-stepped.html b/samples/line-stepped.html deleted file mode 100644 index f6cebe262..000000000 --- a/samples/line-stepped.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - - - - diff --git a/samples/line-x-axis-filter.html b/samples/line-x-axis-filter.html deleted file mode 100644 index e90575df5..000000000 --- a/samples/line-x-axis-filter.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - Chart with xAxis Filtering - - - - - - -
    - -
    -
    -
    - - - - - - - - - diff --git a/samples/line.html b/samples/line.html deleted file mode 100644 index 464072ba1..000000000 --- a/samples/line.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - Line Chart - - - - - - -
    - -
    -
    -
    - - - - - - - - - - diff --git a/samples/line/different-point-sizes.html b/samples/line/different-point-sizes.html new file mode 100644 index 000000000..3137367f7 --- /dev/null +++ b/samples/line/different-point-sizes.html @@ -0,0 +1,134 @@ + + + + + Different Point Sizes + + + + + + +
    + +
    + + + + diff --git a/samples/line-cubicInterpolationMode.html b/samples/line/interpolation-modes.html similarity index 78% rename from samples/line-cubicInterpolationMode.html rename to samples/line/interpolation-modes.html index 2d85c95c9..51469c933 100644 --- a/samples/line-cubicInterpolationMode.html +++ b/samples/line/interpolation-modes.html @@ -3,8 +3,8 @@ Line Chart - Cubic interpolation mode - - + + + + + +
    + +
    + + + + diff --git a/samples/line/line-stacked-area.html b/samples/line/line-stacked-area.html new file mode 100644 index 000000000..a194bba3b --- /dev/null +++ b/samples/line/line-stacked-area.html @@ -0,0 +1,188 @@ + + + + + Line Chart + + + + + + +
    + +
    +
    +
    + + + + + + + + + diff --git a/samples/line/line-stepped.html b/samples/line/line-stepped.html new file mode 100644 index 000000000..94cc61520 --- /dev/null +++ b/samples/line/line-stepped.html @@ -0,0 +1,96 @@ + + + + + Stepped Line Chart + + + + + + +
    + +
    + + + + diff --git a/samples/line/line-styles.html b/samples/line/line-styles.html new file mode 100644 index 000000000..a2c06f935 --- /dev/null +++ b/samples/line/line-styles.html @@ -0,0 +1,115 @@ + + + + + Line Styles + + + + + + +
    + +
    + + + + diff --git a/samples/line-skip-points.html b/samples/line/line.html similarity index 60% rename from samples/line-skip-points.html rename to samples/line/line.html index 3ef275587..980feb747 100644 --- a/samples/line-skip-points.html +++ b/samples/line/line.html @@ -3,10 +3,10 @@ Line Chart - - + + + + + +
    +
    + + + + diff --git a/samples/linearScale/min-max-settings.html b/samples/linearScale/min-max-settings.html new file mode 100644 index 000000000..529eb481f --- /dev/null +++ b/samples/linearScale/min-max-settings.html @@ -0,0 +1,64 @@ + + + + + Min/Max Settings + + + + + + +
    + +
    + + + + diff --git a/samples/linearScale/step-size.html b/samples/linearScale/step-size.html new file mode 100644 index 000000000..c65cc9ab4 --- /dev/null +++ b/samples/linearScale/step-size.html @@ -0,0 +1,116 @@ + + + + + Line Chart + + + + + + +
    + +
    +
    +
    + + + + + + + + + diff --git a/samples/linearScale/suggested-min-max-settings.html b/samples/linearScale/suggested-min-max-settings.html new file mode 100644 index 000000000..54396f92e --- /dev/null +++ b/samples/linearScale/suggested-min-max-settings.html @@ -0,0 +1,67 @@ + + + + + Suggested Min/Max Settings + + + + + + +
    + +
    + + + + diff --git a/samples/logarithmicScale/line-logarithmic.html b/samples/logarithmicScale/line-logarithmic.html new file mode 100644 index 000000000..fe3d5b110 --- /dev/null +++ b/samples/logarithmicScale/line-logarithmic.html @@ -0,0 +1,97 @@ + + + + + Logarithmic Line Chart + + + + + + +
    + +
    + + + + + diff --git a/samples/scatter-logX.html b/samples/logarithmicScale/scatter-logX.html similarity index 76% rename from samples/scatter-logX.html rename to samples/logarithmicScale/scatter-logX.html index 8c7fe2ad0..6864b4090 100644 --- a/samples/scatter-logX.html +++ b/samples/logarithmicScale/scatter-logX.html @@ -3,8 +3,8 @@ Scatter Chart - - + + - - - -
    - -
    -
    - -
    - -
    - - - - - - diff --git a/samples/pie.html b/samples/pie.html index b376b5abd..2b1578536 100644 --- a/samples/pie.html +++ b/samples/pie.html @@ -4,12 +4,12 @@ Pie Chart - + -
    - +
    +
    @@ -18,12 +18,6 @@ var randomScalingFactor = function() { return Math.round(Math.random() * 100); }; - var randomColorFactor = function() { - return Math.round(Math.random() * 255); - }; - var randomColor = function(opacity) { - return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')'; - }; var config = { type: 'pie', @@ -37,49 +31,20 @@ randomScalingFactor(), ], backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", - ], - }, { - data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - ], - backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", - ], - }, { - data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - ], - backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", + window.chartColors.red, + window.chartColors.orange, + window.chartColors.yellow, + window.chartColors.green, + window.chartColors.blue, ], + label: 'Dataset 1' }], labels: [ "Red", - "Green", + "Orange", "Yellow", - "Grey", - "Dark Grey" + "Green", + "Blue" ] }, options: { @@ -92,27 +57,37 @@ window.myPie = new Chart(ctx, config); }; - $('#randomizeData').click(function() { - $.each(config.data.datasets, function(i, piece) { - $.each(piece.data, function(j, value) { - config.data.datasets[i].data[j] = randomScalingFactor(); - config.data.datasets[i].backgroundColor[j] = randomColor(0.7); + document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); }); }); + window.myPie.update(); }); - $('#addDataset').click(function() { + var colorNames = Object.keys(window.chartColors); + document.getElementById('addDataset').addEventListener('click', function() { var newDataset = { - backgroundColor: [randomColor(0.7), randomColor(0.7), randomColor(0.7), randomColor(0.7), randomColor(0.7)], - data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()] + backgroundColor: [], + data: [], + label: 'New dataset ' + config.data.datasets.length, }; + for (var index = 0; index < config.data.labels.length; ++index) { + newDataset.data.push(randomScalingFactor()); + + var colorName = colorNames[index % colorNames.length];; + var newColor = window.chartColors[colorName]; + newDataset.backgroundColor.push(newColor); + } + config.data.datasets.push(newDataset); window.myPie.update(); }); - $('#removeDataset').click(function() { + document.getElementById('removeDataset').addEventListener('click', function() { config.data.datasets.splice(0, 1); window.myPie.update(); }); diff --git a/samples/polar-area.html b/samples/polar-area.html index 16572dd45..0b382c484 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -4,7 +4,7 @@ Polar Area Chart - + - - - -
    - -
    - - - - - - - - - diff --git a/samples/radar/radar-skip-points.html b/samples/radar/radar-skip-points.html new file mode 100644 index 000000000..40ad4779e --- /dev/null +++ b/samples/radar/radar-skip-points.html @@ -0,0 +1,109 @@ + + + + + Radar Chart + + + + + + +
    + +
    + + + + + diff --git a/samples/radar.html b/samples/radar/radar.html similarity index 53% rename from samples/radar.html rename to samples/radar/radar.html index eadcd9eca..98bc7086d 100644 --- a/samples/radar.html +++ b/samples/radar/radar.html @@ -3,8 +3,8 @@ Radar Chart - - + + - - - -
    -
    - -
    -
    - - - - - diff --git a/samples/scatter.html b/samples/scatter.html deleted file mode 100644 index af026e7b1..000000000 --- a/samples/scatter.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - Scatter Chart - - - - - - -
    -
    - -
    -
    - - - - - diff --git a/samples/scatter/scatter-multi-axis.html b/samples/scatter/scatter-multi-axis.html new file mode 100644 index 000000000..96edac994 --- /dev/null +++ b/samples/scatter/scatter-multi-axis.html @@ -0,0 +1,143 @@ + + + + + Scatter Chart Multi Axis + + + + + + +
    + +
    + + + + + diff --git a/samples/scatter/scatter.html b/samples/scatter/scatter.html new file mode 100644 index 000000000..c5de6bae0 --- /dev/null +++ b/samples/scatter/scatter.html @@ -0,0 +1,111 @@ + + + + + Scatter Chart + + + + + + +
    + +
    + + + + + diff --git a/samples/timeScale/combo-time-scale.html b/samples/timeScale/combo-time-scale.html index f6c178a7d..23a5f777e 100644 --- a/samples/timeScale/combo-time-scale.html +++ b/samples/timeScale/combo-time-scale.html @@ -4,8 +4,8 @@ Line Chart - Combo Time Scale - - + + - - - -
    - -
    -
    -
    - - - - - - - - - diff --git a/samples/tooltips/dataPoints-customTooltips.html b/samples/tooltips/dataPoints-customTooltips.html new file mode 100644 index 000000000..1ae7f90ab --- /dev/null +++ b/samples/tooltips/dataPoints-customTooltips.html @@ -0,0 +1,128 @@ + + + + + Custom Tooltips using Data Points + + + + + + + +
    + +
    +
    +
    + + + + diff --git a/samples/tooltips/interaction-modes.html b/samples/tooltips/interaction-modes.html new file mode 100644 index 000000000..bf9bf971a --- /dev/null +++ b/samples/tooltips/interaction-modes.html @@ -0,0 +1,119 @@ + + + + + Tooltip Interaction Modes + + + + + + +
    +
    + + + + diff --git a/samples/tooltips/line-customTooltips.html b/samples/tooltips/line-customTooltips.html new file mode 100644 index 000000000..cc6e356b3 --- /dev/null +++ b/samples/tooltips/line-customTooltips.html @@ -0,0 +1,168 @@ + + + + + Line Chart with Custom Tooltips + + + + + + +
    + +
    + + + + diff --git a/samples/tooltips/pie-customTooltips.html b/samples/tooltips/pie-customTooltips.html new file mode 100644 index 000000000..abc047bfd --- /dev/null +++ b/samples/tooltips/pie-customTooltips.html @@ -0,0 +1,146 @@ + + + + + Pie Chart with Custom Tooltips + + + + + + + +
    + +
    + +
    +
    +
    + + + + + diff --git a/samples/tooltips/position-modes.html b/samples/tooltips/position-modes.html new file mode 100644 index 000000000..596eb6dfe --- /dev/null +++ b/samples/tooltips/position-modes.html @@ -0,0 +1,86 @@ + + + + + Tooltip Interaction Modes + + + + + + +
    +
    + + + + diff --git a/samples/tooltips/tooltip-callbacks.html b/samples/tooltips/tooltip-callbacks.html new file mode 100644 index 000000000..397348b01 --- /dev/null +++ b/samples/tooltips/tooltip-callbacks.html @@ -0,0 +1,111 @@ + + + + + Tooltip Hooks + + + + + + +
    + +
    + + + + From 274aa290ca69f38aac5fd1a5fb55cc5043ea5d8c Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 22 Oct 2016 23:12:39 -0400 Subject: [PATCH 45/58] Fix bug in 'y' tooltip mode --- src/core/core.interaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 2e680bea5..2d7b99597 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -260,7 +260,7 @@ module.exports = function(Chart) { var position = helpers.getRelativePosition(e, chart.chart); var items = []; parseVisibleItems(chart, function(element) { - if (element.inYRange(position.x)) { + if (element.inYRange(position.y)) { items.push(element); } }); From 80bd08bef9f3447738c8fde4c0d0b436312a2cb9 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 23 Oct 2016 14:04:00 -0400 Subject: [PATCH 46/58] Update chartColors.js to utils.js and move randomScalingFactor function there --- .../progress-bar.html | 8 +------- samples/bar/bar-horizontal.html | 14 +------------- samples/bar/bar-multi-axis.html | 6 +----- samples/bar/bar-stacked.html | 6 +----- samples/bar/bar.html | 13 +------------ samples/bubble.html | 7 +------ samples/combo-bar-line.html | 6 +----- samples/data_labelling.html | 6 +----- samples/doughnut.html | 2 +- samples/legend/pointstyle.html | 6 +----- samples/legend/positions.html | 6 +----- samples/line/different-point-sizes.html | 6 +----- samples/line/interpolation-modes.html | 2 +- samples/line/line-multi-axis.html | 6 +----- samples/line/line-skip-points.html | 6 +----- samples/line/line-stacked-area.html | 7 +------ samples/line/line-stepped.html | 5 +---- samples/line/line-styles.html | 6 +----- samples/line/line.html | 7 +------ samples/line/point-styles.html | 5 ++++- samples/pie.html | 2 +- samples/polar-area.html | 2 +- samples/radar/radar-skip-points.html | 2 +- samples/radar/radar.html | 2 +- .../display-settings.html | 2 +- .../filtering-labels.html | 2 +- samples/{generalScales => scales}/gridlines.html | 2 +- .../line-non-numeric-y.html | 14 +------------- .../linear}/min-max-settings.html | 4 ++-- .../{linearScale => scales/linear}/step-size.html | 4 ++-- .../linear}/suggested-min-max-settings.html | 4 ++-- .../logarithmic}/line-logarithmic.html | 4 ++-- .../logarithmic}/scatter-logX.html | 4 ++-- .../multiline-labels.html | 2 +- .../time}/combo-time-scale.html | 8 ++------ .../time}/line-time-point-data.html | 8 ++------ .../time}/line-time-scale.html | 10 +++------- samples/scatter/scatter-multi-axis.html | 6 +----- samples/scatter/scatter.html | 6 +----- samples/tooltips/dataPoints-customTooltips.html | 5 +---- samples/tooltips/interaction-modes.html | 2 +- samples/tooltips/line-customTooltips.html | 7 ++----- samples/tooltips/pie-customTooltips.html | 2 +- samples/tooltips/position-modes.html | 2 +- samples/tooltips/tooltip-callbacks.html | 6 +----- samples/{chartColors.js => utils.js} | 4 ++++ 46 files changed, 62 insertions(+), 184 deletions(-) rename samples/{AnimationCallbacks => animation}/progress-bar.html (95%) rename samples/{generalScales => scales}/display-settings.html (98%) rename samples/{generalScales => scales}/filtering-labels.html (98%) rename samples/{generalScales => scales}/gridlines.html (97%) rename samples/{generalScales => scales}/line-non-numeric-y.html (82%) rename samples/{linearScale => scales/linear}/min-max-settings.html (94%) rename samples/{linearScale => scales/linear}/step-size.html (97%) rename samples/{linearScale => scales/linear}/suggested-min-max-settings.html (95%) rename samples/{logarithmicScale => scales/logarithmic}/line-logarithmic.html (96%) rename samples/{logarithmicScale => scales/logarithmic}/scatter-logX.html (96%) rename samples/{generalScales => scales}/multiline-labels.html (98%) rename samples/{timeScale => scales/time}/combo-time-scale.html (95%) rename samples/{timeScale => scales/time}/line-time-point-data.html (94%) rename samples/{timeScale => scales/time}/line-time-scale.html (96%) rename samples/{chartColors.js => utils.js} (64%) diff --git a/samples/AnimationCallbacks/progress-bar.html b/samples/animation/progress-bar.html similarity index 95% rename from samples/AnimationCallbacks/progress-bar.html rename to samples/animation/progress-bar.html index ddfc12e3c..b0c495a02 100644 --- a/samples/AnimationCallbacks/progress-bar.html +++ b/samples/animation/progress-bar.html @@ -3,7 +3,7 @@ Animation Callbacks - +