From 88c69f5f8c842052c27347ca1a0e7ef00a69402c Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Fri, 23 Sep 2016 00:12:40 +0000 Subject: [PATCH 001/685] 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 43e762ba408..8d81d06a683 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 002/685] 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 | 52 ++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 5364d191f8e..8609acdd09d 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'; - - if (hiddenIframe.classlist) { - // can use classlist - hiddenIframe.classlist.add(hiddenIframeClass); - } else { - hiddenIframe.setAttribute('class', hiddenIframeClass); - } - - // 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; + var iframe = document.createElement('iframe'); + + 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;'; + + // 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 003/685] 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 b3dab98d11a..f4a25cf2ff8 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 e901b26c3f1..4d2bb804288 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 004/685] 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 | 198 ++++++++++++++++++++++++++++++------ src/core/core.helpers.js | 3 - src/core/core.js | 46 +-------- 3 files changed, 170 insertions(+), 77 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 30ba1341888..1d169adeac2 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; + 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 8609acdd09d..e404b61b1cd 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 577db878e16..d2899783816 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 005/685] 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 73c96ad802d..38a9d578234 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 ae21e1d44ab..16572dd4514 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 9d0816d4b47..c1680afd0c9 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 4203cff1554..eadcd9ecab8 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 1648e9faca6..d17bd5d8108 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 3c050595a83..6232165f38b 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 006/685] 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 8d81d06a683..dd16e6d2c93 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 007/685] 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 0a8c1a5aa54..eb9c4fc0499 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 008/685] 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 ce1c437ea9e..b8ecd18e5cb 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 009/685] 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 00000000000..83eba3baf4d --- /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 9590b53608c..48e5ce30406 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 010/685] 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 e82c06ea3ed..3afbabcf3e6 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 011/685] 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 5edfd0075f8..d0068ee674d 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 012/685] 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 c776355ebc7..313cec15a7c 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 013/685] 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 123da31841d..808e18e5019 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 659ccc727cd..3df77325848 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 be6350341e9..221e61c4bd2 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 61b6d968917..dea9b1b0354 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 014/685] 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 e0f378daed5..3dcd33e9f63 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 015/685] Update default config tests and re-enable --- gulpfile.js | 1 - test/defaultConfig.tests.js | 559 ++++++++++++++++++------------------ 2 files changed, 281 insertions(+), 279 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b3dab98d11a..26ee0bd04cd 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 0123f420f9f..fb34d33ec2c 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; - - // Title is always blank - expect(config.tooltips.callbacks.title()).toBe(''); - - // 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("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 + }); + + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[0]]; + chart.tooltip.update(); + + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['My dataset: (10, 12, 5)'], + after: [] + }]); + }); }); -}); - -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'); - }); - - it('should return the correct html legend', function() { - var config = Chart.defaults.doughnut; - - 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.doughnut; - 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: { + 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 + }); + + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); + + // 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 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 + }); + + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); + + // 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); - }); - - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.doughnut; - - 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 - }; + }] + }, + 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 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); + + 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' + }] + }, + options: config + }); + var meta = chart.getDatasetMeta(0); - expect(config.tooltips.callbacks.label(tooltipItem, data)).toBe('label2: 20'); - }); + spyOn(chart, 'update').and.callThrough(); - it('should return the correct html legend', function() { - var config = Chart.defaults.polarArea; + var legendItem = chart.legend.legendItems[0]; + config.legend.onClick.call(chart.legend, null, legendItem); - var chart = { - id: 'mychart', - data: { - labels: ['label1', 'label2'], - datasets: [{ - data: [10, 20], - backgroundColor: ['red', 'green'] - }] - } - }; - var expectedLegend = '
  • label1
  • label2
'; + expect(meta.data[0].hidden).toBe(true); + expect(chart.update).toHaveBeenCalled(); - expect(config.legendCallback(chart)).toBe(expectedLegend); + config.legend.onClick.call(chart.legend, null, legendItem); + expect(meta.data[0].hidden).toBe(false); + }); }); - 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: { + 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 + }); + + // fake out the tooltip hover and force the tooltip to update + chart.tooltip._active = [chart.getDatasetMeta(0).data[1]]; + chart.tooltip.update(); + + // Title is always blank + expect(chart.tooltip._model.title).toEqual([]); + expect(chart.tooltip._model.body).toEqual([{ + before: [], + lines: ['label2: 20'], + after: [] + }]); + }); + + 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 + }); + + var expectedLegend = '
  • label1
  • label2
'; + expect(chart.generateLegend()).toBe(expectedLegend); + }); + + 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' - } - } - } - }; - expect(config.legend.labels.generateLabels.call({ chart: chart }, data)).toEqual(expected); - }); - - it('should hide the correct arc when a legend item is clicked', function() { - var config = Chart.defaults.polarArea; - - 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]); + }] + }, + 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 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); + + 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(); + + 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 016/685] 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 a12890aa06b..aa0d46c45ce 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 808e18e5019..890e04c42e3 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 00000000000..f55b8d40e0a --- /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 f2700a3a4ae..e8afa84f8f1 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 37caa035933..b8bc5c979e1 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 50417528a5b..a23f2eed413 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 361ad3cbab1..cc7c6557d74 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 017/685] 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 b27daf4170d..78234ea59d4 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 fb34d33ec2c..84c9655e6c7 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 018/685] 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 | 29 +++++++++++++++-------------- src/core/core.helpers.js | 17 +++++++++++++---- test/core.controller.tests.js | 32 +++++++++++++++++++++++++++++++- test/mockContext.js | 8 ++++---- 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 1d169adeac2..e335c3a3a7c 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) { + // 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) { + helpers.addResizeListener(canvas.parentNode, function() { me.resize(); - } - }); - - // Add the chart instance to the global namespace - Chart.instances[me.id] = me; + }); - if (me.options.responsive) { - // Silent resize before chart draws + // 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 e404b61b1cd..6d63284a709 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'); + if (!node || !node._chartjs) { + return; + } - // Remove the resize detect iframe - if (hiddenIframe) { - hiddenIframe.parentNode.removeChild(hiddenIframe); + 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 83eba3baf4d..37661b70730 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 48e5ce30406..83b73e31501 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 019/685] 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 e335c3a3a7c..52f24d2ce51 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 37661b70730..d7548d4ce1f 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 020/685] 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 | 187 ++++++++++++++++++++++---- test/core.datasetController.tests.js | 188 +++++++++++++++++++++++++++ 5 files changed, 363 insertions(+), 43 deletions(-) create mode 100644 test/core.datasetController.tests.js diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index bfc061af22b..7c36396bb01 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 6232165f38b..cd62da4768e 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 52f24d2ce51..b2928c6b863 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 29b1aacea35..2faff1b6e9c 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); + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // 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 021/685] 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 | 92 ++- 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, 1118 insertions(+), 161 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 dd16e6d2c93..5ed712a97cd 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 7d79d6c1d8c..fad7bd48934 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 9c8ec3642b7..ee027aaf8d3 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 e85a72b26bc..685af83db78 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 926eecfba8b..0fb231f5232 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 97dac46ca43..2d85c95c930 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 92e5e5b5ab4..201d4c07e51 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 03da24bdabb..0b296ff69ed 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 3fd0de5e142..b9d08aee7a3 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 2d760d2e1f6..3ef27558755 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 88f14e2bd81..26616f5dea8 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 e618a698cfa..f6cebe26271 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 ffca9df3b1e..464072ba1db 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 43d27e50228..842f8891a4b 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 88a660515fe..08055ece8ca 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 aa0d46c45ce..2c5e628264c 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 d9fa7bfeb05..f828eb79e91 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 b2928c6b863..9712d8fb627 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 6d63284a709..d3fd727419d 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 00000000000..571f3c3c018 --- /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 d2899783816..319219f5d20 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 d0068ee674d..9210ed529ac 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 b114e6f04b7..a61d19a3a5e 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 474ef252f53..fe3a6696bd1 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 6752228c870..3f4ea10f907 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 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, 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 ? - (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 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}; }, - inLabelRange: function(mouseX) { + getArea: function() { var vm = this._view; - return vm ? (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) : false; + 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 00000000000..312ac8fe663 --- /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 4ba8545754c..7ce70517d24 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 c257f7375ca..9cbac8ea560 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 8c28970b1c3..bd32ff3ac23 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 022/685] 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 5ed712a97cd..1aeaab940fb 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 9210ed529ac..1c9a1fdb1bb 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 21d25ebaa16..9927debc01b 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 023/685] 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 4d2bb804288..1e16185de7b 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 024/685] 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 f55b8d40e0a..78ec07c3b94 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 3df77325848..4eb585a7b66 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 025/685] 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 1c9a1fdb1bb..54f9f49f5f7 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 026/685] 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 b1c6474e0cc..abe807b3bb2 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 027/685] 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 54f9f49f5f7..8a0afe3dce0 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 028/685] 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 | 530 ++++++++++++++++++++---------------- 2 files changed, 294 insertions(+), 242 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 9712d8fb627..6302dd11cef 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 8a0afe3dce0..dcb9e89d31e 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 - }); - - // We need to determine alignment of - var tooltipSize = me.getTooltipSize(model); - me.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas - - helpers.extend(model, me.getBackgroundPoint(model, tooltipSize)); + 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); + + // 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; + + // 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 029/685] 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 85cc6507afe..fd2125e171f 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 030/685] 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 d3fd727419d..2cafdcb6ca2 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 8ca5bb59cb4..3d431606234 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 031/685] 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 426edad8dc9..7d57bbd0c0c 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 032/685] 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 1aeaab940fb..ef0a5b57f46 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 d81e7861cd5..b63ece78c90 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 38a56fe3e97..0f495c9d1fa 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 033/685] Bar chart performance improvements --- src/controllers/controller.bar.js | 222 ++++++++++++++--------------- src/core/core.datasetController.js | 8 +- src/core/core.scale.js | 9 +- src/elements/element.rectangle.js | 7 +- 4 files changed, 119 insertions(+), 127 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index f828eb79e91..e9a85aabd22 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, - - // Desired view properties - _model: { - x: me.calculateBarX(index, me.index), - y: reset ? scaleBase : me.calculateBarY(index, me.index), - - // Tooltip - label: me.chart.data.labels[index], - datasetLabel: dataset.label, - - // 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._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = 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, + + // 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) + }; + 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, - - // Desired view properties - _model: { - x: reset ? scaleBase : me.calculateBarX(index, me.index), - y: me.calculateBarY(index, me.index), - - // 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) - }, + rectangle._xScale = xScale; + rectangle._yScale = yScale; + rectangle._datasetIndex = me.index; + rectangle._index = index; - 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; - - // 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; - } + 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, + + // 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; + + 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; - } + 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]; - } + 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)); - } + // 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(); - } + 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 2faff1b6e9c..2e218dfe990 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 890e04c42e3..e2d5f56bc8d 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 3f4ea10f907..427916791ed 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 034/685] 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 ef0a5b57f46..c08e23d44f6 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 6302dd11cef..5e3632e4fde 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 035/685] 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 | 309 +++++++++++++++++++++++------------ 2 files changed, 211 insertions(+), 112 deletions(-) diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 571f3c3c018..2e680bea507 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 9927debc01b..dd6ee17f59c 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)' +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: [] }, { - label: 'Dataset 2', - data: [40, 40, 40], - pointHoverBorderColor: 'rgb(0, 0, 255)', - pointHoverBackgroundColor: 'rgb(0, 255, 255)' + before: [], + lines: ['Dataset 2: 40'], + after: [] }], - labels: ['Point 1', 'Point 2', 'Point 3'] - }, - options: { - tooltips: { - mode: 'label' - } - } - }); - - // 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(); + 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)' + }] + })); - var evt = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.y).toBeCloseToPixel(155); }); - // 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 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 + } + } + }); + + // 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, + + // 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, + })); + }); }); 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 036/685] 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 | 90 +++++++++++++++++++++++------------ 6 files changed, 185 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 478edb5eca4..8ef0139ea96 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 910ea5bea0c..b7eb09dfc08 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 5e3632e4fde..c08d0192864 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 319219f5d20..cea3cf30bae 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 d7548d4ce1f..a7b504ecdcc 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 83b73e31501..8904cf88612 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; - - 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; - } + 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; + + // 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 037/685] 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 | 98 +++++++++++-------- 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, 518 insertions(+), 443 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 57cbd1cc607..58b8bf8fa29 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 3ef9cabdf52..a77eea22ead 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 ec61a0b50bc..977b2d517b1 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 0d31653a733..d9319bb807a 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 9afc9cec055..3c66b268ec0 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 648117a4c10..3bd95eb231e 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 421ff9eb388..449d295c9c4 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 6443de099d0..7d194562e73 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 3d431606234..e9cbb8e4354 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 312ac8fe663..15189a4771d 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 860b4b64900..4fa1039d569 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 caa35883a19..6d539fd1ec2 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 0d4186af8d6..59a71b1c55e 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 ca0eaf20d6c..aacac9879a9 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 b8ecd18e5cb..7046052781e 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 dd6ee17f59c..29c6b721a49 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 84c9655e6c7..0e859901434 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 7ce70517d24..b2caaadaa4c 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 0f495c9d1fa..0a2a6eced56 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 9cbac8ea560..61fe3d09a4e 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 bd32ff3ac23..b833862bd6f 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 8904cf88612..58a2ccf2355 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 me = this; + var methodName; + var addMethod = function(name, method) { - scope[methodName] = function() { - scope.record(name, arguments); - return method.apply(scope, arguments); + me[methodName] = function() { + me.record(name, arguments); + return method.apply(me, arguments); }; - } + }; - for (var methodName in methods) { + 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 5307ba6527f..13955d35be4 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 4eb585a7b66..7102a6f4a8c 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 221e61c4bd2..9636a0b375c 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 dea9b1b0354..a47214f4fff 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 5179191bafa..896ff00d4f7 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 038/685] 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 3a361bcfff3..5ea957179a6 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 c08d0192864..3fb4ff1c995 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 a7b504ecdcc..1700805596e 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 039/685] 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 c08e23d44f6..c12a102af9e 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 58b8bf8fa29..530ae210192 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 7336deac0d2..220a2534484 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 4fa1039d569..fcc2b7dcb52 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 040/685] 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 530ae210192..1ce312d9098 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 fcc2b7dcb52..951ea234e70 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 041/685] 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 5ea957179a6..db346bc94d2 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 6695cb66346..9272caff8b6 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 af8d3197cd8..fca8a581ec4 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 29c6b721a49..5890e79e036 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 043/685] 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 bf9eb5baa61..912d826eb73 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 fca8a581ec4..ecf7ce646cd 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 5890e79e036..0c13707aef9 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 044/685] 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 | 65 +++-- 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, 3551 insertions(+), 3300 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 fad7bd48934..ddfc12e3cf6 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 a5e1e8d37ea..00000000000 --- 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 00000000000..849de80d364 --- /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 0fb231f5232..00000000000 --- 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 38a9d578234..e2fd662d8af 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 00000000000..21cb0cc8918 --- /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 00000000000..e133e4e8d41 --- /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 c0b74a15062..3ec760e92d2 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 00000000000..e90b63def52 --- /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 00000000000..f1edd7c6ff1 --- /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 9272caff8b6..00000000000 --- 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 201d4c07e51..00000000000 --- 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 290ea43ea79..00000000000 --- 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 b9d08aee7a3..00000000000 --- 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 26616f5dea8..00000000000 --- 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 f6cebe26271..00000000000 --- 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 e90575df579..00000000000 --- 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 464072ba1db..00000000000 --- 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 00000000000..3137367f725 --- /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 2d85c95c930..51469c93325 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 00000000000..a194bba3b1e --- /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 00000000000..94cc6152083 --- /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 00000000000..a2c06f9352a --- /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 3ef27558755..980feb74732 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 00000000000..529eb481f2b --- /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 00000000000..c65cc9ab4c6 --- /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 00000000000..54396f92eba --- /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 00000000000..fe3d5b11032 --- /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 8c7fe2ad01e..6864b409023 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 b376b5abd31..2b1578536b8 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 16572dd4514..0b382c484de 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -4,7 +4,7 @@ Polar Area Chart - + -
    +
    @@ -25,13 +25,9 @@ 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 chartColors = window.chartColors; + var color = Chart.helpers.color; var config = { data: { datasets: [{ @@ -43,26 +39,26 @@ randomScalingFactor(), ], backgroundColor: [ - "#F7464A", - "#46BFBD", - "#FDB45C", - "#949FB1", - "#4D5360", + color(chartColors.red).alpha(0.5).rgbString(), + color(chartColors.orange).alpha(0.5).rgbString(), + color(chartColors.yellow).alpha(0.5).rgbString(), + color(chartColors.green).alpha(0.5).rgbString(), + color(chartColors.blue).alpha(0.5).rgbString(), ], label: 'My dataset' // for legend }], labels: [ "Red", - "Green", + "Orange", "Yellow", - "Grey", - "Dark Grey" + "Green", + "Blue" ] }, options: { responsive: true, legend: { - position: 'top', + position: 'right', }, title: { display: true, @@ -86,37 +82,33 @@ window.myPolarArea = Chart.PolarArea(ctx, config); }; - $('#randomizeData').click(function() { - $.each(config.data.datasets, function(i, piece) { - $.each(piece.data, function(j, value) { + document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(piece, i) { + piece.data.forEach(function(value, j) { config.data.datasets[i].data[j] = randomScalingFactor(); - config.data.datasets[i].backgroundColor[j] = randomColor(); }); }); window.myPolarArea.update(); }); - $('#addData').click(function() { + var colorNames = Object.keys(window.chartColors); + document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - config.data.labels.push('dataset #' + config.data.labels.length); - - $.each(config.data.datasets, function(i, dataset) { - dataset.backgroundColor.push(randomColor()); + config.data.labels.push('data #' + config.data.labels.length); + config.data.datasets.forEach(function(dataset) { + var colorName = colorNames[config.data.labels.length % colorNames.length]; + dataset.backgroundColor.push(window.chartColors[colorName]); dataset.data.push(randomScalingFactor()); }); - window.myPolarArea.update(); } }); - - $('#removeData').click(function() { + document.getElementById('removeData').addEventListener('click', function() { config.data.labels.pop(); // remove the label first - - $.each(config.data.datasets, function(i, dataset) { + config.data.datasets.forEach(function(dataset) { dataset.backgroundColor.pop(); dataset.data.pop(); }); - window.myPolarArea.update(); }); diff --git a/samples/radar-skip-points.html b/samples/radar-skip-points.html deleted file mode 100644 index c1680afd0c9..00000000000 --- a/samples/radar-skip-points.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - Radar Chart - - - - - - -
    - -
    - - - - - - - - - diff --git a/samples/radar/radar-skip-points.html b/samples/radar/radar-skip-points.html new file mode 100644 index 00000000000..40ad4779efb --- /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 eadcd9ecab8..98bc7086d97 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 af026e7b134..00000000000 --- 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 00000000000..96edac99444 --- /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 00000000000..c5de6bae006 --- /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 f6c178a7db5..23a5f777eeb 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 00000000000..1ae7f90ab80 --- /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 00000000000..bf9bf971a47 --- /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 00000000000..cc6e356b353 --- /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 00000000000..abc047bfd21 --- /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 00000000000..596eb6dfeb6 --- /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 00000000000..397348b01b8 --- /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 045/685] 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 2e680bea507..2d7b99597f2 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 046/685] 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 ddfc12e3cf6..b0c495a02db 100644 --- a/samples/AnimationCallbacks/progress-bar.html +++ b/samples/animation/progress-bar.html @@ -3,7 +3,7 @@ Animation Callbacks - + diff --git a/samples/polar-area.html b/samples/polar-area.html index 0b382c484de..48fe984afab 100644 --- a/samples/polar-area.html +++ b/samples/polar-area.html @@ -4,7 +4,7 @@ Polar Area Chart - + + + + +
    + +
    + + + + + diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index ecf43b19bce..1d41386b6d7 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -35,21 +35,33 @@ module.exports = function(Chart) { initialize: function(chart, datasetIndex) { Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex); + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + meta.stack = dataset.stack; // Use this to indicate that this is a bar dataset. - this.getMeta().bar = true; + meta.bar = true; }, - // Get the number of datasets that display bars. We use this to correctly calculate the bar width - getBarCount: function() { + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { var me = this; - var barCount = 0; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + + var stacks = []; helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { - var meta = me.chart.getDatasetMeta(datasetIndex); - if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) { - ++barCount; + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } }, me); - return barCount; + + return stacks.length; }, update: function(reset) { @@ -103,7 +115,8 @@ module.exports = function(Chart) { var base = yScale.getBaseValue(); var original = base; - if (yScale.options.stacked) { + if ((yScale.options.stacked === true) || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -111,7 +124,8 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); } @@ -127,18 +141,18 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); - var tickWidth = xScale.width / xScale.ticks.length; + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; - var fullBarWidth = categoryWidth / datasetCount; + var fullBarWidth = categoryWidth / stackCount; var barWidth = fullBarWidth * xScale.options.barPercentage; var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickWidth: tickWidth, categoryWidth: categoryWidth, categorySpacing: categorySpacing, @@ -149,46 +163,50 @@ module.exports = function(Chart) { }, calculateBarWidth: function(ruler) { - var xScale = this.getScaleForId(this.getMeta().xAxisID); + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - return xScale.options.stacked ? ruler.categoryWidth * xScale.options.barPercentage : ruler.barWidth; + return ruler.barWidth; }, - // Get bar index from the given dataset index accounting for the fact that not all bars are visible - getBarIndex: function(datasetIndex) { - var barIndex = 0; - var meta, j; + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var yScale = me.getScaleForId(meta.yAxisID); + var dsMeta, j; + var stacks = [meta.stack]; for (j = 0; j < datasetIndex; ++j) { - meta = this.chart.getDatasetMeta(j); - if (meta.bar && this.chart.isDatasetVisible(j)) { - ++barIndex; + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } } - return barIndex; + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } - return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + - (ruler.barWidth * barIndex) + + (ruler.barWidth * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); }, calculateBarY: function(index, datasetIndex) { @@ -197,16 +215,17 @@ module.exports = function(Chart) { var yScale = me.getScaleForId(meta.yAxisID); var value = Number(me.getDataset().data[index]); - if (yScale.options.stacked) { - + if (yScale.options.stacked || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var base = yScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -324,6 +343,27 @@ module.exports = function(Chart) { }; Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); + + var stacks = []; + helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + }, me); + + return stacks.length; + }, + updateElement: function(rectangle, index, reset) { var me = this; var meta = me.getMeta(); @@ -368,7 +408,8 @@ module.exports = function(Chart) { var base = xScale.getBaseValue(); var originalBase = base; - if (xScale.options.stacked) { + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -376,7 +417,8 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); } @@ -392,18 +434,18 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); var tickHeight = yScale.height / yScale.ticks.length; var categoryHeight = tickHeight * yScale.options.categoryPercentage; var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; - var fullBarHeight = categoryHeight / datasetCount; + var fullBarHeight = categoryHeight / stackCount; var barHeight = fullBarHeight * yScale.options.barPercentage; var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickHeight: tickHeight, categoryHeight: categoryHeight, categorySpacing: categorySpacing, @@ -415,11 +457,33 @@ module.exports = function(Chart) { calculateBarHeight: function(ruler) { var me = this; - var yScale = me.getScaleForId(me.getMeta().yAxisID); + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - return yScale.options.stacked ? ruler.categoryHeight * yScale.options.barPercentage : ruler.barHeight; + return ruler.barHeight; + }, + + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var xScale = me.getScaleForId(meta.xAxisID); + var dsMeta, j; + var stacks = [meta.stack]; + + for (j = 0; j < datasetIndex; ++j) { + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + } + + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex) { @@ -428,16 +492,17 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var value = Number(me.getDataset().data[index]); - if (xScale.options.stacked) { - + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var base = xScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -460,20 +525,16 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; - if (yScale.options.stacked) { - return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; - } - return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + - (ruler.barHeight * barIndex) + + (ruler.barHeight * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); } }); }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 301af6bc986..319243b23e5 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -28,21 +28,43 @@ module.exports = function(Chart) { me.min = null; me.max = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = { + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = { positiveValues: [], negativeValues: [] }; } // Store these per type - var positiveValues = valuesPerType[meta.type].positiveValues; - var negativeValues = valuesPerType[meta.type].negativeValues; + var positiveValues = valuesPerStack[key].positiveValues; + var negativeValues = valuesPerStack[key].negativeValues; if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { @@ -65,7 +87,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); var minVal = helpers.min(values); var maxVal = helpers.max(values); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 160d35f23fd..5d1d329f9ab 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -32,18 +32,40 @@ module.exports = function(Chart) { me.max = null; me.minNotZero = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = []; + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = []; } helpers.each(dataset.data, function(rawValue, index) { - var values = valuesPerType[meta.type]; + var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); if (isNaN(value) || meta.data[index].hidden) { return; @@ -61,7 +83,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var minVal = helpers.min(valuesForType); var maxVal = helpers.max(valuesForType); me.min = me.min === null ? minVal : Math.min(me.min, minVal); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index fea88633ce9..38f1b733a1a 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -52,41 +52,612 @@ describe('Bar controller tests', function() { expect(meta.yAxisID).toBe('firstYScaleID'); }); - it('should correctly count the number of bar datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: [], type: 'line'}, - {data: [], hidden: true}, - {data: []}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks ignoring datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(2); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarCount()).toBe(2); + it('should correctly count the number of stacks when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); + }); }); - it('should correctly get the bar index accounting for hidden datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: []}, - {data: [], hidden: true}, - {data: [], type: 'line'}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(1); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarIndex(0)).toBe(0); - expect(meta.controller.getBarIndex(3)).toBe(1); + it('should correctly count the number of stacks when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(3); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly get the stack index accounting for datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(0); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(2); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); }); it('should create rectangle elements for each data item during initialization', function() { @@ -239,9 +810,7 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', - stacked: true, - barPercentage: 1 + type: 'category' }], yAxes: [{ type: 'linear', @@ -254,10 +823,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 93, x: 86, y: 161}, - {b: 290, w: 93, x: 202, y: 419}, - {b: 290, w: 93, x: 318, y: 161}, - {b: 290, w: 93, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -268,10 +837,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 93, x: 86, y: 32}, - {b: 290, w: 93, x: 202, y: 97}, - {b: 161, w: 93, x: 318, y: 161}, - {b: 419, w: 93, x: 436, y: 471} + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -296,9 +865,7 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', - stacked: true, - barPercentage: 1 + type: 'category' }], yAxes: [{ type: 'linear', @@ -311,10 +878,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 93, x: 86, y: 161}, - {b: 290, w: 93, x: 202, y: 419}, - {b: 290, w: 93, x: 318, y: 161}, - {b: 290, w: 93, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -325,10 +892,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 93, x: 86, y: 32}, - {b: 290, w: 93, x: 202, y: 97}, - {b: 161, w: 93, x: 318, y: 161}, - {b: 419, w: 93, x: 436, y: 471} + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -337,6 +904,144 @@ describe('Bar controller tests', function() { }); }); + it('should get the correct bar points for grouped stacked chart if the group name is same', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1', + stack: 'stack1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + stack: 'stack1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + type: 'linear', + stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta = chart.getDatasetMeta(1); + + [ + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is different', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [1, 2], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + + [ + {x: 108, y: 258}, + {x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(484); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [0.5, 1], + stack: 'stack2' + }, { + data: [0.5, 1], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(2); + + [ + {b: 371, x: 108, y: 258}, + {b: 258, x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + it('should update elements when the scales are stacked and the y axis is logarithmic', function() { var chart = window.acquireChart({ type: 'bar', @@ -564,10 +1269,11 @@ describe('Bar controller tests', function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); var xScale = chart.scales[meta.xAxisID]; + var yScale = chart.scales[meta.yAxisID]; var categoryPercentage = xScale.options.categoryPercentage; var barPercentage = xScale.options.barPercentage; - var stacked = xScale.options.stacked; + var stacked = yScale.options.stacked; var totalBarWidth = 0; for (var i = 0; i < chart.data.datasets.length; i++) { @@ -613,8 +1319,10 @@ describe('Bar controller tests', function() { ticks: { min: 'March', max: 'May', - }, - stacked: true, + } + }], + yAxes: [{ + stacked: true }] } } @@ -638,11 +1346,12 @@ describe('Bar controller tests', function() { afterEach(function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; var yScale = chart.scales[meta.yAxisID]; var categoryPercentage = yScale.options.categoryPercentage; var barPercentage = yScale.options.barPercentage; - var stacked = yScale.options.stacked; + var stacked = xScale.options.stacked; var totalBarHeight = 0; for (var i = 0; i < chart.data.datasets.length; i++) { @@ -684,12 +1393,14 @@ describe('Bar controller tests', function() { data: this.data, options: { scales: { + xAxes: [{ + stacked: true + }], yAxes: [{ ticks: { min: 'March', max: 'May', - }, - stacked: true, + } }] } } From 7c3e71d58b6cb4b169ef03ca22f7bf2cfd09b912 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 2 Jan 2017 13:26:51 -0500 Subject: [PATCH 104/685] update copyright date --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 1ce312d9098..7442cfe6d25 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -29,7 +29,7 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2016 Nick Downie\n" + + " * Copyright 2017 Nick Downie\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; From 83c54194aeba03c07210188d4084f123d0df66a7 Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Thu, 5 Jan 2017 22:00:05 +0800 Subject: [PATCH 105/685] Fix : Scale label display at top and right. (#3741) Fix Scale position at rotation when scale is top. --- src/core/core.scale.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 06047e4e39a..b7d04354755 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -338,8 +338,13 @@ module.exports = function(Chart) { // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height - me.paddingLeft = me.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = me.labelRotation !== 0 ? (sinRotation * lineSpace) + 3 : lastLabelWidth / 2 + 3; // when rotated + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom'? (cosRotation * firstLabelWidth) + 3: (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom'? (cosRotation * lineSpace) + 3: (cosRotation * lastLabelWidth) + 3; + } else { + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; + } } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first // Account for padding @@ -578,15 +583,21 @@ module.exports = function(Chart) { var textBaseline = 'middle'; if (isHorizontal) { - if (!isRotated) { - textBaseline = options.position === 'top' ? 'bottom' : 'top'; - } - textAlign = isRotated ? 'right' : 'center'; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated? 'top':'middle'; + textAlign = !isRotated? 'center': 'right'; + labelY = me.top + tl; + } else { + // top + textBaseline = !isRotated? 'bottom':'middle'; + textAlign = !isRotated? 'center': 'left'; + labelY = me.bottom - tl; + } var xLineValue = me.getPixelForTick(index) + helpers.aliasPixel(lineWidth); // xvalues for grid lines labelX = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = (isRotated) ? me.top + 12 : options.position === 'top' ? me.bottom - tl : me.top + tl; tx1 = tx2 = x1 = x2 = xLineValue; ty1 = yTickStart; From 27b2e332c686e76cfe373a2d5377d53d01a7a84a Mon Sep 17 00:00:00 2001 From: mdewilde Date: Sun, 8 Jan 2017 14:54:03 +0100 Subject: [PATCH 106/685] Correct anchor link (#3772) --- docs/02-Scales.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-Scales.md b/docs/02-Scales.md index ae7758326f1..af7812ce03b 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -36,7 +36,7 @@ afterFit | Function | undefined | Callback that runs after the scale fits to the afterUpdate | Function | undefined | Callback that runs at the end of the update process. Passed a single argument, the scale instance. **gridLines** | Object | - | See [grid line configuration](#grid-line-configuration) section. **scaleLabel** | Object | | See [scale title configuration](#scale-title-configuration) section. -**ticks** | Object | | See [ticks configuration](#ticks-configuration) section. +**ticks** | Object | | See [tick configuration](#tick-configuration) section. #### Grid Line Configuration From 9e9b9cf46d452dfbfb2fa19a9cd362d4405abfdc Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 6 Jan 2017 21:41:10 -0500 Subject: [PATCH 107/685] when the cutoutPercentage is 0, the inner radius should be 0 --- src/controllers/controller.doughnut.js | 2 +- test/controller.doughnut.tests.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 3dcd33e9f63..70ee22e1841 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -183,7 +183,7 @@ module.exports = function(Chart) { chart.borderWidth = me.getMaxBorderWidth(meta.data); chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 1, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); chart.offsetX = offset.x * chart.outerRadius; chart.offsetY = offset.y * chart.outerRadius; diff --git a/test/controller.doughnut.tests.js b/test/controller.doughnut.tests.js index d9319bb807a..f4c0c959e30 100644 --- a/test/controller.doughnut.tests.js +++ b/test/controller.doughnut.tests.js @@ -40,6 +40,20 @@ describe('Doughnut controller tests', function() { expect(meta.data[3] instanceof Chart.elements.Arc).toBe(true); }); + it('should set the innerRadius to 0 if the config option is 0', function() { + var chart = window.acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: [] + } + }); + + expect(chart.innerRadius).toBe(0); + }); + it ('should reset and update elements', function() { var chart = window.acquireChart({ type: 'doughnut', From 312773ba7b7e2acf6cd1273c92017d3c197a992b Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 14 Jan 2017 14:38:56 +0100 Subject: [PATCH 108/685] Platform event API abstraction Move base platform definition and logic in src/platform/platform.js and simplify the browser -> Chart.js event mapping by listing only different naming then fallback to the native type. Replace `createEvent` by `add/removeEventListener` methods which dispatch Chart.js IEvent objects instead of native events. Move `add/removeResizeListener` implementation into the DOM platform which is now accessible via `platform.add/removeEventListener(chart, 'resize', listener)`. Finally, remove `bindEvent` and `unbindEvent` from the helpers since the implementation is specific to the chart controller (and should be private). --- src/chart.js | 4 +- src/core/core.controller.js | 86 ++++++++----- src/core/core.helpers.js | 84 ------------- src/core/core.interaction.js | 6 +- src/core/core.legend.js | 2 +- src/core/core.tooltip.js | 2 +- src/platforms/platform.dom.js | 220 ++++++++++++++++++++-------------- src/platforms/platform.js | 69 +++++++++++ test/platform.dom.tests.js | 4 - 9 files changed, 263 insertions(+), 214 deletions(-) create mode 100644 src/platforms/platform.js diff --git a/src/chart.js b/src/chart.js index 186d07a42f5..77d2bb636c8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -4,6 +4,7 @@ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); +require('./platforms/platform.js')(Chart); require('./core/core.canvasHelpers')(Chart); require('./core/core.plugin.js')(Chart); require('./core/core.element')(Chart); @@ -19,9 +20,6 @@ require('./core/core.legend')(Chart); require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); -// By default, we only load the browser platform. -Chart.platform = require('./platforms/platform.dom')(Chart); - require('./elements/element.arc')(Chart); require('./elements/element.line')(Chart); require('./elements/element.point')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e64cb3a9dde..616df547be7 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; + var platform = Chart.platform; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -63,7 +64,7 @@ module.exports = function(Chart) { config = initConfig(config); - var context = Chart.platform.acquireContext(item, config); + var context = platform.acquireContext(item, config); var canvas = context && context.canvas; var height = canvas && canvas.height; var width = canvas && canvas.width; @@ -99,21 +100,6 @@ module.exports = function(Chart) { 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. - // See https://github.com/chartjs/Chart.js/issues/2210 - if (me.options.responsive) { - helpers.addResizeListener(canvas.parentNode, function() { - me.resize(); - }); - - // Initial resize before chart draws (must be silent to preserve initial animations). - me.resize(true); - } - me.initialize(); return me; @@ -126,8 +112,15 @@ module.exports = function(Chart) { // Before init plugin notification Chart.plugins.notify(me, 'beforeInit'); + helpers.retinaScale(me.chart); + me.bindEvents(); + if (me.options.responsive) { + // Initial resize before chart draws (must be silent to preserve initial animations). + me.resize(true); + } + // Make sure controllers are built first so that each dataset is bound to an axis before the scales // are built me.ensureScalesHaveIDs(); @@ -559,10 +552,9 @@ module.exports = function(Chart) { } if (canvas) { - helpers.unbindEvents(me, me.events); - helpers.removeResizeListener(canvas.parentNode); + me.unbindEvents(); helpers.clear(me.chart); - Chart.platform.releaseContext(me.chart.ctx); + platform.releaseContext(me.chart.ctx); me.chart.canvas = null; me.chart.ctx = null; } @@ -587,10 +579,48 @@ module.exports = function(Chart) { me.tooltip.initialize(); }, + /** + * @private + */ bindEvents: function() { var me = this; - helpers.bindEvents(me, me.options.events, function(evt) { - me.eventHandler(evt); + var listeners = me._listeners = {}; + var listener = function() { + me.eventHandler.apply(me, arguments); + }; + + helpers.each(me.options.events, function(type) { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }); + + // 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) { + listener = function() { + me.resize(); + }; + + platform.addEventListener(me, 'resize', listener); + listeners.resize = listener; + } + }, + + /** + * @private + */ + unbindEvents: function() { + var me = this; + var listeners = me._listeners; + if (!listeners) { + return; + } + + delete me._listeners; + helpers.each(listeners, function(listener, type) { + platform.removeEventListener(me, type, listener); }); }, @@ -606,6 +636,9 @@ module.exports = function(Chart) { } }, + /** + * @private + */ eventHandler: function(e) { var me = this; var tooltip = me.tooltip; @@ -615,12 +648,9 @@ module.exports = function(Chart) { me._bufferedRender = true; me._bufferedRequest = null; - // Create platform agnostic chart event using platform specific code - var chartEvent = Chart.platform.createEvent(e, me.chart); - - var changed = me.handleEvent(chartEvent); - changed |= tooltip && tooltip.handleEvent(chartEvent); - changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]); + var changed = me.handleEvent(e); + changed |= tooltip && tooltip.handleEvent(e); + changed |= Chart.plugins.notify(me, 'onEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -644,7 +674,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * param e {Core.Event} the event to handle + * @param {IEvent} event the event to handle * @return {Boolean} true if the chart needs to re-render */ handleEvent: function(e) { diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bfbcf23d5f5..f95af269f6b 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -734,23 +734,6 @@ module.exports = function(Chart) { node['on' + eventType] = helpers.noop; } }; - helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { - // Create the events object if it's not already present - var events = chartInstance.events = chartInstance.events || {}; - - helpers.each(arrayOfEvents, function(eventName) { - events[eventName] = function() { - handler.apply(chartInstance, arguments); - }; - helpers.addEvent(chartInstance.chart.canvas, eventName, events[eventName]); - }); - }; - helpers.unbindEvents = function(chartInstance, arrayOfEvents) { - var canvas = chartInstance.chart.canvas; - helpers.each(arrayOfEvents, function(handler, eventName) { - helpers.removeEvent(canvas, eventName, handler); - }); - }; // Private helper function to convert max-width/max-height values that may be percentages into a number function parseMaxStyle(styleValue, node, parentProperty) { @@ -941,73 +924,6 @@ module.exports = function(Chart) { return color(c); }; - helpers.addResizeListener = function(node, callback) { - var iframe = document.createElement('iframe'); - 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;'; - - // Prevent the iframe to gain focus on tab. - // https://github.com/chartjs/Chart.js/issues/3090 - iframe.tabIndex = -1; - - // Let's keep track of this added iframe and thus avoid DOM query when removing it. - var stub = node._chartjs = { - resizer: iframe, - ticking: false - }; - - // Throttle the callback notification until the next animation frame. - var notify = function() { - if (!stub.ticking) { - stub.ticking = true; - helpers.requestAnimFrame.call(window, function() { - if (stub.resizer) { - stub.ticking = false; - return callback(); - } - }); - } - }; - - // If the iframe is re-attached to the DOM, the resize listener is removed because the - // content is reloaded, so make sure to install the handler after the iframe is loaded. - // https://github.com/chartjs/Chart.js/issues/3521 - helpers.addEvent(iframe, 'load', function() { - helpers.addEvent(iframe.contentWindow || iframe, 'resize', notify); - - // The iframe size might have changed while loading, which can also - // happen if the size has been changed while detached from the DOM. - notify(); - }); - - node.insertBefore(iframe, node.firstChild); - }; - helpers.removeResizeListener = function(node) { - if (!node || !node._chartjs) { - return; - } - - var iframe = node._chartjs.resizer; - if (iframe) { - iframe.parentNode.removeChild(iframe); - node._chartjs.resizer = null; - } - - delete node._chartjs; - }; helpers.isArray = Array.isArray? function(obj) { return Array.isArray(obj); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 0888fa9591b..03423c0d604 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -5,8 +5,8 @@ module.exports = function(Chart) { /** * Helper function to get relative position for an event - * @param e {Event|Core.Event} the event to get the position for - * @param chart {chart} the chart + * @param {Event|IEvent} event - The event to get the position for + * @param {Chart} chart - The chart * @returns {Point} the event position */ function getRelativePosition(e, chart) { @@ -135,8 +135,8 @@ module.exports = function(Chart) { */ /** - * @namespace Chart.Interaction * Contains interaction related functions + * @namespace Chart.Interaction */ Chart.Interaction = { // Helper function for different modes diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 45f51d05923..b80bdbe25ad 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -439,7 +439,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Core.Event} the event to handle + * @param {IEvent} event - The event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c1ac7830739..32589b652e6 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -763,7 +763,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Core.Event} the event to handle + * @param {IEvent} event - The event to handle * @returns {Boolean} true if the tooltip changed */ handleEvent: function(e) { diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index fb41f88259f..abfb3dee3e1 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -1,57 +1,13 @@ 'use strict'; -/** - * @interface IPlatform - * Allows abstracting platform dependencies away from the chart - */ -/** - * Creates a chart.js event from a platform specific event - * @method IPlatform#createEvent - * @param e {Event} : the platform event to translate - * @returns {Core.Event} chart.js event - */ -/** - * @method IPlatform#acquireContext - * @param item {Object} the context or canvas to use - * @param config {ChartOptions} the chart options - * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard. - */ -/** - * @method IPlatform#releaseContext - * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext} - */ - // Chart.Platform implementation for targeting a web browser module.exports = function(Chart) { var helpers = Chart.helpers; - /* - * Key is the browser event type - * Chart.js internal events are: - * mouseenter - * mousedown - * mousemove - * mouseup - * mouseout - * click - * dblclick - * contextmenu - * keydown - * keypress - * keyup - */ - var typeMap = { - // Mouse events - mouseenter: 'mouseenter', - mousedown: 'mousedown', - mousemove: 'mousemove', - mouseup: 'mouseup', - mouseout: 'mouseout', - mouseleave: 'mouseout', - click: 'click', - dblclick: 'dblclick', - contextmenu: 'contextmenu', - + // DOM event types -> Chart.js event types. + // Note: only events with different types are mapped. + // https://developer.mozilla.org/en-US/docs/Web/Events + var eventTypeMap = { // Touch events touchstart: 'mousedown', touchmove: 'mousemove', @@ -63,12 +19,7 @@ module.exports = function(Chart) { pointermove: 'mousemove', pointerup: 'mouseup', pointerleave: 'mouseout', - pointerout: 'mouseout', - - // Key events - keydown: 'keydown', - keypress: 'keypress', - keyup: 'keyup', + pointerout: 'mouseout' }; /** @@ -141,38 +92,97 @@ module.exports = function(Chart) { return canvas; } - return { - /** - * Creates a Chart.js event from a raw event - * @method BrowserPlatform#createEvent - * @implements IPlatform.createEvent - * @param e {Event} the raw event (such as a mouse event) - * @param chart {Chart} the chart to use - * @returns {Core.Event} the chart.js event for this event - */ - createEvent: function(e, chart) { - var relativePosition = helpers.getRelativePosition(e, chart); - return { - // allow access to the native event - native: e, - - // our interal event type - type: typeMap[e.type], - - // width and height of chart - width: chart.width, - height: chart.height, - - // Position relative to the canvas - x: relativePosition.x, - y: relativePosition.y - }; - }, + function createEvent(type, chart, x, y, native) { + return { + type: type, + chart: chart, + native: native || null, + x: x !== undefined? x : null, + y: y !== undefined? y : null, + }; + } + + function fromNativeEvent(event, chart) { + var type = eventTypeMap[event.type] || event.type; + var pos = helpers.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); + } + + function createResizer(handler) { + var iframe = document.createElement('iframe'); + 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;'; - /** - * @method BrowserPlatform#acquireContext - * @implements IPlatform#acquireContext - */ + // Prevent the iframe to gain focus on tab. + // https://github.com/chartjs/Chart.js/issues/3090 + iframe.tabIndex = -1; + + // If the iframe is re-attached to the DOM, the resize listener is removed because the + // content is reloaded, so make sure to install the handler after the iframe is loaded. + // https://github.com/chartjs/Chart.js/issues/3521 + helpers.addEvent(iframe, 'load', function() { + helpers.addEvent(iframe.contentWindow || iframe, 'resize', handler); + + // The iframe size might have changed while loading, which can also + // happen if the size has been changed while detached from the DOM. + handler(); + }); + + return iframe; + } + + function addResizeListener(node, listener, chart) { + var stub = node._chartjs = { + ticking: false + }; + + // Throttle the callback notification until the next animation frame. + var notify = function() { + if (!stub.ticking) { + stub.ticking = true; + helpers.requestAnimFrame.call(window, function() { + if (stub.resizer) { + stub.ticking = false; + return listener(createEvent('resize', chart)); + } + }); + } + }; + + // Let's keep track of this added iframe and thus avoid DOM query when removing it. + stub.resizer = createResizer(notify); + + node.insertBefore(stub.resizer, node.firstChild); + } + + function removeResizeListener(node) { + if (!node || !node._chartjs) { + return; + } + + var resizer = node._chartjs.resizer; + if (resizer) { + resizer.parentNode.removeChild(resizer); + node._chartjs.resizer = null; + } + + delete node._chartjs; + } + + return { acquireContext: function(item, config) { if (typeof item === 'string') { item = document.getElementById(item); @@ -200,11 +210,6 @@ module.exports = function(Chart) { return null; }, - /** - * Restores the canvas initial state, such as render/display sizes and style. - * @method BrowserPlatform#releaseContext - * @implements IPlatform#releaseContext - */ releaseContext: function(context) { var canvas = context.canvas; if (!canvas._chartjs) { @@ -232,6 +237,41 @@ module.exports = function(Chart) { canvas.width = canvas.width; delete canvas._chartjs; + }, + + addEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + addResizeListener(canvas.parentNode, listener, chart.chart); + return; + } + + var stub = listener._chartjs || (listener._chartjs = {}); + var proxies = stub.proxies || (stub.proxies = {}); + var proxy = proxies[chart.id + '_' + type] = function(event) { + listener(fromNativeEvent(event, chart.chart)); + }; + + helpers.addEvent(canvas, type, proxy); + }, + + removeEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + removeResizeListener(canvas.parentNode, listener); + return; + } + + var stub = listener._chartjs || {}; + var proxies = stub.proxies || {}; + var proxy = proxies[chart.id + '_' + type]; + if (!proxy) { + return; + } + + helpers.removeEvent(canvas, type, proxy); } }; }; diff --git a/src/platforms/platform.js b/src/platforms/platform.js new file mode 100644 index 00000000000..0f27e5868a6 --- /dev/null +++ b/src/platforms/platform.js @@ -0,0 +1,69 @@ +'use strict'; + +// By default, select the browser (DOM) platform. +// @TODO Make possible to select another platform at build time. +var implementation = require('./platform.dom.js'); + +module.exports = function(Chart) { + /** + * @namespace Chart.platform + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html + * @since 2.4.0 + */ + Chart.platform = { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {*} item - The native item from which to acquire context (platform specific) + * @param {Object} options - The chart options + * @returns {CanvasRenderingContext2D} context2d instance + */ + acquireContext: function() {}, + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {Boolean} true if the method succeeded, else false + */ + releaseContext: function() {}, + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {String} type - The ({@link IEvent}) type to listen for + * @param {Function} listener - Receives a notification (an object that implements + * the {@link IEvent} interface) when an event of the specified type occurs. + */ + addEventListener: function() {}, + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart -Chart from which to remove the listener + * @param {String} type - The ({@link IEvent}) type to remove + * @param {Function} listener - The listener function to remove from the event target. + */ + removeEventListener: function() {} + }; + + /** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + * @borrows Chart.platform.acquireContext as acquireContext + * @borrows Chart.platform.releaseContext as releaseContext + * @borrows Chart.platform.addEventListener as addEventListener + * @borrows Chart.platform.removeEventListener as removeEventListener + */ + + /** + * @interface IEvent + * @prop {String} type - The event type name, possible values are: + * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout', + * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize' + * @prop {*} native - The original native event (null for emulated events, e.g. 'resize') + * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events) + * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events) + */ + + Chart.helpers.extend(Chart.platform, implementation(Chart)); +}; diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index f20b6e1ff68..19c5bbe1aab 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -361,10 +361,6 @@ describe('Platform.dom', function() { // Is type correctly translated expect(notifiedEvent.type).toBe(evt.type); - // Canvas width and height - expect(notifiedEvent.width).toBe(chart.chart.width); - expect(notifiedEvent.height).toBe(chart.chart.height); - // Relative Position expect(notifiedEvent.x).toBe(chart.chart.width / 2); expect(notifiedEvent.y).toBe(chart.chart.height / 2); From c20e57bc8ae86f2881b1f894b1767d95194bada1 Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Fri, 13 Jan 2017 17:55:28 +0000 Subject: [PATCH 109/685] Only generate ticks we care about Instead of cloning `me.scaleSizeInUnits` moments and probably throwing the vast majority away, only clone what we need. --- src/scales/scale.time.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 61796c6ebcc..9a3e31e63b3 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -324,7 +324,7 @@ module.exports = function(Chart) { me.ticks.push(me.firstTick.clone()); // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = 1; i <= me.scaleSizeInUnits; ++i) { + for (var i = me.unitScale; i <= me.scaleSizeInUnits; i += me.unitScale) { var newTick = roundedStart.clone().add(i, me.tickUnit); // Are we greater than the max time @@ -332,9 +332,7 @@ module.exports = function(Chart) { break; } - if (i % me.unitScale === 0) { - me.ticks.push(newTick); - } + me.ticks.push(newTick); } // Always show the right tick From ceec907bee33d39da68cb7af111400622aefee23 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 21 Jan 2017 13:12:12 +0100 Subject: [PATCH 110/685] Ignore .gitignore (and more) from Bower packages --- gulpfile.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 7442cfe6d25..6cd6efa5d91 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -71,7 +71,15 @@ function bowerTask() { homepage: package.homepage, license: package.license, version: package.version, - main: outDir + "Chart.js" + main: outDir + "Chart.js", + ignore: [ + '.github', + '.codeclimate.yml', + '.gitignore', + '.npmignore', + '.travis.yml', + 'scripts' + ] }, null, 2); return file('bower.json', json, { src: true }) From 696f8d3a391ca8db6cbd1a1291cf8f3170fc0f1c Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Sat, 21 Jan 2017 16:42:21 -0800 Subject: [PATCH 111/685] Documentation update on requiring Chart.js using CommonJS and es6 (#3788) --- docs/00-Getting-Started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index b7eb09dfc08..81e03c0a986 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -44,11 +44,11 @@ To import Chart.js using an awesome module loader: ```javascript // Using CommonJS -var Chart = require('src/chart.js') +var Chart = require('chart.js') var myChart = new Chart({...}) // ES6 -import Chart from 'src/chart.js' +import Chart from 'chart.js' let myChart = new Chart({...}) // Using requirejs From 1934358663ad16773082a44974da91f0be78b4e3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 22 Jan 2017 13:23:42 -0500 Subject: [PATCH 112/685] remove unnecessary extra init steps --- src/core/core.controller.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 616df547be7..1d2366152ec 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -121,13 +121,9 @@ module.exports = function(Chart) { me.resize(true); } - // Make sure controllers are built first so that each dataset is bound to an axis before the scales - // are built + // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildOrUpdateControllers(); me.buildScales(); - me.updateLayout(); - me.resetElements(); me.initToolTip(); me.update(); @@ -256,10 +252,6 @@ module.exports = function(Chart) { Chart.scaleService.addScalesToLayout(this); }, - updateLayout: function() { - Chart.layoutService.update(this, this.chart.width, this.chart.height); - }, - buildOrUpdateControllers: function() { var me = this; var types = []; From 1ef9fbf7a65763c13fa4bdf42bf4c68da852b1db Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 22 Jan 2017 12:46:27 -0500 Subject: [PATCH 113/685] inner radius could be slightly negative due to numerical errors --- src/controllers/controller.doughnut.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 70ee22e1841..3d478058279 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -191,7 +191,7 @@ module.exports = function(Chart) { meta.total = me.calculateTotal(); me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = me.outerRadius - chart.radiusLength; + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); helpers.each(meta.data, function(arc, index) { me.updateElement(arc, index, reset); From c6fa4e55822e20b2bdd31d39bc18e19d53cd8966 Mon Sep 17 00:00:00 2001 From: Jakub Juszczak Date: Fri, 27 Jan 2017 13:51:30 +0100 Subject: [PATCH 114/685] =?UTF-8?q?=F0=9F=93=9D=20Add=20vue-chartjs=20to?= =?UTF-8?q?=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vue-chartjs is a wrapper written in vue for chartjs. --- docs/10-Notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index c6ad42910a1..e292a374046 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -109,3 +109,6 @@ There are many extensions which are available for use with popular frameworks. S #### Laravel - laravel-chartjs + +#### Vue.js + - vue-chartjs From 979341ecb094d9c6a95de8a47e7836f01587e7d2 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 22 Jan 2017 20:13:40 +0100 Subject: [PATCH 115/685] Plugin hooks and jsdoc enhancements Make all `before` hooks cancellable (except `beforeInit`), meaning that if any plugin return explicitly `false`, the current action is not performed. Ensure that `init` hooks are called before `update` hooks and add associated calling order unit tests. Deprecate `Chart.PluginBase` in favor of `IPlugin` (no more an inheritable class) and document plugin hooks (also rename `extension` by `hook`). --- docs/09-Advanced.md | 2 +- src/core/core.controller.js | 140 +++++++++++++++------------ src/core/core.plugin.js | 174 +++++++++++++++++++++++++++------- test/core.controller.tests.js | 71 ++++++++++++++ 4 files changed, 296 insertions(+), 91 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 4c677c904d4..d3d69d9afc9 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -412,7 +412,7 @@ Plugins will be called at the following times * Before an animation is started * When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled -Plugins should derive from Chart.PluginBase and implement the following interface +Plugins should implement the `IPlugin` interface: ```javascript { beforeInit: function(chartInstance) { }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 1d2366152ec..e6c19659387 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; + var plugins = Chart.plugins; var platform = Chart.platform; // Create a dictionary of chart types, to allow for extension of existing types @@ -101,16 +102,17 @@ module.exports = function(Chart) { } me.initialize(); + me.update(); return me; }; - helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { + helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller.prototype */ { initialize: function() { var me = this; // Before init plugin notification - Chart.plugins.notify(me, 'beforeInit'); + plugins.notify(me, 'beforeInit'); helpers.retinaScale(me.chart); @@ -125,10 +127,9 @@ module.exports = function(Chart) { me.ensureScalesHaveIDs(); me.buildScales(); me.initToolTip(); - me.update(); // After init plugin notification - Chart.plugins.notify(me, 'afterInit'); + plugins.notify(me, 'afterInit'); return me; }, @@ -170,7 +171,7 @@ module.exports = function(Chart) { if (!silent) { // Notify any plugins about the resize var newSize = {width: newWidth, height: newHeight}; - Chart.plugins.notify(me, 'resize', [newSize]); + plugins.notify(me, 'resize', [newSize]); // Notify of resize if (me.options.onResize) { @@ -287,7 +288,6 @@ module.exports = function(Chart) { /** * Reset the elements of all datasets - * @method resetElements * @private */ resetElements: function() { @@ -299,19 +299,20 @@ module.exports = function(Chart) { /** * 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; updateConfig(me); - Chart.plugins.notify(me, 'beforeUpdate'); + + if (plugins.notify(me, 'beforeUpdate') === false) { + return; + } // In case the entire data object changed me.tooltip._data = me.data; @@ -324,10 +325,7 @@ module.exports = function(Chart) { me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); }, me); - Chart.layoutService.update(me, me.chart.width, me.chart.height); - - // Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes - Chart.plugins.notify(me, 'afterScaleUpdate'); + me.updateLayout(); // Can only reset the new controllers after the scales have been updated helpers.each(newControllers, function(controller) { @@ -337,7 +335,7 @@ module.exports = function(Chart) { me.updateDatasets(); // Do this before render so that any plugins that need final scale updates can use it - Chart.plugins.notify(me, 'afterUpdate'); + plugins.notify(me, 'afterUpdate'); if (me._bufferedRender) { me._bufferedRequest = { @@ -350,51 +348,64 @@ module.exports = function(Chart) { }, /** - * @method beforeDatasetsUpdate - * @description Called before all datasets are updated. If a plugin returns false, - * the datasets update will be cancelled until another chart update is triggered. - * @param {Object} instance the chart instance being updated. - * @returns {Boolean} false to cancel the datasets update. - * @memberof Chart.PluginBase - * @since version 2.1.5 - * @instance + * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` + * hook, in which case, plugins will not be called on `afterLayout`. + * @private */ + updateLayout: function() { + var me = this; - /** - * @method afterDatasetsUpdate - * @description Called after all datasets have been updated. Note that this - * extension will not be called if the datasets update has been cancelled. - * @param {Object} instance the chart instance being updated. - * @memberof Chart.PluginBase - * @since version 2.1.5 - * @instance - */ + if (plugins.notify(me, 'beforeLayout') === false) { + return; + } + + Chart.layoutService.update(this, this.chart.width, this.chart.height); + + /** + * Provided for backward compatibility, use `afterLayout` instead. + * @method IPlugin#afterScaleUpdate + * @deprecated since version 2.5.0 + * @todo remove at version 3 + */ + plugins.notify(me, 'afterScaleUpdate'); + plugins.notify(me, 'afterLayout'); + }, /** - * Updates all datasets unless a plugin returns false to the beforeDatasetsUpdate - * extension, in which case no datasets will be updated and the afterDatasetsUpdate - * notification will be skipped. - * @protected - * @instance + * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` + * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. + * @private */ updateDatasets: function() { var me = this; - var i, ilen; - if (Chart.plugins.notify(me, 'beforeDatasetsUpdate')) { - for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { - me.getDatasetMeta(i).controller.update(); - } + if (plugins.notify(me, 'beforeDatasetsUpdate') === false) { + return; + } - Chart.plugins.notify(me, 'afterDatasetsUpdate'); + for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.getDatasetMeta(i).controller.update(); } + + plugins.notify(me, 'afterDatasetsUpdate'); }, render: function(duration, lazy) { var me = this; - Chart.plugins.notify(me, 'beforeRender'); + + if (plugins.notify(me, 'beforeRender') === false) { + return; + } var animationOptions = me.options.animation; + var onComplete = function() { + plugins.notify(me, 'afterRender'); + var callback = animationOptions && animationOptions.onComplete; + if (callback && callback.call) { + callback.call(me); + } + }; + if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { var animation = new Chart.Animation(); animation.numSteps = (duration || animationOptions.duration) / 16.66; // 60 fps @@ -411,15 +422,14 @@ module.exports = function(Chart) { // user events animation.onAnimationProgress = animationOptions.onProgress; - animation.onAnimationComplete = animationOptions.onComplete; + animation.onAnimationComplete = onComplete; Chart.animationService.addAnimation(me, animation, duration, lazy); } else { me.draw(); - if (animationOptions && animationOptions.onComplete && animationOptions.onComplete.call) { - animationOptions.onComplete.call(me); - } + onComplete(); } + return me; }, @@ -428,31 +438,45 @@ module.exports = function(Chart) { var easingDecimal = ease || 1; me.clear(); - Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]); + plugins.notify(me, 'beforeDraw', [easingDecimal]); // Draw all the scales helpers.each(me.boxes, function(box) { box.draw(me.chartArea); }, me); + if (me.scale) { me.scale.draw(); } - Chart.plugins.notify(me, 'beforeDatasetsDraw', [easingDecimal]); + me.drawDatasets(easingDecimal); + + // Finally draw the tooltip + me.tooltip.transition(easingDecimal).draw(); + + plugins.notify(me, 'afterDraw', [easingDecimal]); + }, + + /** + * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` + * hook, in which case, plugins will not be called on `afterDatasetsDraw`. + * @private + */ + drawDatasets: function(easingValue) { + var me = this; + + if (plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) { + return; + } // Draw each dataset via its respective controller (reversed to support proper line stacking) helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { - me.getDatasetMeta(datasetIndex).controller.draw(ease); + me.getDatasetMeta(datasetIndex).controller.draw(easingValue); } }, me, true); - Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]); - - // Finally draw the tooltip - me.tooltip.transition(easingDecimal).draw(); - - Chart.plugins.notify(me, 'afterDraw', [easingDecimal]); + plugins.notify(me, 'afterDatasetsDraw', [easingValue]); }, // Get the single element that was clicked on @@ -551,7 +575,7 @@ module.exports = function(Chart) { me.chart.ctx = null; } - Chart.plugins.notify(me, 'destroy'); + plugins.notify(me, 'destroy'); delete Chart.instances[me.id]; }, @@ -642,7 +666,7 @@ module.exports = function(Chart) { var changed = me.handleEvent(e); changed |= tooltip && tooltip.handleEvent(e); - changed |= Chart.plugins.notify(me, 'onEvent', [e]); + changed |= plugins.notify(me, 'onEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index 657c85a3545..f89b412d2e1 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -3,7 +3,6 @@ module.exports = function(Chart) { var helpers = Chart.helpers; - var noop = helpers.noop; Chart.defaults.global.plugins = {}; @@ -86,15 +85,15 @@ module.exports = function(Chart) { }, /** - * Calls enabled plugins for chart, on the specified extension and with the given args. + * Calls enabled plugins for `chart` on the specified hook and with the given args. * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. - * @param {Object} chart chart instance for which plugins should be called. - * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] extra arguments to apply to the extension call. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. * @returns {Boolean} false if any of the plugins return false, else returns true. */ - notify: function(chart, extension, args) { + notify: function(chart, hook, args) { var descriptors = this.descriptors(chart); var ilen = descriptors.length; var i, descriptor, plugin, params, method; @@ -102,7 +101,7 @@ module.exports = function(Chart) { for (i=0; i Date: Sun, 22 Jan 2017 21:01:46 +0100 Subject: [PATCH 116/685] Make `beforeDraw` cancellable and fix easing value --- src/core/core.controller.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e6c19659387..f67b29ff934 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -433,12 +433,18 @@ module.exports = function(Chart) { return me; }, - draw: function(ease) { + draw: function(easingValue) { var me = this; - var easingDecimal = ease || 1; + me.clear(); - plugins.notify(me, 'beforeDraw', [easingDecimal]); + if (easingValue === undefined || easingValue === null) { + easingValue = 1; + } + + if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { + return; + } // Draw all the scales helpers.each(me.boxes, function(box) { @@ -449,12 +455,12 @@ module.exports = function(Chart) { me.scale.draw(); } - me.drawDatasets(easingDecimal); + me.drawDatasets(easingValue); // Finally draw the tooltip - me.tooltip.transition(easingDecimal).draw(); + me.tooltip.transition(easingValue).draw(); - plugins.notify(me, 'afterDraw', [easingDecimal]); + plugins.notify(me, 'afterDraw', [easingValue]); }, /** From 7205ff5e2aa4515bae0c62bb9d8355745837270e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 22 Jan 2017 21:10:17 +0100 Subject: [PATCH 117/685] Replace `onEvent` by `before/afterEvent` --- docs/09-Advanced.md | 9 +++------ src/core/core.controller.js | 10 +++++++--- src/core/core.legend.js | 2 +- src/core/core.plugin.js | 16 ++++++++++++++++ test/platform.dom.tests.js | 2 +- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index d3d69d9afc9..05642fcab43 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -439,12 +439,9 @@ Plugins should implement the `IPlugin` interface: destroy: function(chartInstance) { } - /** - * Called when an event occurs on the chart - * @param e {Core.Event} the Chart.js wrapper around the native event. e.native is the original event - * @return {Boolean} true if the chart is changed and needs to re-render - */ - onEvent: function(chartInstance, e) {} + // Called when an event occurs on the chart + beforeEvent: function(chartInstance, event) {} + afterEvent: function(chartInstance, event) {} } ``` diff --git a/src/core/core.controller.js b/src/core/core.controller.js index f67b29ff934..b38df58b291 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -664,7 +664,10 @@ module.exports = function(Chart) { eventHandler: function(e) { var me = this; var tooltip = me.tooltip; - var hoverOptions = me.options.hover; + + if (plugins.notify(me, 'beforeEvent', [e]) === false) { + return; + } // Buffer any update calls so that renders do not occur me._bufferedRender = true; @@ -672,7 +675,8 @@ module.exports = function(Chart) { var changed = me.handleEvent(e); changed |= tooltip && tooltip.handleEvent(e); - changed |= plugins.notify(me, 'onEvent', [e]); + + plugins.notify(me, 'afterEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -684,7 +688,7 @@ module.exports = function(Chart) { // We only need to render at this point. Updating will cause scales to be // recomputed generating flicker & using more memory than necessary. - me.render(hoverOptions.animationDuration, true); + me.render(me.options.hover.animationDuration, true); } me._bufferedRender = false; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index b80bdbe25ad..4816b285f2e 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -526,7 +526,7 @@ module.exports = function(Chart) { delete chartInstance.legend; } }, - onEvent: function(chartInstance, e) { + afterEvent: function(chartInstance, e) { var legend = chartInstance.legend; if (legend) { legend.handleEvent(e); diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index f89b412d2e1..bda2ff45739 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -274,6 +274,22 @@ module.exports = function(Chart) { * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. * @param {Object} options - The plugin options. */ + /** + * @method IPlugin#beforeEvent + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ + /** + * @method IPlugin#afterEvent + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ /** * @method IPlugin#resize * @desc Called after the chart as been resized. diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index 19c5bbe1aab..a022cc75d5a 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -320,7 +320,7 @@ describe('Platform.dom', function() { it('should notify plugins about events', function() { var notifiedEvent; var plugin = { - onEvent: function(chart, e) { + afterEvent: function(chart, e) { notifiedEvent = e; } }; From 9a3af51618b2627a328bdb66f0e3030522cd756e Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 28 Jan 2017 11:28:36 -0500 Subject: [PATCH 118/685] bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 762f233d7c5..40eca264e17 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT", "main": "src/chart.js", "repository": { From 6ff34a5d4a29afea087f08568e01e2514e9d8b11 Mon Sep 17 00:00:00 2001 From: Matthisk Heimensen Date: Sat, 4 Feb 2017 00:17:33 +0100 Subject: [PATCH 119/685] Added Django-Jchart link to docs/notes.md (#3865) --- docs/10-Notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index e292a374046..451a8a8cc90 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -102,6 +102,7 @@ There are many extensions which are available for use with popular frameworks. S - react-chartjs-2 #### Django + - Django JChart - Django Chartjs #### Ruby on Rails From 8c90b9f2de6863cca3051276648e363aa332d6a2 Mon Sep 17 00:00:00 2001 From: etimberg Date: Wed, 8 Feb 2017 18:45:55 -0500 Subject: [PATCH 120/685] Fix issue with how Chart.PluginBase is defined --- src/chart.js | 2 +- src/core/core.plugin.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chart.js b/src/chart.js index 77d2bb636c8..72d5bdf735c 100644 --- a/src/chart.js +++ b/src/chart.js @@ -6,8 +6,8 @@ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); require('./platforms/platform.js')(Chart); require('./core/core.canvasHelpers')(Chart); -require('./core/core.plugin.js')(Chart); require('./core/core.element')(Chart); +require('./core/core.plugin.js')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index bda2ff45739..eeadc83aafd 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -321,5 +321,5 @@ module.exports = function(Chart) { * @todo remove at version 3 * @private */ - Chart.PluginBase = helpers.inherits({}); + Chart.PluginBase = Chart.Element.extend({}); }; From a0077d41178e576aa473490b13adf2c695bf2faa Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 28 Jan 2017 12:43:54 +0100 Subject: [PATCH 121/685] Deprecate Chart.Controller (and nested `chart`) --- src/core/core.controller.js | 117 +++++++++++++++++++++--------------- src/core/core.js | 4 +- 2 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index b38df58b291..070aa0356b3 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -38,7 +38,7 @@ module.exports = function(Chart) { /** * Updates the config of the chart - * @param chart {Chart.Controller} chart to update the options for + * @param chart {Chart} chart to update the options for */ function updateConfig(chart) { var newOptions = chart.options; @@ -56,58 +56,66 @@ module.exports = function(Chart) { chart.tooltip._options = newOptions.tooltips; } - /** - * @class Chart.Controller - * The main controller of a chart. - */ - Chart.Controller = function(item, config, instance) { - var me = this; - - config = initConfig(config); - - var context = platform.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 = width; - instance.height = height; - instance.aspectRatio = height? width / height : null; - - me.id = helpers.uid(); - 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; - - Object.defineProperty(me, 'data', { - get: function() { - return me.config.data; - } - }); - - 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.extend(Chart.prototype, /** @lends Chart */ { + /** + * @private + */ + construct: function(item, config) { + var me = this; - me.initialize(); - me.update(); + config = initConfig(config); + + var context = platform.acquireContext(item, config); + var canvas = context && context.canvas; + var height = canvas && canvas.height; + var width = canvas && canvas.width; + + me.id = helpers.uid(); + me.ctx = context; + me.canvas = canvas; + me.config = config; + me.width = width; + me.height = height; + me.aspectRatio = height? width / height : null; + me.options = config.options; + me._bufferedRender = false; - return me; - }; + /** + * Provided for backward compatibility, Chart and Chart.Controller have been merged, + * the "instance" still need to be defined since it might be called from plugins. + * @prop Chart#chart + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + me.chart = me; + me.controller = me; // chart.chart.controller #inception + + // Add the chart instance to the global namespace + Chart.instances[me.id] = me; - helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller.prototype */ { + Object.defineProperty(me, 'data', { + get: function() { + return me.config.data; + } + }); + + 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.initialize(); + me.update(); + }, + + /** + * @private + */ initialize: function() { var me = this; @@ -749,4 +757,13 @@ module.exports = function(Chart) { return changed; } }); + + /** + * Provided for backward compatibility, use Chart instead. + * @class Chart.Controller + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + Chart.Controller = Chart; }; diff --git a/src/core/core.js b/src/core/core.js index cea3cf30bae..69e9cf9a037 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -4,8 +4,8 @@ module.exports = function() { // Occupy the global variable of Chart, and create a simple base class var Chart = function(item, config) { - this.controller = new Chart.Controller(item, config, this); - return this.controller; + this.construct(item, config); + return this; }; // Globally expose the defaults to allow for user updating/changing From 13c61218764a3b542427dc45c812b8618a94571f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 28 Jan 2017 12:50:29 +0100 Subject: [PATCH 122/685] Remove deprecated nested `chart` accesses --- samples/data_labelling.html | 34 ++++++++++++++--------------- src/controllers/controller.bar.js | 15 +++++++------ src/controllers/controller.line.js | 9 ++++---- src/core/core.controller.js | 35 +++++++++++++++--------------- src/core/core.datasetController.js | 4 ++-- src/core/core.interaction.js | 14 ++++++------ src/core/core.legend.js | 2 +- src/core/core.title.js | 2 +- src/platforms/platform.dom.js | 8 +++---- test/core.controller.tests.js | 24 ++++++++++---------- test/core.interaction.tests.js | 34 ++++++++++++++--------------- test/core.tooltip.tests.js | 14 ++++++------ test/mockContext.js | 26 ++++++++++------------ test/platform.dom.tests.js | 28 ++++++++++++------------ 14 files changed, 123 insertions(+), 126 deletions(-) diff --git a/samples/data_labelling.html b/samples/data_labelling.html index 939e517a9f2..4b6b19e16db 100644 --- a/samples/data_labelling.html +++ b/samples/data_labelling.html @@ -30,12 +30,12 @@ backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), borderColor: window.chartColors.red, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ] }, { @@ -44,12 +44,12 @@ backgroundColor: color(window.chartColors.blue).alpha(0.2).rgbString(), borderColor: window.chartColors.blue, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ] }, { @@ -58,12 +58,12 @@ backgroundColor: color(window.chartColors.green).alpha(0.2).rgbString(), borderColor: window.chartColors.green, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ] }] @@ -73,7 +73,7 @@ Chart.plugins.register({ afterDatasetsDraw: function(chartInstance, easing) { // To only draw at the end of animation, check for easing === 1 - var ctx = chartInstance.chart.ctx; + var ctx = chartInstance.ctx; chartInstance.data.datasets.forEach(function (dataset, i) { var meta = chartInstance.getDatasetMeta(i); @@ -120,7 +120,7 @@ document.getElementById('randomizeData').addEventListener('click', function() { barChartData.datasets.forEach(function(dataset) { - dataset.data = dataset.data.map(function() { + dataset.data = dataset.data.map(function() { return randomScalingFactor(); }) }); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 1d41386b6d7..cd746701e05 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -143,7 +143,7 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var stackCount = me.getStackCount(); - var tickWidth = xScale.width / xScale.ticks.length; + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; var fullBarWidth = categoryWidth / stackCount; @@ -169,7 +169,7 @@ module.exports = function(Chart) { if (xScale.options.barThickness) { return xScale.options.barThickness; } - return ruler.barWidth; + return ruler.barWidth; }, // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible @@ -219,7 +219,7 @@ module.exports = function(Chart) { (yScale.options.stacked === undefined && meta.stack !== undefined)) { var base = yScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; @@ -246,19 +246,20 @@ module.exports = function(Chart) { draw: function(ease) { var me = this; + var chart = me.chart; var easingDecimal = ease || 1; var metaData = me.getMeta().data; var dataset = me.getDataset(); var i, len; - Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); + Chart.canvasHelpers.clipArea(chart.ctx, chart.chartArea); for (i = 0, len = metaData.length; i < len; ++i) { var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { metaData[i].transition(easingDecimal).draw(); } } - Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); + Chart.canvasHelpers.unclipArea(chart.ctx); }, setHoverStyle: function(rectangle) { @@ -483,7 +484,7 @@ module.exports = function(Chart) { } } - return stacks.length - 1; + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex) { @@ -496,7 +497,7 @@ module.exports = function(Chart) { (xScale.options.stacked === undefined && meta.stack !== undefined)) { var base = xScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 806ab4f3f58..577ed75e2da 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -282,6 +282,7 @@ module.exports = function(Chart) { draw: function(ease) { var me = this; + var chart = me.chart; var meta = me.getMeta(); var points = meta.data || []; var easingDecimal = ease || 1; @@ -292,16 +293,16 @@ module.exports = function(Chart) { points[i].transition(easingDecimal); } - Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); + Chart.canvasHelpers.clipArea(chart.ctx, chart.chartArea); // Transition and Draw the line - if (lineEnabled(me.getDataset(), me.chart.options)) { + if (lineEnabled(me.getDataset(), chart.options)) { meta.dataset.transition(easingDecimal).draw(); } - Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); + Chart.canvasHelpers.unclipArea(chart.ctx); // Draw the points for (i=0, ilen=points.length; i 0) { @@ -210,7 +210,7 @@ module.exports = function(Chart) { * @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 = getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart); return getIntersectItems(chart, position); }, @@ -223,7 +223,7 @@ module.exports = function(Chart) { * @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 = getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart); var nearestItems = getNearestItems(chart, position, options.intersect); // We have multiple items at the same distance from the event. Now sort by smallest @@ -255,7 +255,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ x: function(chart, e, options) { - var position = getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart); var items = []; var intersectsItem = false; @@ -286,7 +286,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ y: function(chart, e, options) { - var position = getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart); var items = []; var intersectsItem = false; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 4816b285f2e..9874adfea54 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -493,7 +493,7 @@ module.exports = function(Chart) { function createNewLegendAndAttach(chartInstance, legendOpts) { var legend = new Chart.Legend({ - ctx: chartInstance.chart.ctx, + ctx: chartInstance.ctx, options: legendOpts, chart: chartInstance }); diff --git a/src/core/core.title.js b/src/core/core.title.js index 5b2d989f8a7..8e1416497e5 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -183,7 +183,7 @@ module.exports = function(Chart) { function createNewTitleBlockAndAttach(chartInstance, titleOpts) { var title = new Chart.Title({ - ctx: chartInstance.chart.ctx, + ctx: chartInstance.ctx, options: titleOpts, chart: chartInstance }); diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index abfb3dee3e1..de451b935a6 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -240,24 +240,24 @@ module.exports = function(Chart) { }, addEventListener: function(chart, type, listener) { - var canvas = chart.chart.canvas; + var canvas = chart.canvas; if (type === 'resize') { // Note: the resize event is not supported on all browsers. - addResizeListener(canvas.parentNode, listener, chart.chart); + addResizeListener(canvas.parentNode, listener, chart); return; } var stub = listener._chartjs || (listener._chartjs = {}); var proxies = stub.proxies || (stub.proxies = {}); var proxy = proxies[chart.id + '_' + type] = function(event) { - listener(fromNativeEvent(event, chart.chart)); + listener(fromNativeEvent(event, chart)); }; helpers.addEvent(canvas, type, proxy); }, removeEventListener: function(chart, type, listener) { - var canvas = chart.chart.canvas; + var canvas = chart.canvas; if (type === 'resize') { // Note: the resize event is not supported on all browsers. removeResizeListener(canvas.parentNode, listener); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index da4b77d1d21..9c5e9e26dd2 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -1,7 +1,7 @@ describe('Chart.Controller', function() { function waitForResize(chart, callback) { - var resizer = chart.chart.canvas.parentNode._chartjs.resizer; + var resizer = chart.canvas.parentNode._chartjs.resizer; var content = resizer.contentWindow || resizer; var state = content.document.readyState || 'complete'; var handler = function() { @@ -104,7 +104,7 @@ describe('Chart.Controller', function() { } }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; expect(wrapper.childNodes.length).toBe(1); expect(wrapper.firstChild.tagName).toBe('CANVAS'); }); @@ -152,7 +152,7 @@ describe('Chart.Controller', function() { rw: 300, rh: 350, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.width = '455px'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -192,7 +192,7 @@ describe('Chart.Controller', function() { rw: 300, rh: 350, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.height = '455px'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -233,7 +233,7 @@ describe('Chart.Controller', function() { rw: 320, rh: 350, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.height = '355px'; wrapper.style.width = '455px'; waitForResize(chart, function() { @@ -261,7 +261,7 @@ describe('Chart.Controller', function() { } }); - var canvas = chart.chart.canvas; + var canvas = chart.canvas; canvas.style.display = 'block'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -288,7 +288,7 @@ describe('Chart.Controller', function() { } }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.display = 'block'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -321,7 +321,7 @@ describe('Chart.Controller', function() { rw: 320, rh: 350, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; var parent = wrapper.parentNode; parent.removeChild(wrapper); parent.appendChild(wrapper); @@ -371,7 +371,7 @@ describe('Chart.Controller', function() { rw: 300, rh: 150, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.width = '450px'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -411,7 +411,7 @@ describe('Chart.Controller', function() { rw: 320, rh: 160, }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; wrapper.style.height = '455px'; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ @@ -487,7 +487,7 @@ describe('Chart.Controller', function() { } }); - var wrapper = chart.chart.canvas.parentNode; + var wrapper = chart.canvas.parentNode; var resizer = wrapper.firstChild; expect(wrapper.childNodes.length).toBe(2); @@ -635,7 +635,7 @@ describe('Chart.Controller', function() { } }); - chart.chart.canvas.parentNode.style.width = '400px'; + chart.canvas.parentNode.style.width = '400px'; waitForResize(chart, function() { chart.destroy(); diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js index 7d8e339caf9..fedcbfad864 100644 --- a/test/core.interaction.tests.js +++ b/test/core.interaction.tests.js @@ -27,7 +27,7 @@ describe('Core.Interaction', function() { var meta1 = chartInstance.getDatasetMeta(1); var point = meta0.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -63,7 +63,7 @@ describe('Core.Interaction', function() { }); // Trigger an event at (0, 0) - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var evt = { view: window, bubbles: true, @@ -103,7 +103,7 @@ describe('Core.Interaction', function() { var meta1 = chartInstance.getDatasetMeta(1); var point = meta0.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -142,7 +142,7 @@ describe('Core.Interaction', function() { var meta0 = chartInstance.getDatasetMeta(0); var meta1 = chartInstance.getDatasetMeta(1); - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -183,7 +183,7 @@ describe('Core.Interaction', function() { var meta = chartInstance.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -218,7 +218,7 @@ describe('Core.Interaction', function() { } }); - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -259,7 +259,7 @@ describe('Core.Interaction', function() { // Trigger an event over top of the var meta = chartInstance.getDatasetMeta(1); - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var evt = { view: window, bubbles: true, @@ -305,7 +305,7 @@ describe('Core.Interaction', function() { y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -352,7 +352,7 @@ describe('Core.Interaction', function() { y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -393,7 +393,7 @@ describe('Core.Interaction', function() { var meta = chartInstance.getDatasetMeta(1); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -450,7 +450,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -496,7 +496,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -542,7 +542,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -591,7 +591,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -649,7 +649,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -709,7 +709,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -767,7 +767,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index d96dfa302d4..f4d409466fa 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -30,7 +30,7 @@ describe('Core.Tooltip', function() { var meta = chartInstance.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -146,7 +146,7 @@ describe('Core.Tooltip', function() { var meta = chartInstance.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -234,7 +234,7 @@ describe('Core.Tooltip', function() { var meta = chartInstance.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -377,7 +377,7 @@ describe('Core.Tooltip', function() { var meta = chartInstance.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -494,7 +494,7 @@ describe('Core.Tooltip', function() { var meta0 = chartInstance.getDatasetMeta(0); var point0 = meta0.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -576,7 +576,7 @@ describe('Core.Tooltip', function() { var meta0 = chartInstance.getDatasetMeta(0); var point0 = meta0.data[1]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -644,7 +644,7 @@ describe('Core.Tooltip', function() { var datasetIndex = 0; var meta = chartInstance.getDatasetMeta(datasetIndex); var point = meta.data[pointIndex]; - var node = chartInstance.chart.canvas; + var node = chartInstance.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { view: window, diff --git a/test/mockContext.js b/test/mockContext.js index 0d12384778d..b6d2c13f535 100644 --- a/test/mockContext.js +++ b/test/mockContext.js @@ -177,20 +177,17 @@ 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)) { + if (!(actual instanceof Chart)) { + message = 'Expected ' + actual + ' to be an instance of Chart'; + } else if (!(actual.canvas instanceof HTMLCanvasElement)) { message = 'Expected canvas to be an instance of HTMLCanvasElement'; - } else if (!(chart.ctx instanceof CanvasRenderingContext2D)) { + } else if (!(actual.ctx instanceof CanvasRenderingContext2D)) { message = 'Expected context to be an instance of CanvasRenderingContext2D'; - } else if (typeof chart.height !== 'number' || !isFinite(chart.height)) { + } else if (typeof actual.height !== 'number' || !isFinite(actual.height)) { message = 'Expected height to be a strict finite number'; - } else if (typeof chart.width !== 'number' || !isFinite(chart.width)) { + } else if (typeof actual.width !== 'number' || !isFinite(actual.width)) { message = 'Expected width to be a strict finite number'; } @@ -211,8 +208,7 @@ } var message = null; - var chart = actual.chart; - var canvas = chart.ctx.canvas; + var canvas = actual.ctx.canvas; var style = getComputedStyle(canvas); var pixelRatio = window.devicePixelRatio; var dh = parseInt(style.height, 10); @@ -223,10 +219,10 @@ var orw = rw / pixelRatio; // sanity checks - if (chart.height !== orh) { - message = 'Expected chart height ' + chart.height + ' to be equal to original render height ' + orh; - } else if (chart.width !== orw) { - message = 'Expected chart width ' + chart.width + ' to be equal to original render width ' + orw; + if (actual.height !== orh) { + message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh; + } else if (actual.width !== orw) { + message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw; } // validity checks diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index a022cc75d5a..06562c9191f 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -1,7 +1,7 @@ describe('Platform.dom', function() { function waitForResize(chart, callback) { - var resizer = chart.chart.canvas.parentNode._chartjs.resizer; + var resizer = chart.canvas.parentNode._chartjs.resizer; var content = resizer.contentWindow || resizer; var state = content.document.readyState || 'complete'; var handler = function() { @@ -40,8 +40,8 @@ describe('Platform.dom', function() { var chart = new Chart(canvasId); expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); @@ -51,8 +51,8 @@ describe('Platform.dom', function() { var chart = new Chart(canvas); expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); @@ -63,8 +63,8 @@ describe('Platform.dom', function() { var chart = new Chart(context); expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(context); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(context); chart.destroy(); }); @@ -74,8 +74,8 @@ describe('Platform.dom', function() { var chart = new Chart([canvas]); expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); @@ -254,7 +254,7 @@ describe('Platform.dom', function() { describe('controller.destroy', function() { it('should reset context to default values', function() { var chart = acquireChart({}); - var context = chart.chart.ctx; + var context = chart.ctx; chart.destroy(); @@ -294,7 +294,7 @@ describe('Platform.dom', function() { } }); - var canvas = chart.chart.canvas; + var canvas = chart.canvas; var wrapper = canvas.parentNode; wrapper.style.width = '475px'; waitForResize(chart, function() { @@ -338,7 +338,7 @@ describe('Platform.dom', function() { plugins: [plugin] }); - var node = chart.chart.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var clientX = (rect.left + rect.right) / 2; var clientY = (rect.top + rect.bottom) / 2; @@ -362,8 +362,8 @@ describe('Platform.dom', function() { expect(notifiedEvent.type).toBe(evt.type); // Relative Position - expect(notifiedEvent.x).toBe(chart.chart.width / 2); - expect(notifiedEvent.y).toBe(chart.chart.height / 2); + expect(notifiedEvent.x).toBe(chart.width / 2); + expect(notifiedEvent.y).toBe(chart.height / 2); }); }); }); From 28ea6c4967cc9cf414f8b3c6ca41135d9e2dfffd Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 28 Jan 2017 12:52:40 +0100 Subject: [PATCH 123/685] Rename `chartInstance` to `chart` --- docs/01-Chart-Configuration.md | 22 ++--- docs/02-Scales.md | 10 +- docs/09-Advanced.md | 32 +++---- samples/data_labelling.html | 8 +- src/core/core.animation.js | 25 ++--- src/core/core.controller.js | 6 +- src/core/core.layoutService.js | 42 ++++----- src/core/core.legend.js | 34 +++---- src/core/core.scaleService.js | 6 +- src/core/core.title.js | 30 +++--- src/core/core.tooltip.js | 11 +-- test/core.interaction.tests.js | 162 ++++++++++++++++----------------- test/core.tooltip.tests.js | 60 ++++++------ 13 files changed, 224 insertions(+), 224 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 865a7558379..883d74ec009 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -21,7 +21,7 @@ yLabels | Array[string] | Optional parameter that is used with the category axis To create a chart with configuration options, simply pass an object containing your configuration to the constructor. In the example below, a line chart is created and configured to not be responsive. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -42,13 +42,13 @@ The following example would set the hover mode to 'nearest' for all charts where Chart.defaults.global.hover.mode = 'nearest'; // Hover mode is set to nearest because it was not overridden here -var chartInstanceHoverModeNearest = new Chart(ctx, { +var chartHoverModeNearest = new Chart(ctx, { type: 'line', data: data, }); // This chart would have the hover mode that was passed in -var chartInstanceDifferentHoverMode = new Chart(ctx, { +var chartDifferentHoverMode = new Chart(ctx, { type: 'line', data: data, options: { @@ -115,7 +115,7 @@ text | String | '' | Title text The example below would enable a title of 'Custom Chart Title' on the chart that is created. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -200,7 +200,7 @@ Items passed to the legend `onClick` function are the ones returned from `labels The following example will create a chart with the legend enabled and turn all of the text red in color. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'bar', data: data, options: { @@ -267,7 +267,7 @@ afterTitle | `Array[tooltipItem], data` | Text to render after the title beforeBody | `Array[tooltipItem], data` | Text to render before the body section beforeLabel | `tooltipItem, data` | Text to render before an individual label label | `tooltipItem, data` | Text to render for an individual item in the tooltip -labelColor | `tooltipItem, chartInstance` | Returns the colors to render for the tooltip item. Return as an object containing two parameters: `borderColor` and `backgroundColor`. +labelColor | `tooltipItem, chart` | Returns the colors to render for the tooltip item. Return as an object containing two parameters: `borderColor` and `backgroundColor`. afterLabel | `tooltipItem, data` | Text to render after an individual label afterBody | `Array[tooltipItem], data` | Text to render after the body section beforeFooter | `Array[tooltipItem], data` | Text to render before the footer section @@ -317,13 +317,13 @@ When configuring interaction with the graph via hover or tooltips, a number of d The following table details the modes and how they behave in conjunction with the `intersect` setting -Mode | Behaviour ---- | --- +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. +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 = false` 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 @@ -346,8 +346,8 @@ The `onProgress` and `onComplete` callbacks are useful for synchronizing an exte ```javascript { - // Chart object - chartInstance, + // Chart instance + chart, // Contains details of the on-going animation animationObject, diff --git a/docs/02-Scales.md b/docs/02-Scales.md index af7812ce03b..a8abfb4b382 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -98,7 +98,7 @@ The `callback` method may be used for advanced tick customization. In the follow If the callback returns `null` or `undefined` the associated grid line will be hidden. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -153,7 +153,7 @@ suggestedMin | Number | - | User defined minimum number for the scale, overrides The following example creates a line chart with a vertical axis that goes from 0 to 5 in 0.5 sized steps. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -188,7 +188,7 @@ max | Number | - | User defined maximum number for the scale, overrides maximum The following example creates a chart with a logarithmic X axis that ranges from 1 to 1000. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -250,7 +250,7 @@ year | 'YYYY' For example, to set the display format for the 'quarter' unit to show the month and year, the following config would be passed to the chart constructor. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { @@ -285,7 +285,7 @@ The following time measurements are supported. The names can be passed as string For example, to create a chart with a time scale that always displayed units per month, the following config could be used. ```javascript -var chartInstance = new Chart(ctx, { +var chart = new Chart(ctx, { type: 'line', data: data, options: { diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 05642fcab43..bdf02e7044b 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -415,33 +415,33 @@ Plugins will be called at the following times Plugins should implement the `IPlugin` interface: ```javascript { - beforeInit: function(chartInstance) { }, - afterInit: function(chartInstance) { }, + beforeInit: function(chart) { }, + afterInit: function(chart) { }, - resize: function(chartInstance, newChartSize) { }, + resize: function(chart, newChartSize) { }, - beforeUpdate: function(chartInstance) { }, - afterScaleUpdate: function(chartInstance) { } - beforeDatasetsUpdate: function(chartInstance) { } - afterDatasetsUpdate: function(chartInstance) { } - afterUpdate: function(chartInstance) { }, + beforeUpdate: function(chart) { }, + afterScaleUpdate: function(chart) { } + beforeDatasetsUpdate: function(chart) { } + afterDatasetsUpdate: function(chart) { } + afterUpdate: function(chart) { }, // This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw // to do something on each animation frame - beforeRender: function(chartInstance) { }, + beforeRender: function(chart) { }, // Easing is for animation - beforeDraw: function(chartInstance, easing) { }, - afterDraw: function(chartInstance, easing) { }, + beforeDraw: function(chart, easing) { }, + afterDraw: function(chart, easing) { }, // Before the datasets are drawn but after scales are drawn - beforeDatasetsDraw: function(chartInstance, easing) { }, - afterDatasetsDraw: function(chartInstance, easing) { }, + beforeDatasetsDraw: function(chart, easing) { }, + afterDatasetsDraw: function(chart, easing) { }, - destroy: function(chartInstance) { } + destroy: function(chart) { } // Called when an event occurs on the chart - beforeEvent: function(chartInstance, event) {} - afterEvent: function(chartInstance, event) {} + beforeEvent: function(chart, event) {} + afterEvent: function(chart, event) {} } ``` diff --git a/samples/data_labelling.html b/samples/data_labelling.html index 4b6b19e16db..8c64bcf0920 100644 --- a/samples/data_labelling.html +++ b/samples/data_labelling.html @@ -71,12 +71,12 @@ // Define a plugin to provide data labels Chart.plugins.register({ - afterDatasetsDraw: function(chartInstance, easing) { + afterDatasetsDraw: function(chart, easing) { // To only draw at the end of animation, check for easing === 1 - var ctx = chartInstance.ctx; + var ctx = chart.ctx; - chartInstance.data.datasets.forEach(function (dataset, i) { - var meta = chartInstance.getDatasetMeta(i); + chart.data.datasets.forEach(function (dataset, i) { + var meta = chart.getDatasetMeta(i); if (!meta.hidden) { meta.data.forEach(function(element, index) { // Draw the text in black, with the specified font diff --git a/src/core/core.animation.js b/src/core/core.animation.js index dcb9c2ee0de..c0f4a35f707 100644 --- a/src/core/core.animation.js +++ b/src/core/core.animation.js @@ -30,20 +30,20 @@ module.exports = function(Chart) { /** * @function Chart.animationService.addAnimation - * @param chartInstance {ChartController} the chart to animate + * @param chart {ChartController} the chart to animate * @param animationObject {IAnimation} the animation that we will animate * @param duration {Number} length of animation in ms * @param lazy {Boolean} if true, the chart is not marked as animating to enable more responsive interactions */ - addAnimation: function(chartInstance, animationObject, duration, lazy) { + addAnimation: function(chart, animationObject, duration, lazy) { var me = this; if (!lazy) { - chartInstance.animating = true; + chart.animating = true; } for (var index = 0; index < me.animations.length; ++index) { - if (me.animations[index].chartInstance === chartInstance) { + if (me.animations[index].chart === chart) { // replacing an in progress animation me.animations[index].animationObject = animationObject; return; @@ -51,7 +51,8 @@ module.exports = function(Chart) { } me.animations.push({ - chartInstance: chartInstance, + chart: chart, + chartInstance: chart, // deprecated, backward compatibility animationObject: animationObject }); @@ -61,14 +62,14 @@ module.exports = function(Chart) { } }, // Cancel the animation for a given chart instance - cancelAnimation: function(chartInstance) { + cancelAnimation: function(chart) { var index = helpers.findIndex(this.animations, function(animationWrapper) { - return animationWrapper.chartInstance === chartInstance; + return animationWrapper.chart === chart; }); if (index !== -1) { this.animations.splice(index, 1); - chartInstance.animating = false; + chart.animating = false; } }, requestAnimationFrame: function() { @@ -106,18 +107,18 @@ module.exports = function(Chart) { me.animations[i].animationObject.currentStep = me.animations[i].animationObject.numSteps; } - me.animations[i].animationObject.render(me.animations[i].chartInstance, me.animations[i].animationObject); + me.animations[i].animationObject.render(me.animations[i].chart, me.animations[i].animationObject); if (me.animations[i].animationObject.onAnimationProgress && me.animations[i].animationObject.onAnimationProgress.call) { - me.animations[i].animationObject.onAnimationProgress.call(me.animations[i].chartInstance, me.animations[i]); + me.animations[i].animationObject.onAnimationProgress.call(me.animations[i].chart, me.animations[i]); } if (me.animations[i].animationObject.currentStep === me.animations[i].animationObject.numSteps) { if (me.animations[i].animationObject.onAnimationComplete && me.animations[i].animationObject.onAnimationComplete.call) { - me.animations[i].animationObject.onAnimationComplete.call(me.animations[i].chartInstance, me.animations[i]); + me.animations[i].animationObject.onAnimationComplete.call(me.animations[i].chart, me.animations[i]); } // executed the last frame. Remove the animation. - me.animations[i].chartInstance.animating = false; + me.animations[i].chart.animating = false; me.animations.splice(i, 1); } else { diff --git a/src/core/core.controller.js b/src/core/core.controller.js index bf973929886..57a6cfe4210 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -419,12 +419,12 @@ module.exports = function(Chart) { animation.easing = animationOptions.easing; // render function - animation.render = function(chartInstance, animationObject) { + animation.render = function(chart, animationObject) { var easingFunction = helpers.easingEffects[animationObject.easing]; var stepDecimal = animationObject.currentStep / animationObject.numSteps; var easeDecimal = easingFunction(stepDecimal); - chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); + chart.draw(easeDecimal, stepDecimal, animationObject.currentStep); }; // user events @@ -601,7 +601,7 @@ module.exports = function(Chart) { var me = this; me.tooltip = new Chart.Tooltip({ _chart: me, - _chartInstance: me, + _chartInstance: me, // deprecated, backward compatibility _data: me.data, _options: me.options.tooltips }, me); diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 39cf26b9006..bd82e4780e9 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -10,29 +10,29 @@ module.exports = function(Chart) { Chart.layoutService = { defaults: {}, - // Register a box to a chartInstance. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins. - addBox: function(chartInstance, box) { - if (!chartInstance.boxes) { - chartInstance.boxes = []; + // Register a box to a chart. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins. + addBox: function(chart, box) { + if (!chart.boxes) { + chart.boxes = []; } - chartInstance.boxes.push(box); + chart.boxes.push(box); }, - removeBox: function(chartInstance, box) { - if (!chartInstance.boxes) { + removeBox: function(chart, box) { + if (!chart.boxes) { return; } - chartInstance.boxes.splice(chartInstance.boxes.indexOf(box), 1); + chart.boxes.splice(chart.boxes.indexOf(box), 1); }, // The most important function - update: function(chartInstance, width, height) { + update: function(chart, width, height) { - if (!chartInstance) { + if (!chart) { return; } - var layoutOptions = chartInstance.options.layout; + var layoutOptions = chart.options.layout; var padding = layoutOptions ? layoutOptions.padding : null; var leftPadding = 0; @@ -53,21 +53,21 @@ module.exports = function(Chart) { bottomPadding = padding.bottom || 0; } - var leftBoxes = helpers.where(chartInstance.boxes, function(box) { + var leftBoxes = helpers.where(chart.boxes, function(box) { return box.options.position === 'left'; }); - var rightBoxes = helpers.where(chartInstance.boxes, function(box) { + var rightBoxes = helpers.where(chart.boxes, function(box) { return box.options.position === 'right'; }); - var topBoxes = helpers.where(chartInstance.boxes, function(box) { + var topBoxes = helpers.where(chart.boxes, function(box) { return box.options.position === 'top'; }); - var bottomBoxes = helpers.where(chartInstance.boxes, function(box) { + var bottomBoxes = helpers.where(chart.boxes, function(box) { return box.options.position === 'bottom'; }); // Boxes that overlay the chartarea such as the radialLinear scale - var chartAreaBoxes = helpers.where(chartInstance.boxes, function(box) { + var chartAreaBoxes = helpers.where(chart.boxes, function(box) { return box.options.position === 'chartArea'; }); @@ -348,7 +348,7 @@ module.exports = function(Chart) { helpers.each(bottomBoxes, placeBox); // Step 8 - chartInstance.chartArea = { + chart.chartArea = { left: totalLeftBoxesWidth, top: totalTopBoxesHeight, right: totalLeftBoxesWidth + maxChartAreaWidth, @@ -357,10 +357,10 @@ module.exports = function(Chart) { // Step 9 helpers.each(chartAreaBoxes, function(box) { - box.left = chartInstance.chartArea.left; - box.top = chartInstance.chartArea.top; - box.right = chartInstance.chartArea.right; - box.bottom = chartInstance.chartArea.bottom; + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; box.update(maxChartAreaWidth, maxChartAreaHeight); }); diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 9874adfea54..9e181193208 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -491,43 +491,43 @@ module.exports = function(Chart) { } }); - function createNewLegendAndAttach(chartInstance, legendOpts) { + function createNewLegendAndAttach(chart, legendOpts) { var legend = new Chart.Legend({ - ctx: chartInstance.ctx, + ctx: chart.ctx, options: legendOpts, - chart: chartInstance + chart: chart }); - chartInstance.legend = legend; - Chart.layoutService.addBox(chartInstance, legend); + chart.legend = legend; + Chart.layoutService.addBox(chart, legend); } // Register the legend plugin Chart.plugins.register({ - beforeInit: function(chartInstance) { - var legendOpts = chartInstance.options.legend; + beforeInit: function(chart) { + var legendOpts = chart.options.legend; if (legendOpts) { - createNewLegendAndAttach(chartInstance, legendOpts); + createNewLegendAndAttach(chart, legendOpts); } }, - beforeUpdate: function(chartInstance) { - var legendOpts = chartInstance.options.legend; + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; if (legendOpts) { legendOpts = helpers.configMerge(Chart.defaults.global.legend, legendOpts); - if (chartInstance.legend) { - chartInstance.legend.options = legendOpts; + if (chart.legend) { + chart.legend.options = legendOpts; } else { - createNewLegendAndAttach(chartInstance, legendOpts); + createNewLegendAndAttach(chart, legendOpts); } } else { - Chart.layoutService.removeBox(chartInstance, chartInstance.legend); - delete chartInstance.legend; + Chart.layoutService.removeBox(chart, chart.legend); + delete chart.legend; } }, - afterEvent: function(chartInstance, e) { - var legend = chartInstance.legend; + afterEvent: function(chart, e) { + var legend = chart.legend; if (legend) { legend.handleEvent(e); } diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 81a95079053..4f3dea242cb 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -30,10 +30,10 @@ module.exports = function(Chart) { defaults[type] = helpers.extend(defaults[type], additions); } }, - addScalesToLayout: function(chartInstance) { + addScalesToLayout: function(chart) { // Adds each scale to the chart.boxes array to be sized accordingly - helpers.each(chartInstance.scales, function(scale) { - Chart.layoutService.addBox(chartInstance, scale); + helpers.each(chart.scales, function(scale) { + Chart.layoutService.addBox(chart, scale); }); } }; diff --git a/src/core/core.title.js b/src/core/core.title.js index 8e1416497e5..a1a3c8f4309 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -181,39 +181,39 @@ module.exports = function(Chart) { } }); - function createNewTitleBlockAndAttach(chartInstance, titleOpts) { + function createNewTitleBlockAndAttach(chart, titleOpts) { var title = new Chart.Title({ - ctx: chartInstance.ctx, + ctx: chart.ctx, options: titleOpts, - chart: chartInstance + chart: chart }); - chartInstance.titleBlock = title; - Chart.layoutService.addBox(chartInstance, title); + chart.titleBlock = title; + Chart.layoutService.addBox(chart, title); } // Register the title plugin Chart.plugins.register({ - beforeInit: function(chartInstance) { - var titleOpts = chartInstance.options.title; + beforeInit: function(chart) { + var titleOpts = chart.options.title; if (titleOpts) { - createNewTitleBlockAndAttach(chartInstance, titleOpts); + createNewTitleBlockAndAttach(chart, titleOpts); } }, - beforeUpdate: function(chartInstance) { - var titleOpts = chartInstance.options.title; + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; if (titleOpts) { titleOpts = helpers.configMerge(Chart.defaults.global.title, titleOpts); - if (chartInstance.titleBlock) { - chartInstance.titleBlock.options = titleOpts; + if (chart.titleBlock) { + chart.titleBlock.options = titleOpts; } else { - createNewTitleBlockAndAttach(chartInstance, titleOpts); + createNewTitleBlockAndAttach(chart, titleOpts); } } else { - Chart.layoutService.removeBox(chartInstance, chartInstance.titleBlock); - delete chartInstance.titleBlock; + Chart.layoutService.removeBox(chart, chart.titleBlock); + delete chart.titleBlock; } } }); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 32589b652e6..4084b956869 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -70,8 +70,8 @@ module.exports = function(Chart) { var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; return datasetLabel + ': ' + tooltipItem.yLabel; }, - labelColor: function(tooltipItem, chartInstance) { - var meta = chartInstance.getDatasetMeta(tooltipItem.datasetIndex); + labelColor: function(tooltipItem, chart) { + var meta = chart.getDatasetMeta(tooltipItem.datasetIndex); var activeElement = meta.data[tooltipItem.index]; var view = activeElement._view; return { @@ -249,7 +249,7 @@ module.exports = function(Chart) { function determineAlignment(tooltip, size) { var model = tooltip._model; var chart = tooltip._chart; - var chartArea = tooltip._chartInstance.chartArea; + var chartArea = tooltip._chart.chartArea; var xAlign = 'center'; var yAlign = 'center'; @@ -452,7 +452,6 @@ module.exports = function(Chart) { 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 = { @@ -501,7 +500,7 @@ module.exports = function(Chart) { // Determine colors for boxes helpers.each(tooltipItems, function(tooltipItem) { - labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance)); + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); }); // Build the Text Lines @@ -777,7 +776,7 @@ module.exports = function(Chart) { if (e.type === 'mouseout') { me._active = []; } else { - me._active = me._chartInstance.getElementsAtEventForMode(e, options.mode, options); + me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); } // Remember Last Actives diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js index fedcbfad864..377fb401bd1 100644 --- a/test/core.interaction.tests.js +++ b/test/core.interaction.tests.js @@ -4,7 +4,7 @@ describe('Core.Interaction', function() { describe('point mode', function() { it ('should return all items under the point', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -23,11 +23,11 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); var point = meta0.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -39,12 +39,12 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.point(chartInstance, evt); + var elements = Chart.Interaction.modes.point(chart, evt); expect(elements).toEqual([point, meta1.data[1]]); }); it ('should return an empty array when no items are found', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -63,7 +63,7 @@ describe('Core.Interaction', function() { }); // Trigger an event at (0, 0) - var node = chartInstance.canvas; + var node = chart.canvas; var evt = { view: window, bubbles: true, @@ -73,14 +73,14 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.point(chartInstance, evt); + var elements = Chart.Interaction.modes.point(chart, evt); expect(elements).toEqual([]); }); }); describe('index mode', function() { it ('should return all items at the same index', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -99,11 +99,11 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); var point = meta0.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -115,12 +115,12 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: true}); + var elements = Chart.Interaction.modes.index(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -139,10 +139,10 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -154,14 +154,14 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.index(chartInstance, evt, {intersect: false}); + var elements = Chart.Interaction.modes.index(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -180,10 +180,10 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); + var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -195,12 +195,12 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: true}); + var elements = Chart.Interaction.modes.dataset(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -218,7 +218,7 @@ describe('Core.Interaction', function() { } }); - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { @@ -230,16 +230,16 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.dataset(chartInstance, evt, {intersect: false}); + var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: false}); - var meta = chartInstance.getDatasetMeta(1); + var meta = chart.getDatasetMeta(1); expect(elements).toEqual(meta.data); }); }); describe('nearest mode', function() { it ('should return the nearest item', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -258,8 +258,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(1); - var node = chartInstance.canvas; + var meta = chart.getDatasetMeta(1); + var node = chart.canvas; var evt = { view: window, bubbles: true, @@ -270,12 +270,12 @@ 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(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -296,8 +296,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -305,7 +305,7 @@ describe('Core.Interaction', function() { y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -317,12 +317,12 @@ 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(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -343,8 +343,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -352,7 +352,7 @@ describe('Core.Interaction', function() { y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -364,14 +364,14 @@ 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(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -390,10 +390,10 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(1); + var meta = chart.getDatasetMeta(1); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -405,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(chart, evt, {intersect: true}); expect(elements).toEqual([]); evt = { @@ -416,12 +416,12 @@ 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(chart, evt, {intersect: true}); expect(elements).toEqual([point]); }); it ('should return the nearest item even if 2 intersect', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -442,7 +442,7 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); + var meta0 = chart.getDatasetMeta(0); // Halfway between 2 mid points var pt = { @@ -450,7 +450,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -462,12 +462,12 @@ 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(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -488,7 +488,7 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); + var meta0 = chart.getDatasetMeta(0); // Halfway between 2 mid points var pt = { @@ -496,7 +496,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -508,12 +508,12 @@ 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(chart, 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({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -534,7 +534,7 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); + var meta0 = chart.getDatasetMeta(0); // Halfway between 2 mid points var pt = { @@ -542,7 +542,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -554,14 +554,14 @@ 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(chart, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1]]); }); }); describe('x mode', function() { it('should return items at the same x value when intersect is false', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -582,8 +582,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -591,7 +591,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -602,7 +602,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: false}); + var elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); evt = { @@ -614,12 +614,12 @@ describe('Core.Interaction', function() { currentTarget: node }; - elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: false}); + elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}); expect(elements).toEqual([]); }); it('should return items at the same x value when intersect is true', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -640,8 +640,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -649,7 +649,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -660,7 +660,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: true}); + var elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}); expect(elements).toEqual([]); // we don't intersect anything evt = { @@ -672,14 +672,14 @@ describe('Core.Interaction', function() { currentTarget: node }; - elements = Chart.Interaction.modes.x(chartInstance, evt, {intersect: true}); + elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); }); }); describe('y mode', function() { it('should return items at the same y value when intersect is false', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -700,8 +700,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -709,7 +709,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -720,7 +720,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: false}); + var elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}); expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); evt = { @@ -732,12 +732,12 @@ describe('Core.Interaction', function() { currentTarget: node }; - elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: false}); + elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}); expect(elements).toEqual([]); }); it('should return items at the same y value when intersect is true', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -758,8 +758,8 @@ describe('Core.Interaction', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var meta1 = chartInstance.getDatasetMeta(1); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { @@ -767,7 +767,7 @@ describe('Core.Interaction', function() { y: meta0.data[1]._view.y }; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = { view: window, @@ -778,7 +778,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - var elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: true}); + var elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}); expect(elements).toEqual([]); // we don't intersect anything evt = { @@ -790,7 +790,7 @@ describe('Core.Interaction', function() { currentTarget: node }; - elements = Chart.Interaction.modes.y(chartInstance, evt, {intersect: true}); + elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}); expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); }); }); diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index f4d409466fa..6640bbd536d 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -2,7 +2,7 @@ describe('Core.Tooltip', function() { describe('index mode', function() { it('Should only use x distance when intersect is false', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -27,10 +27,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); + var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -45,7 +45,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ @@ -118,7 +118,7 @@ describe('Core.Tooltip', function() { }); it('Should only display if intersecting if intersect is set', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -143,10 +143,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); + var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -161,7 +161,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ @@ -207,7 +207,7 @@ describe('Core.Tooltip', function() { }); it('Should display in single mode', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -231,10 +231,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); + var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -249,7 +249,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ @@ -315,7 +315,7 @@ describe('Core.Tooltip', function() { }); it('Should display information from user callbacks', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -374,10 +374,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta = chartInstance.getDatasetMeta(0); + var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -392,7 +392,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ @@ -464,7 +464,7 @@ describe('Core.Tooltip', function() { }); it('Should display information from user callbacks', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -491,10 +491,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); + var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -509,7 +509,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; expect(tooltip._view).toEqual(jasmine.objectContaining({ // Positioning @@ -544,7 +544,7 @@ describe('Core.Tooltip', function() { }); it('should filter items from the tooltip using the callback', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -573,10 +573,10 @@ describe('Core.Tooltip', function() { }); // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); + var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { @@ -591,7 +591,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; expect(tooltip._view).toEqual(jasmine.objectContaining({ // Positioning @@ -616,7 +616,7 @@ describe('Core.Tooltip', function() { }); it('Should have dataPoints', function() { - var chartInstance = window.acquireChart({ + var chart = window.acquireChart({ type: 'line', data: { datasets: [{ @@ -642,9 +642,9 @@ describe('Core.Tooltip', function() { // Trigger an event over top of the var pointIndex = 1; var datasetIndex = 0; - var meta = chartInstance.getDatasetMeta(datasetIndex); + var meta = chart.getDatasetMeta(datasetIndex); var point = meta.data[pointIndex]; - var node = chartInstance.canvas; + var node = chart.canvas; var rect = node.getBoundingClientRect(); var evt = new MouseEvent('mousemove', { view: window, @@ -658,7 +658,7 @@ describe('Core.Tooltip', function() { node.dispatchEvent(evt); // Check and see if tooltip was displayed - var tooltip = chartInstance.tooltip; + var tooltip = chart.tooltip; expect(tooltip._view instanceof Object).toBe(true); expect(tooltip._view.dataPoints instanceof Array).toBe(true); @@ -666,10 +666,10 @@ describe('Core.Tooltip', function() { 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] + chart.config.data.labels[pointIndex] ); expect(tooltip._view.dataPoints[0].yLabel).toEqual( - chartInstance.config.data.datasets[datasetIndex].data[pointIndex] + chart.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 ba6e041713a50eaf1c31000c91c187fb580f88a6 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 28 Jan 2017 13:17:38 +0100 Subject: [PATCH 124/685] Unit tests for Chart constructor and deprecations --- src/core/core.controller.js | 1 + src/core/core.interaction.js | 4 ++ test/core.controller.tests.js | 13 ++++- test/global.deprecations.tests.js | 94 +++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 test/global.deprecations.tests.js diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 57a6cfe4210..0d4c128bad7 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -373,6 +373,7 @@ module.exports = function(Chart) { * @method IPlugin#afterScaleUpdate * @deprecated since version 2.5.0 * @todo remove at version 3 + * @private */ plugins.notify(me, 'afterScaleUpdate'); plugins.notify(me, 'afterLayout'); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 53a479f61fc..c2c4ffcbf91 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -158,6 +158,8 @@ module.exports = function(Chart) { /** * @function Chart.Interaction.modes.label * @deprecated since version 2.4.0 + * @todo remove at version 3 + * @private */ label: indexMode, @@ -196,6 +198,8 @@ module.exports = function(Chart) { /** * @function Chart.Interaction.modes.x-axis * @deprecated since version 2.4.0. Use index mode and intersect == true + * @todo remove at version 3 + * @private */ 'x-axis': function(chart, e) { return indexMode(chart, e, true); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 9c5e9e26dd2..f29542cbf59 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -1,4 +1,4 @@ -describe('Chart.Controller', function() { +describe('Chart', function() { function waitForResize(chart, callback) { var resizer = chart.canvas.parentNode._chartjs.resizer; @@ -13,6 +13,17 @@ describe('Chart.Controller', function() { Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); } + // https://github.com/chartjs/Chart.js/issues/2481 + // See global.deprecations.tests.js for backward compatibility + it('should be defined and prototype of chart instances', function() { + var chart = acquireChart({}); + expect(Chart).toBeDefined(); + expect(Chart instanceof Object).toBeTruthy(); + expect(chart.constructor).toBe(Chart); + expect(chart instanceof Chart).toBeTruthy(); + expect(Chart.prototype.isPrototypeOf(chart)).toBeTruthy(); + }); + describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); diff --git a/test/global.deprecations.tests.js b/test/global.deprecations.tests.js new file mode 100644 index 00000000000..a6b4f70f55c --- /dev/null +++ b/test/global.deprecations.tests.js @@ -0,0 +1,94 @@ +describe('Deprecations', function() { + describe('Version 2.6.0', function() { + // https://github.com/chartjs/Chart.js/issues/2481 + describe('Chart.Controller', function() { + it('should be defined and an alias of Chart', function() { + expect(Chart.Controller).toBeDefined(); + expect(Chart.Controller).toBe(Chart); + }); + it('should be prototype of chart instances', function() { + var chart = acquireChart({}); + expect(chart.constructor).toBe(Chart.Controller); + expect(chart instanceof Chart.Controller).toBeTruthy(); + expect(Chart.Controller.prototype.isPrototypeOf(chart)).toBeTruthy(); + }); + }); + + describe('chart.chart', function() { + it('should be defined and an alias of chart', function() { + var chart = acquireChart({}); + var proxy = chart.chart; + expect(proxy).toBeDefined(); + expect(proxy).toBe(chart); + }); + it('should defined previously existing properties', function() { + var chart = acquireChart({}, { + canvas: { + style: 'width: 140px; height: 320px' + } + }); + + var proxy = chart.chart; + expect(proxy.config instanceof Object).toBeTruthy(); + expect(proxy.controller instanceof Chart.Controller).toBeTruthy(); + expect(proxy.canvas instanceof HTMLCanvasElement).toBeTruthy(); + expect(proxy.ctx instanceof CanvasRenderingContext2D).toBeTruthy(); + expect(proxy.currentDevicePixelRatio).toBe(window.devicePixelRatio || 1); + expect(proxy.aspectRatio).toBe(140/320); + expect(proxy.height).toBe(320); + expect(proxy.width).toBe(140); + }); + }); + }); + + describe('Version 2.5.0', function() { + describe('Chart.PluginBase', function() { + it('should exist and extendable', function() { + expect(Chart.PluginBase).toBeDefined(); + expect(Chart.PluginBase.extend).toBeDefined(); + }); + }); + + describe('IPlugin.afterScaleUpdate', function() { + it('should be called after the chart as been layed out', function() { + var sequence = []; + var plugin = {}; + var hooks = [ + 'beforeLayout', + 'afterScaleUpdate', + 'afterLayout' + ]; + + var override = Chart.layoutService.update; + Chart.layoutService.update = function() { + sequence.push('layoutUpdate'); + override.apply(this, arguments); + }; + + hooks.forEach(function(name) { + plugin[name] = function() { + sequence.push(name); + }; + }); + + window.acquireChart({plugins: [plugin]}); + expect(sequence).toEqual([].concat( + 'beforeLayout', + 'layoutUpdate', + 'afterScaleUpdate', + 'afterLayout' + )); + }); + }); + }); + + describe('Version 2.1.5', function() { + // https://github.com/chartjs/Chart.js/pull/2752 + describe('Chart.pluginService', function() { + it('should be defined and an alias of Chart.plugins', function() { + expect(Chart.pluginService).toBeDefined(); + expect(Chart.pluginService).toBe(Chart.plugins); + }); + }); + }); +}); From 074ab2aa879704ef37b9e507f05e610059ccc237 Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Fri, 13 Jan 2017 08:39:03 -0800 Subject: [PATCH 125/685] Fixed HorizontalBar: stacked axis, displays NaN when all legends unselected (#3770) added ability to take snapshot of chart limits in order to be used when max and min value is not finite added ignore files --- src/scales/scale.linear.js | 6 ++++++ test/scale.linear.tests.js | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 319243b23e5..1fe840eec73 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -12,6 +12,7 @@ module.exports = function(Chart) { }; var LinearScale = Chart.LinearScaleBase.extend({ + determineDataLimits: function() { var me = this; var opts = me.options; @@ -19,6 +20,8 @@ module.exports = function(Chart) { var data = chart.data; var datasets = data.datasets; var isHorizontal = me.isHorizontal(); + var DEFAULT_MIN = 0; + var DEFAULT_MAX = 1; function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; @@ -121,6 +124,9 @@ module.exports = function(Chart) { }); } + me.min = isFinite(me.min) ? me.min : DEFAULT_MIN; + me.max = isFinite(me.max) ? me.max : DEFAULT_MAX; + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero this.handleTickRangeOptions(); }, diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 44c4dcf29e3..0f63e8ce587 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -801,4 +801,47 @@ describe('Linear Scale', function() { var yScale = chart.scales.yScale0; expect(yScale.width).toBeCloseToPixel(0); }); + + it('max and min value should be valid and finite when charts datasets are hidden', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'Closed', + backgroundColor: '#382765', + data: [2500, 2000, 1500] + }, { + label: 'In Progress', + backgroundColor: '#7BC225', + data: [1000, 2000, 1500] + }, { + label: 'Assigned', + backgroundColor: '#ffC225', + data: [1000, 2000, 1500] + }] + }; + + var chart = window.acquireChart({ + type: 'horizontalBar', + data: barData, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + barData.datasets.forEach(function(data, index) { + var meta = chart.getDatasetMeta(index); + meta.hidden = true; + chart.update(); + }); + + expect(chart.scales['x-axis-0'].min).toEqual(0); + expect(chart.scales['x-axis-0'].max).toEqual(1); + }); }); From 9f3b51a80ce96578718267711e8b65c1ec8c25c1 Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Tue, 24 Jan 2017 18:06:18 +0000 Subject: [PATCH 126/685] Reuse parsed results rather than redoing work The input labels/data is converted into moments in `determineDataLimits`, reuse them instead of duplicating the work. --- src/scales/scale.time.js | 99 +++++++++++----------------------------- test/scale.time.tests.js | 64 -------------------------- 2 files changed, 27 insertions(+), 136 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 9a3e31e63b3..e922734fb00 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -76,17 +76,6 @@ module.exports = function(Chart) { Chart.Scale.prototype.initialize.call(this); }, - getLabelMoment: function(datasetIndex, index) { - if (datasetIndex === null || index === null) { - return null; - } - - if (typeof this.labelMoments[datasetIndex] !== 'undefined') { - return this.labelMoments[datasetIndex][index]; - } - - return null; - }, getLabelDiff: function(datasetIndex, index) { var me = this; if (datasetIndex === null || index === null) { @@ -114,23 +103,26 @@ module.exports = function(Chart) { var me = this; me.labelMoments = []; + function appendLabel(array, label) { + var labelMoment = me.parseTime(label); + if (labelMoment.isValid()) { + if (me.options.time.round) { + labelMoment.startOf(me.options.time.round); + } + array.push(labelMoment); + } + } + // Only parse these once. If the dataset does not have data as x,y pairs, we will use // these var scaleLabelMoments = []; if (me.chart.data.labels && me.chart.data.labels.length > 0) { helpers.each(me.chart.data.labels, function(label) { - var labelMoment = me.parseTime(label); - - if (labelMoment.isValid()) { - if (me.options.time.round) { - labelMoment.startOf(me.options.time.round); - } - scaleLabelMoments.push(labelMoment); - } + appendLabel(scaleLabelMoments, label); }, me); - me.firstTick = moment.min.call(me, scaleLabelMoments); - me.lastTick = moment.max.call(me, scaleLabelMoments); + me.firstTick = moment.min(scaleLabelMoments); + me.lastTick = moment.max(scaleLabelMoments); } else { me.firstTick = null; me.lastTick = null; @@ -138,25 +130,19 @@ module.exports = function(Chart) { helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { var momentsForDataset = []; - var datasetVisible = me.chart.isDatasetVisible(datasetIndex); if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null) { helpers.each(dataset.data, function(value) { - var labelMoment = me.parseTime(me.getRightValue(value)); - - if (labelMoment.isValid()) { - if (me.options.time.round) { - labelMoment.startOf(me.options.time.round); - } - momentsForDataset.push(labelMoment); - - if (datasetVisible) { - // May have gone outside the scale ranges, make sure we keep the first and last ticks updated - me.firstTick = me.firstTick !== null ? moment.min(me.firstTick, labelMoment) : labelMoment; - me.lastTick = me.lastTick !== null ? moment.max(me.lastTick, labelMoment) : labelMoment; - } - } + appendLabel(momentsForDataset, me.getRightValue(value)); }, me); + + if (me.chart.isDatasetVisible(datasetIndex)) { + // May have gone outside the scale ranges, make sure we keep the first and last ticks updated + var min = moment.min(momentsForDataset); + var max = moment.max(momentsForDataset); + me.firstTick = me.firstTick !== null ? moment.min(me.firstTick, min) : min; + me.lastTick = me.lastTick !== null ? moment.max(me.lastTick, max) : max; + } } else { // We have no labels. Use the ones from the scale momentsForDataset = scaleLabelMoments; @@ -180,43 +166,12 @@ module.exports = function(Chart) { }, buildLabelDiffs: function() { var me = this; - me.labelDiffs = []; - var scaleLabelDiffs = []; - // Parse common labels once - if (me.chart.data.labels && me.chart.data.labels.length > 0) { - helpers.each(me.chart.data.labels, function(label) { - var labelMoment = me.parseTime(label); - - if (labelMoment.isValid()) { - if (me.options.time.round) { - labelMoment.startOf(me.options.time.round); - } - scaleLabelDiffs.push(labelMoment.diff(me.firstTick, me.tickUnit, true)); - } - }, me); - } - - helpers.each(me.chart.data.datasets, function(dataset) { - var diffsForDataset = []; - if (typeof dataset.data[0] === 'object' && dataset.data[0] !== null) { - helpers.each(dataset.data, function(value) { - var labelMoment = me.parseTime(me.getRightValue(value)); - - if (labelMoment.isValid()) { - if (me.options.time.round) { - labelMoment.startOf(me.options.time.round); - } - diffsForDataset.push(labelMoment.diff(me.firstTick, me.tickUnit, true)); - } - }, me); - } else { - // We have no labels. Use common ones - diffsForDataset = scaleLabelDiffs; - } - - me.labelDiffs.push(diffsForDataset); - }, me); + me.labelDiffs = me.labelMoments.map(function(datasetLabels) { + return datasetLabels.map(function(label) { + return label.diff(me.firstTick, me.tickUnit, true); + }); + }); }, buildTicks: function() { var me = this; diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index ba2ef2f8a2b..43022bcde08 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -485,68 +485,4 @@ describe('Time scale tests', function() { threshold: 0.75 }); }); - - 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'], - datasets: [{ - xAxisID: 'xScale0', - data: [5] - }] - }, - options: { - scales: { - xAxes: [{ - id: 'xScale0', - display: true, - type: 'time', - }] - } - } - }); - - var xScale = chart.scales.xScale0; - var getOutOfBoundLabelMoment = function() { - xScale.getLabelMoment(12, 0); - }; - - expect(getOutOfBoundLabelMoment).not.toThrow(); - }); - - 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'], - datasets: [{ - xAxisID: 'xScale0', - data: [5] - }] - }, - options: { - scales: { - xAxes: [{ - id: 'xScale0', - display: true, - type: 'time', - }] - } - } - }); - - var xScale = chart.scales.xScale0; - - var getNullDatasetIndexLabelMoment = function() { - xScale.getLabelMoment(null, 1); - }; - - var getNullIndexLabelMoment = function() { - xScale.getLabelMoment(1, null); - }; - - expect(getNullDatasetIndexLabelMoment).not.toThrow(); - expect(getNullIndexLabelMoment).not.toThrow(); - }); }); From 8f217182b83fc6058fd10ebb73584fbbbbc9703a Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 10 Feb 2017 15:51:37 -0800 Subject: [PATCH 127/685] Update tooltip only when active element has changed (#3856) Resolves #3746 --- src/core/core.tooltip.js | 6 ++++ test/core.tooltip.tests.js | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 4084b956869..8496b50f71e 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -781,6 +781,12 @@ module.exports = function(Chart) { // Remember Last Actives changed = !helpers.arrayEquals(me._active, me._lastActive); + + // If tooltip didn't change, do not handle the target event + if (!changed) { + return false; + } + me._lastActive = me._active; if (options.enabled || options.custom) { diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 6640bbd536d..503e2d2ca4b 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -674,4 +674,65 @@ describe('Core.Tooltip', function() { expect(tooltip._view.dataPoints[0].x).toBeCloseToPixel(point._model.x); expect(tooltip._view.dataPoints[0].y).toBeCloseToPixel(point._model.y); }); + + it('Should not update if active element has not changed', function() { + var chart = window.acquireChart({ + type: 'bar', + 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', + callbacks: { + title: function() { + return 'registering callback...'; + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var firstPoint = meta.data[1]; + + var node = chart.chart.canvas; + var rect = node.getBoundingClientRect(); + + var firstEvent = new MouseEvent('mousemove', { + view: window, + bubbles: false, + cancelable: true, + clientX: rect.left + firstPoint._model.x, + clientY: rect.top + firstPoint._model.y + }); + + var tooltip = chart.tooltip; + spyOn(tooltip, 'update'); + + /* Manually trigger rather than having an async test */ + + // First dispatch change event, should update tooltip + node.dispatchEvent(firstEvent); + expect(tooltip.update).toHaveBeenCalledWith(true); + + // Reset calls + tooltip.update.calls.reset(); + + // Second dispatch change event (same event), should not update tooltip + node.dispatchEvent(firstEvent); + expect(tooltip.update).not.toHaveBeenCalled(); + }); }); From fed42e218a3b8cb3280e15828e0e33da6eee8a2e Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 28 Jan 2017 17:06:20 -0500 Subject: [PATCH 128/685] When the dataset label is not defined, the tooltip label string should not include a ':' character. Added a test to cover this case. --- src/core/core.tooltip.js | 9 +++++++-- test/core.tooltip.tests.js | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 8496b50f71e..c9c953b23a5 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -67,8 +67,13 @@ module.exports = function(Chart) { // Args are: (tooltipItem, data) beforeLabel: helpers.noop, label: function(tooltipItem, data) { - var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; - return datasetLabel + ': ' + tooltipItem.yLabel; + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + + if (label) { + label += ': '; + } + label += tooltipItem.yLabel; + return label; }, labelColor: function(tooltipItem, chart) { var meta = chart.getDatasetMeta(tooltipItem.datasetIndex); diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 503e2d2ca4b..e6e3960779f 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -1,5 +1,31 @@ // Test the rectangle element describe('Core.Tooltip', function() { + describe('config', function() { + it('should not include the dataset label in the body string if not defined', function() { + var data = { + datasets: [{ + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }; + var tooltipItem = { + index: 1, + datasetIndex: 0, + xLabel: 'Point 2', + yLabel: '20' + }; + + var label = Chart.defaults.global.tooltips.callbacks.label(tooltipItem, data); + expect(label).toBe('20'); + + data.datasets[0].label = 'My dataset'; + label = Chart.defaults.global.tooltips.callbacks.label(tooltipItem, data); + expect(label).toBe('My dataset: 20'); + }); + }); + describe('index mode', function() { it('Should only use x distance when intersect is false', function() { var chart = window.acquireChart({ @@ -463,7 +489,7 @@ describe('Core.Tooltip', function() { expect(tooltip._view.y).toBeCloseToPixel(190); }); - it('Should display information from user callbacks', function() { + it('Should allow sorting items', function() { var chart = window.acquireChart({ type: 'line', data: { From 8dafbc02c0a222ce841d0a028eabd39c76dd5c86 Mon Sep 17 00:00:00 2001 From: etimberg Date: Fri, 10 Feb 2017 20:37:31 -0500 Subject: [PATCH 129/685] when axes are in the wrong place, update the config position --- src/core/core.controller.js | 19 ++++++++++++++++--- test/core.controller.tests.js | 20 ++++++++++++++++++++ test/scale.logarithmic.tests.js | 29 ++++++++++++++--------------- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 0d4c128bad7..02433ce5292 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -56,6 +56,10 @@ module.exports = function(Chart) { chart.tooltip._options = newOptions.tooltips; } + function positionIsHorizontal(position) { + return position === 'top' || position === 'bottom'; + } + helpers.extend(Chart.prototype, /** @lends Chart */ { /** * @private @@ -220,16 +224,21 @@ module.exports = function(Chart) { if (options.scales) { items = items.concat( (options.scales.xAxes || []).map(function(xAxisOptions) { - return {options: xAxisOptions, dtype: 'category'}; + return {options: xAxisOptions, dtype: 'category', dposition: 'bottom'}; }), (options.scales.yAxes || []).map(function(yAxisOptions) { - return {options: yAxisOptions, dtype: 'linear'}; + return {options: yAxisOptions, dtype: 'linear', dposition: 'left'}; }) ); } if (options.scale) { - items.push({options: options.scale, dtype: 'radialLinear', isDefault: true}); + items.push({ + options: options.scale, + dtype: 'radialLinear', + isDefault: true, + dposition: 'chartArea' + }); } helpers.each(items, function(item) { @@ -240,6 +249,10 @@ module.exports = function(Chart) { return; } + if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + var scale = new scaleClass({ id: scaleOptions.id, options: scaleOptions, diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index f29542cbf59..7b5e4933216 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -105,6 +105,26 @@ describe('Chart', function() { expect(options.hover.mode).toBe('dataset'); expect(options.title.position).toBe('bottom'); }); + + it('should override axis positions that are incorrect', function() { + var chart = acquireChart({ + type: 'line', + options: { + scales: { + xAxes: [{ + position: 'left', + }], + yAxes: [{ + position: 'bottom' + }] + } + } + }); + + var scaleOptions = chart.options.scales; + expect(scaleOptions.xAxes[0].position).toBe('bottom'); + expect(scaleOptions.yAxes[0].position).toBe('left'); + }); }); describe('config.options.responsive: false', function() { diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 016737c71ee..4562aa384f5 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -690,22 +690,21 @@ describe('Logarithmic Scale tests', function() { it('should get the correct pixel value for a point', function() { var chart = window.acquireChart({ - type: 'bar', + type: 'line', data: { datasets: [{ xAxisID: 'xScale', // for the horizontal scale yAxisID: 'yScale', - data: [10, 5, 1, 25, 78] + data: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}] }], - labels: [] }, options: { scales: { - yAxes: [{ + xAxes: [{ id: 'xScale', - type: 'logarithmic', - position: 'bottom' - }, { + type: 'logarithmic' + }], + yAxes: [{ id: 'yScale', type: 'logarithmic' }] @@ -714,24 +713,24 @@ describe('Logarithmic Scale tests', function() { }); var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(482); // right - paddingRight + expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37); // left + paddingLeft - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(270); // halfway + expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(278); // halfway expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37); // 0 is invalid, put it on the left. - expect(xScale.getValueForPixel(481.5)).toBeCloseToPixel(80); + expect(xScale.getValueForPixel(495)).toBeCloseToPixel(80); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(270)).toBeCloseTo(10, 1e-4); + expect(xScale.getValueForPixel(278)).toBeCloseTo(10, 1e-4); 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(10, 0, 0)).toBeCloseToPixel(234); // halfway + expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom + expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway 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); - expect(yScale.getValueForPixel(234)).toBeCloseTo(10, 1e-4); + expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); + expect(yScale.getValueForPixel(246)).toBeCloseTo(10, 1e-4); }); it('should get the correct pixel value for a point when 0 values are present', function() { From 85ee592b2a93950bf22503f0e5951627afda5d5e Mon Sep 17 00:00:00 2001 From: potatopeelings Date: Sun, 12 Feb 2017 22:59:20 +1100 Subject: [PATCH 130/685] #3849 - Stack bars in z dimension --- src/controllers/controller.bar.js | 12 +++++-- test/controller.bar.tests.js | 55 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index cd746701e05..0aa35a2e3a9 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -169,7 +169,7 @@ module.exports = function(Chart) { if (xScale.options.barThickness) { return xScale.options.barThickness; } - return ruler.barWidth; + return xScale.options.stacked ? ruler.categoryWidth * xScale.options.barPercentage : ruler.barWidth; }, // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible @@ -201,6 +201,10 @@ module.exports = function(Chart) { var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; + if (xScale.options.stacked) { + return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; + } + return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + @@ -463,7 +467,7 @@ module.exports = function(Chart) { if (yScale.options.barThickness) { return yScale.options.barThickness; } - return ruler.barHeight; + return yScale.options.stacked ? ruler.categoryHeight * yScale.options.barPercentage : ruler.barHeight; }, // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible @@ -530,6 +534,10 @@ module.exports = function(Chart) { var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; + if (yScale.options.stacked) { + return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; + } + return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 38f1b733a1a..1a083767046 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -849,6 +849,61 @@ describe('Bar controller tests', function() { }); }); + it('should update elements when only the category scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [20, -10, 10, -10], + label: 'dataset1' + }, { + data: [10, 15, 0, -14], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category', + stacked: true + }], + yAxes: [{ + type: 'linear' + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 290, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 290, w: 83, x: 318, y: 290}, + {b: 290, w: 83, x: 434, y: 471} + ].forEach(function(values, i) { + expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + it('should update elements when the scales are stacked and data is strings', function() { var chart = window.acquireChart({ type: 'bar', From cc90c5c643b4965edcb67c31da9f1eded9d2aabb Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 11 Feb 2017 16:02:15 +0100 Subject: [PATCH 131/685] Add chart data property setter and unit tests Chart data can now be entirely replaced using `chart.data = {...}` thanks to the new property setter (instead of using `chart.config.data = {}`). Also update the documentation, as suggested by @ldaguise and @kennethkalmer, with a note about versions prior 2.6. --- docs/09-Advanced.md | 4 +++- src/core/core.controller.js | 4 ++++ test/controller.bar.tests.js | 4 ++-- test/core.controller.tests.js | 19 +++++++++++++++++++ test/core.tooltip.tests.js | 4 ++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index bdf02e7044b..9d1f2834137 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -25,7 +25,7 @@ myLineChart.destroy(); #### .update(duration, lazy) -Triggers an update of the chart. This can be safely called after replacing the entire data object. This will update all scales, legends, and then re-render the chart. +Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. ```javascript // duration is the time for the animation of the redraw in milliseconds @@ -34,6 +34,8 @@ 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. ``` +> **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. + #### .reset() Reset the chart to it's state before the initial animation. A new animation can then be triggered using `update`. diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 02433ce5292..fc71401fa02 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -98,9 +98,13 @@ module.exports = function(Chart) { // Add the chart instance to the global namespace Chart.instances[me.id] = me; + // Define alias to the config data: `chart.data === chart.config.data` Object.defineProperty(me, 'data', { get: function() { return me.config.data; + }, + set: function(value) { + me.config.data = value; } }); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 1a083767046..db3cf07460e 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -738,8 +738,8 @@ describe('Bar controller tests', function() { expect(meta.data[i]._model.base).toBeCloseToPixel(484); expect(meta.data[i]._model.width).toBeCloseToPixel(40); expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ - datasetLabel: chart.config.data.datasets[1].label, - label: chart.config.data.labels[i], + datasetLabel: chart.data.datasets[1].label, + label: chart.data.labels[i], backgroundColor: 'red', borderSkipped: 'top', borderColor: 'blue', diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 7b5e4933216..68535eb88df 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -57,6 +57,25 @@ describe('Chart', function() { expect(chart.data.datasets[1].data).toBe(ds1.data); }); + it('should define chart.data as an alias for config.data', function() { + var config = {data: {labels: [], datasets: []}}; + var chart = acquireChart(config); + + expect(chart.data).toBe(config.data); + + chart.data = {labels: [1, 2, 3], datasets: [{data: [4, 5, 6]}]}; + + expect(config.data).toBe(chart.data); + expect(config.data.labels).toEqual([1, 2, 3]); + expect(config.data.datasets[0].data).toEqual([4, 5, 6]); + + config.data = {labels: [7, 8, 9], datasets: [{data: [10, 11, 12]}]}; + + expect(chart.data).toBe(config.data); + expect(chart.data.labels).toEqual([7, 8, 9]); + expect(chart.data.datasets[0].data).toEqual([10, 11, 12]); + }); + it('should initialize config with default options', function() { var callback = function() {}; diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index e6e3960779f..93cfb3bea36 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -692,10 +692,10 @@ describe('Core.Tooltip', function() { expect(tooltip._view.dataPoints[0].index).toEqual(pointIndex); expect(tooltip._view.dataPoints[0].datasetIndex).toEqual(datasetIndex); expect(tooltip._view.dataPoints[0].xLabel).toEqual( - chart.config.data.labels[pointIndex] + chart.data.labels[pointIndex] ); expect(tooltip._view.dataPoints[0].yLabel).toEqual( - chart.config.data.datasets[datasetIndex].data[pointIndex] + chart.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 4741c6f09da431e75fa860b4e08417a4d961fadf Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 18 Feb 2017 11:58:45 +0100 Subject: [PATCH 132/685] Add new dataset update and draw plugin hooks In order to take full advantage of the new plugin hooks called before and after a dataset is drawn, all drawing operations must happen on stable meta data, so make sure that transitions are performed before. --- .gitignore | 1 + src/controllers/controller.bar.js | 15 +++--- src/controllers/controller.line.js | 21 ++++---- src/controllers/controller.radar.js | 18 ------- src/core/core.controller.js | 76 ++++++++++++++++++++++++++--- src/core/core.datasetController.js | 33 ++++++++++--- src/core/core.plugin.js | 46 +++++++++++++++++ test/core.controller.tests.js | 31 +++++++++++- 8 files changed, 190 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 172413437e6..e76d361a54a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ .vscode bower.json +*.log *.swp diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 0aa35a2e3a9..3863b3ca5c3 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -248,19 +248,20 @@ module.exports = function(Chart) { return yScale.getPixelForValue(value); }, - draw: function(ease) { + draw: function() { var me = this; var chart = me.chart; - var easingDecimal = ease || 1; - var metaData = me.getMeta().data; + var elements = me.getMeta().data; var dataset = me.getDataset(); - var i, len; + var ilen = elements.length; + var i = 0; + var d; Chart.canvasHelpers.clipArea(chart.ctx, chart.chartArea); - for (i = 0, len = metaData.length; i < len; ++i) { - var d = dataset.data[i]; + for (; i= 0; --i) { + if (me.isDatasetVisible(i)) { + me.drawDataset(i, easingValue); } - }, me, true); + } plugins.notify(me, 'afterDatasetsDraw', [easingValue]); }, + /** + * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` + * hook, in which case, plugins will not be called on `afterDatasetDraw`. + * @private + */ + drawDataset: function(index, easingValue) { + var me = this; + var meta = me.getDatasetMeta(index); + var args = { + meta: meta, + index: index, + easingValue: easingValue + }; + + if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { + return; + } + + meta.controller.draw(easingValue); + + plugins.notify(me, 'afterDatasetDraw', [args]); + }, + // 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) { diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index afd5ea37dc9..8f8495b3622 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -208,12 +208,33 @@ module.exports = function(Chart) { update: helpers.noop, - draw: function(ease) { - var easingDecimal = ease || 1; - var i, len; - var metaData = this.getMeta().data; - for (i = 0, len = metaData.length; i < len; ++i) { - metaData[i].transition(easingDecimal).draw(); + transition: function(easingValue) { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; + + for (; i Date: Tue, 21 Feb 2017 20:45:56 -0500 Subject: [PATCH 133/685] Fix color dependency for builds --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40eca264e17..1dd362e9513 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "main": "Chart.js" }, "dependencies": { - "chartjs-color": "^2.0.0", + "chartjs-color": "~2.0.0", "moment": "^2.10.6" } } From af11be3ce451bc342bb08e8635143f63f5724e74 Mon Sep 17 00:00:00 2001 From: Marcelo Tedeschi Date: Sun, 5 Feb 2017 22:24:16 +0100 Subject: [PATCH 134/685] Added possibility to draw tooltip borders --- samples/tooltips/tooltip-border.html | 86 ++++++++++++++++++++++++++++ src/core/core.tooltip.js | 16 +++++- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 samples/tooltips/tooltip-border.html diff --git a/samples/tooltips/tooltip-border.html b/samples/tooltips/tooltip-border.html new file mode 100644 index 00000000000..b0752fc5cdf --- /dev/null +++ b/samples/tooltips/tooltip-border.html @@ -0,0 +1,86 @@ + + + + + Tooltip Interaction Modes + + + + + + +
    +
    + + + + diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c9c953b23a5..57f6fd82948 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -38,6 +38,8 @@ module.exports = function(Chart) { cornerRadius: 6, multiKeyBackground: '#fff', displayColors: true, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 0, callbacks: { // Args are: (tooltipItems, data) beforeTitle: helpers.noop, @@ -176,7 +178,9 @@ module.exports = function(Chart) { backgroundColor: tooltipOpts.backgroundColor, opacity: 0, legendColorBackground: tooltipOpts.multiKeyBackground, - displayColors: tooltipOpts.displayColors + displayColors: tooltipOpts.displayColors, + borderColor: tooltipOpts.borderColor, + borderWidth: tooltipOpts.borderWidth }; } @@ -608,11 +612,18 @@ module.exports = function(Chart) { } ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); + ctx.lineWidth = vm.borderWidth; + ctx.strokeStyle = vm.borderColor; + ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); + ctx.stroke(); + ctx.fill(); ctx.closePath(); + + helpers.drawRoundedRectangle(ctx, ptX, ptY, size.width, size.height, vm.cornerRadius); ctx.fill(); }, drawTitle: function(pt, vm, ctx, opacity) { @@ -720,6 +731,9 @@ module.exports = function(Chart) { drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius); + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + ctx.stroke(); ctx.fill(); }, draw: function() { From a038459d8c74d8b3247b416bc9c114cc817e42aa Mon Sep 17 00:00:00 2001 From: Marcelo Tedeschi Date: Sun, 5 Feb 2017 23:00:12 +0100 Subject: [PATCH 135/685] Added configuration to documentation --- docs/01-Chart-Configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 883d74ec009..b01af7fb9c3 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -252,6 +252,8 @@ 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 +borderColor | Color | 'rgba(0,0,0,0)' | Color of the border +borderWidth | Number | 0 | Size of the border #### Tooltip Callbacks From f97cab12b1c4db4d5782d92f95e362093ad31860 Mon Sep 17 00:00:00 2001 From: Marcelo Tedeschi Date: Wed, 8 Feb 2017 09:09:48 +0100 Subject: [PATCH 136/685] Refactored drawCaret and drawBackground functions to draw the background together with the caret in the same path --- samples/tooltips/tooltip-border.html | 2 +- src/core/core.tooltip.js | 96 ++++++++++++++++++---------- 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/samples/tooltips/tooltip-border.html b/samples/tooltips/tooltip-border.html index b0752fc5cdf..25c649e548e 100644 --- a/samples/tooltips/tooltip-border.html +++ b/samples/tooltips/tooltip-border.html @@ -2,7 +2,7 @@ - Tooltip Interaction Modes + Tooltip Border + +
    +
    +
    +
    +
    + +
    + + +
    +
    + + + + diff --git a/samples/area/line-datasets.html b/samples/area/line-datasets.html new file mode 100644 index 00000000000..37719df26b3 --- /dev/null +++ b/samples/area/line-datasets.html @@ -0,0 +1,157 @@ + + + + + + + area > datasets | Chart.js sample + + + + + + +
    +
    + +
    +
    + + + +
    +
    +
    + + + + diff --git a/samples/area/radar.html b/samples/area/radar.html new file mode 100644 index 00000000000..23f2bdd3627 --- /dev/null +++ b/samples/area/radar.html @@ -0,0 +1,136 @@ + + + + + + + area > radar | Chart.js sample + + + + + + +
    +
    + +
    +
    + + + +
    +
    +
    + + + + diff --git a/samples/style.css b/samples/style.css new file mode 100644 index 00000000000..5b586506418 --- /dev/null +++ b/samples/style.css @@ -0,0 +1,64 @@ +body, html { + font-family: sans-serif; + padding: 0; + margin: 0; +} + +.content { + max-width: 800px; + margin: auto; + padding: 16px; +} + +.wrapper { + min-height: 400px; + padding: 16px 0; + position: relative; +} + +.wrapper.col-2 { + display: inline-block; + min-height: 256px; + width: 49%; +} + +@media (max-width: 400px) { + .wrapper.col-2 { + width: 100% + } +} + +.wrapper canvas { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.toolbar { + display: flex; +} + +.toolbar > * { + margin: 0 8px 0 0; +} + +.btn-on { + border-style: inset; +} + +.analyser table { + color: #333; + font-size: 0.9rem; + margin: 8px 0; + width: 100% +} + +.analyser th { + background-color: #f0f0f0; + padding: 2px; +} + +.analyser td { + padding: 2px; + text-align: center; +} diff --git a/samples/utils.js b/samples/utils.js index 34965ce66ca..dc5bd12aea2 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -1,3 +1,7 @@ +/* global Chart */ + +'use strict'; + window.chartColors = { red: 'rgb(255, 99, 132)', orange: 'rgb(255, 159, 64)', @@ -5,9 +9,111 @@ window.chartColors = { green: 'rgb(75, 192, 192)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', - grey: 'rgb(231,233,237)' + grey: 'rgb(201, 203, 207)' }; window.randomScalingFactor = function() { return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100); -} \ No newline at end of file +}; + +(function(global) { + var Months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + + var Samples = global.Samples || (global.Samples = {}); + Samples.utils = { + // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ + srand: function(seed) { + this._seed = seed; + }, + + rand: function(min, max) { + var seed = this._seed; + min = min === undefined? 0 : min; + max = max === undefined? 1 : max; + this._seed = (seed * 9301 + 49297) % 233280; + return min + (this._seed / 233280) * (max - min); + }, + + numbers: function(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 1; + var from = cfg.from || []; + var count = cfg.count || 8; + var decimals = cfg.decimals || 8; + var continuity = cfg.continuity || 1; + var dfactor = Math.pow(10, decimals) || 0; + var data = []; + var i, value; + + for (i=0; i= count) { + return false; + } + + return target; + } + + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } + } + + function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; + + if (isFinite(fill)) { + return null; + } + + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } + + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; + } + + if (typeof target === 'number' && isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal? target : null, + y: horizontal? null : target + }; + } + } + + return null; + } + + function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; + + if (!propagate) { + return fill; + } + + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isFinite(fill)) { + return fill; + } + + target = sources[fill]; + if (!target) { + return false; + } + + if (target.visible) { + return fill; + } + + visited.push(fill); + fill = target.fill; + } + + return false; + } + + function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; + + if (fill === false) { + return null; + } + + if (!isFinite(fill)) { + type = 'boundary'; + } + + return mappers[type](source); + } + + function isDrawable(point) { + return point && !point.skip; + } + + function drawArea(ctx, curve0, curve1, len0, len1) { + var i; + + if (!len0 || !len1) { + return; + } + + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i=1; i0; --i) { + helpers.canvas.lineTo(ctx, curve1[i], curve1[i-1], true); + } + } + + function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i%count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); + } + } + } + } + + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + } + + return { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof Chart.elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; + } + + meta.$filler = source; + sources.push(source); + } + + for (i=0; iR|FtK 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json new file mode 100644 index 00000000000..6f89e62c4bf --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json @@ -0,0 +1,62 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0, null, null], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "ticks": { + "display": false + } + }], + "yAxes": [{ + "ticks": { + "display": false + } + }] + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png new file mode 100644 index 0000000000000000000000000000000000000000..7fb4e403e4c30435e61755ad3121c701e52689cb GIT binary patch literal 25819 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline.json b/test/fixtures/plugin.filler/fill-line-dataset-spline.json new file mode 100644 index 00000000000..55054f1f5b6 --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline.json @@ -0,0 +1,62 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0, null, null], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "ticks": { + "display": false + } + }], + "yAxes": [{ + "ticks": { + "display": false + } + }] + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline.png b/test/fixtures/plugin.filler/fill-line-dataset-spline.png new file mode 100644 index 0000000000000000000000000000000000000000..41d52bc77db885b5333bd79545deabc8a0356c85 GIT binary patch literal 23223 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.filler/fill-line-dataset.json b/test/fixtures/plugin.filler/fill-line-dataset.json new file mode 100644 index 00000000000..d82bd7440b4 --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset.json @@ -0,0 +1,62 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0, null, null], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "ticks": { + "display": false + } + }], + "yAxes": [{ + "ticks": { + "display": false + } + }] + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset.png b/test/fixtures/plugin.filler/fill-line-dataset.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a7e09d3b7881bf1f7fe8620fa904f594182397 GIT binary patch literal 22331 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 UE~qqvAPL%JVQqeAy+_3V0jXP*J^%m! literal 0 HcmV?d00001 diff --git a/test/fixtures/element.line/fill-radar-spline.json b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json similarity index 97% rename from test/fixtures/element.line/fill-radar-spline.json rename to test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json index c22d8a6517c..8640adcf89c 100644 --- a/test/fixtures/element.line/fill-radar-spline.json +++ b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json @@ -36,7 +36,7 @@ "line": { "borderColor": "transparent", "tension": 0.5, - "fill": "zero" + "fill": "origin" } } } diff --git a/test/fixtures/element.line/fill-radar-spline.png b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.png similarity index 100% rename from test/fixtures/element.line/fill-radar-spline.png rename to test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.png diff --git a/test/fixtures/element.line/fill-radar-zero.json b/test/fixtures/plugin.filler/fill-radar-boundary-origin.json similarity index 97% rename from test/fixtures/element.line/fill-radar-zero.json rename to test/fixtures/plugin.filler/fill-radar-boundary-origin.json index 172007943a7..fafe29791af 100644 --- a/test/fixtures/element.line/fill-radar-zero.json +++ b/test/fixtures/plugin.filler/fill-radar-boundary-origin.json @@ -35,7 +35,7 @@ }, "line": { "borderColor": "transparent", - "fill": "zero" + "fill": "origin" } } } diff --git a/test/fixtures/element.line/fill-radar-zero.png b/test/fixtures/plugin.filler/fill-radar-boundary-origin.png similarity index 100% rename from test/fixtures/element.line/fill-radar-zero.png rename to test/fixtures/plugin.filler/fill-radar-boundary-origin.png diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index 2341b403cd8..f1fe61facc5 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -509,48 +509,6 @@ describe('Line controller tests', function() { }); - it('should find the correct scale zero when the data is all positive', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - data: [10, 15, 20, 20], - label: 'dataset1', - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - }); - - var meta = chart.getDatasetMeta(0); - - expect(meta.dataset._model).toEqual(jasmine.objectContaining({ - scaleTop: 32, - scaleBottom: 484, - scaleZero: 484, - })); - }); - - it('should find the correct scale zero when the data is all negative', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - data: [-10, -15, -20, -20], - label: 'dataset1', - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - }); - - var meta = chart.getDatasetMeta(0); - - expect(meta.dataset._model).toEqual(jasmine.objectContaining({ - scaleTop: 32, - scaleBottom: 484, - scaleZero: 32, - })); - }); - it('should fall back to the line styles for points', function() { var chart = window.acquireChart({ type: 'line', diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 4b9c1f696dc..8f621c96bb4 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -111,10 +111,6 @@ describe('Radar controller tests', function() { meta.controller.reset(); // reset first // Line element - expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32); - expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512); - expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256); - expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272); expect(meta.dataset._model).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderCapStyle: 'round', @@ -198,10 +194,6 @@ describe('Radar controller tests', function() { meta.controller.update(); - expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32); - expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512); - expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256); - expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272); expect(meta.dataset._model).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(98, 98, 98)', borderCapStyle: 'butt', @@ -262,10 +254,6 @@ describe('Radar controller tests', function() { meta.controller.update(); - expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32); - expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512); - expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256); - expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272); expect(meta.dataset._model).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(55, 55, 54)', borderCapStyle: 'square', diff --git a/test/specs/element.line.tests.js b/test/specs/element.line.tests.js index 52d94cd377c..c4d1722b4ad 100644 --- a/test/specs/element.line.tests.js +++ b/test/specs/element.line.tests.js @@ -1,7 +1,5 @@ // Tests for the line element describe('Chart.elements.Line', function() { - describe('auto', jasmine.specsFromFixtures('element.line')); - it('should be constructed', function() { var line = new Chart.elements.Line({ _datasetindex: 2, @@ -74,8 +72,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: false, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: 0 + tension: 0, // no bezier curve for now } }); @@ -111,14 +108,14 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { name: 'stroke', args: [], @@ -193,8 +190,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: false, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: 0 + tension: 0, // no bezier curve for now } }); @@ -312,8 +308,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: false, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: 0 + tension: 0, // no bezier curve for now } }); @@ -436,8 +431,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now borderCapStyle: 'round', borderColor: 'rgb(255, 255, 0)', @@ -454,36 +448,6 @@ describe('Chart.elements.Line', function() { 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'] @@ -510,327 +474,15 @@ describe('Chart.elements.Line', function() { }, { 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 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] + args: [5, 0] }, { 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] + args: [15, -10] }, { 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] + args: [19, -5] }, { name: 'stroke', args: [] @@ -903,48 +555,14 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now } - }); - - 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: 'lineTo', - args: [5, 2] - }, { - name: 'lineTo', - args: [19, 2] - }, { - name: 'lineTo', - args: [19, -5] - }, { - name: 'lineTo', - args: [19, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', + }); + + line.draw(); + + var expected = [{ + name: 'save', args: [] }, { name: 'setLineCap', @@ -973,8 +591,8 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { name: 'moveTo', args: [19, -5] @@ -1050,8 +668,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now spanGaps: true } }); @@ -1061,33 +678,6 @@ describe('Chart.elements.Line', function() { 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, 19, -5, 19, -5] - }, { - name: 'lineTo', - args: [19, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1115,11 +705,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { name: 'stroke', args: [] @@ -1195,8 +785,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now spanGaps: true } }); @@ -1206,21 +795,6 @@ describe('Chart.elements.Line', function() { var expected = [{ name: 'save', args: [] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1316,8 +890,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now } }); @@ -1326,36 +899,6 @@ describe('Chart.elements.Line', function() { var expected = [{ name: 'save', args: [] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 2] - }, { - name: 'lineTo', - args: [5, 2] - }, { - name: 'lineTo', - args: [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: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1383,11 +926,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { name: 'stroke', args: [] @@ -1460,8 +1003,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now spanGaps: true } }); @@ -1471,36 +1013,6 @@ describe('Chart.elements.Line', function() { var expected = [{ name: 'save', args: [] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 2] - }, { - name: 'lineTo', - args: [5, 2] - }, { - name: 'lineTo', - args: [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: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1528,11 +1040,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { name: 'stroke', args: [] @@ -1605,8 +1117,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now } }); @@ -1615,36 +1126,6 @@ describe('Chart.elements.Line', function() { 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: 'lineTo', - args: [15, 2] - }, { - name: 'lineTo', - args: [15, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1672,11 +1153,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { name: 'stroke', args: [] @@ -1749,8 +1230,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, - scaleZero: 2, // for filling lines - tension: 0.0, // no bezier curve for now + tension: 0, // no bezier curve for now spanGaps: true } }); @@ -1760,33 +1240,6 @@ describe('Chart.elements.Line', function() { 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: 'lineTo', - args: [15, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1814,11 +1267,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { name: 'stroke', args: [] @@ -1893,11 +1346,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: { - x: 3, - y: 2 - }, + tension: 0, // no bezier curve for now } }); @@ -1906,36 +1355,6 @@ describe('Chart.elements.Line', function() { expect(mockContext.getCalls()).toEqual([{ name: 'save', args: [], - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [3, 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: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -1963,17 +1382,17 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { - name: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] + name: 'lineTo', + args: [0, 10] }, { name: 'stroke', args: [], @@ -2048,11 +1467,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: { - x: 3, - y: 2 - }, + tension: 0, // no bezier curve for now } }); @@ -2061,36 +1476,6 @@ describe('Chart.elements.Line', function() { expect(mockContext.getCalls()).toEqual([{ name: 'save', args: [], - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [3, 2] - }, { - name: 'lineTo', - args: [0, 10] - }, { - name: 'lineTo', - args: [3, 2] - }, { - name: 'lineTo', - args: [15, -10] - }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] - }, { - name: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -2121,11 +1506,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { - name: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] + name: 'lineTo', + args: [0, 10] }, { name: 'stroke', args: [], @@ -2200,11 +1585,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: { - x: 3, - y: 2 - }, + tension: 0, // no bezier curve for now spanGaps: true } }); @@ -2214,33 +1595,6 @@ describe('Chart.elements.Line', function() { expect(mockContext.getCalls()).toEqual([{ name: 'save', args: [], - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [3, 2] - }, { - name: 'lineTo', - args: [0, 10] - }, { - name: 'bezierCurveTo', - args: [0, 10, 15, -10, 15, -10] - }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] - }, { - name: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -2268,14 +1622,14 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { - name: 'bezierCurveTo', - args: [19, -5, 0, 10, 0, 10] + name: 'lineTo', + args: [0, 10] }, { name: 'stroke', args: [], @@ -2350,11 +1704,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: { - x: 3, - y: 2 - }, + tension: 0, // no bezier curve for now } }); @@ -2363,33 +1713,6 @@ describe('Chart.elements.Line', function() { expect(mockContext.getCalls()).toEqual([{ name: 'save', args: [], - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [3, 2] - }, { - name: 'lineTo', - args: [5, 0] - }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] - }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] - }, { - name: 'lineTo', - args: [3, 2] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -2417,11 +1740,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { - name: 'bezierCurveTo', - args: [15, -10, 19, -5, 19, -5] + name: 'lineTo', + args: [19, -5] }, { name: 'stroke', args: [], @@ -2496,11 +1819,7 @@ describe('Chart.elements.Line', function() { // Need to provide some settings _view: { fill: true, // don't want to fill - tension: 0.0, // no bezier curve for now - scaleZero: { - x: 3, - y: 2 - }, + tension: 0, // no bezier curve for now } }); @@ -2509,36 +1828,6 @@ describe('Chart.elements.Line', function() { expect(mockContext.getCalls()).toEqual([{ name: 'save', args: [], - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [3, 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: 'lineTo', - args: [3, 2] - }, { - name: 'lineTo', - args: [0, 10] - }, { - name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [] }, { name: 'setLineCap', args: ['butt'] @@ -2566,11 +1855,11 @@ describe('Chart.elements.Line', function() { name: 'moveTo', args: [0, 10] }, { - name: 'bezierCurveTo', - args: [0, 10, 5, 0, 5, 0] + name: 'lineTo', + args: [5, 0] }, { - name: 'bezierCurveTo', - args: [5, 0, 15, -10, 15, -10] + name: 'lineTo', + args: [15, -10] }, { name: 'moveTo', args: [0, 10] diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 63f383ed6cd..3c21d784aac 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -86,6 +86,27 @@ describe('Deprecations', function() { }, 200); }); }); + + describe('Chart.elements.Line: fill option', function() { + it('should decode "zero", "top" and "bottom" as "origin", "start" and "end"', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'zero'}, + {fill: 'bottom'}, + {fill: 'top'}, + ] + } + }); + + ['origin', 'start', 'end'].forEach(function(expected, index) { + var meta = chart.getDatasetMeta(index); + expect(meta.$filler).toBeDefined(); + expect(meta.$filler.fill).toBe(expected); + }); + }); + }); }); describe('Version 2.5.0', function() { diff --git a/test/specs/plugin.filler.tests.js b/test/specs/plugin.filler.tests.js new file mode 100644 index 00000000000..117f680360e --- /dev/null +++ b/test/specs/plugin.filler.tests.js @@ -0,0 +1,265 @@ +describe('Plugin.filler', function() { + function decodedFillValues(chart) { + return chart.data.datasets.map(function(dataset, index) { + var meta = chart.getDatasetMeta(index) || {}; + expect(meta.$filler).toBeDefined(); + return meta.$filler.fill; + }); + } + + describe('auto', jasmine.specsFromFixtures('plugin.filler')); + + describe('dataset.fill', function() { + it('should support boundaries', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'origin'}, + {fill: 'start'}, + {fill: 'end'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual(['origin', 'start', 'end']); + }); + + it('should support absolute dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 1}, + {fill: 3}, + {fill: 0}, + {fill: 2}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([1, 3, 0, 2]); + }); + + it('should support relative dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: '+3'}, + {fill: '-1'}, + {fill: '+1'}, + {fill: '-2'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + 3, // 0 + 3 + 0, // 1 - 1 + 3, // 2 + 1 + 1, // 3 - 2 + ]); + }); + + it('should handle default fill when true (origin)', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: true}, + {fill: false}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual(['origin', false]); + }); + + it('should ignore self dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 0}, + {fill: '-0'}, + {fill: '+0'}, + {fill: 3}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 === 0 + false, // 1 === 1 - 0 + false, // 2 === 2 + 0 + false, // 3 === 3 + ]); + }); + + it('should ignore out of bounds dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: -2}, + {fill: 4}, + {fill: '-3'}, + {fill: '+1'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 - 2 < 0 + false, // 1 + 4 > 3 + false, // 2 - 3 < 0 + false, // 3 + 1 > 3 + ]); + }); + + it('should ignore invalid values', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'foo'}, + {fill: '+foo'}, + {fill: '-foo'}, + {fill: '+1.1'}, + {fill: '-2.2'}, + {fill: 3.3}, + {fill: -4.4}, + {fill: NaN}, + {fill: Infinity}, + {fill: ''}, + {fill: null}, + {fill: []}, + {fill: {}}, + {fill: function() {}} + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // NaN (string) + false, // NaN (string) + false, // NaN (string) + false, // float (string) + false, // float (string) + false, // float (number) + false, // float (number) + false, // NaN + false, // !isFinite + false, // empty string + false, // null + false, // array + false, // object + false, // function + ]); + }); + }); + + describe('options.plugins.filler.propagate', function() { + it('should compute propagated fill targets if true', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'start', hidden: true}, + {fill: '-1', hidden: true}, + {fill: 1, hidden: true}, + {fill: '-2', hidden: true}, + {fill: '+1'}, + {fill: '+2'}, + {fill: '-1'}, + {fill: 'end', hidden: true}, + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } + }); + + + expect(decodedFillValues(chart)).toEqual([ + 'start', // 'start' + 'start', // 1 - 1 -> 0 (hidden) -> 'start' + 'start', // 1 (hidden) -> 0 (hidden) -> 'start' + 'start', // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 'start' + 5, // 4 + 1 + 'end', // 5 + 2 -> 7 (hidden) -> 'end' + 5, // 6 - 1 -> 5 + 'end', // 'end' + ]); + }); + + it('should preserve initial fill targets if false', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'start', hidden: true}, + {fill: '-1', hidden: true}, + {fill: 1, hidden: true}, + {fill: '-2', hidden: true}, + {fill: '+1'}, + {fill: '+2'}, + {fill: '-1'}, + {fill: 'end', hidden: true}, + ] + }, + options: { + plugins: { + filler: { + propagate: false + } + } + } + }); + + expect(decodedFillValues(chart)).toEqual([ + 'start', // 'origin' + 0, // 1 - 1 + 1, // 1 + 1, // 3 - 2 + 5, // 4 + 1 + 7, // 5 + 2 + 5, // 6 - 1 + 'end', // 'end' + ]); + }); + + it('should prevent recursive propagation', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: '+2', hidden: true}, + {fill: '-1', hidden: true}, + {fill: '-1', hidden: true}, + {fill: '-2'} + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 + 2 -> 2 (hidden) -> 1 (hidden) -> 0 (loop) + false, // 1 - 1 -> 0 (hidden) -> 2 (hidden) -> 1 (loop) + false, // 2 - 1 -> 1 (hidden) -> 0 (hidden) -> 2 (loop) + false, // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 2 (hidden) -> 1 (loop) + ]); + }); + }); +}); From bd60fa2dfd25bae95520e7968a72eecac218f7ba Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 18 Mar 2017 11:54:56 +0100 Subject: [PATCH 150/685] Correctly handle decimal display size (#4009) Fix the `readUsedSize` regular expression to correctly parse (truncate) pixel decimal values. --- src/platforms/platform.dom.js | 2 +- test/specs/platform.dom.tests.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 761337b061c..317d4bfed73 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -33,7 +33,7 @@ module.exports = function(Chart) { */ function readUsedSize(element, property) { var value = helpers.getStyle(element, property); - var matches = value && value.match(/(\d+)px/); + var matches = value && value.match(/^(\d+)(\.\d+)?px$/); return matches? Number(matches[1]) : undefined; } diff --git a/test/specs/platform.dom.tests.js b/test/specs/platform.dom.tests.js index 06562c9191f..898de9c4842 100644 --- a/test/specs/platform.dom.tests.js +++ b/test/specs/platform.dom.tests.js @@ -226,6 +226,24 @@ describe('Platform.dom', function() { rw: 165, rh: 85, }); }); + + // https://github.com/chartjs/Chart.js/issues/3860 + it('should support decimal display width and/or height', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345.42px; height: 125.42px;' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { From 3e94b9431a5b3d6e4bfbfd6a2339a350999b3292 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 20 Mar 2017 20:36:54 -0400 Subject: [PATCH 151/685] Update the docs structure/content to use GitBook (#3751) Update the docs structure/content to use GitBook --- .gitignore | 3 +- book.json | 19 + docs/00-Getting-Started.md | 126 ------- docs/01-Chart-Configuration.md | 511 --------------------------- docs/02-Scales.md | 370 ------------------- docs/03-Line-Chart.md | 187 ---------- docs/04-Bar-Chart.md | 176 --------- docs/05-Radar-Chart.md | 125 ------- docs/06-Polar-Area-Chart.md | 108 ------ docs/07-Pie-Doughnut-Chart.md | 115 ------ docs/08-Bubble-Chart.md | 100 ------ docs/09-Advanced.md | 467 ------------------------ docs/10-Notes.md | 115 ------ docs/README.md | 65 ++++ docs/SUMMARY.md | 50 +++ docs/axes/README.md | 57 +++ docs/axes/cartesian/README.md | 104 ++++++ docs/axes/cartesian/category.md | 36 ++ docs/axes/cartesian/linear.md | 74 ++++ docs/axes/cartesian/logarithmic.md | 12 + docs/axes/cartesian/time.md | 97 +++++ docs/axes/labelling.md | 42 +++ docs/axes/radial/README.md | 5 + docs/axes/radial/linear.md | 110 ++++++ docs/axes/styling.md | 35 ++ docs/charts/README.md | 11 + docs/charts/bar.md | 149 ++++++++ docs/charts/bubble.md | 60 ++++ docs/charts/doughnut.md | 79 +++++ docs/charts/line.md | 213 +++++++++++ docs/charts/mixed.md | 37 ++ docs/charts/polar.md | 69 ++++ docs/charts/radar.md | 97 +++++ docs/charts/scatter.md | 49 +++ docs/configuration/README.md | 34 ++ docs/configuration/animations.md | 96 +++++ docs/configuration/elements.md | 69 ++++ docs/configuration/layout.md | 29 ++ docs/configuration/legend.md | 166 +++++++++ docs/configuration/title.md | 41 +++ docs/configuration/tooltip.md | 292 +++++++++++++++ docs/developers/README.md | 22 ++ docs/developers/api.md | 143 ++++++++ docs/developers/axes.md | 131 +++++++ docs/developers/charts.md | 116 ++++++ docs/developers/contributing.md | 34 ++ docs/developers/plugins.md | 140 ++++++++ docs/general/README.md | 8 + docs/general/colors.md | 49 +++ docs/general/fonts.md | 28 ++ docs/general/interactions/README.md | 9 + docs/general/interactions/events.md | 21 ++ docs/general/interactions/modes.md | 104 ++++++ docs/general/responsive.md | 10 + docs/getting-started/README.md | 43 +++ docs/getting-started/installation.md | 41 +++ docs/getting-started/integration.md | 34 ++ docs/getting-started/usage.md | 65 ++++ docs/notes/README.md | 1 + docs/notes/comparison.md | 32 ++ docs/notes/extensions.md | 26 ++ docs/notes/license.md | 3 + docs/style.css | 11 + gulpfile.js | 16 +- package.json | 2 + 65 files changed, 3286 insertions(+), 2403 deletions(-) create mode 100644 book.json delete mode 100644 docs/00-Getting-Started.md delete mode 100644 docs/01-Chart-Configuration.md delete mode 100644 docs/02-Scales.md delete mode 100644 docs/03-Line-Chart.md delete mode 100644 docs/04-Bar-Chart.md delete mode 100644 docs/05-Radar-Chart.md delete mode 100644 docs/06-Polar-Area-Chart.md delete mode 100644 docs/07-Pie-Doughnut-Chart.md delete mode 100644 docs/08-Bubble-Chart.md delete mode 100644 docs/09-Advanced.md delete mode 100644 docs/10-Notes.md create mode 100644 docs/README.md create mode 100644 docs/SUMMARY.md create mode 100644 docs/axes/README.md create mode 100644 docs/axes/cartesian/README.md create mode 100644 docs/axes/cartesian/category.md create mode 100644 docs/axes/cartesian/linear.md create mode 100644 docs/axes/cartesian/logarithmic.md create mode 100644 docs/axes/cartesian/time.md create mode 100644 docs/axes/labelling.md create mode 100644 docs/axes/radial/README.md create mode 100644 docs/axes/radial/linear.md create mode 100644 docs/axes/styling.md create mode 100644 docs/charts/README.md create mode 100644 docs/charts/bar.md create mode 100644 docs/charts/bubble.md create mode 100644 docs/charts/doughnut.md create mode 100644 docs/charts/line.md create mode 100644 docs/charts/mixed.md create mode 100644 docs/charts/polar.md create mode 100644 docs/charts/radar.md create mode 100644 docs/charts/scatter.md create mode 100644 docs/configuration/README.md create mode 100644 docs/configuration/animations.md create mode 100644 docs/configuration/elements.md create mode 100644 docs/configuration/layout.md create mode 100644 docs/configuration/legend.md create mode 100644 docs/configuration/title.md create mode 100644 docs/configuration/tooltip.md create mode 100644 docs/developers/README.md create mode 100644 docs/developers/api.md create mode 100644 docs/developers/axes.md create mode 100644 docs/developers/charts.md create mode 100644 docs/developers/contributing.md create mode 100644 docs/developers/plugins.md create mode 100644 docs/general/README.md create mode 100644 docs/general/colors.md create mode 100644 docs/general/fonts.md create mode 100644 docs/general/interactions/README.md create mode 100644 docs/general/interactions/events.md create mode 100644 docs/general/interactions/modes.md create mode 100644 docs/general/responsive.md create mode 100644 docs/getting-started/README.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/integration.md create mode 100644 docs/getting-started/usage.md create mode 100644 docs/notes/README.md create mode 100644 docs/notes/comparison.md create mode 100644 docs/notes/extensions.md create mode 100644 docs/notes/license.md create mode 100644 docs/style.css diff --git a/.gitignore b/.gitignore index e76d361a54a..b23981942a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,12 @@ +/_book /coverage /custom /dist /docs/index.md /node_modules - .DS_Store .idea .vscode bower.json - *.log *.swp diff --git a/book.json b/book.json new file mode 100644 index 00000000000..a598877304c --- /dev/null +++ b/book.json @@ -0,0 +1,19 @@ +{ + "root": "./docs", + "author": "chartjs", + "gitbook": "3.2.2", + "plugins": ["anchorjs", "chartjs"], + "pluginsConfig": { + "anchorjs": { + "icon": "#", + "placement": "left", + "visible": "always" + }, + "theme-default": { + "showLevel": false, + "styles": { + "website": "style.css" + } + } + } +} diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md deleted file mode 100644 index 81e03c0a986..00000000000 --- a/docs/00-Getting-Started.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Getting started -anchor: getting-started ---- - -### Download Chart.js - -You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest) or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. -If you download or clone the repository, you must run `gulp build` to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. - -### Installation - -#### npm - -```bash -npm install chart.js --save -``` - -#### bower - -```bash -bower install chart.js --save -``` - -### Selecting the Correct Build - -Chart.js provides two different builds that are available for your use. The `Chart.js` and `Chart.min.js` files include Chart.js and the accompanying color parsing library. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. - -The `Chart.bundle.js` and `Chart.bundle.min.js` builds include Moment.js in a single file. This version should be used if you require time axes and want a single file to include, select this version. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. - -### Usage - -To import Chart.js using an old-school script tag: - -```html - - -``` - -To import Chart.js using an awesome module loader: - -```javascript - -// Using CommonJS -var Chart = require('chart.js') -var myChart = new Chart({...}) - -// ES6 -import Chart from 'chart.js' -let myChart = new Chart({...}) - -// Using requirejs -require(['path/to/Chartjs'], function(Chart){ - var myChart = new Chart({...}) -}) - -``` - -### Creating a Chart - -To create a chart, we need to instantiate the `Chart` class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart. Here's an example. - -```html - -``` - -```javascript -// Any of the following formats may be used -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! - -The following example instantiates a bar chart showing the number of votes for different colors and the y-axis starting at 0. - -```html - - -``` - -It's that easy to get started using Chart.js! From here you can explore the many options that can help you customise your charts with scales, tooltips, labels, colors, custom actions, and much more. - -There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attatched to every [release](https://github.com/chartjs/Chart.js/releases). diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md deleted file mode 100644 index 104c6953ad8..00000000000 --- a/docs/01-Chart-Configuration.md +++ /dev/null @@ -1,511 +0,0 @@ ---- -title: Chart Configuration -anchor: chart-configuration ---- - -Chart.js provides a number of options for changing the behaviour of created charts. These configuration options can be changed on a per chart basis by passing in an options object when creating the chart. Alternatively, the global configuration can be changed which will be used by all charts created after that point. - -### Chart Data - -To display data, the chart must be passed a data object that contains all of the information needed by the chart. The data object can contain the following parameters - -Name | Type | Description ---- | --- | ---- -datasets | Array[object] | Contains data for each dataset. See the documentation for each chart type to determine the valid options that can be attached to the dataset -labels | Array[string] | Optional parameter that is used with the [category axis](#scales-category-scale). -xLabels | Array[string] | Optional parameter that is used with the category axis and is used if the axis is horizontal -yLabels | Array[string] | Optional parameter that is used with the category axis and is used if the axis is vertical - -### Creating a Chart with Options - -To create a chart with configuration options, simply pass an object containing your configuration to the constructor. In the example below, a line chart is created and configured to not be responsive. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - responsive: false - } -}); -``` - -### Global Configuration - -This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. - -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 '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 = 'nearest'; - -// Hover mode is set to nearest because it was not overridden here -var chartHoverModeNearest = new Chart(ctx, { - type: 'line', - data: data, -}); - -// This chart would have the hover mode that was passed in -var chartDifferentHoverMode = new Chart(ctx, { - type: 'line', - data: data, - options: { - hover: { - // Overrides the global setting - mode: 'index' - } - } -}) -``` - -#### Global Font Settings - -There are 4 special global settings that can change all of the fonts on the chart. These options are in `Chart.defaults.global`. - -Name | Type | Default | Description ---- | --- | --- | --- -defaultFontColor | Color | '#666' | Default font color for all text -defaultFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Default font family for all text -defaultFontSize | Number | 12 | Default font size (in px) for text. Does not apply to radialLinear scale point labels -defaultFontStyle | String | 'normal' | Default font style. Does not apply to tooltip title or footer. Does not apply to chart title - -### Common Chart Configuration - -The following options are applicable to all charts. The can be set on the [global configuration](#chart-configuration-global-configuration), or they can be passed to the chart constructor. - -Name | Type | Default | Description ---- | --- | --- | --- -responsive | Boolean | true | Resizes the chart canvas when its container does. -responsiveAnimationDuration | Number | 0 | Duration in milliseconds it takes to animate to new size after a resize event. -maintainAspectRatio | Boolean | true | Maintain the original canvas aspect ratio `(width / height)` when resizing -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 the event and 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 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`. - -Name | Type | Default | Description ---- | --- | --- | --- -display | Boolean | false | Display the title block -position | String | 'top' | Position of the title. Possible values are 'top', 'left', 'bottom' and 'right'. -fullWidth | Boolean | true | Marks that this box should take the full width of the canvas (pushing down other boxes) -fontSize | Number | 12 | Font size inherited from global configuration -fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family inherited from global configuration -fontColor | Color | "#666" | Font color inherited from global configuration -fontStyle | String | 'bold' | Font styling of the title. -padding | Number | 10 | Number of pixels to add above and below the title text -text | String | '' | Title text - -#### Example Usage - -The example below would enable a title of 'Custom Chart Title' on the chart that is created. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - title: { - display: true, - text: 'Custom Chart Title' - } - } -}) -``` - -### Legend Configuration - -The legend configuration is passed into the `options.legend` namespace. The global options for the chart legend is defined in `Chart.defaults.global.legend`. - -Name | Type | Default | Description ---- | --- | --- | --- -display | Boolean | true | Is the legend displayed -position | String | 'top' | Position of the legend. Possible values are 'top', 'left', 'bottom' and 'right'. -fullWidth | Boolean | true | Marks that this box should take the full width of the canvas (pushing down other boxes) -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 - -The legend label configuration is nested below the legend configuration using the `labels` key. - -Name | Type | Default | Description ---- | --- | --- | --- -boxWidth | Number | 40 | Width of coloured box -fontSize | Number | 12 | Font size inherited from global configuration -fontStyle | String | "normal" | Font style inherited from global configuration -fontColor | Color | "#666" | Font color inherited from global configuration -fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family inherited from global configuration -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. -filter | Function | null | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#chart-configuration-legend-item-interface) and the chart data -usePointStyle | Boolean | false | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). - -#### Legend Item Interface - -Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface. - -```javascript -{ - // Label that will be displayed - text: String, - - // Fill style of the legend box - fillStyle: Color, - - // If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect - hidden: Boolean, - - // For box border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap - lineCap: String, - - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash - lineDash: Array[Number], - - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset - lineDashOffset: Number, - - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin - lineJoin: String, - - // Width of box border - lineWidth: Number, - - // Stroke style of the legend box - strokeStyle: Color - - // Point style of the legend box (only used if usePointStyle is true) - pointStyle: String -} -``` - -#### Example - -The following example will create a chart with the legend enabled and turn all of the text red in color. - -```javascript -var chart = new Chart(ctx, { - type: 'bar', - data: data, - options: { - legend: { - display: true, - labels: { - fontColor: 'rgb(255, 99, 132)' - } - } -} -}); -``` - -### Tooltip Configuration - -The tooltip configuration is passed into the `options.tooltips` namespace. The global options for the chart tooltips is defined in `Chart.defaults.global.tooltips`. - -Name | Type | Default | Description ---- | --- | --- | --- -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. -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 -titleFontStyle | String | "bold" | -titleFontColor | Color | "#fff" | Font color for tooltip title -titleSpacing | Number | 2 | Spacing to add to top and bottom of each title line. -titleMarginBottom | Number | 6 | Margin to add on bottom of title section -bodyFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip items inherited from global font family -bodyFontSize | Number | 12 | Font size for tooltip items inherited from global font size -bodyFontStyle | String | "normal" | -bodyFontColor | Color | "#fff" | Font color for tooltip items. -bodySpacing | Number | 2 | Spacing to add to top and bottom of each tooltip item -footerFontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for tooltip footer inherited from global font family. -footerFontSize | Number | 12 | Font size for tooltip footer inherited from global font size. -footerFontStyle | String | "bold" | Font style for tooltip footer. -footerFontColor | Color | "#fff" | Font color for tooltip footer. -footerSpacing | Number | 2 | Spacing to add to top and bottom of each footer line. -footerMarginTop | Number | 6 | Margin to add before drawing the footer -xPadding | Number | 6 | Padding to add on left and right of tooltip -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 -borderColor | Color | 'rgba(0,0,0,0)' | Color of the border -borderWidth | Number | 0 | Size of the border - -#### Tooltip Callbacks - -The tooltip label configuration is nested below the tooltip configuration using the `callbacks` key. The tooltip has the following callbacks for providing text. For all functions, 'this' will be the tooltip object created from the Chart.Tooltip constructor. - -All functions are called with the same arguments: a [tooltip item](#chart-configuration-tooltip-item-interface) and the data object passed to the chart. All functions must return either a string or an array of strings. Arrays of strings are treated as multiple lines of text. - -Callback | Arguments | Description ---- | --- | --- -beforeTitle | `Array[tooltipItem], data` | Text to render before the title -title | `Array[tooltipItem], data` | Text to render as the title -afterTitle | `Array[tooltipItem], data` | Text to render after the title -beforeBody | `Array[tooltipItem], data` | Text to render before the body section -beforeLabel | `tooltipItem, data` | Text to render before an individual label -label | `tooltipItem, data` | Text to render for an individual item in the tooltip -labelColor | `tooltipItem, chart` | Returns the colors to render for the tooltip item. Return as an object containing two parameters: `borderColor` and `backgroundColor`. -afterLabel | `tooltipItem, data` | Text to render after an individual label -afterBody | `Array[tooltipItem], data` | Text to render after the body section -beforeFooter | `Array[tooltipItem], data` | Text to render before the footer section -footer | `Array[tooltipItem], data` | Text to render as the footer -afterFooter | `Array[tooltipItem], data` | Text to render after the footer section -dataPoints | `Array[tooltipItem]` | List of matching point informations. - -#### Tooltip Item Interface - -The tooltip items passed to the tooltip callbacks implement the following interface. - -```javascript -{ - // X Value of the tooltip as a string - xLabel: String, - - // Y value of the tooltip as a string - yLabel: String, - - // Index of the dataset the item comes from - datasetIndex: Number, - - // Index of this data item in the dataset - index: Number, - - // X position of matching point - x: Number, - - // Y position of matching point - y: Number, -} -``` - -### Hover Configuration - -The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. - -Name | Type | Default | Description ---- | --- | --- | --- -mode | String | 'nearest' | 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 the event and 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 = false` -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`. - -Name | Type | Default | Description ---- |:---:| --- | --- -duration | Number | 1000 | The number of milliseconds an animation takes. -easing | String | "easeOutQuart" | Easing function to use. Available options are: `'linear'`, `'easeInQuad'`, `'easeOutQuad'`, `'easeInOutQuad'`, `'easeInCubic'`, `'easeOutCubic'`, `'easeInOutCubic'`, `'easeInQuart'`, `'easeOutQuart'`, `'easeInOutQuart'`, `'easeInQuint'`, `'easeOutQuint'`, `'easeInOutQuint'`, `'easeInSine'`, `'easeOutSine'`, `'easeInOutSine'`, `'easeInExpo'`, `'easeOutExpo'`, `'easeInOutExpo'`, `'easeInCirc'`, `'easeOutCirc'`, `'easeInOutCirc'`, `'easeInElastic'`, `'easeOutElastic'`, `'easeInOutElastic'`, `'easeInBack'`, `'easeOutBack'`, `'easeInOutBack'`, `'easeInBounce'`, `'easeOutBounce'`, `'easeInOutBounce'`. See [Robert Penner's easing equations](http://robertpenner.com/easing/). -onProgress | Function | none | Callback called on each step of an animation. Passed a single argument, a `Chart.Animation` instance, see below. -onComplete | Function | none | Callback called at the end of an animation. Passed a single argument, a `Chart.Animation` instance, see below. - -#### Animation Callbacks - -The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed a `Chart.Animation` instance: - -```javascript -{ - // Chart instance - chart, - - // Current Animation frame number - currentStep: Number, - - // Number of animation frames - numSteps: Number, - - // Animation easing to use - easing: String, - - // Function that renders the chart - render: Function, - - // User callback - onAnimationProgress: Function, - - // User callback - onAnimationComplete: Function -} -``` - -An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/animation/progress-bar.html): this sample displays a progress bar showing how far along the animation is. - -### Element Configuration - -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. - -#### Arc Configuration - -Arcs are used in the polar area, doughnut and pie charts. They can be configured with the following options. The global arc options are stored in `Chart.defaults.global.elements.arc`. - -Name | Type | Default | Description ---- | --- | --- | --- -backgroundColor | Color | 'rgba(0,0,0,0.1)' | Default fill color for arcs. Inherited from the global default -borderColor | Color | '#fff' | Default stroke color for arcs -borderWidth | Number | 2 | Default stroke width for arcs - -#### Line Configuration - -Line elements are used to represent the line in a line chart. The global line options are stored in `Chart.defaults.global.elements.line`. - -Name | Type | Default | Description ---- | --- | --- | --- -tension | Number | 0.4 | Default bezier curve tension. Set to `0` for no bezier curves. -backgroundColor | Color | 'rgba(0,0,0,0.1)' | Default line fill color -borderWidth | Number | 3 | Default line stroke width -borderColor | Color | 'rgba(0,0,0,0.1)' | Default line stroke color -borderCapStyle | String | 'butt' | Default line cap style. See [MDN](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap) -borderDash | Array | `[]` | Default line dash. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) -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 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 - -Point elements are used to represent the points in a line chart or a bubble chart. The global point options are stored in `Chart.defaults.global.elements.point`. - -Name | Type | Default | Description ---- | --- | --- | --- -radius | Number | 3 | Default point radius -pointStyle | String | 'circle' | Default point style -backgroundColor | Color | 'rgba(0,0,0,0.1)' | Default point fill color -borderWidth | Number | 1 | Default point stroke width -borderColor | Color | 'rgba(0,0,0,0.1)' | Default point stroke color -hitRadius | Number | 1 | Extra radius added to point radius for hit detection -hoverRadius | Number | 4 | Default point radius when hovered -hoverBorderWidth | Number | 1 | Default stroke width when hovered - -#### Rectangle Configuration - -Rectangle elements are used to represent the bars in a bar chart. The global rectangle options are stored in `Chart.defaults.global.elements.rectangle`. - -Name | Type | Default | Description ---- | --- | --- | --- -backgroundColor | Color | 'rgba(0,0,0,0.1)' | Default bar fill color -borderWidth | Number | 0 | Default bar stroke width -borderColor | Color | 'rgba(0,0,0,0.1)' | Default bar stroke color -borderSkipped | String | 'bottom' | Default skipped (excluded) border for rectangle. Can be one of `bottom`, `left`, `top`, `right` - -### Colors - -When supplying colors to Chart options, you can use a number of formats. You can specify the color as a string in hexadecimal, RGB, or HSL notations. If a color is needed, but not specified, Chart.js will use the global default color. This color is stored at `Chart.defaults.global.defaultColor`. It is initially set to 'rgba(0, 0, 0, 0.1)'; - -You can also pass a [CanvasGradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient) object. You will need to create this before passing to the chart, but using it you can achieve some interesting effects. - -### Patterns - -An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) object. For example, if you wanted to fill a dataset with a pattern from an image you could do the following. - -```javascript -var img = new Image(); -img.src = 'https://example.com/my_image.png'; -img.onload = function() { - var ctx = document.getElementById('canvas').getContext('2d'); - var fillPattern = ctx.createPattern(img, 'repeat'); - - var chart = new Chart(ctx, { - data: { - labels: ['Item 1', 'Item 2', 'Item 3'], - datasets: [{ - data: [10, 20, 30], - backgroundColor: fillPattern - }] - } - }) -} -``` - -Using pattern fills for data graphics can help viewers with vision deficiencies (e.g. color-blindness or partial sight) to [more easily understand your data](http://betweentwobrackets.com/data-graphics-and-colour-vision/). - -Using the [Patternomaly](https://github.com/ashiguruma/patternomaly) library you can generate patterns to fill datasets. - -```javascript -var chartData = { - datasets: [{ - data: [45, 25, 20, 10], - backgroundColor: [ - pattern.draw('square', '#ff6384'), - pattern.draw('circle', '#36a2eb'), - pattern.draw('diamond', '#cc65fe'), - pattern.draw('triangle', '#ffce56'), - ] - }], - labels: ['Red', 'Blue', 'Purple', 'Yellow'] -}; -``` - -### Mixed Chart Types - -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. - -When creating the chart you must set the overall `type` as `bar`. - -```javascript -var myChart = new Chart(ctx, { - type: 'bar', - data: { - labels: ['Item 1', 'Item 2', 'Item 3'], - datasets: [ - { - type: 'bar', - label: 'Bar Component', - data: [10, 20, 30], - }, - { - type: 'line', - label: 'Line Component', - data: [30, 20, 10], - } - ] - } -}); -``` diff --git a/docs/02-Scales.md b/docs/02-Scales.md deleted file mode 100644 index ba0e470afa4..00000000000 --- a/docs/02-Scales.md +++ /dev/null @@ -1,370 +0,0 @@ ---- -title: Scales -anchor: scales ---- - -Scales in v2.0 of Chart.js are significantly more powerful, but also different than those of v1.0. -* Multiple X & Y axes are supported. -* A built-in label auto-skip feature detects would-be overlapping ticks and labels and removes every nth label to keep things displaying normally. -* Scale titles are supported -* New scale types can be extended without writing an entirely new chart type - - -### Common Configuration - -Every scale extends a core scale class with the following options: - -Name | Type | Default | Description ---- | --- | --- | --- -type | String | Chart specific. | Type of scale being employed. Custom scales can be created and registered with a string key. Options: ["category"](#scales-category-scale), ["linear"](#scales-linear-scale), ["logarithmic"](#scales-logarithmic-scale), ["time"](#scales-time-scale), ["radialLinear"](#scales-radial-linear-scale) -display | Boolean | true | If true, show the scale including gridlines, ticks, and labels. Overrides *gridLines.display*, *scaleLabel.display*, and *ticks.display*. -position | String | "left" | Position of the scale. Possible values are 'top', 'left', 'bottom' and 'right'. -id | String | | The ID is used to link datasets and scale axes together. The properties `datasets.xAxisID` or `datasets.yAxisID` have to match the scale properties `scales.xAxes.id` or `scales.yAxes.id`. This is especially needed if multi-axes charts are used. -beforeUpdate | Function | undefined | Callback called before the update process starts. Passed a single argument, the scale instance. -beforeSetDimensions | Function | undefined | Callback that runs before dimensions are set. Passed a single argument, the scale instance. -afterSetDimensions | Function | undefined | Callback that runs after dimensions are set. Passed a single argument, the scale instance. -beforeDataLimits | Function | undefined | Callback that runs before data limits are determined. Passed a single argument, the scale instance. -afterDataLimits | Function | undefined | Callback that runs after data limits are determined. Passed a single argument, the scale instance. -beforeBuildTicks | Function | undefined | Callback that runs before ticks are created. Passed a single argument, the scale instance. -afterBuildTicks | Function | undefined | Callback that runs after ticks are created. Useful for filtering ticks. Passed a single argument, the scale instance. -beforeTickToLabelConversion | Function | undefined | Callback that runs before ticks are converted into strings. Passed a single argument, the scale instance. -afterTickToLabelConversion | Function | undefined | Callback that runs after ticks are converted into strings. Passed a single argument, the scale instance. -beforeCalculateTickRotation | Function | undefined | Callback that runs before tick rotation is determined. Passed a single argument, the scale instance. -afterCalculateTickRotation | Function | undefined | Callback that runs after tick rotation is determined. Passed a single argument, the scale instance. -beforeFit | Function | undefined | Callback that runs before the scale fits to the canvas. Passed a single argument, the scale instance. -afterFit | Function | undefined | Callback that runs after the scale fits to the canvas. Passed a single argument, the scale instance. -afterUpdate | Function | undefined | Callback that runs at the end of the update process. Passed a single argument, the scale instance. -**gridLines** | Object | - | See [grid line configuration](#grid-line-configuration) section. -**scaleLabel** | Object | | See [scale title configuration](#scale-title-configuration) section. -**ticks** | Object | | See [tick configuration](#tick-configuration) section. - -#### Grid Line Configuration - -The grid line configuration is nested under the scale configuration in the `gridLines` key. It defines options for the grid lines that run perpendicular to the axis. - -Name | Type | Default | Description ---- | --- | --- | --- -display | Boolean | true | -color | Color or Array[Color] | "rgba(0, 0, 0, 0.1)" | Color of the grid lines. -borderDash | Array[Number] | [] | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) -borderDashOffset | Number | 0.0 | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -lineWidth | Number or Array[Number] | 1 | Stroke width of grid lines -drawBorder | Boolean | true | If true draw border on the edge of the chart -drawOnChartArea | Boolean | true | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn -drawTicks | Boolean | true | If true, draw lines beside the ticks in the axis area beside the chart. -tickMarkLength | Number | 10 | Length in pixels that the grid lines will draw into the axis area. -zeroLineWidth | Number | 1 | Stroke width of the grid line for the first index (index 0). -zeroLineColor | Color | "rgba(0, 0, 0, 0.25)" | Stroke color of the grid line for the first index (index 0). -offsetGridLines | Boolean | false | If true, labels are shifted to be between grid lines. This is used in the bar chart. - -#### Scale Title Configuration - -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 ---- | --- | --- | --- -display | Boolean | false | -labelString | String | "" | The text for the title. (i.e. "# of People", "Response Choices") -fontColor | Color | "#666" | Font color for the scale title. -fontFamily| String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for the scale title, follows CSS font-family options. -fontSize | Number | 12 | Font size for the scale title. -fontStyle | String | "normal" | Font style for the scale title, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). - -#### Tick Configuration - -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 ---- | --- | --- | --- -autoSkip | Boolean | true | If true, automatically calculates how many labels that can be shown and hides labels accordingly. Turn it off to show all labels no matter what -autoSkipPadding | Number | 0 | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. *Note: Only applicable to horizontal scales.* -callback | Function | `function(value) { return helpers.isArray(value) ? value : '' + value; }` | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](#scales-creating-custom-tick-formats) section below. -display | Boolean | true | If true, show the ticks. -fontColor | Color | "#666" | Font color for the tick labels. -fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family for the tick labels, follows CSS font-family options. -fontSize | Number | 12 | Font size for the tick labels. -fontStyle | String | "normal" | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). -labelOffset | Number | 0 | Distance in pixels to offset the label from the centre point of the tick (in the y direction for the x axis, and the x direction for the y axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* -maxRotation | Number | 90 | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* -minRotation | Number | 0 | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* -mirror | Boolean | false | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* -padding | Number | 10 | Padding between the tick label and the axis. *Note: Only applicable to horizontal scales.* -reverse | Boolean | false | Reverses order of tick labels. - -#### Creating Custom Tick Formats - -The `callback` method may be used for advanced tick customization. In the following example, every label of the Y axis would be displayed in scientific notation. - -If the callback returns `null` or `undefined` the associated grid line will be hidden. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - yAxes: [{ - ticks: { - // Create scientific notation labels - callback: function(value, index, values) { - return value.toExponential(); - } - } - }] - } - } -}); -``` - -### Category Scale - -The category scale will be familiar to those who have used v1.0. Labels are drawn from one of the label arrays included in the chart data. If only `data.labels` is defined, this will be used. If `data.xLabels` is defined and the axis is horizontal, this will be used. Similarly, if `data.yLabels` is defined and the axis is vertical, this property will be used. Using both `xLabels` and `yLabels` together can create a chart that uses strings for both the X and Y axes. - -#### Configuration Options - -The category scale has the following additional options that can be set. - -Name | Type | Default | Description ---- | --- | --- | --- -ticks.min | String | - | The minimum item to display. Must be a value in the `labels` array -ticks.max | String | - | The maximum item to display. Must be a value in the `labels` array - -### Linear Scale - -The linear scale is use to chart numerical data. It can be placed on either the x or y axis. The scatter chart type automatically configures a line chart to use one of these scales for the x axis. As the name suggests, linear interpolation is used to determine where a value lies on the axis. - -#### Configuration Options - -The following options are provided by the linear scale. They are all located in the `ticks` sub options. - -Name | Type | Default | Description ---- | --- | --- | --- -beginAtZero | Boolean | - | if true, scale will include 0 if it is not already included. -min | Number | - | User defined minimum number for the scale, overrides minimum value from data. -max | Number | - | User defined maximum number for the scale, overrides maximum value from data. -maxTicksLimit | Number | 11 | Maximum number of ticks and gridlines to show. If not defined, it will limit to 11 ticks but will show all gridlines. -fixedStepSize | Number | - | User defined fixed step size for the scale. If set, the scale ticks will be enumerated by multiple of stepSize, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. -stepSize | Number | - | if defined, it can be used along with the min and the max to give a custom number of steps. See the example below. -suggestedMax | Number | - | User defined maximum number for the scale, overrides maximum value *except for if* it is lower than the maximum value. -suggestedMin | Number | - | User defined minimum number for the scale, overrides minimum value *except for if* it is higher than the minimum value. - -#### Example Configuration - -The following example creates a line chart with a vertical axis that goes from 0 to 5 in 0.5 sized steps. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - yAxes: [{ - ticks: { - max: 5, - min: 0, - stepSize: 0.5 - } - }] - } - } -}); -``` - -### Logarithmic Scale - -The logarithmic scale is use to chart numerical data. It can be placed on either the x or y axis. As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. - -#### Configuration Options - -The following options are provided by the logarithmic scale. They are all located in the `ticks` sub options. - -Name | Type | Default | Description ---- | --- | --- | --- -min | Number | - | User defined minimum number for the scale, overrides minimum value from data. -max | Number | - | User defined maximum number for the scale, overrides maximum value from data. - -#### Example Configuration - -The following example creates a chart with a logarithmic X axis that ranges from 1 to 1000. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - xAxes: [{ - type: 'logarithmic', - position: 'bottom', - ticks: { - min: 1, - max: 1000 - } - }] - } - } -}) -``` - -### Time Scale - -The time scale is used to display times and dates. It can only be placed on the X axis. When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale. - -#### Configuration Options - -The following options are provided by the time scale. They are all located in the `time` sub options. - -Name | Type | Default | Description ---- | --- | --- | --- -displayFormats | Object | - | See [Display Formats](#scales-display-formats) section below. -isoWeekday | Boolean | false | If true and the unit is set to 'week', iso weekdays will be used. -max | [Time](#scales-date-formats) | - | If defined, this will override the data maximum -min | [Time](#scales-date-formats) | - | If defined, this will override the data minimum -parser | String or Function | - | If defined as a string, it is interpreted as a custom format to be used by moment to parse the date. If this is a function, it must return a moment.js object given the appropriate data value. -round | String | - | If defined, dates will be rounded to the start of this unit. See [Time Units](#scales-time-units) below for the allowed units. -tooltipFormat | String | '' | The moment js format string to use for the tooltip. -unit | String | - | If defined, will force the unit to be a certain type. See [Time Units](#scales-time-units) section below for details. -unitStepSize | Number | 1 | The number of units between grid lines. -minUnit | String | 'millisecond' | The minimum display format to be used for a time unit - -#### Date Formats - -When providing data for the time scale, Chart.js supports all of the formats that Moment.js accepts. See [Moment.js docs](http://momentjs.com/docs/#/parsing/) for details. - -#### Display Formats - -The following display formats are used to configure how different time units are formed into strings for the axis tick marks. See [moment.js](http://momentjs.com/docs/#/displaying/format/) for the allowable format strings. - -Name | Default ---- | --- -millisecond | 'SSS [ms]' -second | 'h:mm:ss a' -minute | 'h:mm:ss a' -hour | 'MMM D, hA' -day | 'll' -week | 'll' -month | 'MMM YYYY' -quarter | '[Q]Q - YYYY' -year | 'YYYY' - -For example, to set the display format for the 'quarter' unit to show the month and year, the following config would be passed to the chart constructor. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - xAxes: [{ - type: 'time', - time: { - displayFormats: { - quarter: 'MMM YYYY' - } - } - }] - } - } -}) -``` - -#### Time Units - -The following time measurements are supported. The names can be passed as strings to the `time.unit` config option to force a certain unit. - -* millisecond -* second -* minute -* hour -* day -* week -* month -* quarter -* year - -For example, to create a chart with a time scale that always displayed units per month, the following config could be used. - -```javascript -var chart = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - xAxes: [{ - time: { - unit: 'month' - } - }] - } - } -}) -``` - -### Radial Linear Scale - -The radial linear scale is used specifically for the radar and polar area chart types. It overlays the chart area, rather than being positioned on one of the edges. - -#### Configuration Options - -The following additional configuration options are provided by the radial linear scale. - -Name | Type | Default | Description ---- | --- | --- | --- -*gridLines*.circular | Boolean | false | if true, radial lines are circular. If false, they are straight lines connecting the the different angle line locations. -angleLines | Object | - | See the Angle Line Options section below for details. -pointLabels | Object | - | See the Point Label Options section below for details. -ticks | Object | - | See the Ticks table below for options. - -#### Angle Line Options - -The following options are used to configure angled lines that radiate from the center of the chart to the point labels. They can be found in the `angleLines` sub options. Note that these options only apply if `angleLines.display` is true. - -Name | Type | Default | Description ---- | --- | --- | --- -display | Boolean | true | If true, angle lines are shown. -color | Color | 'rgba(0, 0, 0, 0.1)' | Color of angled lines -lineWidth | Number | 1 | Width of angled lines - -#### Point Label Options - -The following options are used to configure the point labels that are shown on the perimeter of the scale. They can be found in the `pointLabels` sub options. Note that these options only apply if `pointLabels.display` is true. - -Name | Type | Default | Description ---- | --- | --- | --- -display | Boolean | true | If true, point labels are shown -callback | Function | - | Callback function to transform data label to axis label -fontColor | Color | '#666' | Font color -fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family to render -fontSize | Number | 10 | Font size in pixels -fontStyle | String | 'normal' | Font Style to use - - -#### Tick Options - -Name | Type | Default | Description ---- | --- | --- | --- -backdropColor | Color | 'rgba(255, 255, 255, 0.75)' | Color of label backdrops -backdropPaddingX | Number | 2 | Horizontal padding of label backdrop -backdropPaddingY | Number | 2 | Vertical padding of label backdrop -beginAtZero | Boolean | - | if true, scale will include 0 if it is not already included. -min | Number | - | User defined minimum number for the scale, overrides minimum value from data. -max | Number | - | User defined maximum number for the scale, overrides maximum value from data. -maxTicksLimit | Number | 11 | Maximum number of ticks and gridlines to show. If not defined, it will limit to 11 ticks but will show all gridlines. -showLabelBackdrop | Boolean | true | If true, draw a background behind the tick labels -fixedStepSize | Number | - | User defined fixed step size for the scale. If set, the scale ticks will be enumerated by multiple of stepSize, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. -stepSize | Number | - | if defined, it can be used along with the min and the max to give a custom number of steps. See the example below. -suggestedMax | Number | - | User defined maximum number for the scale, overrides maximum value *except for if* it is lower than the maximum value. -suggestedMin | Number | - | User defined minimum number for the scale, overrides minimum value *except for if* it is higher than the minimum value. - -### Update Default Scale config - -The default configuration for a scale can be easily changed using the scale service. Pass in a partial configuration that will be merged with the current scale default configuration. - -For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0. -``` -Chart.scaleService.updateScaleDefaults('linear', { - ticks: { - min: 0 - } -}) -``` diff --git a/docs/03-Line-Chart.md b/docs/03-Line-Chart.md deleted file mode 100644 index 24b77cfe6df..00000000000 --- a/docs/03-Line-Chart.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: Line Chart -anchor: line-chart ---- -### Introduction -A line chart is a way of plotting data points on a line. Often, it is used to show trend data, and the comparison of two data sets. - -

    - -
    - -### Example Usage -```javascript -var myLineChart = new Chart(ctx, { - type: 'line', - data: data, - options: options -}); -``` - -Alternatively a line chart can be created using syntax similar to the v1.0 syntax -```javascript -var myLineChart = Chart.Line(ctx, { - data: data, - options: options -}); -``` - -### Dataset Structure - -The following options can be included in a line chart dataset to configure options for that specific dataset. - -All point* properties can be specified as an array. If these are set to an array value, the first value applies to the first point, the second value to the second point, and so on. - -Property | Type | Usage ---- | --- | --- -data | See [data point](#line-chart-data-points) section | The data to plot in a line -label | `String` | The label for the dataset which appears in the legend and tooltips -xAxisID | `String` | The ID of the x axis to plot this dataset on -yAxisID | `String` | The ID of the y axis to plot this dataset on -fill | `Boolean` | If true, fill the area under the line -cubicInterpolationMode | `String` | Algorithm used to interpolate a smooth curve from the discrete data points. Options are 'default' and 'monotone'. The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. -lineTension | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. *Note* This was renamed from 'tension' but the old name still works. -backgroundColor | `Color` | The fill color under the line. See [Colors](#chart-configuration-colors) -borderWidth | `Number` | The width of the line in pixels -borderColor | `Color` | The color of the line. -borderCapStyle | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) -borderDash | `Array` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) -borderDashOffset | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -borderJoinStyle | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) -pointBorderColor | `Color or Array` | The border color for points. -pointBackgroundColor | `Color or Array` | The fill color for points -pointBorderWidth | `Number or Array` | The width of the point border in pixels -pointRadius | `Number or Array` | The radius of the point shape. If set to 0, nothing is rendered. -pointHoverRadius | `Number or Array` | The radius of the point when hovered -pointHitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events -pointHoverBackgroundColor | `Color or Array` | Point background color when hovered -pointHoverBorderColor | `Color or Array` | Point border color when hovered -pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String, Array, Image, Array` | The style of point. Options are 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash'. If the option is an image, that image is drawn on the canvas using `drawImage`. -showLine | `Boolean` | If false, the line is not drawn for this dataset -spanGaps | `Boolean` | If true, lines will be drawn between points with no or null data -steppedLine | `Boolean` | If true, the line is shown as a stepped line and 'lineTension' will be ignored - -An example data object using these attributes is shown below. -```javascript -var data = { - labels: ["January", "February", "March", "April", "May", "June", "July"], - datasets: [ - { - label: "My First dataset", - fill: false, - lineTension: 0.1, - backgroundColor: "rgba(75,192,192,0.4)", - borderColor: "rgba(75,192,192,1)", - borderCapStyle: 'butt', - borderDash: [], - borderDashOffset: 0.0, - borderJoinStyle: 'miter', - pointBorderColor: "rgba(75,192,192,1)", - pointBackgroundColor: "#fff", - pointBorderWidth: 1, - pointHoverRadius: 5, - pointHoverBackgroundColor: "rgba(75,192,192,1)", - pointHoverBorderColor: "rgba(220,220,220,1)", - pointHoverBorderWidth: 2, - pointRadius: 1, - pointHitRadius: 10, - data: [65, 59, 80, 81, 56, 55, 40], - spanGaps: false, - } - ] -}; -``` - -The line chart usually requires an array of labels. This labels are shown on the X axis. There must be one label for each data point. More labels than datapoints are allowed, in which case the line ends at the last data point. -The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation. - -The label key on each dataset is optional, and can be used when generating a scale for the chart. - -When `spanGaps` is set to true, the gaps between points in sparse datasets are filled in. By default, it is off. - -### Data Points - -The data passed to the chart can be passed in two formats. The most common method is to pass the data array as an array of numbers. In this case, the `data.labels` array must be specified and must contain a label for each point or, in the case of labels to be displayed over multiple lines an array of labels (one for each line) i.e `[["June","2015"], "July"]`. - -The alternate is used for sparse datasets. Data is specified using an object containing `x` and `y` properties. This is used for scatter charts as documented below. - -### Scatter Line Charts - -Scatter line charts can be created by changing the X axis to a linear axis. To use a scatter chart, data must be passed as objects containing X and Y properties. The example below creates a scatter chart with 3 points. - -```javascript -var scatterChart = new Chart(ctx, { - type: 'line', - data: { - datasets: [{ - label: 'Scatter Dataset', - data: [{ - x: -10, - y: 0 - }, { - x: 0, - y: 10 - }, { - x: 10, - y: 5 - }] - }] - }, - options: { - scales: { - xAxes: [{ - type: 'linear', - position: 'bottom' - }] - } - } -}); -``` - -### Chart Options - -These are the customisation options specific to Line charts. These options are merged with the [global chart configuration options](#chart-configuration-global-configuration), and form the options of the chart. - -Name | Type | Default | Description ---- | --- | --- | --- -showLines | Boolean | true | If false, the lines between points are not drawn -spanGaps | Boolean | false | If true, NaN data does not break the line - -You can override these for your `Chart` instance by passing a member `options` into the `Line` method. - -For example, we could have a line chart display without an X axis by doing the following. The config merge is smart enough to handle arrays so that you do not need to specify all axis settings to change one thing. - -```javascript -new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - xAxes: [{ - display: false - }] - } - } -}); -``` - -We can also change these defaults values for each Line type that is created, this object is available at `Chart.defaults.line`. - -### Stacked Charts - -Stacked area charts can be created by setting the Y axis to a stacked configuration. The following example would have stacked lines. - -```javascript -var stackedLine = new Chart(ctx, { - type: 'line', - data: data, - options: { - scales: { - yAxes: [{ - stacked: true - }] - } - } -}); -``` \ No newline at end of file diff --git a/docs/04-Bar-Chart.md b/docs/04-Bar-Chart.md deleted file mode 100644 index abf83074615..00000000000 --- a/docs/04-Bar-Chart.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Bar Chart -anchor: bar-chart ---- - -### Introduction -A bar chart is a way of showing data as bars. - -It is sometimes used to show trend data, and the comparison of multiple data sets side by side. - -
    - -
    - -### Example Usage -```javascript -var myBarChart = new Chart(ctx, { - type: 'bar', - data: data, - options: options -}); -``` - -Or if you want horizontal bars. - -```javascript -var myBarChart = new Chart(ctx, { - type: 'horizontalBar', - data: data, - options: options -}); -``` - -### Dataset Structure -The following options can be included in a bar chart dataset to configure options for that specific dataset. - -Some properties can be specified as an array. If these are set to an array value, the first value applies to the first bar, the second value to the second bar, and so on. - -Property | Type | Usage ---- | --- | --- -data | `Array` | The data to plot as bars -label | `String` | The label for the dataset which appears in the legend and tooltips -xAxisID | `String` | The ID of the x axis to plot this dataset on -yAxisID | `String` | The ID of the y axis to plot this dataset on -backgroundColor | `Color or Array` | The fill color of the bars. See [Colors](#chart-configuration-colors) -borderColor | `Color or Array` | Bar border color -borderWidth | `Number or Array` | Border width of bar in pixels -borderSkipped | `String or Array` | Which edge to skip drawing the border for. Options are 'bottom', 'left', 'top', and 'right' -hoverBackgroundColor | `Color or Array` | Bar background color when hovered -hoverBorderColor | `Color or Array` | Bar border color when hovered -hoverBorderWidth | `Number or Array` | Border width of bar when hovered -stack | `String` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack) - -An example data object using these attributes is shown below. - -```javascript -var data = { - labels: ["January", "February", "March", "April", "May", "June", "July"], - datasets: [ - { - label: "My First dataset", - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255,99,132,1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' - ], - borderWidth: 1, - data: [65, 59, 80, 81, 56, 55, 40], - } - ] -}; -``` -The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. -We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example. - -### Chart Options - -These are the customisation options specific to Bar charts. These options are merged with the [global chart configuration options](#global-chart-configuration), and form the options of the chart. - -The default options for bar chart are defined in `Chart.defaults.bar`. - -Name | Type | Default | Description ---- |:---:| --- | --- -*hover*.mode | String | "label" | Label's hover mode. "label" is used since the x axis displays data by the index in the dataset. -scales | Object | - | - -*scales*.xAxes | Array | | The bar chart officially supports only 1 x-axis but uses an array to keep the API consistent. Use a scatter chart if you need multiple x axes. -*Options for xAxes* | | | -type | String | "Category" | As defined in [Scales](#scales-category-scale). -display | Boolean | true | If true, show the scale. -id | String | "x-axis-0" | Id of the axis so that data can bind to it -stacked | Boolean | false | If true, bars are stacked on the x-axis -barThickness | Number | | Manually set width of each bar in pixels. If not set, the bars are sized automatically. -maxBarThickness | Number | | Set this to ensure that the automatically sized bars are not sized thicker than this. Only works if barThickness is not set (automatic sizing is enabled). -categoryPercentage | Number | 0.8 | Percent (0-1) of the available width (the space between the gridlines for small datasets) for each data-point to use for the bars. [Read More](#bar-chart-barpercentage-vs-categorypercentage) -barPercentage | Number | 0.9 | Percent (0-1) of the available width each bar should be within the category percentage. 1.0 will take the whole category width and put the bars right next to each other. [Read More](#bar-chart-barpercentage-vs-categorypercentage) -gridLines | Object | [See Scales](#scales) | -*gridLines*.offsetGridLines | Boolean | true | If true, the bars for a particular data point fall between the grid lines. If false, the grid line will go right down the middle of the bars. -| | | -*scales*.yAxes | Array | `[{ type: "linear" }]` | -*Options for yAxes* | | | -type | String | "linear" | As defined in [Scales](#scales-linear-scale). -display | Boolean | true | If true, show the scale. -id | String | "y-axis-0" | Id of the axis so that data can bind to it. -stacked | Boolean | false | If true, bars are stacked on the y-axis -barThickness | Number | | Manually set height of each bar in pixels. If not set, the bars are sized automatically. -maxBarThickness | Number | | Set this to ensure that the automatically sized bars are not sized thicker than this. Only works if barThickness is not set (automatic sizing is enabled). - -You can override these for your `Chart` instance by passing a second argument into the `Bar` method as an object with the keys you want to override. - -For example, we could have a bar chart without a stroke on each bar by doing the following: - -```javascript -new Chart(ctx, { - type: "bar", - data: data, - options: { - scales: { - xAxes: [{ - stacked: true - }], - yAxes: [{ - stacked: true - }] - } - } -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Bar chart defaults but this particular instance will have `stacked` set to true -// for both x and y axes. -``` - -We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.bar`. For horizontal bars, this object is available at `Chart.defaults.horizontalBar`. - -The default options for horizontal bar charts are defined in `Chart.defaults.horizontalBar` and are same as those of the bar chart, but with `xAxes` and `yAxes` swapped and the following additional options. - -Name | Type | Default | Description ---- |:---:| --- | --- -*Options for xAxes* | | | -position | String | "bottom" | -*Options for yAxes* | | | -position | String | "left" | - -### barPercentage vs categoryPercentage - -The following shows the relationship between the bar percentage option and the category percentage option. - -```text -// categoryPercentage: 1.0 -// barPercentage: 1.0 -Bar: | 1.0 | 1.0 | -Category: | 1.0 | -Sample: |===========| - -// categoryPercentage: 1.0 -// barPercentage: 0.5 -Bar: |.5| |.5| -Category: | 1.0 | -Sample: |==============| - -// categoryPercentage: 0.5 -// barPercentage: 1.0 -Bar: |1.||1.| -Category: | .5 | -Sample: |==============| -``` diff --git a/docs/05-Radar-Chart.md b/docs/05-Radar-Chart.md deleted file mode 100644 index 977574faf6c..00000000000 --- a/docs/05-Radar-Chart.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Radar Chart -anchor: radar-chart ---- - -### Introduction -A radar chart is a way of showing multiple data points and the variation between them. - -They are often useful for comparing the points of two or more different data sets. - -
    - -
    - -### Example Usage - -```javascript -var myRadarChart = new Chart(ctx, { - type: 'radar', - data: data, - options: options -}); -``` - -### Dataset Structure - -The following options can be included in a radar chart dataset to configure options for that specific dataset. - -All point* properties can be specified as an array. If these are set to an array value, the first value applies to the first point, the second value to the second point, and so on. - -Property | Type | Usage ---- | --- | --- -data | `Array` | The data to plot in a line -label | `String` | The label for the dataset which appears in the legend and tooltips -fill | `Boolean` | If true, fill the area under the line -lineTension | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. *Note* This was renamed from 'tension' but the old name still works. -backgroundColor | `Color` | The fill color under the line. See [Colors](#chart-configuration-colors) -borderWidth | `Number` | The width of the line in pixels -borderColor | `Color` | The color of the line. -borderCapStyle | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) -borderDash | `Array` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) -borderDashOffset | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -borderJoinStyle | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) -pointBorderColor | `Color or Array` | The border color for points. -pointBackgroundColor | `Color or Array` | The fill color for points -pointBorderWidth | `Number or Array` | The width of the point border in pixels -pointRadius | `Number or Array` | The radius of the point shape. If set to 0, nothing is rendered. -pointHoverRadius | `Number or Array` | The radius of the point when hovered -pointHitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events -pointHoverBackgroundColor | `Color or Array` | Point background color when hovered -pointHoverBorderColor | `Color or Array` | Point border color when hovered -pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String or Array` | The style of point. Options include 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash' - -An example data object using these attributes is shown below. - -```javascript -var data = { - labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], - datasets: [ - { - label: "My First dataset", - backgroundColor: "rgba(179,181,198,0.2)", - borderColor: "rgba(179,181,198,1)", - pointBackgroundColor: "rgba(179,181,198,1)", - pointBorderColor: "#fff", - pointHoverBackgroundColor: "#fff", - pointHoverBorderColor: "rgba(179,181,198,1)", - data: [65, 59, 90, 81, 56, 55, 40] - }, - { - label: "My Second dataset", - backgroundColor: "rgba(255,99,132,0.2)", - borderColor: "rgba(255,99,132,1)", - pointBackgroundColor: "rgba(255,99,132,1)", - pointBorderColor: "#fff", - pointHoverBackgroundColor: "#fff", - pointHoverBorderColor: "rgba(255,99,132,1)", - data: [28, 48, 40, 19, 96, 27, 100] - } - ] -}; -``` -For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. -For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values. -The label key on each dataset is optional, and can be used when generating a scale for the chart. - - -### Chart Options - -These are the customisation options specific to Radar charts. These options are merged with the [global chart configuration options](#global-chart-configuration), and form the options of the chart. - -The default options for radar chart are defined in `Chart.defaults.radar`. - -Name | Type | Default | Description ---- | --- | --- | --- -scale | Object | [See Scales](#scales) and [Defaults for Radial Linear Scale](#scales-radial-linear-scale) | Options for the one scale used on the chart. Use this to style the ticks, labels, and grid lines. -*scale*.type | String |"radialLinear" | As defined in ["Radial Linear"](#scales-radial-linear-scale). -*elements*.line | Object | | Options for all line elements used on the chart, as defined in the global elements, duplicated here to show Radar chart specific defaults. -*elements.line*.lineTension | Number | 0 | Tension exhibited by lines when calculating splineCurve. Setting to 0 creates straight lines. -startAngle | Number | 0 | The number of degrees to rotate the chart clockwise. - -You can override these for your `Chart` instance by passing a second argument into the `Radar` method as an object with the keys you want to override. - -For example, we could have a radar chart without a point for each on piece of data by doing the following: - -```javascript -new Chart(ctx, { - type: "radar", - data: data, - options: { - scale: { - reverse: true, - ticks: { - beginAtZero: true - } - } - } -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Radar chart defaults but this particular instance's scale will be reversed as -// well as the ticks beginning at zero. -``` - -We can also change these defaults values for each Radar type that is created, this object is available at `Chart.defaults.radar`. diff --git a/docs/06-Polar-Area-Chart.md b/docs/06-Polar-Area-Chart.md deleted file mode 100644 index d9b8f8a4d06..00000000000 --- a/docs/06-Polar-Area-Chart.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Polar Area Chart -anchor: polar-area-chart ---- -### Introduction -Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. - -This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. - -
    - -
    - -### Example Usage - -```javascript -new Chart(ctx, { - data: data, - type: 'polarArea', - options: options -}); -``` - -### Dataset Structure - -The following options can be included in a polar area chart dataset to configure options for that specific dataset. - -Some properties are specified as arrays. The first value applies to the first bar, the second value to the second bar, and so on. - -Property | Type | Usage ---- | --- | --- -data | `Array` | The data to plot as arcs -label | `String` | The label for the dataset which appears in the legend and tooltips -backgroundColor | `Array` | The fill color of the arcs. See [Colors](#chart-configuration-colors) -borderColor | `Array` | Arc border color -borderWidth | `Array` | Border width of arcs in pixels -hoverBackgroundColor | `Array` | Arc background color when hovered -hoverBorderColor | `Array` | Arc border color when hovered -hoverBorderWidth | `Array` | Border width of arc when hovered - -An example data object using these attributes is shown below. - -```javascript -var data = { - datasets: [{ - data: [ - 11, - 16, - 7, - 3, - 14 - ], - backgroundColor: [ - "#FF6384", - "#4BC0C0", - "#FFCE56", - "#E7E9ED", - "#36A2EB" - ], - label: 'My dataset' // for legend - }], - labels: [ - "Red", - "Green", - "Yellow", - "Grey", - "Blue" - ] -}; -``` -As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. - -### Chart Options - -These are the customisation options specific to Polar Area charts. These options are merged with the [global chart configuration options](#global-chart-configuration), and form the options of the chart. - -Name | Type | Default | Description ---- | --- | --- | --- -startAngle | Number | -0.5 * Math.PI | Sets the starting angle for the first item in a dataset -scale | Object | [See Scales](#scales) and [Defaults for Radial Linear Scale](#scales-radial-linear-scale) | Options for the one scale used on the chart. Use this to style the ticks, labels, and grid. -*scale*.type | String |"radialLinear" | As defined in ["Radial Linear"](#scales-radial-linear-scale). -*animation*.animateRotate | Boolean |true | If true, will animate the rotation of the chart. -*animation*.animateScale | Boolean | true | If true, will animate scaling the chart. -*legend*.*labels*.generateLabels | Function | `function(data) {} ` | Returns labels for each the legend -*legend*.onClick | Function | function(event, legendItem) {} ` | Handles clicking an individual legend item -legendCallback | Function | `function(chart) ` | Generates the HTML legend via calls to `generateLegend` - -You can override these for your `Chart` instance by passing a second argument into the `PolarArea` method as an object with the keys you want to override. - -For example, we could have a polar area chart with a black stroke on each segment like so: - -```javascript -new Chart(ctx, { - data: data, - type: "polarArea", - options: { - elements: { - arc: { - borderColor: "#000000" - } - } - } -}); -// This will create a chart with all of the default options, merged from the global config, -// and the PolarArea chart defaults but this particular instance will have `elements.arc.borderColor` set to `"#000000"`. -``` - -We can also change these defaults values for each PolarArea type that is created, this object is available at `Chart.defaults.polarArea`. diff --git a/docs/07-Pie-Doughnut-Chart.md b/docs/07-Pie-Doughnut-Chart.md deleted file mode 100644 index 99830132281..00000000000 --- a/docs/07-Pie-Doughnut-Chart.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Pie & Doughnut Charts -anchor: doughnut-pie-chart ---- -### Introduction -Pie and doughnut charts are probably the most commonly used charts there are. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. - -They are excellent at showing the relational proportions between data. - -Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutoutPercentage`. This equates what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. - -They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. - -
    - -
    - -
    - -
    -
    - -### Example Usage - -```javascript -// For a pie chart -var myPieChart = new Chart(ctx,{ - type: 'pie', - data: data, - options: options -}); -``` - -```javascript -// And for a doughnut chart -var myDoughnutChart = new Chart(ctx, { - type: 'doughnut', - data: data, - options: options -}); -``` - -### Dataset Structure - -Property | Type | Usage ---- | --- | --- -data | `Array` | The data to plot as arcs -label | `String` | The label for the dataset which appears in the legend and tooltips -backgroundColor | `Array` | The fill color of the arcs. See [Colors](#chart-configuration-colors) -borderColor | `Array` | Arc border color -borderWidth | `Array` | Border width of arcs in pixels -hoverBackgroundColor | `Array` | Arc background color when hovered -hoverBorderColor | `Array` | Arc border color when hovered -hoverBorderWidth | `Array` | Border width of arc when hovered - -An example data object using these attributes is shown below. - -```javascript -var data = { - labels: [ - "Red", - "Blue", - "Yellow" - ], - datasets: [ - { - data: [300, 50, 100], - backgroundColor: [ - "#FF6384", - "#36A2EB", - "#FFCE56" - ], - hoverBackgroundColor: [ - "#FF6384", - "#36A2EB", - "#FFCE56" - ] - }] -}; -``` - -For a pie chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. You can also add an array of background colors. The color attributes should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. - -### Chart Options - -These are the customisation options specific to Pie & Doughnut charts. These options are merged with the [global chart configuration options](#global-chart-configuration), and form the options of the chart. - -Name | Type | Default | Description ---- | --- | --- | --- -cutoutPercentage | Number | 50 - for doughnut, 0 - for pie | The percentage of the chart that is cut out of the middle. -rotation | Number | -0.5 * Math.PI | Starting angle to draw arcs from -circumference | Number | 2 * Math.PI | Sweep to allow arcs to cover -*animation*.animateRotate | Boolean |true | If true, will animate the rotation of the chart. -*animation*.animateScale | Boolean | false | If true, will animate scaling the Doughnut from the centre. -*legend*.*labels*.generateLabels | Function | `function(chart) {} ` | Returns a label for each item to be displayed on the legend. -*legend*.onClick | Function | function(event, legendItem) {} ` | Handles clicking an individual legend item - -You can override these for your `Chart` instance by passing a second argument into the `Doughnut` method as an object with the keys you want to override. - -For example, we could have a doughnut chart that animates by scaling out from the centre like so: - -```javascript -new Chart(ctx,{ - type:"doughnut", - options: { - animation:{ - animateScale:true - } - } -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Doughnut chart defaults but this particular instance will have `animateScale` set to `true`. -``` - -We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.pie`, with the only difference being `cutoutPercentage` being set to 0. diff --git a/docs/08-Bubble-Chart.md b/docs/08-Bubble-Chart.md deleted file mode 100644 index 59b8b773053..00000000000 --- a/docs/08-Bubble-Chart.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Bubble Chart -anchor: bubble-chart ---- -### Introduction -A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. - -
    - -
    -
    - -### Example Usage - -```javascript -// For a bubble chart -var myBubbleChart = new Chart(ctx,{ - type: 'bubble', - data: data, - options: options -}); -``` - -### Dataset Structure - -Property | Type | Usage ---- | --- | --- -data | `Array` | The data to plot as bubbles. See [Data format](#bubble-chart-data-format) -label | `String` | The label for the dataset which appears in the legend and tooltips -backgroundColor | `Color Array` | The fill color of the bubbles. See [Colors](#chart-configuration-colors) -borderColor | `Color or Array` | The stroke color of the bubbles. -borderWidth | `Number or Array` | The stroke width of bubble in pixels. -hoverBackgroundColor | `Color or Array` | The fill color of the bubbles when hovered. -hoverBorderColor | `Color or Array` | The stroke color of the bubbles when hovered. -hoverBorderWidth | `Number or Array` | The stroke width of the bubbles when hovered. -hoverRadius | `Number or Array` | Additional radius to add to data radius on hover. - -An example data object using these attributes is shown below. This example creates a single dataset with 2 different bubbles. - -```javascript -var data = { - datasets: [ - { - label: 'First Dataset', - data: [ - { - x: 20, - y: 30, - r: 15 - }, - { - x: 40, - y: 10, - r: 10 - } - ], - backgroundColor:"#FF6384", - hoverBackgroundColor: "#FF6384", - }] -}; -``` - -### Data Object - -Data for the bubble chart is passed in the form of an object. The object must implement the following interface. It is important to note that the radius property, `r` is **not** scaled by the chart. It is the raw radius in pixels of the bubble that is drawn on the canvas. - -```javascript -{ - // X Value - x: , - - // Y Value - y: , - - // Radius of bubble. This is not scaled. - r: -} -``` - -### Chart Options - -The bubble chart has no unique configuration options. To configure options common to all of the bubbles, the point element options are used. - -For example, to give all bubbles a 1px wide black border, the following options would be used. - -```javascript -new Chart(ctx,{ - type:"bubble", - options: { - elements: { - points: { - borderWidth: 1, - borderColor: 'rgb(0, 0, 0)' - } - } - } -}); -``` - -We can also change the default values for the Bubble chart type. Doing so will give all bubble charts created after this point the new defaults. The default configuration for the bubble chart can be accessed at `Chart.defaults.bubble`. diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md deleted file mode 100644 index 9d1f2834137..00000000000 --- a/docs/09-Advanced.md +++ /dev/null @@ -1,467 +0,0 @@ ---- -title: Advanced usage -anchor: advanced-usage ---- - - -### Prototype Methods - -For each chart, there are a set of global prototype methods on the shared `ChartType` which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. - -```javascript -// For example: -var myLineChart = new Chart(ctx, config); -``` - -#### .destroy() - -Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. -This must be called before the canvas is reused for a new chart. - -```javascript -// Destroys a specific chart instance -myLineChart.destroy(); -``` - -#### .update(duration, lazy) - -Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. - -```javascript -// duration is the time for the animation of the redraw in milliseconds -// lazy is a boolean. if true, the animation can be interrupted by other animations -myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 -myLineChart.update(); // Calling update now animates the position of March from 90 to 50. -``` - -> **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. - -#### .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. - -```javascript -// duration is the time for the animation of the redraw in milliseconds -// lazy is a boolean. if true, the animation can be interrupted by other animations -myLineChart.render(duration, lazy); -``` - -#### .stop() - -Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. - -```javascript -// Stops the charts animation loop at its current frame -myLineChart.stop(); -// => returns 'this' for chainability -``` - -#### .resize() - -Use this to manually resize the canvas element. This is run each time the canvas container is resized, but you can call this method manually if you change the size of the canvas nodes container element. - -```javascript -// Resizes & redraws to fill its container element -myLineChart.resize(); -// => returns 'this' for chainability -``` - -#### .clear() - -Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. - -```javascript -// Will clear the canvas that myLineChart is drawn on -myLineChart.clear(); -// => returns 'this' for chainability -``` - -#### .toBase64Image() - -This returns a base 64 encoded string of the chart in it's current state. - -```javascript -myLineChart.toBase64Image(); -// => returns png data url of the image on the canvas -``` - -#### .generateLegend() - -Returns an HTML string of a legend for that chart. The legend is generated from the `legendCallback` in the options. - -```javascript -myLineChart.generateLegend(); -// => returns HTML string of a legend for this chart -``` - -#### .getElementAtEvent(e) - -Calling `getElementAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the single element at the event position. If there are multiple items within range, only the first is returned - -```javascript -myLineChart.getElementAtEvent(e); -// => returns the first element at the event point. -``` - -#### .getElementsAtEvent(e) - -Looks for the element under the event point, then returns all elements at the same data index. This is used internally for 'label' mode highlighting. - -Calling `getElementsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activePoints = myLineChart.getElementsAtEvent(evt); - // => activePoints is an array of points on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .getDatasetAtEvent(e) - -Looks for the element under the event point, then returns all elements from that dataset. This is used internally for 'dataset' mode highlighting - -```javascript -myLineChart.getDatasetAtEvent(e); -// => returns an array of elements -``` - -#### .getDatasetMeta(index) - -Looks for the dataset that matches the current index and returns that metadata. This returned data has all of the metadata that is used to construct the chart. - -The `data` property of the metadata will contain information about each point, rectangle, etc. depending on the chart type. - -Extensive examples of usage are available in the [Chart.js tests](https://github.com/chartjs/Chart.js/tree/master/test). - -```javascript -var meta = myChart.getDatasetMeta(0); -var x = meta.data[0]._model.x -``` - -### External Tooltips - -You can enable custom tooltips in the global or chart configuration like so: - -```javascript -var myPieChart = new Chart(ctx, { - type: 'pie', - data: data, - options: { - tooltips: { - custom: function(tooltip) { - // tooltip will be false if tooltip is not visible or should be hidden - if (!tooltip) { - return; - } - - // Otherwise, tooltip will be an object with all tooltip properties like: - - // tooltip.caretSize - // tooltip.caretPadding - // tooltip.chart - // tooltip.cornerRadius - // tooltip.fillColor - // tooltip.font... - // tooltip.text - // tooltip.x - // tooltip.y - // tooltip.caretX - // tooltip.caretY - // etc... - } - } - } -}); -``` - -See `samples/tooltips/line-customTooltips.html` for examples on how to get started. - -### Writing New Scale Types - -Starting with Chart.js 2.0 scales can be individually extended. Scales should always derive from Chart.Scale. - -```javascript -var MyScale = Chart.Scale.extend({ - /* extensions ... */ -}); - -// MyScale is now derived from Chart.Scale -``` - -Once you have created your scale class, you need to register it with the global chart object so that it can be used. A default config for the scale may be provided when registering the constructor. The first parameter to the register function is a string key that is used later to identify which scale type to use for a chart. - -```javascript -Chart.scaleService.registerScaleType('myScale', MyScale, defaultConfigObject); -``` - -To use the new scale, simply pass in the string key to the config when creating a chart. - -```javascript -var lineChart = new Chart(ctx, { - data: data, - type: 'line', - options: { - scales: { - yAxes: [{ - type: 'myScale' // this is the same key that was passed to the registerScaleType function - }] - } - } -}) -``` - -#### Scale Properties - -Scale instances are given the following properties during the fitting process. - -```javascript -{ - left: Number, // left edge of the scale bounding box - right: Number, // right edge of the bounding box' - top: Number, - bottom: Number, - width: Number, // the same as right - left - height: Number, // the same as bottom - top - - // Margin on each side. Like css, this is outside the bounding box. - margins: { - left: Number, - right: Number, - top: Number, - bottom: Number, - }, - - // Amount of padding on the inside of the bounding box (like CSS) - paddingLeft: Number, - paddingRight: Number, - paddingTop: Number, - paddingBottom: Number, -} -``` - -#### Scale Interface -To work with Chart.js, custom scale types must implement the following interface. - -```javascript -{ - // Determines the data limits. Should set this.min and this.max to be the data max/min - determineDataLimits: function() {}, - - // Generate tick marks. this.chart is the chart instance. The data object can be accessed as this.chart.data - // buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class - buildTicks: function() {}, - - // Get the value to show for the data at the given index of the the given dataset, ie this.chart.data.datasets[datasetIndex].data[index] - getLabelForIndex: function(index, datasetIndex) {}, - - // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value - // @param index: index into the ticks array - // @param includeOffset: if true, get the pixel halway between the given tick and the next - getPixelForTick: function(index, includeOffset) {}, - - // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value - // @param value : the value to get the pixel for - // @param index : index into the data array of the value - // @param datasetIndex : index of the dataset the value comes from - // @param includeOffset : if true, get the pixel halway between the given tick and the next - getPixelForValue: function(value, index, datasetIndex, includeOffset) {} - - // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) - // @param pixel : pixel value - getValueForPixel: function(pixel) {} -} -``` - -Optionally, the following methods may also be overwritten, but an implementation is already provided by the `Chart.Scale` base class. - -```javascript - // Transform the ticks array of the scale instance into strings. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); - convertTicksToLabels: function() {}, - - // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. - calculateTickRotation: function() {}, - - // Fits the scale into the canvas. - // this.maxWidth and this.maxHeight will tell you the maximum dimensions the scale instance can be. Scales should endeavour to be as efficient as possible with canvas space. - // this.margins is the amount of space you have on either side of your scale that you may expand in to. This is used already for calculating the best label rotation - // You must set this.minSize to be the size of your scale. It must be an object containing 2 properties: width and height. - // You must set this.width to be the width and this.height to be the height of the scale - fit: function() {}, - - // Draws the scale onto the canvas. this.(left|right|top|bottom) will have been populated to tell you the area on the canvas to draw in - // @param chartArea : an object containing four properties: left, right, top, bottom. This is the rectangle that lines, bars, etc will be drawn in. It may be used, for example, to draw grid lines. - draw: function(chartArea) {}, -``` - -The Core.Scale base class also has some utility functions that you may find useful. -```javascript -{ - // Returns true if the scale instance is horizontal - isHorizontal: function() {}, - - // Get the correct value from the value from this.chart.data.datasets[x].data[] - // If dataValue is an object, returns .x or .y depending on the return of isHorizontal() - // If the value is undefined, returns NaN - // Otherwise returns the value. - // Note that in all cases, the returned value is not guaranteed to be a Number - getRightValue: function(dataValue) {}, -} -``` - -### Writing New Chart Types - -Chart.js 2.0 introduces the concept of controllers for each dataset. Like scales, new controllers can be written as needed. - -```javascript -Chart.controllers.MyType = Chart.DatasetController.extend({ - -}); - - -// Now we can create a new instance of our chart, using the Chart.js API -new Chart(ctx, { - // this is the string the constructor was registered at, ie Chart.controllers.MyType - type: 'MyType', - data: data, - options: options -}); -``` - -#### Dataset Controller Interface - -Dataset controllers must implement the following interface. - -```javascript -{ - // Create elements for each piece of data in the dataset. Store elements in an array on the dataset as dataset.metaData - addElements: function() {}, - - // Create a single element for the data at the given index and reset its state - addElementAndReset: function(index) {}, - - // Draw the representation of the dataset - // @param ease : if specified, this number represents how far to transition elements. See the implementation of draw() in any of the provided controllers to see how this should be used - draw: function(ease) {}, - - // Remove hover styling from the given element - removeHoverStyle: function(element) {}, - - // Add hover styling to the given element - setHoverStyle: function(element) {}, - - // Update the elements in response to new data - // @param reset : if true, put the elements into a reset state so they can animate to their final values - update: function(reset) {}, -} -``` - -The following methods may optionally be overridden by derived dataset controllers -```javascript -{ - // Initializes the controller - initialize: function(chart, datasetIndex) {}, - - // Ensures that the dataset represented by this controller is linked to a scale. Overridden to helpers.noop in the polar area and doughnut controllers as these - // chart types using a single scale - linkScales: function() {}, - - // Called by the main chart controller when an update is triggered. The default implementation handles the number of data points changing and creating elements appropriately. - buildOrUpdateElements: function() {} -} -``` - -### Extending Existing Chart Types - -Extending or replacing an existing controller type is easy. Simply replace the constructor for one of the built in types with your own. - -The built in controller types are: -* `Chart.controllers.line` -* `Chart.controllers.bar` -* `Chart.controllers.radar` -* `Chart.controllers.doughnut` -* `Chart.controllers.polarArea` -* `Chart.controllers.bubble` - -#### Bar Controller -The bar controller has a special property that you should be aware of. To correctly calculate the width of a bar, the controller must determine the number of datasets that map to bars. To do this, the bar controller attaches a property `bar` to the dataset during initialization. If you are creating a replacement or updated bar controller, you should do the same. This will ensure that charts with regular bars and your new derived bars will work seamlessly. - -### Creating Plugins - -Starting with v2.1.0, you can create plugins for chart.js. To register your plugin, simply call `Chart.plugins.register` and pass your plugin in. -Plugins will be called at the following times -* Start of initialization -* End of initialization -* Start of update -* After the chart scales have calculated -* Start of datasets update -* End of datasets update -* End of update (before render occurs) -* Start of draw -* End of draw -* Before datasets draw -* After datasets draw -* Resize -* Before an animation is started -* When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled - -Plugins should implement the `IPlugin` interface: -```javascript -{ - beforeInit: function(chart) { }, - afterInit: function(chart) { }, - - resize: function(chart, newChartSize) { }, - - beforeUpdate: function(chart) { }, - afterScaleUpdate: function(chart) { } - beforeDatasetsUpdate: function(chart) { } - afterDatasetsUpdate: function(chart) { } - afterUpdate: function(chart) { }, - - // This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw - // to do something on each animation frame - beforeRender: function(chart) { }, - - // Easing is for animation - beforeDraw: function(chart, easing) { }, - afterDraw: function(chart, easing) { }, - // Before the datasets are drawn but after scales are drawn - beforeDatasetsDraw: function(chart, easing) { }, - afterDatasetsDraw: function(chart, easing) { }, - - destroy: function(chart) { } - - // Called when an event occurs on the chart - beforeEvent: function(chart, event) {} - afterEvent: function(chart, event) {} -} -``` - -### Building Chart.js - -Chart.js uses gulp to build the library into a single JavaScript file. - -Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: - -```bash -npm install -npm install -g gulp -``` - -This will install the local development dependencies for Chart.js, along with a CLI for the JavaScript task runner gulp. - -Now, we can run the `gulp build` task. - -```bash -gulp build -``` diff --git a/docs/10-Notes.md b/docs/10-Notes.md deleted file mode 100644 index 451a8a8cc90..00000000000 --- a/docs/10-Notes.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Notes -anchor: notes ---- -### Previous versions - -Version 2 has a completely different API than earlier versions. - -Most earlier version options have current equivalents or are the same. - -Please use the documentation that is available on [chartjs.org](http://www.chartjs.org/docs/) for the current version of Chart.js. - -Please note - documentation for previous versions are available on the GitHub repo. - -- [1.x Documentation](https://github.com/chartjs/Chart.js/tree/v1.1.1/docs) - -### Browser support - -Chart.js offers support for all browsers where canvas is supported. - -Browser support for the canvas element is available in all modern & major mobile browsers (http://caniuse.com/#feat=canvas). - -Thanks to BrowserStack for allowing our team to test on thousands of browsers. - - -### Bugs & issues - -Please report these on the GitHub page - at github.com/chartjs/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. - - -### Contributing - -New contributions to the library are welcome, but we ask that you please follow these guidelines: - -- Use tabs for indentation, not spaces. -- Only change the individual files in `/src`. -- Check that your code will pass `eslint` code standards, `gulp lint` will run this for you. -- Check that your code will pass tests, `gulp test` will run tests for you. -- Keep pull requests concise, and document new functionality in the relevant `.md` file. -- Consider whether your changes are useful for all users, or if creating a Chart.js plugin would be more appropriate. - -### License - -Chart.js is open source and available under the MIT license. - -### Charting Library Comparison - -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 | ✓ | ✓ | ✓ | ✓ | - -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.) | | ✓ | | - -### Popular Plugins - -There are many plugins that add additional functionality to Chart.js. Some particularly notable ones are listed here. In addition, many plugins can be found on the [Chart.js GitHub organization](https://github.com/chartjs). - - - chartjs-plugin-annotation.js - Draw lines and boxes on chart area - - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport - - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse - - chartjs-plugin-zoom.js - Enable zooming and panning on charts - - Chart.BarFunnel.js - Adds a bar funnel chart type - - Chart.LinearGauge.js - Adds a linear gauge chart type - - Chart.Smith.js - Adds a smith chart type - -### Popular Extensions - -There are many extensions which are available for use with popular frameworks. Some particularly notable ones are listed here. - -#### Angular - - angular-chart.js - - tc-angular-chartjs - - angular-chartjs - - Angular Chart-js Directive - -#### React - - react-chartjs2 - - react-chartjs-2 - -#### Django - - Django JChart - - Django Chartjs - -#### Ruby on Rails - - chartjs-ror - -#### Laravel - - laravel-chartjs - -#### Vue.js - - vue-chartjs diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..bdb4141682e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,65 @@ +# Chart.js + +[![Chart.js on Slack](https://img.shields.io/badge/slack-Chart.js-blue.svg)](https://chart-js-automation.herokuapp.com/) + +## Installation + +You can download the latest version of Chart.js from the [GitHub releases](https://github.com/chartjs/Chart.js/releases/latest) or use a [Chart.js CDN](https://cdnjs.com/libraries/Chart.js). Detailed installation instructions can be found on the [installation](./getting-started/installation.md) page. + +## Creating a Chart + +It's easy to get started with Chart.js. All that's required is the script included in your page along with a single `` node to render the chart. + +In this example, we create a bar chart for a single dataset and render that in our page. You can see all the ways to use Chart.js in the [usage documentation](./getting-started/usage.md) +```html + + +``` + +## Contributing + +Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/CONTRIBUTING.md) first. + +For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). + +## License + +Chart.js is available under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000000..d7950fed9e1 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,50 @@ +# Summary + +* [Chart.js](README.md) +* [Getting Started](getting-started/README.md) + * [Installation](getting-started/installation.md) + * [Integration](getting-started/integration.md) + * [Usage](getting-started/usage.md) +* [General](general/README.md) + * [Responsive](general/responsive.md) + * [Interactions](general/interactions/README.md) + * [Events](general/interactions/events.md) + * [Modes](general/interactions/modes.md) + * [Colors](general/colors.md) + * [Fonts](general/fonts.md) +* [Configuration](configuration/README.md) + * [Animations](configuration/animations.md) + * [Layout](configuration/layout.md) + * [Legend](configuration/legend.md) + * [Title](configuration/title.md) + * [Tooltip](configuration/tooltip.md) + * [Elements](configuration/elements.md) +* [Charts](charts/README.md) + * [Line](charts/line.md) + * [Bar](charts/bar.md) + * [Radar](charts/radar.md) + * [Doughnut & Pie](charts/doughnut.md) + * [Polar Area](charts/polar.md) + * [Bubble](charts/bubble.md) + * [Scatter](charts/scatter.md) + * [Mixed](charts/mixed.md) +* [Axes](axes/README.md) + * [Cartesian](axes/cartesian/README.md) + * [Category](axes/cartesian/category.md) + * [Linear](axes/cartesian/linear.md) + * [Logarithmic](axes/cartesian/logarithmic.md) + * [Time](axes/cartesian/time.md) + * [Radial](axes/radial/README.md) + * [Linear](axes/radial/linear.md) + * [Labelling](axes/labelling.md) + * [Styling](axes/styling.md) +* [Developers](developers/README.md) + * [Chart.js API](developers/api.md) + * [Plugins](developers/plugins.md) + * [New Charts](developers/charts.md) + * [New Axes](developers/axes.md) + * [Contributing](developers/contributing.md) +* [Additional Notes](notes/README.md) + * [Comparison Table](notes/comparison.md) + * [Popular Extensions](notes/extensions.md) + * [License](notes/license.md) diff --git a/docs/axes/README.md b/docs/axes/README.md new file mode 100644 index 00000000000..9f53b14ab47 --- /dev/null +++ b/docs/axes/README.md @@ -0,0 +1,57 @@ +# Axes + +Axes are an integral part of a chart. They are used to determine how data maps to a pixel value on the chart. In a cartesian chart, there is 1 or more X axis and 1 or more Y axis to map points onto the 2 dimensional canvas. These axes are know as ['cartesian axes'](./cartesian/README.md#cartesian-axes). + +In a radial chart, such as a radar chart or a polar area chart, there is a single axis that maps points in the angular and radial directions. These are known as ['radial axes'](./radial/README.md#radial-axes). + +Scales in Chart.js >V2.0 are significantly more powerful, but also different than those of v1.0. +* Multiple X & Y axes are supported. +* A built-in label auto-skip feature detects would-be overlapping ticks and labels and removes every nth label to keep things displaying normally. +* Scale titles are supported +* New scale types can be extended without writing an entirely new chart type + +# Common Configuration + +The following properties are common to all axes provided by Chart.js + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `display` | `Boolean` | `true` | If set to `false` the axis is hidden from view. Overrides *gridLines.display*, *scaleLabel.display*, and *ticks.display*. +| `callbacks` | `Object` | | Callback functions to hook into the axis lifecycle. [more...](#callbacks) + +## Callbacks +There are a number of config callbacks that can be used to change parameters in the scale at different points in the update process. + +| Name | Arguments | Description +| ---- | --------- | ----------- +| `beforeUpdate` | `axis` | Callback called before the update process starts. +| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. +| `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. +| `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. +| `afterDataLimits` | `axis` | Callback that runs after data limits are determined. +| `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. +| `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. +| `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. +| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. +| `beforeCalculateTickRotation` | `axis` | Callback that runs before tick rotation is determined. +| `afterCalculateTickRotation` | `axis` | Callback that runs after tick rotation is determined. +| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. +| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. +| `afterUpdate` | `axis` | Callback that runs at the end of the update process. + +## Updating Axis Defaults + +The default configuration for a scale can be easily changed using the scale service. All you need to do is to pass in a partial configuration that will be merged with the current scale default configuration to form the new default. + +For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0. + +```javascript +Chart.scaleService.updateScaleDefaults('linear', { + ticks: { + min: 0 + } +}); +``` + +## Creating New Axes +To create a new axis, see the [developer docs](../developers/axes.md). diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md new file mode 100644 index 00000000000..c5457161938 --- /dev/null +++ b/docs/axes/cartesian/README.md @@ -0,0 +1,104 @@ +# Cartesian Axes + +Axes that follow a cartesian grid are known as 'Cartesian Axes'. Cartesian axes are used for line, bar, and bubble charts. Four cartesian axes are included in Chart.js by default. + +* [linear](./linear.md#linear-cartesian-axis) +* [logarithmic](./logarithmic.md#logarithmic-cartesian-axis) +* [category](./category.md#category-cartesian-axis) +* [time](./time.md#time-cartesian-axis) + +# Common Configuration + +All of the included cartesian axes support a number of common options. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `type` | `String` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. +| `position` | `String` | | Position of the axis in the chart. Possible values are: `'top'`, `'left'`, `'bottom'`, `'right'` +| `id` | `String` | | The ID is used to link datasets and scale axes together. [more...](#axis-id) +| `gridLines` | `Object` | | Grid line configuration. [more...](../styling.md#grid-line-configuration) +| `scaleLabel` | `Object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) +| `ticks` | `Object` | | Tick configuration. [more...](#tick-configuration) + +## Tick Configuration +The following options are common to all cartesian axes but do not apply to other axes. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `autoSkip` | `Boolean` | `true` | If true, automatically calculates how many labels that can be shown and hides labels accordingly. Turn it off to show all labels no matter what +| `autoSkipPadding` | `Number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. *Note: Only applicable to horizontal scales.* +| `labelOffset` | `Number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the y direction for the x axis, and the x direction for the y axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* +| `maxRotation` | `Number` | `90` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* +| `minRotation` | `Number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* +| `mirror` | `Boolean` | `false` | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* +| `padding` | `Number` | `10` | Padding between the tick label and the axis. *Note: Only applicable to horizontal scales.* + +## Axis ID +The properties `dataset.xAxisID` or `dataset.yAxisID` have to match the scale properties `scales.xAxes.id` or `scales.yAxes.id`. This is especially needed if multi-axes charts are used. + +```javascript +var myChart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + // This dataset appears on the first axis + yAxisID: 'first-y-axis' + }, { + // This dataset appears on the second axis + yAxisID: 'second-y-axis' + }] + }, + options: { + scales: { + yAxes: [{ + id: 'first-y-axis', + type: 'linear' + }, { + id: 'second-y-axis', + type: 'linear' + }] + } + } +}); +``` + +# Creating Multiple Axes + +With cartesian axes, it is possible to create multiple X and Y axes. To do so, you can add multiple configuration objects to the `xAxes` and `yAxes` properties. When adding new axes, it is important to ensure that you specify the type of the new axes as default types are **not** used in this case. + +In the example below, we are creating two Y axes. We then use the `yAxisID` property to map the datasets to their correct axes. + +```javascript +var myChart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [20, 50, 100, 75, 25, 0], + label: 'Left dataset' + + // This binds the dataset to the left y axis + yAxisID: 'left-y-axis' + }, { + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0] + label: 'Right dataset' + + // This binds the dataset to the right y axis + yAxisID: 'right-y-axis', + }], + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] + }, + options: { + scales: { + yAxes: [{ + id: 'left-y-axis', + type: 'linear', + position: 'left' + }, { + id: 'right-y-axis', + type: 'linear', + position: 'right' + }] + } + } +}); +``` \ No newline at end of file diff --git a/docs/axes/cartesian/category.md b/docs/axes/cartesian/category.md new file mode 100644 index 00000000000..41bfd74d7ed --- /dev/null +++ b/docs/axes/cartesian/category.md @@ -0,0 +1,36 @@ +# Category Cartesian Axis + +The category scale will be familiar to those who have used v1.0. Labels are drawn from one of the label arrays included in the chart data. If only `data.labels` is defined, this will be used. If `data.xLabels` is defined and the axis is horizontal, this will be used. Similarly, if `data.yLabels` is defined and the axis is vertical, this property will be used. Using both `xLabels` and `yLabels` together can create a chart that uses strings for both the X and Y axes. + +## Tick Configuration Options + +The category scale provides the following options for configuring tick marks. They are nested in the `ticks` sub object. These options extend the [common tick configuration](README.md#tick-configuration). + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `min` | `String` | | The minimum item to display. [more...](#min-max-configuration) +| `max` | `String` | | The maximum item to display. [more...](#min-max-configuration) + +## Min Max Configuration +For both the `min` and `max` properties, the value must be in the `labels` array. In the example below, the x axis would only display "March" through "June". + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [10, 20, 30, 40, 50, 60] + }], + labels: ['January', 'February', 'March', 'April', 'May', 'June'], + }, + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March' + } + }] + } + } +}); +``` diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md new file mode 100644 index 00000000000..00f7aced1fe --- /dev/null +++ b/docs/axes/cartesian/linear.md @@ -0,0 +1,74 @@ +# Linear Cartesian Axis + +The linear scale is use to chart numerical data. It can be placed on either the x or y axis. The scatter chart type automatically configures a line chart to use one of these scales for the x axis. As the name suggests, linear interpolation is used to determine where a value lies on the axis. + +## Tick Configuration Options + +The following options are provided by the linear scale. They are all located in the `ticks` sub options. These options extend the [common tick configuration](README.md#tick-configuration). + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `beginAtZero` | `Boolean` | | if true, scale will include 0 if it is not already included. +| `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) +| `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) +| `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) +| `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) +| `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) + +## Axis Range Settings + +Given the number of axis range settings, it is important to understand how they all interact with each other. + +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. + +```javascript +let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); +let maxDataValue = Math.max(mostPositiveValue, options.ticks.suggestedMax); +``` + +In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + label: 'First dataset', + data: [0, 20, 40, 50] + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: { + scales: { + yAxes: [{ + ticks: { + suggestedMin: 50 + suggestedMax: 100 + } + }] + } + } +}); +``` + +In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. + +## Step Size + If set, the scale ticks will be enumerated by multiple of stepSize, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. + +This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. + +```javascript +let options = { + scales: { + yAxes: [{ + ticks: { + max: 5, + min: 0, + stepSize: 0.5 + } + }] + } +}; +``` \ No newline at end of file diff --git a/docs/axes/cartesian/logarithmic.md b/docs/axes/cartesian/logarithmic.md new file mode 100644 index 00000000000..207c5502430 --- /dev/null +++ b/docs/axes/cartesian/logarithmic.md @@ -0,0 +1,12 @@ +# Logarithmic Cartesian Axis + +The logarithmic scale is use to chart numerical data. It can be placed on either the x or y axis. As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. + +## Tick Configuration Options + +The following options are provided by the logarithmic scale. They are all located in the `ticks` sub options. These options extend the [common tick configuration](README.md#tick-configuration). + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. +| `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. \ No newline at end of file diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md new file mode 100644 index 00000000000..67830e9aa16 --- /dev/null +++ b/docs/axes/cartesian/time.md @@ -0,0 +1,97 @@ +# Time Cartesian Axis + +The time scale is used to display times and dates. When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale. + +## Configuration Options + +The following options are provided by the time scale. They are all located in the `time` sub options. These options extend the [common tick configuration](README.md#tick-configuration). + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `displayFormats` | `Object` | | Sets how different time units are displayed. [more...](#display-formats) +| `isoWeekday` | `Boolean` | `false` | If true and the unit is set to 'week', iso weekdays will be used. +| `max` | [Time](#date-formats) | | If defined, this will override the data maximum +| `min` | [Time](#date-formats) | | If defined, this will override the data minimum +| `parser` | `String` or `Function` | | Custom parser for dates. [more...](#parser) +| `round` | `String` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#scales-time-units) below for the allowed units. +| `tooltipFormat` | `String` | | The moment js format string to use for the tooltip. +| `unit` | `String` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#scales-time-units) section below for details. +| `unitStepSize` | `Number` | `1` | The number of units between grid lines. +| `minUnit` | `String` | `millisecond` | The minimum display format to be used for a time unit. + +## Date Formats + +When providing data for the time scale, Chart.js supports all of the formats that Moment.js accepts. See [Moment.js docs](http://momentjs.com/docs/#/parsing/) for details. + +## Time Units + +The following time measurements are supported. The names can be passed as strings to the `time.unit` config option to force a certain unit. + +* millisecond +* second +* minute +* hour +* day +* week +* month +* quarter +* year + +For example, to create a chart with a time scale that always displayed units per month, the following config could be used. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + xAxes: [{ + time: { + unit: 'month' + } + }] + } + } +}) +``` + +## Display Formats +The following display formats are used to configure how different time units are formed into strings for the axis tick marks. See [moment.js](http://momentjs.com/docs/#/displaying/format/) for the allowable format strings. + +Name | Default +--- | --- +millisecond | 'SSS [ms]' +second | 'h:mm:ss a' +minute | 'h:mm:ss a' +hour | 'MMM D, hA' +day | 'll' +week | 'll' +month | 'MMM YYYY' +quarter | '[Q]Q - YYYY' +year | 'YYYY' + +For example, to set the display format for the 'quarter' unit to show the month and year, the following config would be passed to the chart constructor. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + displayFormats: { + quarter: 'MMM YYYY' + } + } + }] + } + } +}) +``` + +## Parser +If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. + +If this is a function, it must return a moment.js object given the appropriate data value. \ No newline at end of file diff --git a/docs/axes/labelling.md b/docs/axes/labelling.md new file mode 100644 index 00000000000..c0dda84a96a --- /dev/null +++ b/docs/axes/labelling.md @@ -0,0 +1,42 @@ +# Labeling Axes + +When creating a chart, you want to tell the viewer what data they are viewing. To do this, you need to label the axis. + +## Scale Title Configuration + +The scale label configuration is nested under the scale configuration in the `scaleLabel` key. It defines options for the scale title. Note that this only applies to cartesian axes. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `display` | `Boolean` | `false` | If true, display the axis title. +| `labelString` | `String` | `''` | The text for the title. (i.e. "# of People" or "Respone Choices"). +| `fontColor` | Color | `'#666'` | Font color for scale title. +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the scale title, follows CSS font-family options. +| `fontSize` | `Number` | `12` | Font size for scale title. +| `fontStyle` | `String` | `'normal'` | Font style for the scale title, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). + +## Creating Custom Tick Formats + +It is also common to want to change the tick marks to include information about the data type. For example, adding a dollar sign ('$'). To do this, you need to override the `ticks.callback` method in the axis configuration. +In the following example, every label of the Y axis would be displayed with a dollar sign at the front.. + +If the callback returns `null` or `undefined` the associated grid line will be hidden. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + yAxes: [{ + ticks: { + // Include a dollar sign in the ticks + callback: function(value, index, values) { + return '$' + value; + } + } + }] + } + } +}); +``` \ No newline at end of file diff --git a/docs/axes/radial/README.md b/docs/axes/radial/README.md new file mode 100644 index 00000000000..64ebe82057c --- /dev/null +++ b/docs/axes/radial/README.md @@ -0,0 +1,5 @@ +# Radial Axes + +Radial axes are used specifically for the radar and polar area chart types. These axes overlay the chart area, rather than being positioned on one of the edges. One radial axis is included by default in Chart.js. + +* [linear](./linear.md#linear-radial-axis) \ No newline at end of file diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md new file mode 100644 index 00000000000..f9811c6f985 --- /dev/null +++ b/docs/axes/radial/linear.md @@ -0,0 +1,110 @@ +# Linear Radial Axis + +The linear scale is use to chart numerical data. As the name suggests, linear interpolation is used to determine where a value lies in relation the center of the axis. + +The following additional configuration options are provided by the radial linear scale. + +## Configuration Options + +The axis has configuration properties for ticks, angle lines (line that appear in a radar chart outward from the center), pointLabels (labels around the edge in a radar chart). The following sections define each of the properties in those sections. + +| Name | Type | Description +| ---- | ---- | ----------- +| `angleLines` | `Object` | Angle line configuration. [more...](#angle-line-options) +| `gridLines` | `Object` | Grid line configuration. [more...](../styling.md#grid-line-configuration) +| `pointLabels` | `Object` | Point label configuration. [more...](#point-label-options) +| `ticks` | `Object` | Tick configuration. [more...](#tick-options) + +## Tick Options +The following options are provided by the linear scale. They are all located in the `ticks` sub options. The [common tick configuration](../styling.md#tick-configuration) options are supported by this axis. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `backdropColor` | Color | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops +| `backdropPaddingX` | `Number` | `2` | Horizontal padding of label backdrop. +| `backdropPaddingY` | `Number` | `2` | Vertical padding of label backdrop. +| `beginAtZero` | `Boolean` | `false` | if true, scale will include 0 if it is not already included. +| `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) +| `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) +| `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) +| `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) +| `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) +| `showLabelBackdrop` | `Boolean` | `true` | If true, draw a background behind the tick labels + +## Axis Range Settings + +Given the number of axis range settings, it is important to understand how they all interact with each other. + +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. + +```javascript +let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); +let maxDataValue = Math.max(mostPositiveValue, options.ticks.suggestedMax); +``` + +In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. + +```javascript +let chart = new Chart(ctx, { + type: 'radar', + data: { + datasets: [{ + label: 'First dataset', + data: [0, 20, 40, 50] + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: { + scale: { + ticks: { + suggestedMin: 50 + suggestedMax: 100 + } + } + } +}); +``` + +In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. + +## Step Size + If set, the scale ticks will be enumerated by multiple of stepSize, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. + +This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. + +```javascript +let options = { + scales: { + yAxes: [{ + ticks: { + max: 5, + min: 0, + stepSize: 0.5 + } + }] + } +}; +``` + +## Angle Line Options + +The following options are used to configure angled lines that radiate from the center of the chart to the point labels. They can be found in the `angleLines` sub options. Note that these options only apply if `angleLines.display` is true. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `display` | `Boolean` | `true` | if true, angle lines are shown +| `color` | Color | `rgba(0, 0, 0, 0.1)` | Color of angled lines +| `lineWidth` | `Number` | `1` | Width of angled lines + +## Point Label Options + +The following options are used to configure the point labels that are shown on the perimeter of the scale. They can be found in the `pointLabels` sub options. Note that these options only apply if `pointLabels.display` is true. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. +| `fontColor` | Color | `'#666'` | Font color for point labels. +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. +| `fontSize` | `Number` | 10 | font size in pixels +| `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. \ No newline at end of file diff --git a/docs/axes/styling.md b/docs/axes/styling.md new file mode 100644 index 00000000000..b7fabcd8c8b --- /dev/null +++ b/docs/axes/styling.md @@ -0,0 +1,35 @@ +# Styling + +There are a number of options to allow styling an axis. There are settings to control [grid lines](#grid-line-configuration) and [ticks](#tick-configuration). + +## Grid Line Configuration + +The grid line configuration is nested under the scale configuration in the `gridLines` key. It defines options for the grid lines that run perpendicular to the axis. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `display` | `Boolean` | `true` | If false, do not display grid lines for this axis. +| `color` | Color or Color[] | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. +| `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) +| `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) +| `lineWidth` | `Number or Number[]` | `1` | Stroke width of grid lines. +| `drawBorder` | `Boolean` | `true` | If true, draw border at the edge between the axis and the chart area. +| `drawOnChartArea` | `Boolean` | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. +| `drawTicks` | `Boolean` | `true` | If true, draw lines beside the ticks in the axis area beside the chart. +| `tickMarkLength` | `Number` | `10` | Length in pixels that the grid lines will draw into the axis area. +| `zeroLineWidth` | `Number` | `1` | Stroke width of the grid line for the first index (index 0). +| `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). +| `offsetGridLines` | `Boolean` | `false` | If true, labels are shifted to be between grid lines. This is used in the bar chart and should not generally be used. + +## Tick Configuration +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 +| -----| ---- | --------| ----------- +| `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). +| `display` | `Boolean` | `true` | If true, show tick marks +| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. +| `fontSize` | `Number` | `12` | Font size for the tick labels. +| `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `reverse` | `Boolean` | `false` | Reverses order of tick labels. diff --git a/docs/charts/README.md b/docs/charts/README.md new file mode 100644 index 00000000000..44622096641 --- /dev/null +++ b/docs/charts/README.md @@ -0,0 +1,11 @@ +# Charts + +Chart.js comes with built-in chart types: +* [line](./line.md) +* [bar](./bar.md) +* [radar](./radar.md) +* [polar area](./polar.md) +* [doughnut and pie](./doughnut.md) +* [bubble](./bubble.md) + +To create a new chart type, see the [developer notes](../developers/charts.md#new-charts) \ No newline at end of file diff --git a/docs/charts/bar.md b/docs/charts/bar.md new file mode 100644 index 00000000000..89978622375 --- /dev/null +++ b/docs/charts/bar.md @@ -0,0 +1,149 @@ +# Bar +A bar chart provides a way of showing data values represented as vertical bars. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. + +## Example Usage +```javascript +var myBarChart = new Chart(ctx, { + type: 'bar', + data: data, + options: options +}); +``` + +## Dataset Properties +The bar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of the bars is generally set this way. + +Some properties can be specified as an array. If these are set to an array value, the first value applies to the first bar, the second value to the second bar, and so on. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `xAxisID` | `String` | The ID of the x axis to plot this dataset on. If not specified, this defaults to the ID of the first found x axis +| `yAxisID` | `String` | The ID of the y axis to plot this dataset on. If not specified, this defaults to the ID of the first found y axis. +| `backgroundColor` | `Color/Color[]` | The fill color of the bar. See [Colors](../general/colors.md#colors) +| `borderColor` | `Color/Color[]` | The color of the bar border. See [Colors](../general/colors.md#colors) +| `borderWidth` | `Number/Number[]` | The stroke width of the bar in pixels. +| `borderSkipped` | `String` | Which edge to skip drawing the border for. [more...](#borderSkipped) +| `hoverBackgroundColor` | `Color/Color[]` | The fill colour of the bars when hovered. +| `hoverBorderColor` | `Color/Color[]` | The stroke colour of the bars when hovered. +| `hoverBorderWidth` | `Number/Number[]` | The stroke width of the bars when hovered. + +### borderSkipped +This setting is used to avoid drawing the bar stroke at the base of the fill. In general, this does not need to be changed except when creating chart types that derive from a bar chart. + +Options are: +* 'bottom' +* 'left' +* 'top' +* 'right' + +## Configuration Options + +The bar chart defines the following configuration options. These options are merged with the global chart configuration options, `Chart.defaults.global`, to form the options passed to the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `barPercentage` | `Number` | `0.9` | Percent (0-1) of the available width each bar should be within the category percentage. 1.0 will take the whole category width and put the bars right next to each other. [more...](#bar-chart-barpercentage-vs-categorypercentage) +| `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width (the space between the gridlines for small datasets) for each data-point to use for the bars. [more...](#bar-chart-barpercentage-vs-categorypercentage) +| `barThickness` | `Number` | | Manually set width of each bar in pixels. If not set, the bars are sized automatically using `barPercentage` and `categoryPercentage`; +| `maxBarThickness` | `Number` | | Set this to ensure that the automatically sized bars are not sized thicker than this. Only works if barThickness is not set (automatic sizing is enabled). +| `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. If false, the grid line will go right down the middle of the bars. [more...](#offsetGridLines) + +### offsetGridLines +If true, the bars for a particular data point fall between the grid lines. If false, the grid line will go right down the middle of the bars. It is unlikely that this will ever need to be changed in practice. It exists more as a way to reuse the axis code by configuring the existing axis slightly differently. + +This setting applies to the axis configuration for a bar chart. If axes are added to the chart, this setting will need to be set for each new axis. + +```javascript +options = { + scales: { + xAxes: [{ + gridLines: { + offsetGridLines: true + } + }] + } +} +``` + +## Default Options + +It is common to want to apply a configuration setting to all created bar charts. The global bar chart settings are stored in `Chart.defaults.bar`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +## barPercentage vs categoryPercentage + +The following shows the relationship between the bar percentage option and the category percentage option. + +```text +// categoryPercentage: 1.0 +// barPercentage: 1.0 +Bar: | 1.0 | 1.0 | +Category: | 1.0 | +Sample: |===========| + +// categoryPercentage: 1.0 +// barPercentage: 0.5 +Bar: |.5| |.5| +Category: | 1.0 | +Sample: |==============| + +// categoryPercentage: 0.5 +// barPercentage: 1.0 +Bar: |1.||1.| +Category: | .5 | +Sample: |==============| +``` + +## Data Structure + +The `data` property of a dataset for a bar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. + +```javascript +data: [20, 10] +``` + +# Stacked Bar Chart + +Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + +```javascript +var stackedBar = new Chart(ctx, { + type: 'bar', + data: data, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } +}); +``` + +## Dataset Properties + +The following dataset properties are specific to stacked bar charts. + +| Name | Type | Description +| ---- | ---- | ----------- +| `stack` | `String` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack) + +# Horizontal Bar Chart +A horizontal bar chart is a variation on a vertical bar chart. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. + +## Example +```javascript +var myBarChart = new Chart(ctx, { + type: 'horizontalBar', + data: data, + options: options +}); +``` + +## Config Options +The configuration options for the horizontal bar chart are the same as for the [bar chart](../bar/config-options.md#config-options). However, any options specified on the x axis in a bar chart, are applied to the y axis in a horizontal bar chart. + +The default horizontal bar configuration is specified in `Chart.defaults.horizontalBar`. \ No newline at end of file diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md new file mode 100644 index 00000000000..e784a67e176 --- /dev/null +++ b/docs/charts/bubble.md @@ -0,0 +1,60 @@ +# Bubble Chart + +A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. + +## Example Usage + +```javascript +// For a bubble chart +var myBubbleChart = new Chart(ctx,{ + type: 'bubble', + data: data, + options: options +}); +``` + +## Dataset Properties + +The bubble chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of the bubbles is generally set this way. + +All properties, except `label` can be specified as an array. If these are set to an array value, the first value applies to the first bubble in the dataset, the second value to the second bubble, and so on. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `backgroundColor` | `Color/Color[]` | The fill color for bubbles. +| `borderColor` | `Color/Color[]` | The border color for bubbles. +| `borderWidth` | `Number/Number[]` | The width of the point bubbles in pixels. +| `hoverBackgroundColor` | `Color/Color[]` | Bubble background color when hovered. +| `hoverBorderColor` | `Color/Color[]` | Bubble border color when hovered. +| `hoverBorderWidth` | `Number/Number[]` | Border width of point when hovered. +| `hoverRadius` | `Number/Number[]` | Additional radius to add to data radius on hover. + +## Config Options + +The bubble chart has no unique configuration options. To configure options common to all of the bubbles, the [point element options](../configuration/elements/point.md#point-configuration) are used. + +## Default Options + +We can also change the default values for the Bubble chart type. Doing so will give all bubble charts created after this point the new defaults. The default configuration for the bubble chart can be accessed at `Chart.defaults.bubble`. + +## Data Structure + +For a bubble chart, datasets need to contain an array of data points. Each point must implement the [bubble data object](#bubble-data-object) interface. + +### Bubble Data Object + +Data for the bubble chart is passed in the form of an object. The object must implement the following interface. It is important to note that the radius property, `r` is **not** scaled by the chart. It is the raw radius in pixels of the bubble that is drawn on the canvas. + +```javascript +{ + // X Value + x: , + + // Y Value + y: , + + // Radius of bubble. This is not scaled. + r: +} +``` \ No newline at end of file diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md new file mode 100644 index 00000000000..4b79f57bb27 --- /dev/null +++ b/docs/charts/doughnut.md @@ -0,0 +1,79 @@ +# Doughnut and Pie +Pie and doughnut charts are probably the most commonly used charts. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. + +They are excellent at showing the relational proportions between data. + +Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutoutPercentage`. This equates what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. + +They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. + +## Example Usage + +```javascript +// For a pie chart +var myPieChart = new Chart(ctx,{ + type: 'pie', + data: data, + options: options +}); +``` + +```javascript +// And for a doughnut chart +var myDoughnutChart = new Chart(ctx, { + type: 'doughnut', + data: data, + options: options +}); +``` + +## Dataset Properties + +The doughnut/pie chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a the dataset's arc are generally set this way. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) +| `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) +| `borderWidth` | `Number[]` | The border width of the arcs in the dataset. +| `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered. +| `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered. +| `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered. + +## Config Options + +These are the customisation options specific to Pie & Doughnut charts. These options are merged with the global chart configuration options, and form the options of the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `cutoutPercentage` | `Number` | `50` - for doughnut, `0` - for pie | The percentage of the chart that is cut out of the middle. +| `rotation` | `Number` | `-0.5 * Math.PI` | Starting angle to draw arcs from. +| `circumference` | `Number` | `2 * Math.PI` | Sweep to allow arcs to cover +| `animation.animateRotate` | `Boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. +| `animation.animateScale` | `Boolean` | `false` | If true, will animate scaling the chart from the center outwards. + +## Default Options + +We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.pie`, with the only difference being `cutoutPercentage` being set to 0. + +## Data Structure + +For a pie chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. + +You also need to specify an array of labels so that tooltips appear correctly + +```javascript +data = { + datasets: [{ + data: [10, 20, 30] + }], + + // These labels appear in the legend and in the tooltips when hovering different arcs + labels: [ + 'Red', + 'Yellow', + 'Blue' + ] +}; +``` \ No newline at end of file diff --git a/docs/charts/line.md b/docs/charts/line.md new file mode 100644 index 00000000000..4d0a89cc504 --- /dev/null +++ b/docs/charts/line.md @@ -0,0 +1,213 @@ +# Line +A line chart is a way of plotting data points on a line. Often, it is used to show trend data, or the comparison of two data sets. + +## Example Usage +```javascript +var myLineChart = new Chart(ctx, { + type: 'line', + data: data, + options: options +}); +``` + +## Dataset Properties + +The line chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. + +All point* properties can be specified as an array. If these are set to an array value, the first value applies to the first point, the second value to the second point, and so on. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `xAxisID` | `String` | The ID of the x axis to plot this dataset on. If not specified, this defaults to the ID of the first found x axis +| `yAxisID` | `String` | The ID of the y axis to plot this dataset on. If not specified, this defaults to the ID of the first found y axis. +| `backgroundColor` | `Color/Color[]` | The fill color under the line. See [Colors](../general/colors.md#colors) +| `borderColor` | `Color/Color[]` | The color of the line. See [Colors](../general/colors.md#colors) +| `borderWidth` | `Number/Number[]` | The width of the line in pixels. +| `borderDash` | `Number[]` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) +| `borderDashOffset` | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) +| `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) +| `borderJoinStyle` | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) +| `cubicInterpolationMode` | `String` | Algorithm used to interpolate a smooth curve from the discrete data points. [more...](#cubicInterpolationMode) +| `fill` | `Boolean/String` | How to fill the area under the line. [more...](#fill) +| `lineTension` | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. +| `pointBackgroundColor` | `Color/Color[]` | The fill color for points. +| `pointBorderColor` | `Color/Color[]` | The border color for points. +| `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. +| `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointStyle) +| `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. +| `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. +| `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. +| `pointHoverBorderWidth` | `Number/Number[]` | Border width of point when hovered. +| `pointHoverRadius` | `Number/Number[]` | The radius of the point when hovered. +| `showLine` | `Boolean` | If false, the line is not drawn for this dataset. +| `spanGaps` | `Boolean` | If true, lines will be drawn between points with no or null data. If false, points with `NaN` data will create a break in the line +| `steppedLine` | `Boolean` | If true, the line is shown as a stepped line and 'lineTension' will be ignored. + +### cubicInterpolationMode +The following interpolation modes are supported: +* 'default' +* 'monotone'. + +The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. + +The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. + +If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. + +### fill +If `true`, fill the area under the line. The line is filled to the baseline. If the y axis has a 0 value, the line is filled to that point. If the axis has only negative values, the line is filled to the highest value. If the axis has only positive values, it is filled to the lowest value. + +String values to fill to specific locations are: +* `'zero'` +* `'top'` +* `'bottom'` + +### pointStyle +The style of point. Options are: +* 'circle' +* 'cross' +* 'crossRot' +* 'dash'. +* 'line' +* 'rect' +* 'rectRounded' +* 'rectRot' +* 'star' +* 'triangle' + +If the option is an image, that image is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). + +## Configuration Options + +The line chart defines the following configuration options. These options are merged with the global chart configuration options, `Chart.defaults.global`, to form the options passed to the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `showLines` | `Boolean` | `true` | If false, the lines between points are not drawn. +| `spanGaps` | `Boolean` | `false` | If false, NaN data causes a break in the line. + +## Default Options + +It is common to want to apply a configuration setting to all created line charts. The global line chart settings are stored in `Chart.defaults.line`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +For example, to configure all line charts with `spanGaps = true` you would do: +```javascript +Chart.defaults.line.spanGaps = true; +``` + +## Data Structure + +The `data` property of a dataset for a line chart can be passed in two formats. + +### Number[] +```javascript +data: [20, 10] +``` + +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. + +### Point[] + +```javascript +data: [{ + x: 10, + y: 20 + }, { + x: 15, + y: 10 + }] +``` + +This alternate is used for sparse datasets, such as those in [scatter charts](./scatter.md#scatter-chart). Each data point is specified using an object containing `x` and `y` properties. + +# Stacked Area Chart + +Line charts can be configured into stacked area charts by changing the settings on the y axis to enable stacking. Stacked area charts can be used to show how one data trend is made up of a number of smaller pieces. + +```javascript +var stackedLine = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + yAxes: [{ + stacked: true + }] + } + } +}); +``` + +# High Performance Line Charts + +When charting a lot of data, the chart render time may start to get quite large. In that case, the following strategies can be used to improve performance. + +## Data Decimation + +Decimating your data will achieve the best results. When there is a lot of data to display on the graph, it doesn't make sense to show tens of thousands of data points on a graph that is only a few hundred pixels wide. + +There are many approaches to data decimation and selection of an algorithm will depend on your data and the results you want to achieve. For instance, [min/max](http://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks. + +## Disable Bezier Curves + +If you are drawing lines on your chart, disabling bezier curves will improve render times since drawing a straight line is more performant than a bezier curve. + +To disable bezier curves for an entire chart: + +```javascript +new Chart(ctx, { + type: 'line', + data: data, + options: { + elements: { + line: { + tension: 0, // disables bezier curves + } + } + } +}); +``` + +## Draw Line Drawing + +If you have a lot of data points, it can be more performant to disable rendering of the line for a dataset and only draw points. Doing this means that there is less to draw on the canvas which will improve render performance. + +To disable lines: + +```javascript +new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + showLine: false, // disable for a single dataset + }] + }, + options: { + showLines: false, // disable for all datasets + } +}); +``` + +## Disable Animations + +If your charts have long render times, it is a good idea to disable animations. Doing so will mean that the chart needs to only be rendered once during an update instead of multiple times. This will have the effect of reducing CPU usage and improving general page performance. + +To disable animations + +```javascript +new Chart(ctx, { + type: 'line', + data: data, + options: { + animation: { + duration: 0, // general animation time + }, + hover: { + animationDuration: 0, // duration of animations when hovering an item + }, + responsiveAnimationDuration: 0, // animation duration after a resize + } +}); +``` \ No newline at end of file diff --git a/docs/charts/mixed.md b/docs/charts/mixed.md new file mode 100644 index 00000000000..72c3e40853b --- /dev/null +++ b/docs/charts/mixed.md @@ -0,0 +1,37 @@ +# Mixed Chart Types + +With Chart.js, it is possible to create mixed charts that are a combination of two or more different chart types. A common example is a bar chart that also includes a line dataset. + +Creating a mixed chart starts with the initialization of a basic chart. + +```javascript +var myChart = new Chart(ctx, { + type: 'bar', + data: data, + options: options +}); +``` + +At this point we have a standard bar chart. Now we need to convert one of the datasets to a line dataset. + +```javascript +var mixedChart = new Chart(ctx, { + type: 'bar', + data: { + datasets: [{ + label: 'Bar Dataset', + data: [10, 20, 30, 40] + }, { + label: 'Line Dataset', + data: [50, 50, 50, 50], + + // Changes this dataset to become a line + type: 'line' + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: options +}); +``` + +At this point we have a chart rendering how we'd like. It's important to note that the default options for a line chart are not merged in this case. Only the options for the default type are merged in. In this case, that means that the default options for a bar chart are merged because that is the type specified by the `type` field. \ No newline at end of file diff --git a/docs/charts/polar.md b/docs/charts/polar.md new file mode 100644 index 00000000000..3da3d209a3a --- /dev/null +++ b/docs/charts/polar.md @@ -0,0 +1,69 @@ +# Polar Area + +Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. + +This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. + +## Example Usage + +```javascript +new Chart(ctx, { + data: data, + type: 'polarArea', + options: options +}); +``` + +## Dataset Properties + +The following options can be included in a polar area chart dataset to configure options for that specific dataset. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) +| `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) +| `borderWidth` | `Number[]` | The border width of the arcs in the dataset. +| `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered. +| `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered. +| `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered. + +## Config Options + +These are the customisation options specific to Polar Area charts. These options are merged with the [global chart configuration options](#global-chart-configuration), and form the options of the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `startAngle` | `Number` | `-0.5 * Math.PI` | Starting angle to draw arcs for the first item in a dataset. +| `animation.animateRotate` | `Boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. +| `animation.animateScale` | `Boolean` | `true` | If true, will animate scaling the chart from the center outwards. + +## Default Options + +We can also change these defaults values for each PolarArea type that is created, this object is available at `Chart.defaults.polarArea`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +For example, to configure all new polar area charts with `animateScale = false` you would do: +```javascript +Chart.defaults.polarArea.animation.animateScale = false; +``` + +## Data Structure + +For a polar area chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. + +You also need to specify an array of labels so that tooltips appear correctly for each slice. + +```javascript +data = { + datasets: [{ + data: [10, 20, 30] + }], + + // These labels appear in the legend and in the tooltips when hovering different arcs + labels: [ + 'Red', + 'Yellow', + 'Blue' + ] +}; +``` \ No newline at end of file diff --git a/docs/charts/radar.md b/docs/charts/radar.md new file mode 100644 index 00000000000..0a7da1df2e8 --- /dev/null +++ b/docs/charts/radar.md @@ -0,0 +1,97 @@ +# Radar +A radar chart is a way of showing multiple data points and the variation between them. + +They are often useful for comparing the points of two or more different data sets. + +## Example Usage +```javascript +var myRadarChart = new Chart(ctx, { + type: 'radar', + data: data, + options: options +}); +``` + +## Dataset Properties + +The radar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. + +All point* properties can be specified as an array. If these are set to an array value, the first value applies to the first point, the second value to the second point, and so on. + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `String` | The label for the dataset which appears in the legend and tooltips. +| `backgroundColor` | `Color/Color[]` | The fill color under the line. See [Colors](../general/colors.md#colors) +| `borderColor` | `Color/Color[]` | The color of the line. See [Colors](../general/colors.md#colors) +| `borderWidth` | `Number/Number[]` | The width of the line in pixels. +| `borderDash` | `Number[]` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) +| `borderDashOffset` | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) +| `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) +| `borderJoinStyle` | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) +| `fill` | `Boolean/String` | How to fill the area under the line. [more...](#fill) +| `lineTension` | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. +| `pointBackgroundColor` | `Color/Color[]` | The fill color for points. +| `pointBorderColor` | `Color/Color[]` | The border color for points. +| `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. +| `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointStyle) +| `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. +| `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. +| `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. +| `pointHoverBorderWidth` | `Number/Number[]` | Border width of point when hovered. +| `pointHoverRadius` | `Number/Number[]` | The radius of the point when hovered. + +### pointStyle +The style of point. Options are: +* 'circle' +* 'cross' +* 'crossRot' +* 'dash'. +* 'line' +* 'rect' +* 'rectRounded' +* 'rectRot' +* 'star' +* 'triangle' + +If the option is an image, that image is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). + +## Configuration Options + +Unlike other charts, the radar chart has no chart specific options. + +## Scale Options + +The radar chart supports only a single scale. The options for this scale are defined in the `scale` property. + +```javascript +options = { + scale: { + // Hides the scale + display: false + } +}; +``` + +## Default Options + +It is common to want to apply a configuration setting to all created radar charts. The global radar chart settings are stored in `Chart.defaults.radar`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +## Data Structure + +The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. + +```javascript +data: [20, 10] +``` + +For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. + +```javascript +data: { + labels: ['Running', 'Swimming', 'Eating', 'Cycling'], + datasets: [{ + data: [20, 10, 4, 2] + }] +} +``` \ No newline at end of file diff --git a/docs/charts/scatter.md b/docs/charts/scatter.md new file mode 100644 index 00000000000..ef4e6f71167 --- /dev/null +++ b/docs/charts/scatter.md @@ -0,0 +1,49 @@ +# Scatter Chart + +Scatter charts are based on basic line charts with the x axis changed to a linear axis. To use a scatter chart, data must be passed as objects containing X and Y properties. The example below creates a scatter chart with 3 points. + +```javascript +var scatterChart = new Chart(ctx, { + type: 'scatter', + data: { + datasets: [{ + label: 'Scatter Dataset', + data: [{ + x: -10, + y: 0 + }, { + x: 0, + y: 10 + }, { + x: 10, + y: 5 + }] + }] + }, + options: { + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }] + } + } +}); +``` + +## Dataset Properties +The scatter chart supports all of the same properties as the [line chart](./line.md#dataset-properties). + +## Data Structure + +Unlike the line chart where data can be supplied in two different formats, the scatter chart only accepts data in a point format. + +```javascript +data: [{ + x: 10, + y: 20 + }, { + x: 15, + y: 10 + }] +``` \ No newline at end of file diff --git a/docs/configuration/README.md b/docs/configuration/README.md new file mode 100644 index 00000000000..7b713905e2e --- /dev/null +++ b/docs/configuration/README.md @@ -0,0 +1,34 @@ +# Configuration + +The configuration is used to change how the chart behaves. There are properties to control styling, fonts, the legend, etc. + +## Global Configuration + +This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. + +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 '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 = 'nearest'; + +// Hover mode is set to nearest because it was not overridden here +var chartHoverModeNearest = new Chart(ctx, { + type: 'line', + data: data, +}); + +// This chart would have the hover mode that was passed in +var chartDifferentHoverMode = new Chart(ctx, { + type: 'line', + data: data, + options: { + hover: { + // Overrides the global setting + mode: 'index' + } + } +}) +``` + diff --git a/docs/configuration/animations.md b/docs/configuration/animations.md new file mode 100644 index 00000000000..92e273bb697 --- /dev/null +++ b/docs/configuration/animations.md @@ -0,0 +1,96 @@ +# Animations + +Chart.js animates charts out of the box. A number of options are provided to configure how the animation looks and how long it takes + +## Animation Configuration + +The following animation options are available. The global options for are defined in `Chart.defaults.global.animation`. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `duration` | `Number` | `1000` | The number of milliseconds an animation takes. +| `easing` | `String` | `'easeOutQuart'` | Easing function to use. [more...](#easing) +| `onProgress` | `Function` | `null` | Callback called on each step of an animation. [more...](#animation-callbacks) +| `onComplete` | `Function` | `null` | Callback called at the end of an animation. [more...](#animation-callbacks) + +## Easing + Available options are: +* `'linear'` +* `'easeInQuad'` +* `'easeOutQuad'` +* `'easeInOutQuad'` +* `'easeInCubic'` +* `'easeOutCubic'` +* `'easeInOutCubic'` +* `'easeInQuart'` +* `'easeOutQuart'` +* `'easeInOutQuart'` +* `'easeInQuint'` +* `'easeOutQuint'` +* `'easeInOutQuint'` +* `'easeInSine'` +* `'easeOutSine'` +* `'easeInOutSine'` +* `'easeInExpo'` +* `'easeOutExpo'` +* `'easeInOutExpo'` +* `'easeInCirc'` +* `'easeOutCirc'` +* `'easeInOutCirc'` +* `'easeInElastic'` +* `'easeOutElastic'` +* `'easeInOutElastic'` +* `'easeInBack'` +* `'easeOutBack'` +* `'easeInOutBack'` +* `'easeInBounce'` +* `'easeOutBounce'` +* `'easeInOutBounce'` + +See [Robert Penner's easing equations](http://robertpenner.com/easing/). + +## Animation Callbacks + +The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed a `Chart.Animation` instance: + +```javascript +{ + // Chart object + chart: Chart, + + // Current Animation frame number + currentStep: Number, + + // Number of animation frames + numSteps: Number, + + // Animation easing to use + easing: String, + + // Function that renders the chart + render: Function, + + // User callback + onAnimationProgress: Function, + + // User callback + onAnimationComplete: Function +} +``` + +The following example fills a progress bar during the chart animation. +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + animation: { + onProgress: function(animation) { + progress.value = animation.animationObject.currentStep / animation.animationObject.numSteps; + } + } + } +}); +``` + +Another example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/animation/progress-bar.html): this sample displays a progress bar showing how far along the animation is. diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md new file mode 100644 index 00000000000..0586a14b2c4 --- /dev/null +++ b/docs/configuration/elements.md @@ -0,0 +1,69 @@ +# Elements + +While chart types provide settings to configure the styling of each dataset, you sometimes want to style **all datasets the same way**. A common example would be to stroke all of the bars in a bar chart with the same colour but change the fill per dataset. Options can be configured for four different types of elements: **[arc](#arc-configuration)**, **[lines](#line-configuration)**, **[points](#point-configuration)**, and **[rectangles](#rectangle-configuration)**. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. + +## Global Configuration + +The element options can be specified per chart or globally. The global options for elements are defined in `Chart.defaults.global.elements`. For example, to set the border width of all bar charts globally you would do: + +```javascript +Chart.defaults.global.elements.rectangle.borderWidth = 2; +``` + +## Point Configuration +Point elements are used to represent the points in a line chart or a bubble chart. + +Global point options: `Chart.defaults.global.elements.point` + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `radius` | `Number` | `3` | Point radius. +| `pointStyle` | `String` | `circle` | Point style. +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point fill color. +| `borderWidth` | `Number` | `1` | Point stroke width. +| `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point stroke color. +| `hitRadius` | `Number` | `1` | Extra radius added to point radius for hit detection. +| `hoverRadius` | `Number` | `4` | Point radius when hovered. +| `hoverBorderWidth` | `Number` | `1` | Stroke width when hovered. + +## Line Configuration +Line elements are used to represent the line in a line chart. + +Global line options: `Chart.defaults.global.elements.line` + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `tension` | `Number` | `0.4` | Bézier curve tension (`0` for no Bézier curves). +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Line fill color. +| `borderWidth` | `Number` | `3` | Line stroke width. +| `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Line stroke color. +| `borderCapStyle` | `String` | `'butt'` | Line cap style (see [MDN](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap)). +| `borderDash` | `Array` | `[]` | Line dash (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash)). +| `borderDashOffset` | `Number` | `0` | Line dash offset (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset)). +| `borderJoinStyle` | `String` | `'miter` | Line join style (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin)). +| `capBezierPoints` | `Boolean` | `true` | `true` to keep Bézier control inside the chart, `false` for no restriction. +| `fill` | `Boolean/String` | `true` | Fill location: `'zero'`, `'top'`, `'bottom'`, `true` (eq. `'zero'`) or `false` (no fill). +| `stepped` | `Boolean` | `false` | `true` to show the line as a stepped line (`tension` will be ignored). + +## Rectangle Configuration +Rectangle elements are used to represent the bars in a bar chart. + +Global rectangle options: `Chart.defaults.global.elements.rectangle` + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Bar fill color. +| `borderWidth` | `Number` | `0` | Bar stroke width. +| `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Bar stroke color. +| `borderSkipped` | `String` | `'bottom'` | Skipped (excluded) border: `'bottom'`, `'left'`, `'top'` or `'right'`. + +## Arc Configuration +Arcs are used in the polar area, doughnut and pie charts. + +Global arc options: `Chart.defaults.global.elements.arc`. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Arc fill color. +| `borderColor` | `Color` | `'#fff'` | Arc stroke color. +| `borderWidth`| `Number` | `2` | Arc stroke width. diff --git a/docs/configuration/layout.md b/docs/configuration/layout.md new file mode 100644 index 00000000000..fcd42fdd499 --- /dev/null +++ b/docs/configuration/layout.md @@ -0,0 +1,29 @@ +# 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. [more...](#padding) + +## Padding +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. + +Lets say you wanted to add 50px of padding to the left side of the chart canvas, you would do: + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + layout: { + padding: { + left: 50, + right: 0, + top: 0, + bottom: 0 + } + } + } +}); +``` \ No newline at end of file diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md new file mode 100644 index 00000000000..c398908b485 --- /dev/null +++ b/docs/configuration/legend.md @@ -0,0 +1,166 @@ +# Legend Configuration + +The chart legend displays data about the datasets that area appearing on the chart. + +## Configuration options +The legend configuration is passed into the `options.legend` namespace. The global options for the chart legend is defined in `Chart.defaults.global.legend`. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `display` | `Boolean` | `true` | is the legend shown +| `position` | `String` | `'top'` | Position of the legend. [more...](#position) +| `fullWidth` | `Boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. +| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item +| `onHover` | `Function` | | A callback that is called when a 'mousemove' event is registered on top of a label item +| `reverse` | `Boolean` | `false` | Legend will show datasets in reverse order. +| `labels` | `Object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. + +## Position +Position of the legend. Options are: +* `'top'` +* `'left'` +* `'bottom'` +* `'right'` + +## Legend Label Configuration + +The legend label configuration is nested below the legend configuration using the `labels` key. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `boxWidth` | `Number` | `40` | width of coloured box +| `fontSize` | `Number` | `12` | font size of text +| `fontStyle` | `String` | `'normal'` | font style of text +| `fontColor` | Color | `'#666'` | Color of text +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family of legend text. +| `padding` | `Number` | `10` | Padding between rows of colored boxes. +| `generateLabels` | `Function` | | 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. +| `filter` | `Function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#chart-configuration-legend-item-interface) and the chart data. +| `usePointStyle` | `Boolean` | `false` | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). + +## Legend Item Interface + +Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface. + +```javascript +{ + // Label that will be displayed + text: String, + + // Fill style of the legend box + fillStyle: Color, + + // If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect + hidden: Boolean, + + // For box border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap + lineCap: String, + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash + lineDash: Array[Number], + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset + lineDashOffset: Number, + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + lineJoin: String, + + // Width of box border + lineWidth: Number, + + // Stroke style of the legend box + strokeStyle: Color + + // Point style of the legend box (only used if usePointStyle is true) + pointStyle: String +} +``` + +## Example + +The following example will create a chart with the legend enabled and turn all of the text red in color. + +```javascript +var chart = new Chart(ctx, { + type: 'bar', + data: data, + options: { + legend: { + display: true, + labels: { + fontColor: 'rgb(255, 99, 132)' + } + } +} +}); +``` + +## Custom On Click Actions + +It can be common to want to trigger different behaviour when clicking an item in the legend. This can be easily achieved using a callback in the config object. + +The default legend click handler is: +```javascript +function(e, legendItem) { + var index = legendItem.datasetIndex; + var ci = this.chart; + var meta = ci.getDatasetMeta(index); + + // See controller.isDatasetVisible comment + meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null; + + // We hid a dataset ... rerender the chart + ci.update(); +} +``` + +Lets say we wanted instead to link the display of the first two datasets. We could change the click handler accordingly. + +```javascript +var defaultLegendClickHandler = Chart.defaults.global.legend.onClick; +var newLegendClickHandler = function (e, legendItem) { + var index = legendItem.datasetIndex; + + if (index > 1) { + // Do the original logic + defaultLegendClickHandler(e, legendItem); + } else { + let ci = this.chart; + [ci.getDatasetMeta(0), + ci.getDatasetMeta(1)].forEach(function(meta) { + meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null; + }); + ci.update(); + } +}; + +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + legend: { + + } + } +}); +``` + +Now when you click the legend in this chart, the visibility of the first two datasets will be linked together. + +## HTML Legends + +Sometimes you need a very complex legend. In these cases, it makes sense to generate an HTML legend. Charts provide a `generateLegend()` method on their prototype that returns an HTML string for the legend. + +To configure how this legend is generated, you can change the `legendCallback` config property. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + legendCallback: function(chart) { + // Return the HTML string here. + } + } +}); +``` \ No newline at end of file diff --git a/docs/configuration/title.md b/docs/configuration/title.md new file mode 100644 index 00000000000..a02e6b620be --- /dev/null +++ b/docs/configuration/title.md @@ -0,0 +1,41 @@ +# Title + +The chart title defines text to draw at the top of the chart. + +## 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`. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `display` | `Boolean` | `false` | is the title shown +| `position` | `String` | `'top'` | Position of title. [more...](#position) +| `fontSize` | `Number` | `12` | Font size +| `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the title text. +| `fontColor` | Color | `'#666'` | Font color +| `fontStyle` | `String` | `'bold'` | Font style +| `padding` | `Number` | `10` | Number of pixels to add above and below the title text. +| `text` | `String` | `''` | Title text ti display + +### Position +Possible title position values are: +* `'top'` +* `'left'` +* `'bottom'` +* `'right'` + +## Example Usage + +The example below would enable a title of 'Custom Chart Title' on the chart that is created. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + title: { + display: true, + text: 'Custom Chart Title' + } + } +}) +``` diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md new file mode 100644 index 00000000000..13577f05d6f --- /dev/null +++ b/docs/configuration/tooltip.md @@ -0,0 +1,292 @@ +# Tooltips + +## Tooltip Configuration + +The tooltip configuration is passed into the `options.tooltips` namespace. The global options for the chart tooltips is defined in `Chart.defaults.global.tooltips`. + +| Name | Type | Default | Description +| -----| ---- | --------| ----------- +| `enabled` | `Boolean` | `true` | Are tooltips enabled +| `custom` | `Function` | `null` | See [custom tooltip](#custom-tooltips) section. +| `mode` | `String` | `'nearest'` | Sets which elements appear in the tooltip. [more...](../general/interactions/modes.md#interaction-modes). +| `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. [more...](#position-modes) +| `callbacks` | `Object` | | See the [callbacks section](#tooltip-callbacks) +| `itemSort` | `Function` | | Sort tooltip items. [more...](#sort-callback) +| `filter` | `Function` | | Filter tooltip items. [more...](#filter-callback) +| `backgroundColor` | Color | `'rgba(0,0,0,0.8)'` | Background color of the tooltip. +| `titleFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | title font +| `titleFontSize` | `Number` | `12` | Title font size +| `titleFontStyle` | `String` | `'bold'` | Title font style +| `titleFontColor` | Color | `'#fff'` | Title font color +| `titleSpacing` | `Number` | `2` | Spacing to add to top and bottom of each title line. +| `titleMarginBottom` | `Number` | `6` | Margin to add on bottom of title section. +| `bodyFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | body line font +| `bodyFontSize` | `Number` | `12` | Body font size +| `bodyFontStyle` | `String` | `'normal'` | Body font style +| `bodyFontColor` | Color | `'#fff'` | Body font color +| `bodySpacing` | `Number` | `2` | Spacing to add to top and bottom of each tooltip item. +| `footerFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | footer font +| `footerFontSize` | `Number` | `12` | Footer font size +| `footerFontStyle` | `String` | `'bold'` | Footer font style +| `footerFontColor` | Color | `'#fff'` | Footer font color +| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. +| `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. +| `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. +| `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 +| `borderColor` | Color | `'rgba(0,0,0,0)'` | Color of the border +| `borderWidth` | `Number` | `0` | Size of the border + +### Position Modes + Possible modes are: +* 'average' +* 'nearest' + +'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. + +### Sort Callback + +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 Callback + +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. + +## Tooltip Callbacks + +The tooltip label configuration is nested below the tooltip configuration using the `callbacks` key. The tooltip has the following callbacks for providing text. For all functions, 'this' will be the tooltip object created from the Chart.Tooltip constructor. + +All functions are called with the same arguments: a [tooltip item](#chart-configuration-tooltip-item-interface) and the data object passed to the chart. All functions must return either a string or an array of strings. Arrays of strings are treated as multiple lines of text. + +| Name | Arguments | Description +| ---- | --------- | ----------- +| `beforeTitle` | `Array[tooltipItem], data` | Returns the text to render before the title. +| `title` | `Array[tooltipItem], data` | Returns text to render as the title of the tooltip. +| `afterTitle` | `Array[tooltipItem], data` | Returns text to render after the title. +| `beforeBody` | `Array[tooltipItem], data` | Returns text to render before the body section. +| `beforeLabel` | `tooltipItem, data` | Returns text to render before an individual label. This will be called for each item in the tooltip. +| `label` | `tooltipItem, data` | Returns text to render for an individual item in the tooltip. +| `labelColor` | `tooltipItem, chart` | Returns the colors to render for the tooltip item. [more...](#label-color-callback) +| `afterLabel` | `tooltipItem, data` | Returns text to render after an individual label. +| `afterBody` | `Array[tooltipItem], data` | Returns text to render after the body section +| `beforeFooter` | `Array[tooltipItem], data` | Returns text to render before the footer section. +| `footer` | `Array[tooltipItem], data` | Returns text to render as the footer of the tooltip. +| `afterFooter` | `Array[tooltipItem], data` | Text to render after the footer section + +### Label Color Callback + +For example, to return a red box for each item in the tooltip you could do: +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + callbacks: { + labelColor: function(tooltipItem, chart) { + return { + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(255, 0, 0)' + } + } + } + } + } +}); +``` + + +### Tooltip Item Interface + +The tooltip items passed to the tooltip callbacks implement the following interface. + +```javascript +{ + // X Value of the tooltip as a string + xLabel: String, + + // Y value of the tooltip as a string + yLabel: String, + + // Index of the dataset the item comes from + datasetIndex: Number, + + // Index of this data item in the dataset + index: Number, + + // X position of matching point + x: Number, + + // Y position of matching point + y: Number, +} +``` + +## External (Custom) Tooltips + +Custom tooltips allow you to hook into the tooltip rendering process so that you can render the tooltip in your own custom way. Generally this is used to create an HTML tooltip instead of an oncanvas one. You can enable custom tooltips in the global or chart configuration like so: + +```javascript +var myPieChart = new Chart(ctx, { + type: 'pie', + data: data, + options: { + tooltips: { + custom: function(tooltipModel) { + // Tooltip Element + var tooltipEl = document.getElementById('chartjs-tooltip'); + + // Create element on first render + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.id = 'chartjs-tooltip'; + tooltipEl.innerHTML = "
    " + document.body.appendChild(tooltipEl); + } + + // Hide if no tooltip + if (tooltipModel.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set caret Position + tooltipEl.classList.remove('above', 'below', 'no-transform'); + if (tooltipModel.yAlign) { + tooltipEl.classList.add(tooltipModel.yAlign); + } else { + tooltipEl.classList.add('no-transform'); + } + + function getBody(bodyItem) { + return bodyItem.lines; + } + + // Set Text + if (tooltipModel.body) { + var titleLines = tooltipModel.title || []; + var bodyLines = tooltipModel.body.map(getBody); + + var innerHtml = ''; + + titleLines.forEach(function(title) { + innerHtml += '' + title + ''; + }); + innerHtml += ''; + + bodyLines.forEach(function(body, i) { + var colors = tooltipModel.labelColors[i]; + var style = 'background:' + colors.backgroundColor; + style += '; border-color:' + colors.borderColor; + style += '; border-width: 2px'; + var span = ''; + innerHtml += '' + span + body + ''; + }); + innerHtml += ''; + + var tableRoot = tooltipEl.querySelector('table'); + tableRoot.innerHTML = innerHtml; + } + + // `this` will be the overall tooltip + var position = this._chart.canvas.getBoundingClientRect(); + + // Display, position, and set styles for font + tooltipEl.style.opacity = 1; + tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; + tooltipEl.style.fontFamily = tooltipModel._fontFamily; + tooltipEl.style.fontSize = tooltipModel.fontSize; + tooltipEl.style.fontStyle = tooltipModel._fontStyle; + tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; + } + } + } +}); +``` + +See `samples/tooltips/line-customTooltips.html` for examples on how to get started. + +## Tooltip Model +The tooltip model contains parameters that can be used to render the tooltip. + +```javascript +{ + // The items that we are rendering in the tooltip. See Tooltip Item Interface section + dataPoints: TooltipItem[], + + // Positioning + xPadding: Number, + yPadding: Number, + xAlign: String, + yAlign: String, + + // X and Y properties are the top left of the tooltip + x: Number, + y: Number, + width: Number, + height: Number, + // Where the tooltip points to + caretX: Number, + caretY: Number, + + // Body + // The body lines that need to be rendered + // Each pbject contains 3 parameters + // before: String[] // lines of text before the line with the color square + // lines: String[], // lines of text to render as the main item with color square + // after: String[], // lines of text to render after the main lines + body: Object[], + // lines of text that appear after the title but before the body + beforeBody: String[], + // line of text that appear after the body and before the footer + afterBody: String[], + bodyFontColor: Color, + _bodyFontFamily: String, + _bodyFontStyle: String, + _bodyAlign: String, + bodyFontSize: Number, + bodySpacing: Number, + + // Title + // lines of text that form the title + title: String[], + titleFontColor: Color, + _titleFontFamily: String, + _titleFontStyle: String, + titleFontSize: Number, + _titleAlign: String, + titleSpacing: Number, + titleMarginBottom: Number, + + // Footer + // lines of text that form the footer + footer: String[], + footerFontColor: Color, + _footerFontFamily: String, + _footerFontStyle: String, + footerFontSize: Number, + _footerAlign: String, + footerSpacing: Number, + footerMarginTop: Number, + + // Appearance + caretSize: Number, + cornerRadius: Number, + backgroundColor: Color, + + // colors to render for each item in body[]. This is the color of the squares in the tooltip + labelColors: Color[], + + // 0 opacity is a hidden tooltip + opacity: Number, + legendColorBackground: Color, + displayColors: Boolean, +} +``` diff --git a/docs/developers/README.md b/docs/developers/README.md new file mode 100644 index 00000000000..f8d6e974284 --- /dev/null +++ b/docs/developers/README.md @@ -0,0 +1,22 @@ +# Developers +Developer features allow extending and enhancing Chart.js in many different ways. + +# Browser support + +Chart.js offers support for all browsers where canvas is supported. + +Browser support for the canvas element is available in all modern & major mobile browsers. [CanIUse](http://caniuse.com/#feat=canvas) + +Thanks to [BrowserStack](https://browserstack.com) for allowing our team to test on thousands of browsers. + +# Previous versions + +Version 2 has a completely different API than earlier versions. + +Most earlier version options have current equivalents or are the same. + +Please use the documentation that is available on [chartjs.org](http://www.chartjs.org/docs/) for the current version of Chart.js. + +Please note - documentation for previous versions are available on the GitHub repo. + +- [1.x Documentation](https://github.com/chartjs/Chart.js/tree/v1.1.1/docs) \ No newline at end of file diff --git a/docs/developers/api.md b/docs/developers/api.md new file mode 100644 index 00000000000..03dbe797fd8 --- /dev/null +++ b/docs/developers/api.md @@ -0,0 +1,143 @@ +# Chart Prototype Methods + +For each chart, there are a set of global prototype methods on the shared `ChartType` which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. + +```javascript +// For example: +var myLineChart = new Chart(ctx, config); +``` + +## .destroy() + +Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. +This must be called before the canvas is reused for a new chart. + +```javascript +// Destroys a specific chart instance +myLineChart.destroy(); +``` + +## .update(duration, lazy) + +Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. + +```javascript +// duration is the time for the animation of the redraw in milliseconds +// lazy is a boolean. if true, the animation can be interrupted by other animations +myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 +myLineChart.update(); // Calling update now animates the position of March from 90 to 50. +``` + +> **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. + +## .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. + +```javascript +// duration is the time for the animation of the redraw in milliseconds +// lazy is a boolean. if true, the animation can be interrupted by other animations +myLineChart.render(duration, lazy); +``` + +## .stop() + +Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. + +```javascript +// Stops the charts animation loop at its current frame +myLineChart.stop(); +// => returns 'this' for chainability +``` + +## .resize() + +Use this to manually resize the canvas element. This is run each time the canvas container is resized, but you can call this method manually if you change the size of the canvas nodes container element. + +```javascript +// Resizes & redraws to fill its container element +myLineChart.resize(); +// => returns 'this' for chainability +``` + +## .clear() + +Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. + +```javascript +// Will clear the canvas that myLineChart is drawn on +myLineChart.clear(); +// => returns 'this' for chainability +``` + +## .toBase64Image() + +This returns a base 64 encoded string of the chart in it's current state. + +```javascript +myLineChart.toBase64Image(); +// => returns png data url of the image on the canvas +``` + +## .generateLegend() + +Returns an HTML string of a legend for that chart. The legend is generated from the `legendCallback` in the options. + +```javascript +myLineChart.generateLegend(); +// => returns HTML string of a legend for this chart +``` + +## .getElementAtEvent(e) + +Calling `getElementAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the single element at the event position. If there are multiple items within range, only the first is returned. The value returned from this method is an array with a single parameter. An array is used to keep a consistent API between the `get*AtEvent` methods. + +```javascript +myLineChart.getElementAtEvent(e); +// => returns the first element at the event point. +``` + +## .getElementsAtEvent(e) + +Looks for the element under the event point, then returns all elements at the same data index. This is used internally for 'label' mode highlighting. + +Calling `getElementsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myLineChart.getElementsAtEvent(evt); + // => activePoints is an array of points on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +## .getDatasetAtEvent(e) + +Looks for the element under the event point, then returns all elements from that dataset. This is used internally for 'dataset' mode highlighting + +```javascript +myLineChart.getDatasetAtEvent(e); +// => returns an array of elements +``` + +## .getDatasetMeta(index) + +Looks for the dataset that matches the current index and returns that metadata. This returned data has all of the metadata that is used to construct the chart. + +The `data` property of the metadata will contain information about each point, rectangle, etc. depending on the chart type. + +Extensive examples of usage are available in the [Chart.js tests](https://github.com/chartjs/Chart.js/tree/master/test). + +```javascript +var meta = myChart.getDatasetMeta(0); +var x = meta.data[0]._model.x +``` diff --git a/docs/developers/axes.md b/docs/developers/axes.md new file mode 100644 index 00000000000..8d120195ae9 --- /dev/null +++ b/docs/developers/axes.md @@ -0,0 +1,131 @@ +# New Axes + +Axes in Chart.js can be individually extended. Axes should always derive from Chart.Scale but this is not a mandatory requirement. + +```javascript +let MyScale = Chart.Scale.extend({ + /* extensions ... */ +}); + +// MyScale is now derived from Chart.Scale +``` + +Once you have created your scale class, you need to register it with the global chart object so that it can be used. A default config for the scale may be provided when registering the constructor. The first parameter to the register function is a string key that is used later to identify which scale type to use for a chart. + +```javascript +Chart.scaleService.registerScaleType('myScale', MyScale, defaultConfigObject); +``` + +To use the new scale, simply pass in the string key to the config when creating a chart. + +```javascript +var lineChart = new Chart(ctx, { + data: data, + type: 'line', + options: { + scales: { + yAxes: [{ + type: 'myScale' // this is the same key that was passed to the registerScaleType function + }] + } + } +}) +``` + +## Scale Properties + +Scale instances are given the following properties during the fitting process. + +```javascript +{ + left: Number, // left edge of the scale bounding box + right: Number, // right edge of the bounding box' + top: Number, + bottom: Number, + width: Number, // the same as right - left + height: Number, // the same as bottom - top + + // Margin on each side. Like css, this is outside the bounding box. + margins: { + left: Number, + right: Number, + top: Number, + bottom: Number, + }, + + // Amount of padding on the inside of the bounding box (like CSS) + paddingLeft: Number, + paddingRight: Number, + paddingTop: Number, + paddingBottom: Number, +} +``` + +## Scale Interface +To work with Chart.js, custom scale types must implement the following interface. + +```javascript +{ + // Determines the data limits. Should set this.min and this.max to be the data max/min + determineDataLimits: function() {}, + + // Generate tick marks. this.chart is the chart instance. The data object can be accessed as this.chart.data + // buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class + buildTicks: function() {}, + + // Get the value to show for the data at the given index of the the given dataset, ie this.chart.data.datasets[datasetIndex].data[index] + getLabelForIndex: function(index, datasetIndex) {}, + + // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value + // @param index: index into the ticks array + // @param includeOffset: if true, get the pixel halway between the given tick and the next + getPixelForTick: function(index, includeOffset) {}, + + // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value + // @param value : the value to get the pixel for + // @param index : index into the data array of the value + // @param datasetIndex : index of the dataset the value comes from + // @param includeOffset : if true, get the pixel halway between the given tick and the next + getPixelForValue: function(value, index, datasetIndex, includeOffset) {} + + // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) + // @param pixel : pixel value + getValueForPixel: function(pixel) {} +} +``` + +Optionally, the following methods may also be overwritten, but an implementation is already provided by the `Chart.Scale` base class. + +```javascript + // Transform the ticks array of the scale instance into strings. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); + convertTicksToLabels: function() {}, + + // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. + calculateTickRotation: function() {}, + + // Fits the scale into the canvas. + // this.maxWidth and this.maxHeight will tell you the maximum dimensions the scale instance can be. Scales should endeavour to be as efficient as possible with canvas space. + // this.margins is the amount of space you have on either side of your scale that you may expand in to. This is used already for calculating the best label rotation + // You must set this.minSize to be the size of your scale. It must be an object containing 2 properties: width and height. + // You must set this.width to be the width and this.height to be the height of the scale + fit: function() {}, + + // Draws the scale onto the canvas. this.(left|right|top|bottom) will have been populated to tell you the area on the canvas to draw in + // @param chartArea : an object containing four properties: left, right, top, bottom. This is the rectangle that lines, bars, etc will be drawn in. It may be used, for example, to draw grid lines. + draw: function(chartArea) {}, +``` + +The Core.Scale base class also has some utility functions that you may find useful. +```javascript +{ + // Returns true if the scale instance is horizontal + isHorizontal: function() {}, + + // Get the correct value from the value from this.chart.data.datasets[x].data[] + // If dataValue is an object, returns .x or .y depending on the return of isHorizontal() + // If the value is undefined, returns NaN + // Otherwise returns the value. + // Note that in all cases, the returned value is not guaranteed to be a Number + getRightValue: function(dataValue) {}, +} +``` \ No newline at end of file diff --git a/docs/developers/charts.md b/docs/developers/charts.md new file mode 100644 index 00000000000..f4c9a97469a --- /dev/null +++ b/docs/developers/charts.md @@ -0,0 +1,116 @@ +# New Charts + +Chart.js 2.0 introduces the concept of controllers for each dataset. Like scales, new controllers can be written as needed. + +```javascript +Chart.controllers.MyType = Chart.DatasetController.extend({ + +}); + + +// Now we can create a new instance of our chart, using the Chart.js API +new Chart(ctx, { + // this is the string the constructor was registered at, ie Chart.controllers.MyType + type: 'MyType', + data: data, + options: options +}); +``` + +## Dataset Controller Interface + +Dataset controllers must implement the following interface. + +```javascript +{ + // Create elements for each piece of data in the dataset. Store elements in an array on the dataset as dataset.metaData + addElements: function() {}, + + // Create a single element for the data at the given index and reset its state + addElementAndReset: function(index) {}, + + // Draw the representation of the dataset + // @param ease : if specified, this number represents how far to transition elements. See the implementation of draw() in any of the provided controllers to see how this should be used + draw: function(ease) {}, + + // Remove hover styling from the given element + removeHoverStyle: function(element) {}, + + // Add hover styling to the given element + setHoverStyle: function(element) {}, + + // Update the elements in response to new data + // @param reset : if true, put the elements into a reset state so they can animate to their final values + update: function(reset) {}, +} +``` + +The following methods may optionally be overridden by derived dataset controllers +```javascript +{ + // Initializes the controller + initialize: function(chart, datasetIndex) {}, + + // Ensures that the dataset represented by this controller is linked to a scale. Overridden to helpers.noop in the polar area and doughnut controllers as these + // chart types using a single scale + linkScales: function() {}, + + // Called by the main chart controller when an update is triggered. The default implementation handles the number of data points changing and creating elements appropriately. + buildOrUpdateElements: function() {} +} +``` + +## Extending Existing Chart Types + +Extending or replacing an existing controller type is easy. Simply replace the constructor for one of the built in types with your own. + +The built in controller types are: +* `Chart.controllers.line` +* `Chart.controllers.bar` +* `Chart.controllers.radar` +* `Chart.controllers.doughnut` +* `Chart.controllers.polarArea` +* `Chart.controllers.bubble` + +For example, to derive a new chart type that extends from a bubble chart, you would do the following. + +```javascript +// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. +// We look for the defaults by doing Chart.defaults[chartType] +// It looks like a bug exists when the defaults don't exist +Chart.defaults.derivedBubble = Chart.defaults.bubble; + +// I think the recommend using Chart.controllers.bubble.extend({ extensions here }); +var custom = Chart.controllers.bubble.extend({ + draw: function(ease) { + // Call super method first + Chart.controllers.bubble.prototype.draw.call(this, ease); + + // Now we can do some custom drawing for this dataset. Here we'll draw a red box around the first point in each dataset + var meta = this.getMeta(); + var pt0 = meta.data[0]; + var radius = pt0._view.radius; + + var ctx = this.chart.chart.ctx; + ctx.save(); + ctx.strokeStyle = 'red'; + ctx.lineWidth = 1; + ctx.strokeRect(pt0._view.x - radius, pt0._view.y - radius, 2 * radius, 2 * radius); + ctx.restore(); + } +}); + +// Stores the controller so that the chart initialization routine can look it up with +// Chart.controllers[type] +Chart.controllers.derivedBubble = custom; + +// Now we can create and use our new chart type +new Chart(ctx, { + type: 'derivedBubble', + data: data, + options: options, +}); +``` + +### Bar Controller +The bar controller has a special property that you should be aware of. To correctly calculate the width of a bar, the controller must determine the number of datasets that map to bars. To do this, the bar controller attaches a property `bar` to the dataset during initialization. If you are creating a replacement or updated bar controller, you should do the same. This will ensure that charts with regular bars and your new derived bars will work seamlessly. \ No newline at end of file diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md new file mode 100644 index 00000000000..67c391db5a5 --- /dev/null +++ b/docs/developers/contributing.md @@ -0,0 +1,34 @@ +# Contributing + +New contributions to the library are welcome, but we ask that you please follow these guidelines: + +- Use tabs for indentation, not spaces. +- Only change the individual files in `/src`. +- Check that your code will pass `eslint` code standards, `gulp lint` will run this for you. +- Check that your code will pass tests, `gulp test` will run tests for you. +- Keep pull requests concise, and document new functionality in the relevant `.md` file. +- Consider whether your changes are useful for all users, or if creating a Chart.js plugin would be more appropriate. + +# Building Chart.js + +Chart.js uses gulp to build the library into a single JavaScript file. + +Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: + +```bash +npm install +npm install -g gulp +``` + +This will install the local development dependencies for Chart.js, along with a CLI for the JavaScript task runner gulp. + +Now, we can run the `gulp build` task. + +```bash +gulp build +``` + +# Bugs & issues + +Please report these on the GitHub page - at github.com/chartjs/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. + diff --git a/docs/developers/plugins.md b/docs/developers/plugins.md new file mode 100644 index 00000000000..5ca1921d5d8 --- /dev/null +++ b/docs/developers/plugins.md @@ -0,0 +1,140 @@ +# Plugins + +Plugins are the most efficient way to customize or change the default behavior of a chart. They have been introduced at [version 2.1.0](https://github.com/chartjs/Chart.js/releases/tag/2.1.0) (global plugins only) and extended at [version 2.5.0](https://github.com/chartjs/Chart.js/releases/tag/2.5.0) (per chart plugins and options). + +## Popular Plugins + + - chartjs-plugin-annotation.js - Draw lines and boxes on chart area. + - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport. + - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse. + - chartjs-plugin-zoom.js - Enable zooming and panning on charts. + - Chart.BarFunnel.js - Adds a bar funnel chart type. + - Chart.LinearGauge.js - Adds a linear gauge chart type. + - Chart.Smith.js - Adds a smith chart type. + +In addition, many plugins can be found on the [Chart.js GitHub organization](https://github.com/chartjs) or on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). + +## Using plugins + +Plugins can be shared between chart instances: + +```javascript +var plugin = { /* plugin implementation */ }; + +// chart1 and chart2 use "plugin" +var chart1 = new Chart(ctx, { + plugins: [plugin] +}); + +var chart2 = new Chart(ctx, { + plugins: [plugin] +}); + +// chart3 doesn't use "plugin" +var chart3 = new Chart(ctx, {}); +``` + +Plugins can also be defined directly in the chart `plugins` config (a.k.a. *inline plugins*): + +```javascript +var chart = new Chart(ctx, { + plugins: [{ + beforeInit: function(chart, options) { + //.. + } + }] +}); +``` + +However, this approach is not ideal when the customization needs to apply to many charts. + +## Global plugins + +Plugins can be registered globally to be applied on all charts (a.k.a. *global plugins*): + +```javascript +Chart.plugins.register({ + // plugin implementation +}); +``` + +> Note: *inline* plugins can't be registered globally. + +## Configuration + +### Plugin ID + +Plugins must define a unique id in order to be configurable. + +This id should follow the [npm package name convention](https://docs.npmjs.com/files/package.json#name): + +- can't start with a dot or an underscore +- can't contain any non-URL-safe characters +- can't contain uppercase letters +- should be something short, but also reasonably descriptive + +If a plugin is intended to be released publicly, you may want to check the [registry](https://www.npmjs.com/search?q=chartjs-plugin-) to see if there's something by that name already. Note that in this case, the package name should be prefixed by `chartjs-plugin-` to appear in Chart.js plugin registry. + +### Plugin options + +Plugin options are located under the `options.plugins` config and are scoped by the plugin ID: `options.plugins.{plugin-id}`. + +```javascript +var chart = new Chart(ctx, { + config: { + foo: { ... }, // chart 'foo' option + plugins: { + p1: { + foo: { ... }, // p1 plugin 'foo' option + bar: { ... } + }, + p2: { + foo: { ... }, // p2 plugin 'foo' option + bla: { ... } + } + } + } +}); +``` + +#### Disable plugins + +To disable a global plugin for a specific chart instance, the plugin options must be set to `false`: + +```javascript +Chart.plugins.register({ + id: 'p1', + // ... +}); + +var chart = new Chart(ctx, { + config: { + plugins: { + p1: false // disable plugin 'p1' for this instance + } + } +}); +``` + +## Plugin Core API + +Available hooks (as of version 2.5): + +* beforeInit +* afterInit +* beforeUpdate *(cancellable)* +* afterUpdate +* beforeLayout *(cancellable)* +* afterLayout +* beforeDatasetsUpdate *(cancellable)* +* afterDatasetsUpdate +* beforeRender *(cancellable)* +* afterRender +* beforeDraw *(cancellable)* +* afterDraw +* beforeDatasetsDraw *(cancellable)* +* afterDatasetsDraw +* beforeEvent *(cancellable)* +* afterEvent +* resize +* destroy diff --git a/docs/general/README.md b/docs/general/README.md new file mode 100644 index 00000000000..f075bca2197 --- /dev/null +++ b/docs/general/README.md @@ -0,0 +1,8 @@ +# General Chart.js Configuration + +These sections describe general configuration options that can apply elsewhere in the documentation. + +* [Colors](./colors.md) defines acceptable color values +* [Font](./fonts.md) defines various font options +* [Interactions](./interactions/README.md) defines options that reflect how hovering chart elements works +* [Responsive](./responsive.md) defines responsive chart options that apply to all charts. \ No newline at end of file diff --git a/docs/general/colors.md b/docs/general/colors.md new file mode 100644 index 00000000000..d2d270081e6 --- /dev/null +++ b/docs/general/colors.md @@ -0,0 +1,49 @@ +# Colors + +When supplying colors to Chart options, you can use a number of formats. You can specify the color as a string in hexadecimal, RGB, or HSL notations. If a color is needed, but not specified, Chart.js will use the global default color. This color is stored at `Chart.defaults.global.defaultColor`. It is initially set to `'rgba(0, 0, 0, 0.1)'` + +You can also pass a [CanvasGradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient) object. You will need to create this before passing to the chart, but using it you can achieve some interesting effects. + +## Patterns and Gradients + +An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. + +For example, if you wanted to fill a dataset with a pattern from an image you could do the following. + +```javascript +var img = new Image(); +img.src = 'https://example.com/my_image.png'; +img.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + var fillPattern = ctx.createPattern(img, 'repeat'); + + var chart = new Chart(ctx, { + data: { + labels: ['Item 1', 'Item 2', 'Item 3'], + datasets: [{ + data: [10, 20, 30], + backgroundColor: fillPattern + }] + } + }) +} +``` + +Using pattern fills for data graphics can help viewers with vision deficiencies (e.g. color-blindness or partial sight) to [more easily understand your data](http://betweentwobrackets.com/data-graphics-and-colour-vision/). + +Using the [Patternomaly](https://github.com/ashiguruma/patternomaly) library you can generate patterns to fill datasets. + +```javascript +var chartData = { + datasets: [{ + data: [45, 25, 20, 10], + backgroundColor: [ + pattern.draw('square', '#ff6384'), + pattern.draw('circle', '#36a2eb'), + pattern.draw('diamond', '#cc65fe'), + pattern.draw('triangle', '#ffce56'), + ] + }], + labels: ['Red', 'Blue', 'Purple', 'Yellow'] +}; +``` \ No newline at end of file diff --git a/docs/general/fonts.md b/docs/general/fonts.md new file mode 100644 index 00000000000..dd475294203 --- /dev/null +++ b/docs/general/fonts.md @@ -0,0 +1,28 @@ +# Fonts + +There are 4 special global settings that can change all of the fonts on the chart. These options are in `Chart.defaults.global`. The global font settings only apply when more specific options are not included in the config. + +For example, in this chart the text will all be red except for the labels in the legend. + +```javascript +Chart.defaults.global.defaultFontColor = 'red'; +let chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + legend: { + labels: { + // This more specific font property overrides the global property + fontColor: 'black' + } + } + } +}); +``` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `defaultFontColor` | `Color` | `'#666'` | Default font color for all text. +| `defaultFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Default font family for all text. +| `defaultFontSize` | `Number` | `12` | Default font size (in px) for text. Does not apply to radialLinear scale point labels. +| `defaultFontStyle` | `String` | `'normal'` | Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. diff --git a/docs/general/interactions/README.md b/docs/general/interactions/README.md new file mode 100644 index 00000000000..1ff1d55d8f7 --- /dev/null +++ b/docs/general/interactions/README.md @@ -0,0 +1,9 @@ +# Interactions + +The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `mode` | `String` | `'nearest'` | Sets which elements appear in the tooltip. See [Interaction Modes](./modes.md#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. \ No newline at end of file diff --git a/docs/general/interactions/events.md b/docs/general/interactions/events.md new file mode 100644 index 00000000000..8e21f75024b --- /dev/null +++ b/docs/general/interactions/events.md @@ -0,0 +1,21 @@ +# Events +The following properties define how the chart interacts with events. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `events` | `String[]` | `["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]` | The `events` option defines the browser events that the chart should listen to for tooltips and hovering. [more...](#event-option) +| `onHover` | `Function` | `null` | Called when any of the events fire. Called in the context of the chart and passed the event and an array of active elements (bars, points, etc). +| `onClick` | `Function` | `null` | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed the event and an array of active elements + +## Event Option +For example, to have the chart only respond to click events, you could do +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + // This chart will not respond to mousemove, etc + events: ['click'] + } +}); +``` diff --git a/docs/general/interactions/modes.md b/docs/general/interactions/modes.md new file mode 100644 index 00000000000..6775c590c3b --- /dev/null +++ b/docs/general/interactions/modes.md @@ -0,0 +1,104 @@ +# Interaction Modes + +When configuring interaction with the graph via hover or tooltips, a number of different modes are available. + +The modes are detailed below and how they behave in conjunction with the `intersect` setting. + +## point +Finds all of the items that intersect the point. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: '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. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: 'nearest' + } + } +}) +``` + +## 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. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: 'index' + } + } +}) +``` + +## x-axis (deprecated) +Behaves like `'index'` mode with `intersect = false`. + +## 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. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: 'dataset' + } + } +}) +``` + +## 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 + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: 'x' + } + } +}) +``` + +## 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. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + mode: 'y' + } + } +}) +``` \ No newline at end of file diff --git a/docs/general/responsive.md b/docs/general/responsive.md new file mode 100644 index 00000000000..894dccee0bc --- /dev/null +++ b/docs/general/responsive.md @@ -0,0 +1,10 @@ +# Responsive Charts + +Chart.js provides a few options for controlling resizing behaviour of charts. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `responsive` | `Boolean` | `true` | Resizes the chart canvas when its container does. +| `responsiveAnimationDuration` | `Number` | `0` | Duration in milliseconds it takes to animate to new size after a resize event. +| `maintainAspectRatio` | `Boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. +| `onResize` | `Function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. \ No newline at end of file diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md new file mode 100644 index 00000000000..bb3e17f1f67 --- /dev/null +++ b/docs/getting-started/README.md @@ -0,0 +1,43 @@ +# Getting Started + +Let's get started using Chart.js! + +First, we need to have a canvas in our page. + +```html + +``` + +Now that we have a canvas we can use, we need to include Chart.js in our page. + +```html + + +``` + +## Common JS + +```javascript +var Chart = require('chart.js') +var myChart = new Chart(ctx, {...}) +``` + +## Require JS + +```javascript +require(['path/to/Chartjs/src/chartjs.js'], function(Chart){ + var myChart = new Chart(ctx, {...}) +}) +``` \ No newline at end of file diff --git a/docs/getting-started/usage.md b/docs/getting-started/usage.md new file mode 100644 index 00000000000..739c838596c --- /dev/null +++ b/docs/getting-started/usage.md @@ -0,0 +1,65 @@ +# Usage +Chart.js can be used with ES6 modules, plain JavaScript and module loaders. + +## Creating a Chart + +To create a chart, we need to instantiate the `Chart` class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart. Here's an example. + +```html + +``` + +```javascript +// Any of the following formats may be used +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! + +The following example instantiates a bar chart showing the number of votes for different colors and the y-axis starting at 0. + +```html + + +``` \ No newline at end of file diff --git a/docs/notes/README.md b/docs/notes/README.md new file mode 100644 index 00000000000..2795faebc85 --- /dev/null +++ b/docs/notes/README.md @@ -0,0 +1 @@ +# Additional Notes \ No newline at end of file diff --git a/docs/notes/comparison.md b/docs/notes/comparison.md new file mode 100644 index 00000000000..aa7e25eec42 --- /dev/null +++ b/docs/notes/comparison.md @@ -0,0 +1,32 @@ +# Comparison with Other Charting Libraries + +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 | ✓ | ✓ | ✓ | ✓ | + +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.) | | ✓ | | + diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md new file mode 100644 index 00000000000..5fef6fd9a3e --- /dev/null +++ b/docs/notes/extensions.md @@ -0,0 +1,26 @@ +# Popular Extensions + +There are many extensions which are available for use with popular frameworks. Some particularly notable ones are listed here. + +## Angular + - angular-chart.js + - tc-angular-chartjs + - angular-chartjs + - Angular Chart-js Directive + +## React + - react-chartjs2 + - react-chartjs-2 + +## Django + - Django JChart + - Django Chartjs + +## Ruby on Rails + - chartjs-ror + +## Laravel + - laravel-chartjs + +#### Vue.js + - vue-chartjs diff --git a/docs/notes/license.md b/docs/notes/license.md new file mode 100644 index 00000000000..b933686df54 --- /dev/null +++ b/docs/notes/license.md @@ -0,0 +1,3 @@ +# License + +Chart.js is open source and available under the MIT license. \ No newline at end of file diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 00000000000..89e7c705355 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,11 @@ +a.anchorjs-link { + color: rgba(65, 131, 196, 0.1); + font-weight: 400; + text-decoration: none; + transition: color 100ms ease-out; + z-index: 999; +} + +a.anchorjs-link:hover { + color: rgba(65, 131, 196, 1); +} diff --git a/gulpfile.js b/gulpfile.js index c9d60471333..785004b2c38 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -11,7 +11,7 @@ var streamify = require('gulp-streamify'); var uglify = require('gulp-uglify'); var util = require('gulp-util'); var zip = require('gulp-zip'); -var exec = require('child_process').exec; +var exec = require('child-process-promise').exec; var karma = require('karma'); var browserify = require('browserify'); var source = require('vinyl-source-stream'); @@ -41,6 +41,7 @@ gulp.task('package', packageTask); gulp.task('coverage', coverageTask); gulp.task('watch', watchTask); gulp.task('lint', lintTask); +gulp.task('docs', docsTask); gulp.task('test', ['lint', 'validHTML', 'unittest']); gulp.task('size', ['library-size', 'module-sizes']); gulp.task('server', serverTask); @@ -164,6 +165,19 @@ function lintTask() { .pipe(eslint.failAfterError()); } +function docsTask(done) { + const script = require.resolve('gitbook-cli/bin/gitbook.js'); + const cmd = process.execPath; + + exec([cmd, script, 'install', './'].join(' ')).then(() => { + return exec([cmd, script, 'build', './', './dist/docs'].join(' ')); + }).catch((err) => { + console.error(err.stdout); + }).then(() => { + done(); + }); +} + function validHTMLTask() { return gulp.src('samples/*.html') .pipe(htmlv()); diff --git a/package.json b/package.json index 24a313f4b29..2f50eda4ca2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "browserify": "^13.0.0", "browserify-istanbul": "^0.2.1", "bundle-collapser": "^1.2.1", + "child-process-promise": "^2.2.0", "coveralls": "^2.11.6", + "gitbook-cli": "^2.3.0", "gulp": "3.9.x", "gulp-concat": "~2.1.x", "gulp-connect": "~2.0.5", From da1d1ca5125b7fc6e90dd49b0656f3d82311e8e7 Mon Sep 17 00:00:00 2001 From: Bohdan Khorolets Date: Tue, 21 Mar 2017 02:37:34 +0200 Subject: [PATCH 152/685] Avoid errors when rendering serverside (#3909) --- src/chart.js | 5 ++++- src/core/core.helpers.js | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/chart.js b/src/chart.js index 0ee96bd57d4..591572110e8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -56,4 +56,7 @@ plugins.push(require('./plugins/plugin.filler.js')(Chart)); Chart.plugins.register(plugins); -window.Chart = module.exports = Chart; +module.exports = Chart; +if (typeof window !== 'undefined') { + window.Chart = Chart; +} diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 2d96ac14b47..5ed5c28edd4 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -669,6 +669,11 @@ module.exports = function(Chart) { }; // Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ helpers.requestAnimFrame = (function() { + if (typeof window === 'undefined') { + return function(callback) { + callback(); + }; + } return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || From 36ccf40946ae732e3614999ae86eadac7f248d20 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 20 Mar 2017 20:38:15 -0400 Subject: [PATCH 153/685] Fix for stacked bar charts with log axes (#4010) * Undo fix for #3585 since it has broken the stacked bar charts when the axis has a user defined minimum value. * When a value of 0 is requested for a vertical logarithmic axis, return the bottom point --- src/controllers/controller.bar.js | 20 +++---- src/scales/scale.logarithmic.js | 2 + test/specs/controller.bar.tests.js | 77 +++++++++++++++++++++++---- test/specs/scale.logarithmic.tests.js | 2 +- 4 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 8f7ba3fa2b5..71a75b3af56 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -112,10 +112,9 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var base = yScale.getBaseValue(); - var original = base; + var base = 0; - if ((yScale.options.stacked === true) || + if (yScale.options.stacked === true || (yScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; @@ -127,7 +126,7 @@ module.exports = function(Chart) { if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) && meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); + base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); } } @@ -227,9 +226,8 @@ module.exports = function(Chart) { if (yScale.options.stacked || (yScale.options.stacked === undefined && meta.stack !== undefined)) { - var base = yScale.getBaseValue(); - var sumPos = base, - sumNeg = base; + var sumPos = 0, + sumNeg = 0; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; @@ -418,7 +416,6 @@ module.exports = function(Chart) { var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); var base = xScale.getBaseValue(); - var originalBase = base; if (xScale.options.stacked || (xScale.options.stacked === undefined && meta.stack !== undefined)) { @@ -432,7 +429,7 @@ module.exports = function(Chart) { if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) && meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); + base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); } } @@ -512,9 +509,8 @@ module.exports = function(Chart) { if (xScale.options.stacked || (xScale.options.stacked === undefined && meta.stack !== undefined)) { - var base = xScale.getBaseValue(); - var sumPos = base, - sumNeg = base; + var sumPos = 0, + sumNeg = 0; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 5d1d329f9ab..13d1d5a5312 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -216,6 +216,8 @@ module.exports = function(Chart) { } else { pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } + } else if (newVal === 0) { + pixel = tickOpts.reverse ? me.top : me.bottom; } else { range = helpers.log10(me.end) - helpers.log10(start); innerDimension = me.height; diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index db3cf07460e..9717cff30f2 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -849,6 +849,65 @@ describe('Bar controller tests', function() { }); }); + it('should update elements when the scales are stacked and the y axis has a user definined minimum', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [50, 20, 10, 100], + label: 'dataset1' + }, { + data: [50, 80, 90, 0], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + type: 'linear', + stacked: true, + ticks: { + min: 50, + max: 100 + } + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 936, w: 83, x: 87, y: 484}, + {b: 936, w: 83, x: 202, y: 755}, + {b: 936, w: 83, x: 318, y: 846}, + {b: 936, w: 83, x: 434, y: 32} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 484, w: 83, x: 87, y: 32}, + {b: 755, w: 83, x: 202, y: 32}, + {b: 846, w: 83, x: 318, y: 32}, + {b: 32, w: 83, x: 434, y: 32} + ].forEach(function(values, i) { + expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + it('should update elements when only the category scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', @@ -1119,7 +1178,7 @@ describe('Bar controller tests', function() { }], yAxes: [{ type: 'logarithmic', - stacked: true + stacked: true, }] } } @@ -1128,10 +1187,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 484, w: 92, x: 94, y: 379}, - {b: 484, w: 92, x: 208, y: 122}, - {b: 484, w: 92, x: 322, y: 379}, - {b: 484, w: 92, x: 436, y: 122} + {b: 484, w: 92, x: 94, y: 484}, + {b: 484, w: 92, x: 208, y: 136}, + {b: 484, w: 92, x: 322, y: 484}, + {b: 484, w: 92, x: 436, y: 136} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1142,10 +1201,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 379, w: 92, x: 94, y: 109}, - {b: 122, w: 92, x: 208, y: 109}, - {b: 379, w: 92, x: 322, y: 379}, - {b: 122, w: 92, x: 436, y: 25} + {b: 484, w: 92, x: 94, y: 122}, + {b: 136, w: 92, x: 208, y: 122}, + {b: 484, w: 92, x: 322, y: 484}, + {b: 136, w: 92, x: 436, y: 32} ].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/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 4562aa384f5..a29b28260a3 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -726,7 +726,7 @@ describe('Logarithmic Scale tests', function() { expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // 0 is invalid. force it on top + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // 0 is invalid. force it on bottom expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); From cffa9447a189f58054243a8808ed9ebfdf56ee34 Mon Sep 17 00:00:00 2001 From: Hiroshi Shirosaki Date: Tue, 21 Mar 2017 09:39:18 +0900 Subject: [PATCH 154/685] Fix radar chart horizontal position (#4032) Radar chart position is not center horizontally with v2.5.0. Right and left of `furthestLimits` would be switched wrongly on this refactoring commit. https://github.com/chartjs/Chart.js/pull/3625/commits/e1606f88ed4805815038cba4fdcd6211d7490356 --- src/scales/scale.radialLinear.js | 4 ++-- test/specs/scale.radialLinear.tests.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 5aa0c72c517..d61044effcd 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -144,8 +144,8 @@ module.exports = function(Chart) { // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); var furthestLimits = { - l: scale.width, - r: 0, + r: scale.width, + l: 0, t: scale.height, b: 0 }; diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index baff12eb892..7796e124418 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -344,7 +344,7 @@ describe('Test the radial linear scale', function() { }); expect(chart.scale.drawingArea).toBe(233); - expect(chart.scale.xCenter).toBe(247); + expect(chart.scale.xCenter).toBe(256); expect(chart.scale.yCenter).toBe(280); }); @@ -393,7 +393,7 @@ describe('Test the radial linear scale', function() { expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(233); expect(chart.scale.getPointPositionForValue(1, 5)).toEqual({ - x: 261, + x: 270, y: 275, }); From 8367f90df0ce765d096c3175b44ffa31d677a14e Mon Sep 17 00:00:00 2001 From: Samuel Jo Date: Mon, 20 Mar 2017 20:40:28 -0400 Subject: [PATCH 155/685] Do not draw tooltips that have no items (#4034) --- src/core/core.tooltip.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 16fcb099b36..15574a8274a 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -789,7 +789,10 @@ module.exports = function(Chart) { // IE11/Edge does not like very small opacities, so snap to 0 var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; - if (this._options.enabled) { + // Truthy/falsey value for empty tooltip + var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; + + if (this._options.enabled && hasTooltipContent) { // Draw Background this.drawBackground(pt, vm, ctx, tooltipSize, opacity); From 6e57405a0a7885ec5099e8b8bd7b26822effa628 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 21 Mar 2017 01:40:44 +0100 Subject: [PATCH 156/685] Reorganize samples and list them in index.html (#4043) --- .codeclimate.yml | 2 + .../data-labelling.html} | 4 +- .../{animation => advanced}/progress-bar.html | 0 samples/{ => charts}/area/analyser.js | 0 .../{ => charts}/area/line-boundaries.html | 6 +- samples/{ => charts}/area/line-datasets.html | 6 +- .../area/line-stacked.html} | 52 ++--- samples/{ => charts}/area/radar.html | 6 +- .../bar/horizontal.html} | 28 +-- .../bar/multi-axis.html} | 30 +-- .../bar/stacked-group.html} | 40 ++-- .../bar/stacked.html} | 40 ++-- .../bar.html => charts/bar/vertical.html} | 28 +-- samples/{ => charts}/bubble.html | 4 +- samples/{ => charts}/combo-bar-line.html | 40 ++-- samples/{ => charts}/doughnut.html | 4 +- .../line.html => charts/line/basic.html} | 28 +-- .../line/interpolation-modes.html | 6 +- samples/{ => charts}/line/line-styles.html | 40 ++-- .../line/multi-axis.html} | 28 +-- .../line/point-sizes.html} | 52 ++--- samples/{ => charts}/line/point-styles.html | 4 +- .../line/skip-points.html} | 28 +-- .../line/stepped.html} | 28 +-- samples/{ => charts}/pie.html | 4 +- samples/{ => charts}/polar-area.html | 4 +- .../{radar => charts}/radar-skip-points.html | 0 samples/{radar => charts}/radar.html | 24 +-- .../scatter/basic.html} | 4 +- .../scatter/multi-axis.html} | 6 +- samples/favicon.ico | Bin 0 -> 32988 bytes samples/index.html | 57 ++++++ .../{pointstyle.html => point-style.html} | 0 .../{positions.html => positioning.html} | 0 samples/logo.svg | 1 + samples/samples.js | 181 ++++++++++++++++++ ...y-settings.html => gridlines-display.html} | 0 .../{gridlines.html => gridlines-style.html} | 0 ...x-settings.html => min-max-suggested.html} | 0 .../{min-max-settings.html => min-max.html} | 0 .../{line-logarithmic.html => line.html} | 0 .../{scatter-logX.html => scatter.html} | 0 ...-non-numeric-y.html => non-numeric-y.html} | 0 .../{combo-time-scale.html => combo.html} | 0 ...e-point-data.html => line-point-data.html} | 0 .../time/{line-time-scale.html => line.html} | 0 samples/style.css | 132 ++++++++++++- .../{tooltip-border.html => border.html} | 0 ...{tooltip-callbacks.html => callbacks.html} | 0 ...e-customTooltips.html => custom-line.html} | 0 ...ie-customTooltips.html => custom-pie.html} | 0 ...customTooltips.html => custom-points.html} | 0 ...teraction-modes.html => interactions.html} | 0 .../{position-modes.html => positioning.html} | 0 54 files changed, 643 insertions(+), 274 deletions(-) rename samples/{data_labelling.html => advanced/data-labelling.html} (98%) rename samples/{animation => advanced}/progress-bar.html (100%) rename samples/{ => charts}/area/analyser.js (100%) rename samples/{ => charts}/area/line-boundaries.html (94%) rename samples/{ => charts}/area/line-datasets.html (95%) rename samples/{line/line-stacked-area.html => charts/area/line-stacked.html} (76%) rename samples/{ => charts}/area/radar.html (95%) rename samples/{bar/bar-horizontal.html => charts/bar/horizontal.html} (88%) rename samples/{bar/bar-multi-axis.html => charts/bar/multi-axis.html} (82%) rename samples/{bar/bar-stacked-group.html => charts/bar/stacked-group.html} (73%) rename samples/{bar/bar-stacked.html => charts/bar/stacked.html} (72%) rename samples/{bar/bar.html => charts/bar/vertical.html} (87%) rename samples/{ => charts}/bubble.html (98%) rename samples/{ => charts}/combo-bar-line.html (72%) rename samples/{ => charts}/doughnut.html (97%) rename samples/{line/line.html => charts/line/basic.html} (87%) rename samples/{ => charts}/line/interpolation-modes.html (96%) rename samples/{ => charts}/line/line-styles.html (73%) rename samples/{line/line-multi-axis.html => charts/line/multi-axis.html} (82%) rename samples/{line/different-point-sizes.html => charts/line/point-sizes.html} (73%) rename samples/{ => charts}/line/point-styles.html (96%) rename samples/{line/line-skip-points.html => charts/line/skip-points.html} (77%) rename samples/{line/line-stepped.html => charts/line/stepped.html} (77%) rename samples/{ => charts}/pie.html (96%) rename samples/{ => charts}/polar-area.html (97%) rename samples/{radar => charts}/radar-skip-points.html (100%) rename samples/{radar => charts}/radar.html (88%) rename samples/{scatter/scatter.html => charts/scatter/basic.html} (97%) rename samples/{scatter/scatter-multi-axis.html => charts/scatter/multi-axis.html} (97%) create mode 100644 samples/favicon.ico create mode 100644 samples/index.html rename samples/legend/{pointstyle.html => point-style.html} (100%) rename samples/legend/{positions.html => positioning.html} (100%) create mode 100644 samples/logo.svg create mode 100644 samples/samples.js rename samples/scales/{display-settings.html => gridlines-display.html} (100%) rename samples/scales/{gridlines.html => gridlines-style.html} (100%) rename samples/scales/linear/{suggested-min-max-settings.html => min-max-suggested.html} (100%) rename samples/scales/linear/{min-max-settings.html => min-max.html} (100%) rename samples/scales/logarithmic/{line-logarithmic.html => line.html} (100%) rename samples/scales/logarithmic/{scatter-logX.html => scatter.html} (100%) rename samples/scales/{line-non-numeric-y.html => non-numeric-y.html} (100%) rename samples/scales/time/{combo-time-scale.html => combo.html} (100%) rename samples/scales/time/{line-time-point-data.html => line-point-data.html} (100%) rename samples/scales/time/{line-time-scale.html => line.html} (100%) rename samples/tooltips/{tooltip-border.html => border.html} (100%) rename samples/tooltips/{tooltip-callbacks.html => callbacks.html} (100%) rename samples/tooltips/{line-customTooltips.html => custom-line.html} (100%) rename samples/tooltips/{pie-customTooltips.html => custom-pie.html} (100%) rename samples/tooltips/{dataPoints-customTooltips.html => custom-points.html} (100%) rename samples/tooltips/{interaction-modes.html => interactions.html} (100%) rename samples/tooltips/{position-modes.html => positioning.html} (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index ee3a5fdacb7..99e66913cb2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -4,6 +4,8 @@ engines: config: languages: - javascript + exclude_paths: + - "samples/samples.js" eslint: enabled: true channel: "eslint-3" diff --git a/samples/data_labelling.html b/samples/advanced/data-labelling.html similarity index 98% rename from samples/data_labelling.html rename to samples/advanced/data-labelling.html index 8c64bcf0920..17fc47c8f99 100644 --- a/samples/data_labelling.html +++ b/samples/advanced/data-labelling.html @@ -4,8 +4,8 @@ Labelling Data Points - - + + diff --git a/samples/area/line-datasets.html b/samples/charts/area/line-datasets.html similarity index 95% rename from samples/area/line-datasets.html rename to samples/charts/area/line-datasets.html index 37719df26b3..7bc54e3138a 100644 --- a/samples/area/line-datasets.html +++ b/samples/charts/area/line-datasets.html @@ -5,9 +5,9 @@ area > datasets | Chart.js sample - - - + + + diff --git a/samples/line/line-stacked-area.html b/samples/charts/area/line-stacked.html similarity index 76% rename from samples/line/line-stacked-area.html rename to samples/charts/area/line-stacked.html index 041c9a84b72..26ee6b18f8f 100644 --- a/samples/line/line-stacked-area.html +++ b/samples/charts/area/line-stacked.html @@ -3,8 +3,8 @@ Line Chart - - + + diff --git a/samples/bar/bar-horizontal.html b/samples/charts/bar/horizontal.html similarity index 88% rename from samples/bar/bar-horizontal.html rename to samples/charts/bar/horizontal.html index affcc334927..0cfb78e84d0 100644 --- a/samples/bar/bar-horizontal.html +++ b/samples/charts/bar/horizontal.html @@ -3,8 +3,8 @@ Horizontal Bar Chart - - + + diff --git a/samples/polar-area.html b/samples/charts/polar-area.html similarity index 97% rename from samples/polar-area.html rename to samples/charts/polar-area.html index 48fe984afab..3981651de74 100644 --- a/samples/polar-area.html +++ b/samples/charts/polar-area.html @@ -3,8 +3,8 @@ Polar Area Chart - - + + + +
    +
    +
    Samples
    +
    Simple yet flexible JavaScript charting for designers & developers
    + +
    + +
    +
    + + + + diff --git a/samples/legend/pointstyle.html b/samples/legend/point-style.html similarity index 100% rename from samples/legend/pointstyle.html rename to samples/legend/point-style.html diff --git a/samples/legend/positions.html b/samples/legend/positioning.html similarity index 100% rename from samples/legend/positions.html rename to samples/legend/positioning.html diff --git a/samples/logo.svg b/samples/logo.svg new file mode 100644 index 00000000000..24f5a2bf0ed --- /dev/null +++ b/samples/logo.svg @@ -0,0 +1 @@ +Artboard 6 \ No newline at end of file diff --git a/samples/samples.js b/samples/samples.js new file mode 100644 index 00000000000..c3d489cdb5b --- /dev/null +++ b/samples/samples.js @@ -0,0 +1,181 @@ +(function(global) { + + var Samples = global.Samples || (global.Samples = {}); + + Samples.items = [{ + title: 'Bar charts', + items: [{ + title: 'Vertical', + path: 'charts/bar/vertical.html' + }, { + title: 'Horizontal', + path: 'charts/bar/horizontal.html' + }, { + title: 'Multi axis', + path: 'charts/bar/multi-axis.html' + }, { + title: 'Stacked', + path: 'charts/bar/stacked.html' + }, { + title: 'Stacked groups', + path: 'charts/bar/stacked-group.html' + }] + }, { + title: 'Line charts', + items: [{ + title: 'Basic', + path: 'charts/line/basic.html' + }, { + title: 'Multi axis', + path: 'charts/line/multi-axis.html' + }, { + title: 'Stepped', + path: 'charts/line/stepped.html' + }, { + title: 'Interpolation', + path: 'charts/line/interpolation-modes.html' + }, { + title: 'Line styles', + path: 'charts/line/line-styles.html' + }, { + title: 'Point styles', + path: 'charts/line/point-styles.html' + }, { + title: 'Point sizes', + path: 'charts/line/point-sizes.html' + }] + }, { + title: 'Area charts', + items: [{ + title: 'Boundaries (line)', + path: 'charts/area/line-boundaries.html' + }, { + title: 'Datasets (line)', + path: 'charts/area/line-datasets.html' + }, { + title: 'Stacked (line)', + path: 'charts/area/line-stacked.html' + }, { + title: 'Radar', + path: 'charts/area/radar.html' + }] + }, { + title: 'Other charts', + items: [{ + title: 'Scatter', + path: 'charts/scatter/basic.html' + }, { + title: 'Scatter - Multi axis', + path: 'charts/scatter/multi-axis.html' + }, { + title: 'Doughnut', + path: 'charts/doughnut.html' + }, { + title: 'Pie', + path: 'charts/pie.html' + }, { + title: 'Polar area', + path: 'charts/polar-area.html' + }, { + title: 'Radar', + path: 'charts/radar.html' + }, { + title: 'Combo bar/line', + path: 'charts/combo-bar-line.html' + }] + }, { + title: 'Linear scale', + items: [{ + title: 'Step size', + path: 'scales/linear/step-size.html' + }, { + title: 'Min & max', + path: 'scales/linear/min-max.html' + }, { + title: 'Min & max (suggested)', + path: 'scales/linear/min-max-suggested.html' + }] + }, { + title: 'Logarithmic scale', + items: [{ + title: 'Line', + path: 'scales/logarithmic/line.html' + }, { + title: 'Scatter', + path: 'scales/logarithmic/scatter.html' + }] + }, { + title: 'Time scale', + items: [{ + title: 'Line', + path: 'scales/time/line.html' + }, { + title: 'Line (point data)', + path: 'scales/time/line.html' + }, { + title: 'Combo', + path: 'scales/time/combo.html' + }] + }, { + title: 'Scale options', + items: [{ + title: 'Grid lines display', + path: 'scales/gridlines-display.html' + }, { + title: 'Grid lines style', + path: 'scales/gridlines-style.html' + }, { + title: 'Multiline labels', + path: 'scales/multiline-labels.html' + }, { + title: 'Filtering Labels', + path: 'scales/filtering-labels.html' + }, { + title: 'Non numeric Y Axis', + path: 'scales/non-numeric-y.html' + }] + }, { + title: 'Legend', + items: [{ + title: 'Positioning', + path: 'legend/positioning.html' + }, { + title: 'Point style', + path: 'legend/point-style.html' + }] + }, { + title: 'Tooltip', + items: [{ + title: 'Positioning', + path: 'tooltips/positioning.html' + }, { + title: 'Interactions', + path: 'tooltips/interactions.html' + }, { + title: 'Callbacks', + path: 'tooltips/callbacks.html' + }, { + title: 'Border', + path: 'tooltips/border.html' + }, { + title: 'HTML tooltips (line)', + path: 'tooltips/custom-line.html' + }, { + title: 'HTML tooltips (pie)', + path: 'tooltips/custom-pie.html' + }, { + title: 'HTML tooltips (points)', + path: 'tooltips/custom-points.html' + }] + }, { + title: 'Advanced', + items: [{ + title: 'Progress bar', + path: 'advanced/progress-bar.html' + }, { + title: 'Data labelling (plugin)', + path: 'advanced/data-labelling.html' + }] + }]; + +}(this)); diff --git a/samples/scales/display-settings.html b/samples/scales/gridlines-display.html similarity index 100% rename from samples/scales/display-settings.html rename to samples/scales/gridlines-display.html diff --git a/samples/scales/gridlines.html b/samples/scales/gridlines-style.html similarity index 100% rename from samples/scales/gridlines.html rename to samples/scales/gridlines-style.html diff --git a/samples/scales/linear/suggested-min-max-settings.html b/samples/scales/linear/min-max-suggested.html similarity index 100% rename from samples/scales/linear/suggested-min-max-settings.html rename to samples/scales/linear/min-max-suggested.html diff --git a/samples/scales/linear/min-max-settings.html b/samples/scales/linear/min-max.html similarity index 100% rename from samples/scales/linear/min-max-settings.html rename to samples/scales/linear/min-max.html diff --git a/samples/scales/logarithmic/line-logarithmic.html b/samples/scales/logarithmic/line.html similarity index 100% rename from samples/scales/logarithmic/line-logarithmic.html rename to samples/scales/logarithmic/line.html diff --git a/samples/scales/logarithmic/scatter-logX.html b/samples/scales/logarithmic/scatter.html similarity index 100% rename from samples/scales/logarithmic/scatter-logX.html rename to samples/scales/logarithmic/scatter.html diff --git a/samples/scales/line-non-numeric-y.html b/samples/scales/non-numeric-y.html similarity index 100% rename from samples/scales/line-non-numeric-y.html rename to samples/scales/non-numeric-y.html diff --git a/samples/scales/time/combo-time-scale.html b/samples/scales/time/combo.html similarity index 100% rename from samples/scales/time/combo-time-scale.html rename to samples/scales/time/combo.html diff --git a/samples/scales/time/line-time-point-data.html b/samples/scales/time/line-point-data.html similarity index 100% rename from samples/scales/time/line-time-point-data.html rename to samples/scales/time/line-point-data.html diff --git a/samples/scales/time/line-time-scale.html b/samples/scales/time/line.html similarity index 100% rename from samples/scales/time/line-time-scale.html rename to samples/scales/time/line.html diff --git a/samples/style.css b/samples/style.css index 5b586506418..8224e2c3fdb 100644 --- a/samples/style.css +++ b/samples/style.css @@ -1,13 +1,33 @@ +@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'); +@import url('https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900'); + body, html { - font-family: sans-serif; + color: #333538; + font-family: 'Lato', sans-serif; + line-height: 1.6; padding: 0; margin: 0; } +a { + color: #f27173; + text-decoration: none; +} + +a:hover { + color: #e25f5f; + text-decoration: underline; +} + .content { max-width: 800px; margin: auto; - padding: 16px; + padding: 16px 32px; +} + +.header { + text-align: center; + padding: 32px 0; } .wrapper { @@ -42,10 +62,118 @@ body, html { margin: 0 8px 0 0; } +.btn { + background-color: #aaa; + border-radius: 4px; + color: white; + padding: 0.25rem 0.75rem; +} + +.btn .fa { + font-size: 1rem; +} + +.btn:hover { + background-color: #888; + color: white; + text-decoration: none; +} + +.btn-chartjs { background-color: #f27173; } +.btn-chartjs:hover { background-color: #e25f5f; } +.btn-docs:hover { background-color: #2793db; } +.btn-docs { background-color: #36A2EB; } +.btn-docs:hover { background-color: #2793db; } +.btn-gh { background-color: #444; } +.btn-gh:hover { background-color: #333; } + .btn-on { border-style: inset; } +.chartjs-title { + font-size: 2rem; + font-weight: 600; + white-space: nowrap; +} + +.chartjs-title::before { + background-image: url(logo.svg); + background-position: left center; + background-repeat: no-repeat; + background-size: 40px; + content: 'Chart.js | '; + color: #f27173; + font-weight: 600; + padding-left: 48px; +} + +.chartjs-caption { + font-size: 1.2rem; +} + +.chartjs-links { + display: flex; + justify-content: center; + padding: 8px 0; +} + +.chartjs-links a { + align-items: center; + display: flex; + font-size: 0.9rem; + margin: 0.2rem; +} + +.chartjs-links .fa:before { + margin-right: 0.5em; +} + +.samples-category { + display: inline-block; + margin-bottom: 32px; + vertical-align: top; + width: 25%; +} + +.samples-category > .title { + color: #aaa; + font-weight: 300; + font-size: 1.5rem; +} + +.samples-category:hover > .title { + color: black; +} + +.samples-category > .items { + padding: 8px 0; +} + +.samples-entry { + padding: 0 0 4px 0; +} + +.samples-entry > .title { + font-weight: 700; +} + +@media (max-width: 640px) { + .samples-category { width: 33%; } +} + +@media (max-width: 512px) { + .samples-category { width: 50%; } +} + +@media (max-width: 420px) { + .chartjs-caption { font-size: 1.05rem; } + .chartjs-title::before { content: ''; } + .chartjs-links a { flex-direction: column; } + .chartjs-links .fa { margin: 0 } + .samples-category { width: 100%; } +} + .analyser table { color: #333; font-size: 0.9rem; diff --git a/samples/tooltips/tooltip-border.html b/samples/tooltips/border.html similarity index 100% rename from samples/tooltips/tooltip-border.html rename to samples/tooltips/border.html diff --git a/samples/tooltips/tooltip-callbacks.html b/samples/tooltips/callbacks.html similarity index 100% rename from samples/tooltips/tooltip-callbacks.html rename to samples/tooltips/callbacks.html diff --git a/samples/tooltips/line-customTooltips.html b/samples/tooltips/custom-line.html similarity index 100% rename from samples/tooltips/line-customTooltips.html rename to samples/tooltips/custom-line.html diff --git a/samples/tooltips/pie-customTooltips.html b/samples/tooltips/custom-pie.html similarity index 100% rename from samples/tooltips/pie-customTooltips.html rename to samples/tooltips/custom-pie.html diff --git a/samples/tooltips/dataPoints-customTooltips.html b/samples/tooltips/custom-points.html similarity index 100% rename from samples/tooltips/dataPoints-customTooltips.html rename to samples/tooltips/custom-points.html diff --git a/samples/tooltips/interaction-modes.html b/samples/tooltips/interactions.html similarity index 100% rename from samples/tooltips/interaction-modes.html rename to samples/tooltips/interactions.html diff --git a/samples/tooltips/position-modes.html b/samples/tooltips/positioning.html similarity index 100% rename from samples/tooltips/position-modes.html rename to samples/tooltips/positioning.html From 20a832809e3fecb9a947b82b2d88b7c65902bfc0 Mon Sep 17 00:00:00 2001 From: Lee N Dobryden Date: Tue, 21 Mar 2017 03:38:09 -0700 Subject: [PATCH 157/685] Zero line dash options (#4019) * Add of zero line border dash options * Update Readme with zero line border dash config options --- docs/axes/styling.md | 2 ++ src/core/core.scale.js | 10 +++++++--- test/specs/core.helpers.tests.js | 4 ++++ test/specs/scale.category.tests.js | 2 ++ test/specs/scale.linear.tests.js | 2 ++ test/specs/scale.logarithmic.tests.js | 2 ++ test/specs/scale.radialLinear.tests.js | 2 ++ test/specs/scale.time.tests.js | 2 ++ 8 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/axes/styling.md b/docs/axes/styling.md index b7fabcd8c8b..48a4d7d0539 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -19,6 +19,8 @@ The grid line configuration is nested under the scale configuration in the `grid | `tickMarkLength` | `Number` | `10` | Length in pixels that the grid lines will draw into the axis area. | `zeroLineWidth` | `Number` | `1` | Stroke width of the grid line for the first index (index 0). | `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). +| `zeroLineBorderDash` | `Number[]` | `[]` | Length and spacing of dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) +| `zeroLineBorderDashOffset` | `Number` | `0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) | `offsetGridLines` | `Boolean` | `false` | If true, labels are shifted to be between grid lines. This is used in the bar chart and should not generally be used. ## Tick Configuration diff --git a/src/core/core.scale.js b/src/core/core.scale.js index de453e64a33..f8df92497e9 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -19,6 +19,8 @@ module.exports = function(Chart) { tickMarkLength: 10, zeroLineWidth: 1, zeroLineColor: 'rgba(0,0,0,0.25)', + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, offsetGridLines: false, borderDash: [], borderDashOffset: 0.0 @@ -503,8 +505,6 @@ module.exports = function(Chart) { var tickFont = parseFontOptions(optionTicks); var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; - var borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); - var borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); var scaleLabelFontColor = helpers.getValueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); var scaleLabelFont = parseFontOptions(scaleLabel); @@ -561,14 +561,18 @@ module.exports = function(Chart) { return; } - var lineWidth, lineColor; + var lineWidth, lineColor, borderDash, borderDashOffset; if (index === (typeof me.zeroLineIndex !== 'undefined' ? me.zeroLineIndex : 0)) { // Draw the first index specially lineWidth = gridLines.zeroLineWidth; lineColor = gridLines.zeroLineColor; + borderDash = gridLines.zeroLineBorderDash; + borderDashOffset = gridLines.zeroLineBorderDashOffset; } else { lineWidth = helpers.getValueAtIndexOrDefault(gridLines.lineWidth, index); lineColor = helpers.getValueAtIndexOrDefault(gridLines.color, index); + borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); + borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); } // Common properties diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 296958051d6..65e369d46b3 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -193,6 +193,8 @@ describe('Core helper tests', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, @@ -229,6 +231,8 @@ describe('Core helper tests', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index c474dd9c7fd..6eba2c2e2eb 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -23,6 +23,8 @@ describe('Category scale tests', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 0f63e8ce587..e94741cf6a0 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -21,6 +21,8 @@ describe('Linear Scale', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index a29b28260a3..b1053eefce2 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -20,6 +20,8 @@ describe('Logarithmic Scale tests', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 7796e124418..3d102cf376f 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -28,6 +28,8 @@ describe('Test the radial linear scale', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 43022bcde08..1aa3fa2bf2f 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -45,6 +45,8 @@ describe('Time scale tests', function() { display: true, zeroLineColor: 'rgba(0,0,0,0.25)', zeroLineWidth: 1, + zeroLineBorderDash: [], + zeroLineBorderDashOffset: 0.0, borderDash: [], borderDashOffset: 0.0 }, From c96ad6694559f7f280c4727d1f5e52a296d933a2 Mon Sep 17 00:00:00 2001 From: ellie Date: Wed, 22 Mar 2017 03:35:50 -0700 Subject: [PATCH 158/685] Update line-customTooltips.html (Re issue #4038 ) Add window scroll position to tooltip position calculation so they show up in the intended place instead of potentially off-screen! Moved tooltips inside the canvas parent container since they are being positioned in terms of its location --- samples/tooltips/custom-line.html | 9 +++++---- samples/tooltips/custom-pie.html | 16 ++++++++-------- samples/tooltips/custom-points.html | 11 +++++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 97f543b2653..801b12df680 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -48,7 +48,7 @@ tooltipEl = document.createElement('div'); tooltipEl.id = 'chartjs-tooltip'; tooltipEl.innerHTML = "
    " - document.body.appendChild(tooltipEl); + this._chart.canvas.parentNode.appendChild(tooltipEl); } // Hide if no tooltip @@ -95,12 +95,13 @@ tableRoot.innerHTML = innerHtml; } - var position = this._chart.canvas.getBoundingClientRect(); + var positionY = this._chart.canvas.offsetTop; + var positionX = this._chart.canvas.offsetLeft; // Display, position, and set styles for font tooltipEl.style.opacity = 1; - tooltipEl.style.left = position.left + tooltip.caretX + 'px'; - tooltipEl.style.top = position.top + tooltip.caretY + 'px'; + tooltipEl.style.left = positionX + tooltip.caretX + 'px'; + tooltipEl.style.top = positionY + tooltip.caretY + 'px'; tooltipEl.style.fontFamily = tooltip._fontFamily; tooltipEl.style.fontSize = tooltip.fontSize; tooltipEl.style.fontStyle = tooltip._fontStyle; diff --git a/samples/tooltips/custom-pie.html b/samples/tooltips/custom-pie.html index 4eedd6a3a7a..9c3cd0aa608 100644 --- a/samples/tooltips/custom-pie.html +++ b/samples/tooltips/custom-pie.html @@ -36,11 +36,10 @@
    - -
    - -
    -
    + +
    +
    +
    - + -
    - -
    +
    +
    diff --git a/src/core/core.canvasHelpers.js b/src/core/core.canvasHelpers.js index fad37e3c0f0..7d420524a8d 100644 --- a/src/core/core.canvasHelpers.js +++ b/src/core/core.canvasHelpers.js @@ -123,7 +123,11 @@ module.exports = function(Chart) { helpers.lineTo = function(ctx, previous, target, flip) { if (target.steppedLine) { - ctx.lineTo(target.x, previous.y); + if (target.steppedLine === 'after') { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } ctx.lineTo(target.x, target.y); return; } diff --git a/test/specs/element.line.tests.js b/test/specs/element.line.tests.js index c4d1722b4ad..f9f351d7db6 100644 --- a/test/specs/element.line.tests.js +++ b/test/specs/element.line.tests.js @@ -243,7 +243,141 @@ describe('Chart.elements.Line', function() { }]); }); - it('should draw stepped lines', function() { + it('should draw stepped lines, with "before" interpolation', function() { + + // Both `true` and `'before'` should draw the same steppedLine + var beforeInterpolations = [true, 'before']; + + beforeInterpolations.forEach(function(mode) { + 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, + steppedLine: mode + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 1, + _view: { + x: 5, + y: 0, + controlPointPreviousX: 5, + controlPointPreviousY: 0, + controlPointNextX: 5, + controlPointNextY: 0, + steppedLine: mode + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 2, + _view: { + x: 15, + y: -10, + controlPointPreviousX: 15, + controlPointPreviousY: -10, + controlPointNextX: 15, + controlPointNextY: -10, + steppedLine: mode + } + })); + points.push(new Chart.elements.Point({ + _datasetindex: 2, + _index: 3, + _view: { + x: 19, + y: -5, + controlPointPreviousX: 19, + controlPointPreviousY: -5, + controlPointNextX: 19, + controlPointNextY: -5, + steppedLine: mode + } + })); + + var line = new Chart.elements.Line({ + _datasetindex: 2, + _chart: { + ctx: mockContext, + }, + _children: points, + // Need to provide some settings + _view: { + fill: false, // don't want to fill + tension: 0, // no bezier curve for now + } + }); + + line.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setLineCap', + args: ['butt'] + }, { + name: 'setLineDash', + args: [ + [] + ] + }, { + name: 'setLineDashOffset', + args: [0.0] + }, { + name: 'setLineJoin', + args: ['miter'] + }, { + name: 'setLineWidth', + args: [3] + }, { + name: 'setStrokeStyle', + args: ['rgba(0,0,0,0.1)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, 10] + }, { + name: 'lineTo', + args: [5, 10] + }, { + name: 'lineTo', + args: [5, 0] + }, { + name: 'lineTo', + args: [15, 0] + }, { + name: 'lineTo', + args: [15, -10] + }, { + name: 'lineTo', + args: [19, -10] + }, { + name: 'lineTo', + args: [19, -5] + }, { + name: 'stroke', + args: [], + }, { + name: 'restore', + args: [] + }]); + }); + }); + + it('should draw stepped lines, with "after" interpolation', function() { + var mockContext = window.createMockContext(); // Create our points @@ -256,7 +390,7 @@ describe('Chart.elements.Line', function() { y: 10, controlPointNextX: 0, controlPointNextY: 10, - steppedLine: true + steppedLine: 'after' } })); points.push(new Chart.elements.Point({ @@ -269,7 +403,7 @@ describe('Chart.elements.Line', function() { controlPointPreviousY: 0, controlPointNextX: 5, controlPointNextY: 0, - steppedLine: true + steppedLine: 'after' } })); points.push(new Chart.elements.Point({ @@ -282,7 +416,7 @@ describe('Chart.elements.Line', function() { controlPointPreviousY: -10, controlPointNextX: 15, controlPointNextY: -10, - steppedLine: true + steppedLine: 'after' } })); points.push(new Chart.elements.Point({ @@ -295,7 +429,7 @@ describe('Chart.elements.Line', function() { controlPointPreviousY: -5, controlPointNextX: 19, controlPointNextY: -5, - steppedLine: true + steppedLine: 'after' } })); @@ -345,19 +479,19 @@ describe('Chart.elements.Line', function() { args: [0, 10] }, { name: 'lineTo', - args: [5, 10] + args: [0, 0] }, { name: 'lineTo', args: [5, 0] }, { name: 'lineTo', - args: [15, 0] + args: [5, -10] }, { name: 'lineTo', args: [15, -10] }, { name: 'lineTo', - args: [19, -10] + args: [15, -5] }, { name: 'lineTo', args: [19, -5] From d8385bd358e1c8cdbd8d41d73d9e34a09d6971da Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Thu, 13 Apr 2017 14:59:13 -0700 Subject: [PATCH 173/685] Combine the two contributing docs --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CONTRIBUTING.md | 64 -------------------------------- README.md | 2 +- docs/README.md | 2 +- docs/developers/contributing.md | 17 ++++++++- 5 files changed, 18 insertions(+), 69 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b57d497ec6d..1bb6710a742 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ Please consider the following before submitting a pull request: -Guidelines for contributing: https://github.com/chartjs/Chart.js/blob/master/CONTRIBUTING.md +Guidelines for contributing: https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md Example of changes on an interactive website such as the following: - http://jsbin.com/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6ec10243906..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,64 +0,0 @@ -Contributing to Chart.js -======================== - -Contributions to Chart.js are welcome and encouraged, but please have a look through the guidelines in this document before raising an issue, or writing code for the project. - - -Using issues ------------- - -The [issue tracker](https://github.com/chartjs/Chart.js/issues) is the preferred channel for reporting bugs, requesting new features and submitting pull requests. - -If you're suggesting a new chart type, please take a look at [writing new chart types](https://github.com/chartjs/Chart.js/blob/master/docs/09-Advanced.md#writing-new-chart-types) in the documentation or consider [creating a plugin](https://github.com/chartjs/Chart.js/blob/master/docs/09-Advanced.md#creating-plugins). - -To keep the library lightweight for everyone, it's unlikely we'll add many more chart types to the core of Chart.js, but issues are a good medium to design and spec out how new chart types could work and look. - -Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](http://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow. - - -Reporting bugs --------------- - -Well structured, detailed bug reports are hugely valuable for the project. - -Guidelines for reporting bugs: - - - Check the issue search to see if it has already been reported - - Isolate the problem to a simple test case - - Please include a demonstration of the bug on a website such as [JS Bin](http://jsbin.com/), [JS Fiddle](http://jsfiddle.net/), or [Codepen](http://codepen.io/pen/). ([Template](http://codepen.io/pen?template=JXVYzq)) - -Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. - - -Local development ------------------ - -Run `npm install` to install all the libraries, then run `gulp dev --test` to build and run tests as you make changes. - - -Pull requests -------------- - -Clear, concise pull requests are excellent at continuing the project's community driven growth. But please review [these guidelines](https://github.com/blog/1943-how-to-write-the-perfect-pull-request) and the guidelines below before starting work on the project. - -Be advised that **Chart.js 1.0.2 is in feature-complete status**. Pull requests adding new features to the 1.x branch will be disregarded. - -Guidelines: - - - Please create an issue first and/or talk with a team member: - - For bugs, we can discuss the fixing approach - - For enhancements, we can discuss if it is within the project scope and avoid duplicate effort - - Please make changes to the files in [`/src`](https://github.com/chartjs/Chart.js/tree/master/src), not `Chart.js` or `Chart.min.js` in the repo root directory, this avoids merge conflicts - - Tabs for indentation, not spaces please - - If adding new functionality, please also update the relevant `.md` file in [`/docs`](https://github.com/chartjs/Chart.js/tree/master/docs) - - Please make your commits in logical sections with clear commit messages - -Joining the project -------------- - - Active committers and contributors are invited to introduce yourself and request commit access to this project. Please send an email to hello@nickdownie.com or file an issue. - - We have a very active Slack community that you can join [here](https://chart-js-automation.herokuapp.com/). If you think you can help, we'd love to have you! - -License -------- - -By contributing your code, you agree to license your contribution under the [MIT license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md). diff --git a/README.md b/README.md index 4b0ab288409..86d2b4730d4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ You can find documentation at [www.chartjs.org/docs](http://www.chartjs.org/docs ## Contributing -Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/CONTRIBUTING.md) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). +Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md.) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). ## Building Instructions on building and testing Chart.js can be found in [the documentation](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md#building-and-testing). diff --git a/docs/README.md b/docs/README.md index bdb4141682e..aa587221112 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,7 +56,7 @@ var myChart = new Chart(ctx, { ## Contributing -Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/CONTRIBUTING.md) first. +Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index 70908dd6dbc..ee08b4dd7a3 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -7,7 +7,11 @@ New contributions to the library are welcome, but we ask that you please follow - Check that your code will pass `eslint` code standards, `gulp lint` will run this for you. - Check that your code will pass tests, `gulp test` will run tests for you. - Keep pull requests concise, and document new functionality in the relevant `.md` file. -- Consider whether your changes are useful for all users, or if creating a Chart.js plugin would be more appropriate. +- Consider whether your changes are useful for all users, or if creating a Chart.js [plugin](plugins.md) would be more appropriate. + +# Joining the project + + Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chart-js-automation.herokuapp.com/). If you think you can help, we'd love to have you! # Building and Testing @@ -38,5 +42,14 @@ More information can be found in [gulpfile.js](https://github.com/chartjs/Chart. # Bugs and Issues -Please report these on the GitHub page - at github.com/chartjs/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. +Please report these on the GitHub page - at github.com/chartjs/Chart.js. Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](http://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow. + +Well structured, detailed bug reports are hugely valuable for the project. + +Guidelines for reporting bugs: + + - Check the issue search to see if it has already been reported + - Isolate the problem to a simple test case + - Please include a demonstration of the bug on a website such as [JS Bin](http://jsbin.com/), [JS Fiddle](http://jsfiddle.net/), or [Codepen](http://codepen.io/pen/). ([Template](http://codepen.io/pen?template=JXVYzq)) +Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. From 86fb98eb3720a7256764ff0bd8b34a23c0946ade Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Mon, 17 Apr 2017 07:28:35 -0700 Subject: [PATCH 174/685] Remove extraneous period --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86d2b4730d4..d2ed99631ae 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ You can find documentation at [www.chartjs.org/docs](http://www.chartjs.org/docs ## Contributing -Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md.) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). +Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). ## Building Instructions on building and testing Chart.js can be found in [the documentation](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md#building-and-testing). From 3ec6377f11a97310ea23ca7725a08c5c70eb35df Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Tue, 4 Apr 2017 01:48:55 +0100 Subject: [PATCH 175/685] Fix hidden charts hanging the browser Ensure width cannot be greater than maxWidth. Similar to `minSize.height` calculation. --- src/core/core.scale.js | 2 +- src/scales/scale.time.js | 7 ++++--- test/specs/scale.time.tests.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f8df92497e9..596ccb0ea3b 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -356,7 +356,7 @@ module.exports = function(Chart) { } else { largestTextWidth += me.options.ticks.padding; } - minSize.width += largestTextWidth; + minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); me.paddingTop = tickFont.size / 2; me.paddingBottom = tickFont.size / 2; } diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 567454d2951..710d51410f8 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -145,18 +145,19 @@ module.exports = function(Chart) { var unitSizeInMilliSeconds = unitDefinition.size; var sizeInUnits = Math.ceil((max - min) / unitSizeInMilliSeconds); var multiplier = 1; + var range = max - min; if (unitDefinition.steps) { // Have an array of steps var numSteps = unitDefinition.steps.length; for (var i = 0; i < numSteps && sizeInUnits > maxTicks; i++) { multiplier = unitDefinition.steps[i]; - sizeInUnits = Math.ceil((max - min) / (unitSizeInMilliSeconds * multiplier)); + sizeInUnits = Math.ceil(range / (unitSizeInMilliSeconds * multiplier)); } } else { - while (sizeInUnits > maxTicks) { + while (sizeInUnits > maxTicks && maxTicks > 0) { ++multiplier; - sizeInUnits = Math.ceil((max - min) / (unitSizeInMilliSeconds * multiplier)); + sizeInUnits = Math.ceil(range / (unitSizeInMilliSeconds * multiplier)); } } diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 076acfbf01a..b1ab8410c4b 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -471,4 +471,34 @@ describe('Time scale tests', function() { unit: 'day', }); }); + + it('does not create a negative width chart when hidden', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + min: moment().subtract(1, 'months'), + max: moment(), + } + }], + }, + responsive: true, + }, + }, { + wrapper: { + style: 'display: none', + }, + }); + expect(chart.scales['y-axis-0'].width).toEqual(0); + expect(chart.scales['y-axis-0'].maxWidth).toEqual(0); + expect(chart.width).toEqual(0); + }); }); From f2c569ef25320f26c442cfde4238a2fb536d4888 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 22 Apr 2017 13:59:56 +0200 Subject: [PATCH 176/685] Enhance the responsive documentation Make sure to explain responsiveness limitations with CANVAS elements and how to correctly setup a responsive chart using a dedicated and relatively positioned div wrapper. --- docs/general/responsive.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/general/responsive.md b/docs/general/responsive.md index 894dccee0bc..3d7bb027e19 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -1,10 +1,35 @@ # Responsive Charts -Chart.js provides a few options for controlling resizing behaviour of charts. +When it comes to change the chart size based on the window size, a major limitation is that the canvas *render* size (`canvas.width` and `.height`) can **not** be expressed with relative values, contrary to the *display* size (`canvas.style.width` and `.height`). Furthermore, these sizes are independent from each other and thus the canvas *render* size does not adjust automatically based on the *display* size, making the rendering inaccurate. + +The following examples **do not work**: + +- ``: **invalid** values, the canvas doesn't resize ([example](https://codepen.io/chartjs/pen/oWLZaR)) +- ``: **invalid** behavior, the canvas is resized but becomes blurry ([example](https://codepen.io/chartjs/pen/WjxpmO)) + +Chart.js provides a [few options](#configuration-options) to enable responsiveness and control the resize behavior of charts by detecting when the canvas *display* size changes and update the *render* size accordingly. + +## Configuration Options | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `responsive` | `Boolean` | `true` | Resizes the chart canvas when its container does. +| `responsive` | `Boolean` | `true` | Resizes the chart canvas when its container does ([important note...](#important-note)). | `responsiveAnimationDuration` | `Number` | `0` | Duration in milliseconds it takes to animate to new size after a resize event. | `maintainAspectRatio` | `Boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. -| `onResize` | `Function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. \ No newline at end of file +| `onResize` | `Function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. + +## Important Note + +Detecting when the canvas size changes can not be done directly from the `CANVAS` element. Chart.js uses its parent container to update the canvas *render* and *display* sizes. However, this method requires the container to be **relatively positioned**. It's also strongly recommended to **dedicate this container to the chart canvas only**. Responsiveness can then be achieved by setting relative values for the container size ([example](https://codepen.io/chartjs/pen/YVWZbz)): + +```html +
    + +
    +``` + +The chart can also be programmatically resized by modifying the container size: + +```javascript +chart.canvas.parentNode.style.height = '128px'; +``` From f7d2d7536a03b58b99c3a67cd93b58d24f811425 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 22 Apr 2017 09:49:10 +0200 Subject: [PATCH 177/685] Fix failing instanceof when reading context `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is inside an iframe or when running in a protected environment. We could guess the types from their toString() value but let's keep things flexible and assume it's a sufficient condition if the item has a context2D which has item as `canvas`. --- src/platforms/platform.dom.js | 24 +++++++++++++++--------- test/jasmine.matchers.js | 4 ++-- test/specs/platform.dom.tests.js | 22 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 317d4bfed73..524bc346e37 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -196,15 +196,21 @@ module.exports = function(Chart) { 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; - } + // 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 && item.getContext && item.getContext('2d'); + + // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is + // inside an iframe or when running in a protected environment. We could guess the + // types from their toString() value but let's keep things flexible and assume it's + // a sufficient condition if the item has a context2D which has item as `canvas`. + // https://github.com/chartjs/Chart.js/issues/3887 + // https://github.com/chartjs/Chart.js/issues/4102 + // https://github.com/chartjs/Chart.js/issues/4152 + if (context && context.canvas === item) { + initCanvas(item, config); + return context; } return null; diff --git a/test/jasmine.matchers.js b/test/jasmine.matchers.js index 8a3c9e121a3..832fba35ecf 100644 --- a/test/jasmine.matchers.js +++ b/test/jasmine.matchers.js @@ -93,9 +93,9 @@ function toBeValidChart() { if (!(actual instanceof Chart)) { message = 'Expected ' + actual + ' to be an instance of Chart'; - } else if (!(actual.canvas instanceof HTMLCanvasElement)) { + } else if (Object.prototype.toString.call(actual.canvas) !== '[object HTMLCanvasElement]') { message = 'Expected canvas to be an instance of HTMLCanvasElement'; - } else if (!(actual.ctx instanceof CanvasRenderingContext2D)) { + } else if (Object.prototype.toString.call(actual.ctx) !== '[object CanvasRenderingContext2D]') { message = 'Expected context to be an instance of CanvasRenderingContext2D'; } else if (typeof actual.height !== 'number' || !isFinite(actual.height)) { message = 'Expected height to be a strict finite number'; diff --git a/test/specs/platform.dom.tests.js b/test/specs/platform.dom.tests.js index 898de9c4842..7ca768307e1 100644 --- a/test/specs/platform.dom.tests.js +++ b/test/specs/platform.dom.tests.js @@ -79,6 +79,28 @@ describe('Platform.dom', function() { chart.destroy(); }); + + it('should accept a canvas from an iframe', function(done) { + var iframe = document.createElement('iframe'); + iframe.onload = function() { + var doc = iframe.contentDocument; + doc.body.innerHTML += ''; + var canvas = doc.getElementById('chart'); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + canvas.remove(); + iframe.remove(); + + done(); + }; + + document.body.appendChild(iframe); + }); }); describe('config.options.aspectRatio', function() { From a75ae13b07e8326d7f83b8cd578a53fedff9756f Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 22 Apr 2017 14:47:00 -0400 Subject: [PATCH 178/685] Make it clear that labels need to be specified when using a category axis on a line chart --- docs/charts/line.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/charts/line.md b/docs/charts/line.md index f838bac0dd2..a64f436cfdb 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -142,7 +142,7 @@ The `data` property of a dataset for a line chart can be passed in two formats. data: [20, 10] ``` -When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. ### Point[] From 8811759e1c986fff719af3a0cd2cbdfdcef8ae79 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Tue, 25 Apr 2017 20:29:40 -0700 Subject: [PATCH 179/685] Remove malformed comment Fixes https://github.com/chartjs/Chart.js/issues/4181 --- src/scales/scale.category.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 0388baa00de..cc5478adff9 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -18,7 +18,7 @@ module.exports = function(Chart) { var data = this.chart.data; return (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels; }, - // Implement this so that + determineDataLimits: function() { var me = this; var labels = me.getLabels(); From 34292cb3350063eee3ca158b3e44388a591ecdd8 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Mon, 24 Apr 2017 19:21:47 -0700 Subject: [PATCH 180/685] Add financial chart type to plugin docs --- docs/developers/plugins.md | 13 ------------- docs/notes/extensions.md | 35 ++++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/docs/developers/plugins.md b/docs/developers/plugins.md index 9a1b1c2f953..467759f28bd 100644 --- a/docs/developers/plugins.md +++ b/docs/developers/plugins.md @@ -2,19 +2,6 @@ Plugins are the most efficient way to customize or change the default behavior of a chart. They have been introduced at [version 2.1.0](https://github.com/chartjs/Chart.js/releases/tag/2.1.0) (global plugins only) and extended at [version 2.5.0](https://github.com/chartjs/Chart.js/releases/tag/2.5.0) (per chart plugins and options). -## Popular Plugins - - - chartjs-plugin-annotation.js - Draw lines and boxes on chart area. - - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport. - - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse. - - chartjs-plugin-stacked100.js - Draw 100% stacked bar chart. - - chartjs-plugin-zoom.js - Enable zooming and panning on charts. - - Chart.BarFunnel.js - Adds a bar funnel chart type. - - Chart.LinearGauge.js - Adds a linear gauge chart type. - - Chart.Smith.js - Adds a smith chart type. - -In addition, many plugins can be found on the [Chart.js GitHub organization](https://github.com/chartjs) or on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). - ## Using plugins Plugins can be shared between chart instances: diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 5fef6fd9a3e..43991102e31 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -1,26 +1,47 @@ # Popular Extensions -There are many extensions which are available for use with popular frameworks. Some particularly notable ones are listed here. +Many extensions can be found on the [Chart.js GitHub organization](https://github.com/chartjs) or on the [npm registry](https://www.npmjs.com/search?q=chartjs-). -## Angular +## Charts + + - chartjs-chart-financial - Adds financial chart types such as a candlestick. + - Chart.BarFunnel.js - Adds a bar funnel chart type. + - Chart.LinearGauge.js - Adds a linear gauge chart type. + - Chart.Smith.js - Adds a smith chart type. + +In addition, many charts can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-chart-). + +## Plugins + + - chartjs-plugin-annotation.js - Draw lines and boxes on chart area. + - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport. + - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse. + - chartjs-plugin-stacked100.js - Draw 100% stacked bar chart. + - chartjs-plugin-zoom.js - Enable zooming and panning on charts. + +In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). + +## Integrations + +### Angular - angular-chart.js - tc-angular-chartjs - angular-chartjs - Angular Chart-js Directive -## React +### React - react-chartjs2 - react-chartjs-2 -## Django +### Django - Django JChart - Django Chartjs -## Ruby on Rails +### Ruby on Rails - chartjs-ror -## Laravel +### Laravel - laravel-chartjs -#### Vue.js +### Vue.js - vue-chartjs From f0c6b3f8342e2406c250329336037a7f7004284e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 23 Apr 2017 19:58:11 +0200 Subject: [PATCH 181/685] Fix legend and title layout options update --- src/core/core.layoutService.js | 35 ++++++++++++++++++++++++------- src/plugins/plugin.legend.js | 22 +++++++++---------- src/plugins/plugin.title.js | 18 ++++++++-------- test/specs/plugin.legend.tests.js | 28 +++++++++++++++++++++++++ test/specs/plugin.title.tests.js | 28 +++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 27 deletions(-) diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 045a7490129..8d50db80a99 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -54,18 +54,19 @@ module.exports = function(Chart) { * Register a box to a chart. * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. * @param {Chart} chart - the chart to use - * @param {ILayoutItem} layoutItem - the item to add to be layed out + * @param {ILayoutItem} item - the item to add to be layed out */ - addBox: function(chart, layoutItem) { + addBox: function(chart, item) { if (!chart.boxes) { chart.boxes = []; } - // Ensure that all layout items have a weight - if (!layoutItem.weight) { - layoutItem.weight = 0; - } - chart.boxes.push(layoutItem); + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); }, /** @@ -80,6 +81,26 @@ module.exports = function(Chart) { } }, + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {Object} item - the item to configure with the given options + * @param {Object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i Date: Tue, 25 Apr 2017 20:43:18 -0700 Subject: [PATCH 182/685] Remove unnecessary variable --- src/scales/scale.time.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 710d51410f8..0b356b3c100 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -318,7 +318,7 @@ module.exports = function(Chart) { me.displayFormat = timeOpts.displayFormats[unit]; var stepSize = timeOpts.stepSize || determineStepSize(minTimestamp || dataMin, maxTimestamp || dataMax, unit, maxTicks); - var ticks = me.ticks = Chart.Ticks.generators.time({ + me.ticks = Chart.Ticks.generators.time({ maxTicks: maxTicks, min: minTimestamp, max: maxTimestamp, @@ -332,8 +332,8 @@ module.exports = function(Chart) { // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale - me.max = helpers.max(ticks); - me.min = helpers.min(ticks); + me.max = helpers.max(me.ticks); + me.min = helpers.min(me.ticks); }, // Get tooltip label getLabelForIndex: function(index, datasetIndex) { From d9542227a8df4e7a4f61e9829666d53533f874b2 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 7 May 2017 10:35:17 -0700 Subject: [PATCH 183/685] Remove executable bit from js files (#4222) --- src/core/core.js | 0 src/core/core.tooltip.js | 0 src/scales/scale.time.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/core/core.js mode change 100755 => 100644 src/core/core.tooltip.js mode change 100755 => 100644 src/scales/scale.time.js diff --git a/src/core/core.js b/src/core/core.js old mode 100755 new mode 100644 diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js old mode 100755 new mode 100644 diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js old mode 100755 new mode 100644 From d7335bf1eeec93092b5141dbfc64ca69cdda3e90 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 23 Apr 2017 19:53:31 -0400 Subject: [PATCH 184/685] initial data update docs --- docs/SUMMARY.md | 1 + docs/developers/api.md | 2 ++ docs/developers/updates.md | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 docs/developers/updates.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d7950fed9e1..5ec896e40ca 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -40,6 +40,7 @@ * [Styling](axes/styling.md) * [Developers](developers/README.md) * [Chart.js API](developers/api.md) + * [Updating Charts](developers/updates.md) * [Plugins](developers/plugins.md) * [New Charts](developers/charts.md) * [New Axes](developers/axes.md) diff --git a/docs/developers/api.md b/docs/developers/api.md index 03dbe797fd8..714cb580a36 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -30,6 +30,8 @@ myLineChart.update(); // Calling update now animates the position of March from > **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. +See [Updating Charts](updates.md) for more details. + ## .reset() Reset the chart to it's state before the initial animation. A new animation can then be triggered using `update`. diff --git a/docs/developers/updates.md b/docs/developers/updates.md new file mode 100644 index 00000000000..06550ce0cec --- /dev/null +++ b/docs/developers/updates.md @@ -0,0 +1,31 @@ +# Updating Charts + +It's pretty common to want to update charts after they've been created. When the chart data is changed, Chart.js will animate to the new data values. + +## Adding or Removing Data + +Adding and removing data is supported by changing the data array. To add data, just add data into the data array as seen in this example. + +```javascript +function addData(chart, label, data) { + chart.data.labels.push(label); + chart.data.datasets.forEach((dataset) => { + dataset.data.push(data); + }); + chart.update(); +} +``` + +```javascript +function removeData(chart) { + chart.data.labels.pop(); + chart.data.datasets.forEach((dataset) => { + dataset.data.pop(); + }); + chart.update(); +} +``` + +## Preventing Animations + +Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with a duration of `0`. This will render the chart synchronously and without an animation. \ No newline at end of file From 3ff5d489c14bbc1cd6b7fab383095cd2c25afe71 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 13 May 2017 14:13:05 +0200 Subject: [PATCH 185/685] Document the new filling modes and options (#4251) --- docs/SUMMARY.md | 1 + docs/charts/README.md | 4 ++- docs/charts/area.md | 72 +++++++++++++++++++++++++++++++++++++++++++ docs/charts/line.md | 16 +++------- docs/charts/radar.md | 6 ++-- docs/style.css | 4 +++ 6 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 docs/charts/area.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5ec896e40ca..041be7062af 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -27,6 +27,7 @@ * [Polar Area](charts/polar.md) * [Bubble](charts/bubble.md) * [Scatter](charts/scatter.md) + * [Area](charts/area.md) * [Mixed](charts/mixed.md) * [Axes](axes/README.md) * [Cartesian](axes/cartesian/README.md) diff --git a/docs/charts/README.md b/docs/charts/README.md index 44622096641..26677b04d76 100644 --- a/docs/charts/README.md +++ b/docs/charts/README.md @@ -8,4 +8,6 @@ Chart.js comes with built-in chart types: * [doughnut and pie](./doughnut.md) * [bubble](./bubble.md) -To create a new chart type, see the [developer notes](../developers/charts.md#new-charts) \ No newline at end of file +[Area charts](area.md) can be built from a line or radar chart using the dataset `fill` option. + +To create a new chart type, see the [developer notes](../developers/charts.md#new-charts) diff --git a/docs/charts/area.md b/docs/charts/area.md new file mode 100644 index 00000000000..d51f84754a5 --- /dev/null +++ b/docs/charts/area.md @@ -0,0 +1,72 @@ +# Area Charts + +Both [line](line.md) and [radar](radar.md) charts support a `fill` option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale `origin`, `start` or `end` (see [filling modes](#filling-modes)). + +> **Note:** this feature is implemented by the [`filler` plugin](/chartjs/Chart.js/blob/master/src/plugins/plugin.filler.js). + +## Filling modes + +| Mode | Type | Values | +| :--- | :--- | :--- | +| Absolute dataset index 1 | `Number` | `1`, `2`, `3`, ... | +| Relative dataset index 1 | `String` | `'-1'`, `'-2'`, `'+1'`, ... | +| Boundary 2 | `String` | `'start'`, `'end'`, `'origin'` | +| Disabled 3 | `Boolean` | `false` | + +> 1 dataset filling modes have been introduced in version 2.6.0
    +> 2 prior version 2.6.0, boundary values was `'zero'`, `'top'`, `'bottom'` (deprecated)
    +> 3 for backward compatibility, `fill: true` (default) is equivalent to `fill: 'origin'`
    + +**Example** +```javascript +new Chart(ctx, { + data: { + datasets: [ + {fill: 'origin'}, // 0: fill to 'origin' + {fill: '+2'}, // 1: fill to dataset 3 + {fill: 1}, // 2: fill to dataset 1 + {fill: false}, // 3: no fill + {fill: '-2'} // 4: fill to dataset 2 + ] + } +}) +``` + +## Configuration +| Option | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| [`plugins.filler.propagate`](#propagate) | `Boolean` | `true` | Fill propagation when target is hidden + +### propagate +Boolean (default: `true`) + +If `true`, the fill area will be recursively extended to the visible target defined by the `fill` value of hidden dataset targets: + +**Example** +```javascript +new Chart(ctx, { + data: { + datasets: [ + {fill: 'origin'}, // 0: fill to 'origin' + {fill: '-1'}, // 1: fill to dataset 0 + {fill: 1}, // 2: fill to dataset 1 + {fill: false}, // 3: no fill + {fill: '-2'} // 4: fill to dataset 2 + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } +}) +``` + +`propagate: true`: +- if dataset 2 is hidden, dataset 4 will fill to dataset 1 +- if dataset 2 and 1 are hidden, dataset 4 will fill to `'origin'` + +`propagate: false`: +- if dataset 2 and/or 4 are hidden, dataset 4 will not be filled diff --git a/docs/charts/line.md b/docs/charts/line.md index a64f436cfdb..ba7911fe798 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -56,7 +56,7 @@ All point* properties can be specified as an array. If these are set to an array | `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) | `borderJoinStyle` | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) | `cubicInterpolationMode` | `String` | Algorithm used to interpolate a smooth curve from the discrete data points. [more...](#cubicInterpolationMode) -| `fill` | `Boolean/String` | How to fill the area under the line. [more...](#fill) +| `fill` | `Boolean/String` | How to fill the area under the line. See [area charts](area.md) | `lineTension` | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. | `pointBackgroundColor` | `Color/Color[]` | The fill color for points. | `pointBorderColor` | `Color/Color[]` | The border color for points. @@ -77,20 +77,12 @@ The following interpolation modes are supported: * 'default' * 'monotone'. -The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. +The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. -The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. +The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. -### fill -If `true`, fill the area under the line. The line is filled to the baseline. If the y axis has a 0 value, the line is filled to that point. If the axis has only negative values, the line is filled to the highest value. If the axis has only positive values, it is filled to the lowest value. - -String values to fill to specific locations are: -* `'zero'` -* `'top'` -* `'bottom'` - ### pointStyle The style of point. Options are: * 'circle' @@ -246,4 +238,4 @@ new Chart(ctx, { responsiveAnimationDuration: 0, // animation duration after a resize } }); -``` \ No newline at end of file +``` diff --git a/docs/charts/radar.md b/docs/charts/radar.md index 4728ad7771d..dd5d47b27e4 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -76,8 +76,8 @@ All point* properties can be specified as an array. If these are set to an array | `borderDashOffset` | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) | `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) | `borderJoinStyle` | `String` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) -| `fill` | `Boolean/String` | How to fill the area under the line. [more...](#fill) -| `lineTension` | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. +| `fill` | `Boolean/String` | How to fill the area under the line. See [area charts](area.md) +| `lineTension` | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. | `pointBackgroundColor` | `Color/Color[]` | The fill color for points. | `pointBorderColor` | `Color/Color[]` | The border color for points. | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. @@ -142,4 +142,4 @@ data: { data: [20, 10, 4, 2] }] } -``` \ No newline at end of file +``` diff --git a/docs/style.css b/docs/style.css index 89e7c705355..acdd3e5e946 100644 --- a/docs/style.css +++ b/docs/style.css @@ -9,3 +9,7 @@ a.anchorjs-link { a.anchorjs-link:hover { color: rgba(65, 131, 196, 1); } + +sup { + font-size: 0.75em !important; +} From c4c00b58342c5f46c5c54ec3caddd518c45fdf9f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 13 May 2017 14:14:02 +0200 Subject: [PATCH 186/685] Fix RequireJS doc to use UMD file instead (#4252) --- docs/getting-started/integration.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md index 523883ececf..173448f58b4 100644 --- a/docs/getting-started/integration.md +++ b/docs/getting-started/integration.md @@ -5,30 +5,32 @@ Chart.js can be integrated with plain JavaScript or with different module loader ## ES6 Modules ```javascript -import Chart from 'chart.js' -var myChart = new Chart(ctx, {...}) +import Chart from 'chart.js'; +var myChart = new Chart(ctx, {...}); ``` ## Script Tag ```html - + ``` ## Common JS ```javascript -var Chart = require('chart.js') -var myChart = new Chart(ctx, {...}) +var Chart = require('chart.js'); +var myChart = new Chart(ctx, {...}); ``` ## Require JS ```javascript -require(['path/to/Chartjs/src/chartjs.js'], function(Chart){ - var myChart = new Chart(ctx, {...}) -}) -``` \ No newline at end of file +require(['path/to/chartjs/dist/Chart.js'], function(Chart){ + var myChart = new Chart(ctx, {...}); +}); +``` + +> **Important:** RequireJS [can **not** load CommonJS module as is](http://www.requirejs.org/docs/commonjs.html#intro), so be sure to require one of the built UMD files instead (i.e. `dist/Chart.js`, `dist/Chart.min.js`, etc.). From e45ac3c94587457448bd2f466f23bd8e639f9f47 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 13 May 2017 14:14:47 +0200 Subject: [PATCH 187/685] Make "dedicated to the chart canvas" a requirement (#4253) --- docs/general/responsive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/responsive.md b/docs/general/responsive.md index 3d7bb027e19..bf8e8fa7a5f 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -20,7 +20,7 @@ Chart.js provides a [few options](#configuration-options) to enable responsivene ## Important Note -Detecting when the canvas size changes can not be done directly from the `CANVAS` element. Chart.js uses its parent container to update the canvas *render* and *display* sizes. However, this method requires the container to be **relatively positioned**. It's also strongly recommended to **dedicate this container to the chart canvas only**. Responsiveness can then be achieved by setting relative values for the container size ([example](https://codepen.io/chartjs/pen/YVWZbz)): +Detecting when the canvas size changes can not be done directly from the `CANVAS` element. Chart.js uses its parent container to update the canvas *render* and *display* sizes. However, this method requires the container to be **relatively positioned** and **dedicated to the chart canvas only**. Responsiveness can then be achieved by setting relative values for the container size ([example](https://codepen.io/chartjs/pen/YVWZbz)): ```html
    From 0bbc3fa8f3f3031da23923670429aeca0a3e4af1 Mon Sep 17 00:00:00 2001 From: Jamie McElwain Date: Sat, 13 May 2017 12:11:52 +0100 Subject: [PATCH 188/685] Added width + height arguments to ctx.drawImage Previous functionality meant that images would be drawn at their source file size regardless of whether custom width or height properties were set. --- src/core/core.canvasHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.canvasHelpers.js b/src/core/core.canvasHelpers.js index 7d420524a8d..610095122b8 100644 --- a/src/core/core.canvasHelpers.js +++ b/src/core/core.canvasHelpers.js @@ -10,7 +10,7 @@ module.exports = function(Chart) { if (typeof pointStyle === 'object') { type = pointStyle.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { - ctx.drawImage(pointStyle, x - pointStyle.width / 2, y - pointStyle.height / 2); + ctx.drawImage(pointStyle, x - pointStyle.width / 2, y - pointStyle.height / 2, pointStyle.width, pointStyle.height); return; } } From 50e2ba953de8949347bbce55a7c0ff14e5dad09b Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 13 May 2017 08:24:00 -0700 Subject: [PATCH 189/685] Use https to load scripts from CDN in samples (#4255) --- samples/scales/time/combo.html | 2 +- samples/scales/time/line-point-data.html | 2 +- samples/scales/time/line.html | 2 +- samples/tooltips/custom-points.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/scales/time/combo.html b/samples/scales/time/combo.html index 873e40d1ae3..ddcc5b326bf 100644 --- a/samples/scales/time/combo.html +++ b/samples/scales/time/combo.html @@ -3,7 +3,7 @@ Line Chart - Combo Time Scale - + + +
    +
    +
    + + + +
    +
    + + + + diff --git a/samples/utils.js b/samples/utils.js index b6ea8d7b95d..0b31703a1f1 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -12,10 +12,6 @@ window.chartColors = { grey: 'rgb(201, 203, 207)' }; -window.randomScalingFactor = function() { - return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100); -}; - (function(global) { var Months = [ 'January', @@ -32,6 +28,18 @@ window.randomScalingFactor = function() { 'December' ]; + var COLORS = [ + '#4dc9f6', + '#f67019', + '#f53794', + '#537bc4', + '#acc236', + '#166a8f', + '#00a950', + '#58595b', + '#8549ba' + ]; + var Samples = global.Samples || (global.Samples = {}); Samples.utils = { // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ @@ -105,6 +113,10 @@ window.randomScalingFactor = function() { return values; }, + color: function(index) { + return COLORS[index % COLORS.length]; + }, + transparentize: function(color, opacity) { var alpha = opacity === undefined ? 0.5 : 1 - opacity; return Chart.helpers.color(color).alpha(alpha).rgbString(); @@ -115,5 +127,10 @@ window.randomScalingFactor = function() { Samples.utils.srand(Date.now()); + // DEPRECATED + window.randomScalingFactor = function() { + return Math.round(Samples.utils.rand(-100, 100)); + }; + }(this)); diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index be229da9def..65c6fae01b8 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -41,9 +41,14 @@ defaults._set('bubble', { module.exports = function(Chart) { Chart.controllers.bubble = Chart.DatasetController.extend({ - + /** + * @protected + */ dataElementType: elements.Point, + /** + * @protected + */ update: function(reset) { var me = this; var meta = me.getMeta(); @@ -55,71 +60,121 @@ module.exports = function(Chart) { }); }, + /** + * @protected + */ updateElement: function(point, index, reset) { var me = this; var meta = me.getMeta(); + var custom = point.custom || {}; var xScale = me.getScaleForId(meta.xAxisID); var yScale = me.getScaleForId(meta.yAxisID); - - var custom = point.custom || {}; - var dataset = me.getDataset(); - var data = dataset.data[index]; - var pointElementOptions = me.chart.options.elements.point; + var options = me._resolveElementOptions(point, index); + var data = me.getDataset().data[index]; var dsIndex = me.index; - helpers.extend(point, { - // Utility - _xScale: xScale, - _yScale: yScale, - _datasetIndex: dsIndex, - _index: index, - - // Desired view properties - _model: { - x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex), - y: reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex), - // Appearance - radius: reset ? 0 : custom.radius ? custom.radius : me.getRadius(data), - - // Tooltip - hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.hitRadius, index, pointElementOptions.hitRadius) - } - }); - - // Trick to reset the styles of the point - Chart.DatasetController.prototype.removeHoverStyle.call(me, point, pointElementOptions); - - var model = point._model; - model.skip = custom.skip ? custom.skip : (isNaN(model.x) || isNaN(model.y)); + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); + + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = dsIndex; + point._index = index; + point._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + hitRadius: options.hitRadius, + pointStyle: options.pointStyle, + radius: reset ? 0 : options.radius, + skip: custom.skip || isNaN(x) || isNaN(y), + x: x, + y: y, + }; point.pivot(); }, - getRadius: function(value) { - return value.r || this.chart.options.elements.point.radius; - }, - + /** + * @protected + */ setHoverStyle: function(point) { - var me = this; - Chart.DatasetController.prototype.setHoverStyle.call(me, point); - - // Radius - var dataset = me.chart.data.datasets[point._datasetIndex]; - var index = point._index; - var custom = point.custom || {}; var model = point._model; - model.radius = custom.hoverRadius ? custom.hoverRadius : (helpers.valueAtIndexOrDefault(dataset.hoverRadius, index, me.chart.options.elements.point.hoverRadius)) + me.getRadius(dataset.data[index]); + var options = point._options; + + model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); + model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); + model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); + model.radius = options.radius + options.hoverRadius; }, + /** + * @protected + */ removeHoverStyle: function(point) { - var me = this; - Chart.DatasetController.prototype.removeHoverStyle.call(me, point, me.chart.options.elements.point); + var model = point._model; + var options = point._options; + + model.backgroundColor = options.backgroundColor; + model.borderColor = options.borderColor; + model.borderWidth = options.borderWidth; + model.radius = options.radius; + }, - var dataVal = me.chart.data.datasets[point._datasetIndex].data[point._index]; + /** + * @private + */ + _resolveElementOptions: function(point, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; var custom = point.custom || {}; - var model = point._model; + var options = chart.options.elements.point; + var resolve = helpers.options.resolve; + var data = dataset.data[index]; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + // Custom radius resolution + values.radius = resolve([ + custom.radius, + data ? data.r : undefined, + dataset.radius, + options.radius + ], context, index); - model.radius = custom.radius ? custom.radius : me.getRadius(dataVal); + return values; } }); }; diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js index 2c9dc759b74..8e6c0aadfae 100644 --- a/src/helpers/helpers.options.js +++ b/src/helpers/helpers.options.js @@ -3,7 +3,8 @@ var helpers = require('./helpers.core'); /** - * @namespace Chart.helpers.options + * @alias Chart.helpers.options + * @namespace */ module.exports = { /** @@ -62,5 +63,34 @@ module.exports = { height: t + b, width: l + r }; + }, + + /** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param {Array[]} inputs - An array of values, falling back to the last value. + * @param {Object} [context] - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param {Number} [index] - If defined and the current value is an array, the value + * at `index` become the new input. + * @since 2.7.0 + */ + resolve: function(inputs, context, index) { + var i, ilen, value; + + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + } + if (index !== undefined && helpers.isArray(value)) { + value = value[index]; + } + if (value !== undefined) { + return value; + } + } } }; diff --git a/test/jasmine.index.js b/test/jasmine.index.js index 7e5fd67bee0..c1706130f2d 100644 --- a/test/jasmine.index.js +++ b/test/jasmine.index.js @@ -43,6 +43,7 @@ var utils = require('./jasmine.utils'); '}'); jasmine.specsFromFixtures = utils.specsFromFixtures; + jasmine.triggerMouseEvent = utils.triggerMouseEvent; beforeEach(function() { jasmine.addMatchers(matchers); diff --git a/test/jasmine.utils.js b/test/jasmine.utils.js index 473edfde9dc..c94249406a2 100644 --- a/test/jasmine.utils.js +++ b/test/jasmine.utils.js @@ -158,11 +158,26 @@ function waitForResize(chart, callback) { }; } +function triggerMouseEvent(chart, type, el) { + var node = chart.canvas; + var rect = node.getBoundingClientRect(); + var event = new MouseEvent(type, { + clientX: rect.left + el._model.x, + clientY: rect.top + el._model.y, + cancelable: true, + bubbles: true, + view: window + }); + + node.dispatchEvent(event); +} + module.exports = { injectCSS: injectCSS, createCanvas: createCanvas, acquireChart: acquireChart, releaseChart: releaseChart, specsFromFixtures: specsFromFixtures, + triggerMouseEvent: triggerMouseEvent, waitForResize: waitForResize }; diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 6212b0c3dc6..3fa3eb883cd 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -273,160 +273,138 @@ describe('Bubble controller tests', function() { expect(meta.data[4] instanceof Chart.elements.Point).toBe(true); }); - it('should set hover styles', function() { - var chart = window.acquireChart({ - type: 'bubble', - data: { - datasets: [{ - data: [{ - x: 10, - y: 10, - r: 5 - }, { - x: -15, - y: -10, - r: 1 - }, { - x: 0, - y: -9, - r: 2 - }, { - x: -4, - y: 10, - r: 1 + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bubble', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [{ + x: 5, + y: 5, + r: 20 + }, { + x: -15, + y: -10, + r: 15 + }, { + x: 15, + y: 10, + r: 10 + }, { + x: -15, + y: 10, + r: 5 + }] }] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - elements: { - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3 + }, + options: { + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } } } - } + }); }); - var meta = chart.getDatasetMeta(0); - var point = meta.data[0]; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(229, 230, 0)'); - expect(point._model.borderColor).toBe('rgb(230, 230, 230)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(9); - - // Can set hover style per dataset - chart.data.datasets[0].hoverRadius = 3.3; - chart.data.datasets[0].hoverBackgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].hoverBorderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].hoverBorderWidth = 2.1; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(8.3); - - // Custom style - point.custom = { - hoverRadius: 4.4, - hoverBorderWidth: 5.5, - hoverBackgroundColor: 'rgb(0, 0, 0)', - hoverBorderColor: 'rgb(10, 10, 10)' - }; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); - }); + it ('should handle default hover styles', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); + expect(point._model.borderWidth).toBe(1); + expect(point._model.radius).toBe(20 + 4); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); - it('should remove hover styles', function() { - var chart = window.acquireChart({ - type: 'bubble', - data: { - datasets: [{ - data: [{ - x: 10, - y: 10, - r: 5 - }, { - x: -15, - y: -10, - r: 1 - }, { - x: 0, - y: -9, - r: 2 - }, { - x: -4, - y: 10, - r: 1 - }] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - elements: { - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3 - } - } - } + it ('should handle hover styles defined via dataset properties', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); }); - var meta = chart.getDatasetMeta(0); - var point = meta.data[0]; - - chart.options.elements.point.backgroundColor = 'rgb(45, 46, 47)'; - chart.options.elements.point.borderColor = 'rgb(50, 51, 52)'; - chart.options.elements.point.borderWidth = 10.1; - chart.options.elements.point.radius = 1.01; - - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(45, 46, 47)'); - expect(point._model.borderColor).toBe('rgb(50, 51, 52)'); - expect(point._model.borderWidth).toBe(10.1); - expect(point._model.radius).toBe(5); - - // Can set hover style per dataset - chart.data.datasets[0].radius = 3.3; - chart.data.datasets[0].backgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].borderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].borderWidth = 2.1; - - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(5); - - // Custom style - point.custom = { - radius: 4.4, - borderWidth: 5.5, - backgroundColor: 'rgb(0, 0, 0)', - borderColor: 'rgb(10, 10, 10)' - }; + it ('should handle hover styles defined via element options', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); - meta.controller.removeHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); + it ('should handle hover styles defined via element custom', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + point.custom = { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }; + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(20 + 4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(20); + }); }); }); diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js index 46e5222868f..3188888cb12 100644 --- a/test/specs/helpers.options.tests.js +++ b/test/specs/helpers.options.tests.js @@ -61,4 +61,65 @@ describe('Chart.helpers.options', function() { {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); }); }); + + describe('resolve', function() { + it ('should fallback to the first defined input', function() { + expect(options.resolve([42])).toBe(42); + expect(options.resolve([42, 'foo'])).toBe(42); + expect(options.resolve([undefined, 42, 'foo'])).toBe(42); + expect(options.resolve([42, 'foo', undefined])).toBe(42); + expect(options.resolve([undefined])).toBe(undefined); + }); + it ('should correctly handle empty values (null, 0, "")', function() { + expect(options.resolve([0, 'foo'])).toBe(0); + expect(options.resolve(['', 'foo'])).toBe(''); + expect(options.resolve([null, 'foo'])).toBe(null); + }); + it ('should support indexable options if index is provided', function() { + var input = [42, 'foo', 'bar']; + expect(options.resolve([input], undefined, 0)).toBe(42); + expect(options.resolve([input], undefined, 1)).toBe('foo'); + expect(options.resolve([input], undefined, 2)).toBe('bar'); + }); + it ('should fallback if an indexable option value is undefined', function() { + var input = [42, undefined, 'bar']; + expect(options.resolve([input], undefined, 5)).toBe(undefined); + expect(options.resolve([input, 'foo'], undefined, 1)).toBe('foo'); + expect(options.resolve([input, 'foo'], undefined, 5)).toBe('foo'); + }); + it ('should not handle indexable options if index is undefined', function() { + var array = [42, 'foo', 'bar']; + expect(options.resolve([array])).toBe(array); + expect(options.resolve([array], undefined, undefined)).toBe(array); + }); + it ('should support scriptable options if context is provided', function() { + var input = function(context) { + return context.v * 2; + }; + expect(options.resolve([42], {v: 42})).toBe(42); + expect(options.resolve([input], {v: 42})).toBe(84); + }); + it ('should fallback if a scriptable option returns undefined', function() { + var input = function() {}; + expect(options.resolve([input], {v: 42})).toBe(undefined); + expect(options.resolve([input, 'foo'], {v: 42})).toBe('foo'); + expect(options.resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); + }); + it ('should not handle scriptable options if context is undefined', function() { + var input = function(context) { + return context.v * 2; + }; + expect(options.resolve([input])).toBe(input); + expect(options.resolve([input], undefined)).toBe(input); + }); + it ('should handle scriptable and indexable option', function() { + var input = function(context) { + return [context.v, undefined, 'bar']; + }; + expect(options.resolve([input, 'foo'], {v: 42}, 0)).toBe(42); + expect(options.resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); + expect(options.resolve([input, 'foo'], {v: 42}, 5)).toBe('foo'); + expect(options.resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); + }); + }); }); From e758798798d65f27079be8369dff44730b3bf686 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 24 Aug 2017 10:31:17 +0200 Subject: [PATCH 293/685] Fix scriptable options documentation snippet --- docs/general/options.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/general/options.md b/docs/general/options.md index 2fd59029e64..e05cd33ee4a 100644 --- a/docs/general/options.md +++ b/docs/general/options.md @@ -8,8 +8,11 @@ Example: ```javascript color: function(context) { - return context.data < 0 ? 'red' : // draw negative values in red - index%2 ? 'blue' : 'green'; // else, alternate values in blue and green + var index = context.dataIndex; + var value = context.dataset.data[index]; + return value < 0 ? 'red' : // draw negative values in red + index % 2 ? 'blue' : // else, alternate values in blue and green + 'green'; } ``` From 530c613e5c661996894235a10efae99162a0c8f1 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 25 Aug 2017 00:16:08 +0200 Subject: [PATCH 294/685] Improve bar test stability (#4694) --- test/specs/controller.bar.tests.js | 323 +++++++++++++++++++---------- 1 file changed, 219 insertions(+), 104 deletions(-) diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index dfb83093ba2..e01aecae6e4 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -36,12 +36,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - id: 'firstXScaleID' + id: 'firstXScaleID', + display: false }], yAxes: [{ - id: 'firstYScaleID' + id: 'firstYScaleID', + display: false }] } } @@ -115,12 +119,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -148,12 +156,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -204,12 +216,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -237,12 +253,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -293,12 +313,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -326,12 +350,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -409,12 +437,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -445,12 +477,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -507,12 +543,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -543,12 +583,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -605,12 +649,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: true + stacked: true, + display: false }], yAxes: [{ - stacked: true + stacked: true, + display: false }] } } @@ -641,12 +689,16 @@ describe('Bar controller tests', function() { labels: [] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - stacked: false + stacked: false, + display: false }], yAxes: [{ - stacked: false + stacked: false, + display: false }] } } @@ -695,6 +747,8 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, elements: { rectangle: { backgroundColor: 'red', @@ -706,11 +760,13 @@ describe('Bar controller tests', function() { scales: { xAxes: [{ id: 'firstXScaleID', - type: 'category' + type: 'category', + display: false }], yAxes: [{ id: 'firstYScaleID', - type: 'linear' + type: 'linear', + display: false }] } } @@ -726,8 +782,8 @@ describe('Bar controller tests', function() { expect(meta.data.length).toBe(2); [ - {x: 113, y: 484}, - {x: 229, y: 32} + {x: 89, y: 512}, + {x: 217, y: 0} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(1); expect(meta.data[i]._index).toBe(i); @@ -735,8 +791,8 @@ describe('Bar controller tests', function() { expect(meta.data[i]._yScale).toBe(chart.scales.firstYScaleID); expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model.base).toBeCloseToPixel(936); - expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.base).toBeCloseToPixel(1024); + expect(meta.data[i]._model.width).toBeCloseToPixel(46); expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ datasetLabel: chart.data.datasets[1].label, label: chart.data.labels[i], @@ -771,12 +827,16 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ - type: 'linear' + type: 'linear', + display: false }] } } @@ -788,10 +848,10 @@ describe('Bar controller tests', function() { var bar1 = meta.data[0]; var bar2 = meta.data[1]; - expect(bar1._model.x).toBeCloseToPixel(187); - expect(bar1._model.y).toBeCloseToPixel(132); - expect(bar2._model.x).toBeCloseToPixel(422); - expect(bar2._model.y).toBeCloseToPixel(32); + expect(bar1._model.x).toBeCloseToPixel(179); + expect(bar1._model.y).toBeCloseToPixel(114); + expect(bar2._model.x).toBeCloseToPixel(435); + expect(bar2._model.y).toBeCloseToPixel(0); }); it('should update elements when the scales are stacked', function() { @@ -808,13 +868,17 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ type: 'linear', - stacked: true + stacked: true, + display: false }] } } @@ -823,10 +887,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 83 / 2, x: 63, y: 161}, - {b: 290, w: 83 / 2, x: 179, y: 419}, - {b: 290, w: 83 / 2, x: 295, y: 161}, - {b: 290, w: 83 / 2, x: 411, y: 419} + {b: 293, w: 92 / 2, x: 38, y: 146}, + {b: 293, w: 92 / 2, x: 166, y: 439}, + {b: 293, w: 92 / 2, x: 295, y: 146}, + {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -837,10 +901,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 83 / 2, x: 109, y: 32}, - {b: 290, w: 83 / 2, x: 225, y: 97}, - {b: 161, w: 83 / 2, x: 341, y: 161}, - {b: 419, w: 83 / 2, x: 457, y: 471} + {b: 146, w: 92 / 2, x: 89, y: 0}, + {b: 293, w: 92 / 2, x: 217, y: 73}, + {b: 146, w: 92 / 2, x: 345, y: 146}, + {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -863,13 +927,17 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ type: 'linear', stacked: true, + display: false, ticks: { min: 50, max: 100 @@ -882,10 +950,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 936, w: 83 / 2, x: 65.5, y: 484}, - {b: 936, w: 83 / 2, x: 180.5, y: 755}, - {b: 936, w: 83 / 2, x: 296.5, y: 846}, - {b: 936, w: 83 / 2, x: 411.5, y: 32} + {b: 1024, w: 92 / 2, x: 38, y: 512}, + {b: 1024, w: 92 / 2, x: 166, y: 819}, + {b: 1024, w: 92 / 2, x: 294, y: 922}, + {b: 1024, w: 92 / 2, x: 422.5, y: 0} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -896,10 +964,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 484, w: 83 / 2, x: 111.5, y: 32}, - {b: 755, w: 83 / 2, x: 226.5, y: 32}, - {b: 846, w: 83 / 2, x: 342.5, y: 32}, - {b: 32, w: 83 / 2, x: 457.5, y: 32} + {b: 512, w: 92 / 2, x: 89, y: 0}, + {b: 819, w: 92 / 2, x: 217, y: 0}, + {b: 922, w: 92 / 2, x: 345, y: 0}, + {b: 0, w: 92 / 2, x: 473.5, y: 0} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -922,13 +990,17 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ type: 'category', - stacked: true + stacked: true, + display: false }], yAxes: [{ - type: 'linear' + type: 'linear', + display: false }] } } @@ -937,10 +1009,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 83, x: 86, y: 32}, - {b: 290, w: 83, x: 202, y: 419}, - {b: 290, w: 83, x: 318, y: 161}, - {b: 290, w: 83, x: 434, y: 419} + {b: 293, w: 92, x: 64, y: 0}, + {b: 293, w: 92, x: 192, y: 439}, + {b: 293, w: 92, x: 320, y: 146}, + {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -951,10 +1023,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 290, w: 83, x: 86, y: 161}, - {b: 290, w: 83, x: 202, y: 97}, - {b: 290, w: 83, x: 318, y: 290}, - {b: 290, w: 83, x: 434, y: 471} + {b: 293, w: 92, x: 64, y: 146}, + {b: 293, w: 92, x: 192, y: 73}, + {b: 293, w: 92, x: 320, y: 293}, + {b: 293, w: 92, x: 448, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -977,13 +1049,17 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ type: 'linear', - stacked: true + stacked: true, + display: false }] } } @@ -992,10 +1068,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 83 / 2, x: 63, y: 161}, - {b: 290, w: 83 / 2, x: 179, y: 419}, - {b: 290, w: 83 / 2, x: 295, y: 161}, - {b: 290, w: 83 / 2, x: 411, y: 419} + {b: 293, w: 92 / 2, x: 38, y: 146}, + {b: 293, w: 92 / 2, x: 166, y: 439}, + {b: 293, w: 92 / 2, x: 295, y: 146}, + {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1006,10 +1082,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 83 / 2, x: 109, y: 32}, - {b: 290, w: 83 / 2, x: 225, y: 97}, - {b: 161, w: 83 / 2, x: 341, y: 161}, - {b: 419, w: 83 / 2, x: 457, y: 471} + {b: 146, w: 92 / 2, x: 89, y: 0}, + {b: 293, w: 92 / 2, x: 217, y: 73}, + {b: 146, w: 92 / 2, x: 345, y: 146}, + {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1034,13 +1110,17 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ type: 'linear', - stacked: true + stacked: true, + display: false }] } } @@ -1049,10 +1129,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 83, x: 86, y: 161}, - {b: 290, w: 83, x: 202, y: 419}, - {b: 290, w: 83, x: 318, y: 161}, - {b: 290, w: 83, x: 434, y: 419} + {b: 293, w: 92, x: 64, y: 146}, + {b: 293, w: 92, x: 192, y: 439}, + {b: 293, w: 92, x: 320, y: 146}, + {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1063,10 +1143,10 @@ describe('Bar controller tests', function() { var meta = chart.getDatasetMeta(1); [ - {b: 161, w: 83, x: 86, y: 32}, - {b: 290, w: 83, x: 202, y: 97}, - {b: 161, w: 83, x: 318, y: 161}, - {b: 419, w: 83, x: 434, y: 471} + {b: 146, w: 92, x: 64, y: 0}, + {b: 293, w: 92, x: 192, y: 73}, + {b: 146, w: 92, x: 320, y: 146}, + {b: 439, w: 92, x: 448, y: 497} ].forEach(function(values, i) { expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1089,12 +1169,16 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ stacked: true, + display: false, type: 'linear' }] } @@ -1104,11 +1188,11 @@ describe('Bar controller tests', function() { var meta = chart.getDatasetMeta(1); [ - {x: 108, y: 258}, - {x: 224, y: 32} + {x: 89, y: 256}, + {x: 217, y: 0} ].forEach(function(values, i) { - expect(meta.data[i]._model.base).toBeCloseToPixel(484); - expect(meta.data[i]._model.width).toBeCloseToPixel(42); + expect(meta.data[i]._model.base).toBeCloseToPixel(512); + expect(meta.data[i]._model.width).toBeCloseToPixel(46); expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); }); @@ -1131,12 +1215,16 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ stacked: true, + display: false, type: 'linear' }] } @@ -1146,11 +1234,11 @@ describe('Bar controller tests', function() { var meta = chart.getDatasetMeta(2); [ - {b: 371, x: 108, y: 258}, - {b: 258, x: 224, y: 32} + {b: 384, x: 89, y: 256}, + {b: 256, x: 217, y: 0} ].forEach(function(values, i) { expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta.data[i]._model.width).toBeCloseToPixel(42); + expect(meta.data[i]._model.width).toBeCloseToPixel(46); expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); }); @@ -1170,12 +1258,14 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ type: 'category', stacked: true, + display: false, barPercentage: 1, - display: false }], yAxes: [{ type: 'logarithmic', @@ -1190,9 +1280,9 @@ describe('Bar controller tests', function() { [ {b: 512, w: 102, x: 64, y: 512}, - {b: 512, w: 102, x: 192, y: 143}, + {b: 512, w: 102, x: 192, y: 118}, {b: 512, w: 102, x: 320, y: 512}, - {b: 512, w: 102, x: 449, y: 143} + {b: 512, w: 102, x: 449, y: 118} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1203,10 +1293,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 512, w: 102, x: 64, y: 127}, - {b: 143, w: 102, x: 192, y: 127}, + {b: 512, w: 102, x: 64, y: 102}, + {b: 118, w: 102, x: 192, y: 102}, {b: 512, w: 102, x: 320, y: 512}, - {b: 143, w: 102, x: 449, y: 32} + {b: 118, w: 102, x: 449, y: 0} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -1257,6 +1347,8 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, elements: { rectangle: { backgroundColor: 'rgb(255, 0, 0)', @@ -1321,6 +1413,8 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, elements: { rectangle: { backgroundColor: 'rgb(255, 0, 0)', @@ -1412,8 +1506,11 @@ describe('Bar controller tests', function() { type: 'bar', data: this.data, options: { + legend: false, + title: false, scales: { xAxes: [{ + display: false, ticks: { min: 'March', max: 'May', @@ -1429,14 +1526,18 @@ describe('Bar controller tests', function() { type: 'bar', data: this.data, options: { + legend: false, + title: false, scales: { xAxes: [{ + display: false, ticks: { min: 'March', max: 'May', } }], yAxes: [{ + display: false, stacked: true }] } @@ -1489,6 +1590,8 @@ describe('Bar controller tests', function() { type: 'horizontalBar', data: this.data, options: { + legend: false, + title: false, scales: { yAxes: [{ ticks: { @@ -1506,11 +1609,15 @@ describe('Bar controller tests', function() { type: 'horizontalBar', data: this.data, options: { + legend: false, + title: false, scales: { xAxes: [{ + display: false, stacked: true }], yAxes: [{ + display: false, ticks: { min: 'March', max: 'May', @@ -1537,14 +1644,18 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ id: 'x', type: 'category', + display: false, barThickness: barThickness }], yAxes: [{ - type: 'linear' + type: 'linear', + display: false }] } } @@ -1608,10 +1719,13 @@ describe('Bar controller tests', function() { labels: ['2017', '2018', '2020'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ id: 'x', type: 'time', + display: false, time: { unit: 'year', parser: 'YYYY' @@ -1623,7 +1737,8 @@ describe('Bar controller tests', function() { distribution: distribution }], yAxes: [{ - type: 'linear' + type: 'linear', + display: false }] } } From 31049ebcba71dd205901e439883fae991307bed8 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 26 Aug 2017 00:34:37 +0200 Subject: [PATCH 295/685] Add chartjs-plugin-datalabels plugin link (#4701) --- docs/notes/extensions.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 31105885747..1adc8aa7788 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -13,11 +13,12 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co ## Plugins - - chartjs-plugin-annotation.js - Draw lines and boxes on chart area. - - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport. + - chartjs-plugin-annotation.js - Draws lines and boxes on chart area. + - chartjs-plugin-datalabels.js - Displays labels on data for any type of charts. + - chartjs-plugin-deferred.js - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse. - - chartjs-plugin-stacked100.js - Draw 100% stacked bar chart. - - chartjs-plugin-zoom.js - Enable zooming and panning on charts. + - chartjs-plugin-stacked100.js - Draws 100% stacked bar chart. + - chartjs-plugin-zoom.js - Enables zooming and panning on charts. In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). From 488cbbcbf6ccea2aaeb0be8378fedfaa852d0331 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 26 Aug 2017 07:42:52 +0900 Subject: [PATCH 296/685] Fix flipped stepped line in filler plugin (#4697) --- src/helpers/helpers.canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 54414f694c7..1429a31e6e0 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -165,7 +165,7 @@ var exports = module.exports = { lineTo: function(ctx, previous, target, flip) { if (target.steppedLine) { - if (target.steppedLine === 'after') { + if ((target.steppedLine === 'after' && !flip) || (target.steppedLine !== 'after' && flip)) { ctx.lineTo(previous.x, target.y); } else { ctx.lineTo(target.x, previous.y); From 5a014dc36190107146ebf4f7c29bc2cfb40c36ec Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 26 Aug 2017 07:48:57 +0200 Subject: [PATCH 297/685] Fix wild special zero grid line when undefined (#4700) Remove the special drawing for an undefined zero grid line since it causes issue when the first tick is not aligned on the scale extremity (ie only linear scales now display a special grid line for the origin). Hide scales in the filler plugin unit test fixtures to avoid future failures due to changes unrelated to the tested features. --- src/core/core.scale.js | 2 +- .../fill-line-boundary-end-span.json | 8 ++------ .../fill-line-boundary-end-span.png | Bin 16356 -> 16522 bytes .../plugin.filler/fill-line-boundary-end.json | 8 ++------ .../plugin.filler/fill-line-boundary-end.png | Bin 15057 -> 15720 bytes .../fill-line-boundary-origin-span.json | 8 ++------ .../fill-line-boundary-origin-span.png | Bin 17340 -> 18485 bytes ...fill-line-boundary-origin-spline-span.json | 8 ++------ .../fill-line-boundary-origin-spline-span.png | Bin 20758 -> 21046 bytes .../fill-line-boundary-origin-spline.json | 8 ++------ .../fill-line-boundary-origin-spline.png | Bin 19286 -> 19748 bytes ...ill-line-boundary-origin-stepped-span.json | 8 ++------ ...fill-line-boundary-origin-stepped-span.png | Bin 6922 -> 4137 bytes .../fill-line-boundary-origin-stepped.json | 8 ++------ .../fill-line-boundary-origin-stepped.png | Bin 6892 -> 4152 bytes .../fill-line-boundary-origin.json | 8 ++------ .../fill-line-boundary-origin.png | Bin 16008 -> 17535 bytes .../fill-line-boundary-start-span.json | 8 ++------ .../fill-line-boundary-start-span.png | Bin 17347 -> 18871 bytes .../fill-line-boundary-start.json | 8 ++------ .../fill-line-boundary-start.png | Bin 15653 -> 17895 bytes .../plugin.filler/fill-line-dataset-span.json | 8 ++------ .../plugin.filler/fill-line-dataset-span.png | Bin 24102 -> 23355 bytes .../fill-line-dataset-spline-span.json | 8 ++------ .../fill-line-dataset-spline-span.png | Bin 25819 -> 27312 bytes .../fill-line-dataset-spline.json | 8 ++------ .../fill-line-dataset-spline.png | Bin 23223 -> 24950 bytes .../plugin.filler/fill-line-dataset.json | 8 ++------ .../plugin.filler/fill-line-dataset.png | Bin 22331 -> 21950 bytes .../fill-radar-boundary-origin-spline.json | 7 +------ .../fill-radar-boundary-origin-spline.png | Bin 25918 -> 15638 bytes .../fill-radar-boundary-origin.json | 7 +------ .../fill-radar-boundary-origin.png | Bin 22988 -> 12866 bytes 33 files changed, 31 insertions(+), 97 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 6f63b92dbd7..bc4d1ec9c7b 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -680,7 +680,7 @@ module.exports = function(Chart) { var label = tick.label; var lineWidth, lineColor, borderDash, borderDashOffset; - if (index === (typeof me.zeroLineIndex !== 'undefined' ? me.zeroLineIndex : 0) && (options.offset === gridLines.offsetGridLines)) { + if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { // Draw the first index specially lineWidth = gridLines.zeroLineWidth; lineColor = gridLines.zeroLineColor; diff --git a/test/fixtures/plugin.filler/fill-line-boundary-end-span.json b/test/fixtures/plugin.filler/fill-line-boundary-end-span.json index 3c151a14ffd..d197b406264 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-end-span.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-end-span.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-end-span.png b/test/fixtures/plugin.filler/fill-line-boundary-end-span.png index 83519b05fb72b9c23a2d305937c0c6a2201fef3c..0939727d078ba46e0e9c73eeef486ed3ab96cd26 100644 GIT binary patch literal 16522 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 DBRW>q diff --git a/test/fixtures/plugin.filler/fill-line-boundary-end.json b/test/fixtures/plugin.filler/fill-line-boundary-end.json index 297fe6e25e6..4c99b445bb0 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-end.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-end.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-end.png b/test/fixtures/plugin.filler/fill-line-boundary-end.png index 25c8f13699684a835fab695d023ca3b8a6a29e67..fb3011b42091bb7847fd3b098e8bb65372c71297 100644 GIT binary patch literal 15720 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 literal 15057 zcmZ|0cR1G3|37|S_kG)YgzQlvGex#Lk~ECWY()rBR`!chiK2{1xT#RGSN5GGBcsU7 zERj94`JTIaf3ElEbNzn#qpqvhIp;Ya&&PQ_k8nL5EjnsWY5)M8_UV%s0KmY1B7l+{ z{`Dcc%>e*@pndX$;Z3XAWaoR9eR0EkWkd6awO^Lm8I^VATzfU}v_R2SiKnuKMkSM^|co)xnTJ?auG){x^$1Ph`;lu#?Bew;vP_n%Gw#Zecq;joXOXcdCM&5;2 zFZ^l^qga0DR9Kc>8}x5i+?rW4W9PK(QJk15>FM|1Yg2U2vqaoEZx0WzD^(BkadK_B zI2YBrIeWOH?%Fbf*6T$qx|^RXV@5|h-xdEOm&bKVQg1Mwr$aR*ICTcb(dOBLKJ{)+ru%xka?Tr%_zjM2$S!qD_h!}|ezuz=r@a^4#r`WD=t`^Y) z(UTPL3<+`!*mjYGZjL?xR`S{}x!czyUTZ`68zxZzYCphJQd`LY4lrX;9$JZ3!hGKi zvFa)eUi>qHfS`yF0MwFb03|qp14Z1A0oZU{3o#N>BmE$206a;5I7{ zBN!>;fd5-m4p4w!0ffgp;t{7eK?FEaQ$y((Kf-U70y;WwQ#^%d?C=&W~?^YHi!ee-PT0D0Hf)__l-%CFUc|^ zCqE3|0eIr6Z~x*Y(M3oB!3NNC%gcao$wF)%%4p&px|aV^?wLbB(H<)>`6+N16wAx(9kM`hw^Qa0D9) z@F`xMTa_fY`&kX1dY-qtA?r!u=y9tlr~we%=#t%bDA~7+nO2irnJA=afN%TL_XR1M zw5#HmUpVXwRNllpjscx;M0lhPW((@94apckn(dYnXgb?#8YSKFO z!$`BTk%dXFI}WdMj|1oC6sCsRsa;M5Q*QNS7_ZLvrlY!~o%%->3ntWiSQ1fppgo}EVmlgQt!-(U7)--A^0}D zF~gE6fy`9sSs)y2 zQb7&mi`MB+vCmhY`|bQPQ6HS%hmpq;gX{SM1NX6fOs0E)=RH6&**!5u=Q{wsm22D- zbhxwD*`xDa=M6qvZqUVIkj>V}15!q}JJ=NaSc5L8ijy!($^XzHeqhr_7)%o5FiTl{ zG&3QNA@IhUrSZ>!=uty^y1?)8ceV7&*a0J;Y!=`-b_x(L39nrfx4F4d@Ilydu>TB> zUzT!&9t_G?)S!>-e0+~_DUfVycJyjJI~6dM86s{*&fBSFGNJwNFNm_%^8j+`ss&ap zsTsl-SEqY8A9GUtj6C~t;}#A?`nXFs1z7>S$}uuZM!;cZT0a@?Fv)pD^(3Y`jTGvH zT&J9i7(iZWd~oa4(_Q+O&8cFmK#ctPiUo!Rsl~DdkH;>zI)3@mT5M6a|FYn!leOM7 zP?o3r6xPAkK3TRK3stIM&h!TUv0WBYencf`NV{KCdXFQUj2g^fh}7gC-?~toLqLD^cvU;wd=^WO)H-kaJP z;LgBWOX#@&?UAdo*cMsJ4Es48p*uR^^`C>$&DOErWuQn+_n0#5G8C>XKoHQ{=4^2W z1=g^bHw{>*A~UCg1i5p^pT0@L5@dF?e)8Qm?8|f1x)~00tie!157_VlC_L?c5Lq?O zg5blG>))rftS=aTZaR3%mxR&L+j50=wIhPy$O#k>Rl%D}?soeEo7^a9slc16adrR> z`t7S#xlS7ulZO=`r2`EJKEVBL$YZaf6f&#ZyTN5a5eN> zp5a^`w#1^#W!s^EQ2#N%H)*pQPl|pv4WV_oep!MCpz}7=YMOUi6d9T;f7WGkRP54A z{%qP6)Bg<)@D+|!k@sFzMZMiA0C_uabfAQ5247iK%ug;7C$wBL;!rO+r~jCNy->WlHVZOt}wNn8*Na! zxXsOYrUZ|m??Hap+?;QuMMqj2N3IhE(5@Qi0w`-4IW>?b2XQl*&70n?g$gv4kXp!v z�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 diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-span.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-span.json index 8a09d635e98..391bc98d80a 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-origin-span.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-span.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-span.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-span.png index 727cc0309b0f2baa70c52218ba2975ed790f4547..24ccc2490dabede2f3a52d6b234078765df340d7 100644 GIT binary patch literal 18485 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 E1GxNTRR910 diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.json index f433aa444b5..709df764941 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-span.png index 01aae5a995d0dfe10b4fee9e0feacb2f9f1894e6..75aef6a768fd640ffe2bd0d314313fbc6059c09d 100644 GIT binary patch literal 21046 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 literal 20758 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 U6L9lq?Ew6R1a1fr`qPj64|Qh(i~s-t diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.json index bd1abc29efc..79ee41aa0b9 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped-span.png index 9f7c15b9ddbc5e8e5dffe34cd1b48780a190e813..524a8fb387f4342eeadf383cf7595e653288993b 100644 GIT binary patch literal 4137 zcmeHKeN0nV6hE)8w184D8i#_?%|Bp@W(&%P)biY%LYCP^CC&*Oi9v7*BA-P*`W$Au zEzu@+^A8;~kp-u&62KL3O6MeQWuqMmxFWOERRpaF8x+B|JNG>_{;~bxmVIP1H?KYS zp5MFYoZq?kp8MWYu3Z!0Ki3}s0+JF}tpgCCNP({i|5`Qe832Lzl2)zUpnB8U`)qdS zwps5E=Q$&U{z5_I%k%sM@1Lq}eeQMmCbI9yyroU5Xv^$PKTKPcS+Q8~Ww23`w)(Z@ zekWDscWhfz z0x>Y>X!}GK*aMJ<3ld;+hLfr&XlCiqdj#o(3AEspa-8_yxqV z$^w85ixh?cc0Nwufz#*Tf6qW!Xmqoa0_A72-spd|v8%H3pT|$17!T*INS;+uo`lYZG1Z;&KKij{Grk0BN64!2s;Y0~%;Bkri#6$rKANp6S8STp_mz z9ezRG5&|bX5V6Aj+i|JtLizPiz+{UY;IKLwWw)XtVhz_-R>CeMAG8a&ZiIquEJVpP zgX?O;nH0<#IDEOTX{J<K)Rz8SUkbXCfCjenpKgLo8JHqj~E<-C4yiD zRDx*%ZlWuyGG`w|18EN21twHxZoDosVwJfzaIljy|0>`;?WTOr*NpJv`K1=Q$Y*vI&p5BG>&^X{l;mm-p^~`Ea@$ zsT?#THT-F$ysHMKUJg^Pnwby?qx}t-m2ft`+rcyYSX9XCz3B}kDuUrM2eE>D;VlDP zm}#7$TVTwYJV;`2cw9oI{yZO!jM|%_BVym!l-I@Z&})jL(a;hOYTvKJ1PdR9nnT+} z7-}|#T7;r8fF-u`QAA#{;Tkkj5rGf`MBK|0*Ehx^nPRxY9oFq^2qGMwA{u=mA_vaU z5%Do_BSnl!WeXT=8?+zP{37& zC=vqG zMWU|0osiLUBi9|1KHQLpIc0OnMKZ#b;N)&uTyOFsxc&hxf3>8YM=N+e&&Erw^Tb+aR zwWB*t_HF%4FV?2J02X}$WOi3gU;=c#Sh7;pWM~vO0CSeY$3un&Z{Q`jkjBSN;n0;! zcmZ?b+3uX6ap+T$E^7cjqhcIdABNkdAq1b>fjCqnM=O~z_;iQikfIE~h%DLvFT;D5 z!#EWj$fU?&e+=Bu5`)otCZ3(BL()^qtUpUI$C|*ER{?^S$Ol5n;`eVx%Cnu%_ZzV% z6{TW63}ae6rKUqrg&yDATw)?yh@G_;lhJ2C)J`d|(66)L$4UNnMVy#i6so0n0DPaR z%9oHDpSZFcssC(+Bb3xD%{#UDQIk^I5toMLUF!4Df)Pt6<3IID)lZ5PJ1ZYe^jg=q tQ=~RO?~Cca&>H?RO>P$dHiLV-@KMR)eOq2}e}U{k(kp9M>0e5F=U1mKE@}V( literal 6922 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 IUhNX~Hx<@{asU7T diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.json index 95203904d1f..6e5eafed299 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-stepped.png index 19f5a8d92620793d9df4a07f9e8a280b64ae9a20..60de6ba21a36be7a5ca5a988b0705d90ead0eb65 100644 GIT binary patch literal 4152 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 Bf$abQ diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin.json b/test/fixtures/plugin.filler/fill-line-boundary-origin.json index 09465363947..5aa0bf289fd 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-origin.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin.png b/test/fixtures/plugin.filler/fill-line-boundary-origin.png index f8f0208d4fde7f1f79a312e59ff3f3d5af6ae163..29df37b8731222aeda1a8549c845f3b43bc7e50a 100644 GIT binary patch literal 17535 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 literal 16008 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 diff --git a/test/fixtures/plugin.filler/fill-line-boundary-start.json b/test/fixtures/plugin.filler/fill-line-boundary-start.json index 893db5c171d..fccd681bf01 100644 --- a/test/fixtures/plugin.filler/fill-line-boundary-start.json +++ b/test/fixtures/plugin.filler/fill-line-boundary-start.json @@ -24,14 +24,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-boundary-start.png b/test/fixtures/plugin.filler/fill-line-boundary-start.png index 1cd05e60dfc521ba99969a0a973c8b32e39b2806..da20bdcdea5189f8ad6a5f9635427b9dbbcef0bd 100644 GIT binary patch literal 17895 zcmYj(bySq!_w}6_h8SA9M^Nb$m6TD+B9-n2K|;C*l}17lMFa*=0qI60MoB?RI;Eur z0cm(2~qe(=P_JeRhb;ci~<0V-@Kuq2>=}WD+~}5LO&W~J8S?z zgPRK1?s*xnB$LEiY9;K@{*^G)>fsvKKYJxoK3$9E2x~I-BBdxJMqy1+uv|hY;G2*|jWm7)X zrejDpMLuNlt$r<2~5Q|oXFI_PlVE$B6VTfSm=jLF1AVu z)+_gQ;q2i~%rP}_JTK*T%BLn&Av_oG<59GU@cSg#r(p3X9B+8mdM=mZT;v_zRUzn~ z{{F@UHL@24l3)Z`^1%RI(Zh$m(XohsqD(vK_}>hLaCRL`UdO?Xjs@J(9y+j1kiG>-9c8Bye5I=ph*D3lb@y7`(h^NPW6(OgFE{ z(t_IUBl6uUM+Pucein&-1A9b`38xzybPm-!HH|LpHtnme%^w74-7GI|VCtCkUXn^9 z&1|Z!E&_9Kf}Gr!0IRM{+4U&`JnzZ5m^@0w2{c&SZ7>c(c(B{-WYgqVw?+dw3*VK;9Vc5}thiFd9R}!8TRgF=0xclu`f(D+U*tgm#KVyHsy_en11j40u95<~__xP&#~bqogSSlT z58h~_Sv=9Ch#6%(73#n>lB>;OSI{mDVu%q0W$1w@`-R}1QdA8M&t}F28|6Hjn z8#_#9S~LFYNQz(b`2j1y)oq&IoMG9~%2aY=5B9x>I51CTofx8UJP+TxlMp93eM_^ z7kl^7^zSXay^A?d87MVplbu<|3nmz0<+|jOhMMU2xm;(PEeaJq@}K+#5I3#*f`i=Q zj=}1J6AJKLY$h%o!@U35z&b(0R89%gX*pRDONaXAvzJPQYl!&16|d(^iZ)eY9!V%+?`f-=CAnHqID46AhlH}!pZwMda)Y83Poucr!%=Hcy z^UZQsWJG@~pl3x~G9Vk`yiS}slOr%={9pdNj3Z()q-^Fjk_|)k`8C2K==j{XNWsR)uZX(Vib-0LKWE zYiyP_X%1eFnoG&XD5FXy!$V=g^)#PsmMh;u*h1I97c~L@JEsd{5^f`(^7ntV1^&1$ zM;yW)LA@sHx4Nd0J11)+kCIARxP(coL>$lt!50@wn%%+T#p5bEQyg(shmj>xK-;vx zCy))R9m#)2ySjL~|8{N3YVdjgJfcNj)tWSJRN&M|k6OY?xQXH`#`-KBA6!ND_Dm|e*8S!u<)PQWUeVKnOd^twy|rN< znG2cD$Bh|)_zSmse(mgjNLifZtB;%8>$<#7?w5TrT#UlVz9IU}S9yyqF`P)HWJX=A zz83n`#tSuJ|NCCVV^u#H9HC{9HoB%m2}7yL4X*BYUW* zX7Xoo&-J=$qLA1B>bpoJ{Yx(d6uXb6aIH96OZB z7xAmY-x+(OQvoNz>^hBolz99Ol?7G=tpnHW)P6+g$gA<zd8+r&foR$nDM4{Awss zmh?xQmxGMutOP3)r|BQiN;wJvSTRB040o^H99Aye4QmVoj#caghh>N48eeK_zBI-EdQj9H%PC4#1DpsArCoU)uwAppuWTNs*&rW z8k)UIK%vP-0rsbLP{fgj-?ZRlj9Z#oaLty!iNUoK9_goiq;RPk&U4Vm?S(-ROi>#q zbxl$5VtGx_X?c@hbYUZO&UKlASHGeV;Dk(OSaa6^Zf$}5Il>f874wx@uuZi&XyG0R zjVQlkE~IOW>}_OH3#<{nG_&&kO@bX9wAPEl4KKd8W|ku6*>9&+&-F$UOvPzckst>J z?WZe^aVNq?`^3@IE(@FvAl|5U-;f_XVX(6_)Az zuHzk!^l?>kPKv!Xa#<7du+&HUv3K)-au@;3edj39InuxZtVTC#dWWth!Pn;q9*klvUPjjzXk=T~!WC~Hs*@@jCG%6$8ZvUQ1p)5}!$ zdh^S(l^d$8U3%1w;W(ZkvK6V|=@~H|Tmr!PUMij}w<2$_iqxG0Xc{iljpzJoA%ur6 z8G@_IOEVC+M4#8NhBlhv?po_j#|WeCR0wi)L9?Ho^R{L4b8@;iKgdTwq_3VV1gjyi zN!@pBG$7<-(!$CT{&u1Nn?#^~BprS(aM{?ZNpHMJ4tjAB3&k`2&HaDwsl>B*P%VCm zC~}%Ekfg7fr@pMsp4U`O##nb|Shvjlp*Igz%-L&A_Ue00NMd#w%Fik1i4_ldOtR3A z0j7~n1A8}wxg`-w1?kim`oE19_(e>2cFLfLVSumVel_P&baVe3fmGg$)$u2Rr?^`d zJN24nQie55`MJjO7{PH$JF!O|#x{#2Kr86|ywwGOos&#Ph26i8<~BzPXqjF%4*bI} zcQ4m>&*wZnA-Lz_jOQ5J+vxK@Z#QiceH!;8buna76CFWL-c z$2$aR!Ho}19qN|#7x@XFw!Xiv0YyNmz!*=LF6s=cS^ zez{Y(!+*kz7}4Em*sYjX>8O{Q%RyfA{Krp8spbc<6YEyQ!S<87c2&y>T~g7)2R?qB`^0{8G@P$+P(FM*PzFGVF8^CCD#$qVm4K4@eQ%d{ck^LUUd zo6fN4q=?oyYUc8JttcL+P6gCS32En}c}>%2-}JGdvIstsCqm(2UAW5DPc=Hj9pFXb zZ&-CS+@HHLun~#-6;HoWl>K&ur+a{o{utzS0pYwE_6!4XX{+kkvl>m!vtAl7iUzKp zEPWK{)rh7>_c*iMzfFXD<&*7-P6LB@!TjBFWo4P=D<+fydLxXG$ky+SR|= zh0OxYR(Ke7AyjLa}%eZIRdqVy3A` zp&PL-!TBlZ)&m&0&(KUPA|mzB@+N2vWVnwani8cC%Z$y}J|2Su+y#!iVC0Q*UVm8I zKsq3r>P~;=VCR4UixN=Y_F(5yh0PV*8@iW4i1jd%h74~4fm^oJAhxr`an=oo4v@)) zhOB=n97ch2gXtA4$~wo^ag#R!A$wDoENQ28$&xFwgw=n*$V5k3tG=U$fj-PSmj^FU zCUNtL4esp>@_N$3qj$C(0k%>t2yR#-NyRhSYGy|YrQKlTJiF2dvb!db+3vstkXbBP{nA>jy~(8J_$5gP~5qLeyuju zynkOr)p$I`{|#}&bm=R41aSW)h$NOCQ_DArghTEedCU0LG#P-xfSrRai4R}7g^n#< zbg+Im2aod9H>35BPe-GTxW|Kku#8v5y{~s=o^nI8XSx@`!2As~8~LWVjFfD2)6!3j zOjDU_plUO~zRh5S8L*qUewQ;g+nIHS-)^o87xwrV$g{i`0t^NBSx2`&r6rf|AZ2SH|sw z?(p|~xF_cMomV-0irofGrblKb?6zFhVS9P`PuhfYu|87FKbw9!&xRZanQZu$6k4XJ zOiF;*;-G8Co6_tiV+PN($&M?l!`gfTf+(MQdAxOV%O#wC$1w49!drsT@KH)EhsIe? zE0bu3=#*@DI;i_F6fN=2D{sesfQ2kxDr^`akT88(H5vT$xwV z74YFJp~El`txw0o`P5;i7i&BqI%0ABe8@&|DLCf^2F44pZdtPIZ$^Lr;wKM0o2A~% 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 U-Ti^S{tP+?k7~Z&Z+-dy0oSp1CIA2c diff --git a/test/fixtures/plugin.filler/fill-line-dataset-span.json b/test/fixtures/plugin.filler/fill-line-dataset-span.json index 34b4ddc7f59..e1b159c01a2 100644 --- a/test/fixtures/plugin.filler/fill-line-dataset-span.json +++ b/test/fixtures/plugin.filler/fill-line-dataset-span.json @@ -32,14 +32,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-dataset-span.png b/test/fixtures/plugin.filler/fill-line-dataset-span.png index a6de728589e16a85fbf1115e3e807779cef35e0d..7c8a856c678aece91d3b24e4aaefcfa7fdc88b70 100644 GIT binary patch literal 23355 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 literal 24102 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 diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json index a37ce02a902..5635b6c9198 100644 --- a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.json @@ -32,14 +32,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png index 7fb4e403e4c30435e61755ad3121c701e52689cb..5f66359f3b57decaba1c133c1cd2de1be17d08bf 100644 GIT binary patch literal 27312 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 ee=bIiU+)1^>zb literal 25819 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 diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline.json b/test/fixtures/plugin.filler/fill-line-dataset-spline.json index 329e7123c09..fbddcc3a5a0 100644 --- a/test/fixtures/plugin.filler/fill-line-dataset-spline.json +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline.json @@ -32,14 +32,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline.png b/test/fixtures/plugin.filler/fill-line-dataset-spline.png index 41d52bc77db885b5333bd79545deabc8a0356c85..8147e4d8073d5fa8b45f1107ad52f5532a61516f 100644 GIT binary patch literal 24950 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

    x3 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 LiBA1Rr|AC!D>l(` literal 23223 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 diff --git a/test/fixtures/plugin.filler/fill-line-dataset.json b/test/fixtures/plugin.filler/fill-line-dataset.json index 4224b78bcfc..dcf118cdee7 100644 --- a/test/fixtures/plugin.filler/fill-line-dataset.json +++ b/test/fixtures/plugin.filler/fill-line-dataset.json @@ -32,14 +32,10 @@ "title": false, "scales": { "xAxes": [{ - "ticks": { - "display": false - } + "display": false }], "yAxes": [{ - "ticks": { - "display": false - } + "display": false }] }, "elements": { diff --git a/test/fixtures/plugin.filler/fill-line-dataset.png b/test/fixtures/plugin.filler/fill-line-dataset.png index d8a7e09d3b7881bf1f7fe8620fa904f594182397..88f9d4fd8559efaa1eb8761e6f9f0892f7e8a874 100644 GIT binary patch literal 21950 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 UE~qqvAPL%JVQqeAy+_3V0jXP*J^%m! diff --git a/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json index 8640adcf89c..e5c4b2f8009 100644 --- a/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json +++ b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.json @@ -22,12 +22,7 @@ "legend": false, "title": false, "scale": { - "pointLabels": { - "fontSize": 0 - }, - "ticks": { - "display": false - } + "display": false }, "elements": { "point": { diff --git a/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.png b/test/fixtures/plugin.filler/fill-radar-boundary-origin-spline.png index 29b6c8e185ecd19a9626affda59b0ac318088905..74124a3e7f26734e987839c3c202748eb137620d 100644 GIT binary patch literal 15638 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 M(#ld*62<}l1Et}jfdBvi literal 25918 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 literal 22988 zcmXtgb95cw_x9Wy+qT)*wrw>@T7>#ZG?&tfi_pf``%v$^G zdCtaj_TDowYAUiQh=hm$0HDarNofE82=W&Mz{5g*j9ja%0Dv5jmlD_VHa_k798bLx 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 Date: Sat, 26 Aug 2017 14:50:39 +0900 Subject: [PATCH 298/685] Fix bar chart with {x, y} data points (#4673) --- docs/charts/bar.md | 2 +- src/controllers/controller.bar.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 974cf8a7104..524c795214c 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -155,7 +155,7 @@ data: [20, 10] You can also specify the dataset as x/y coordinates. ```javascript -data: [{x:'2016-12-25', y:20}, {'2016-12-26', y:10}] +data: [{x:'2016-12-25', y:20}, {x:'2016-12-26', y:10}] ``` # Stacked Bar Chart diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index baea1d94975..ae4aee45d17 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -362,7 +362,7 @@ module.exports = function(Chart) { draw: function() { var me = this; var chart = me.chart; - var scale = me.getIndexScale(); + var scale = me.getValueScale(); var rects = me.getMeta().data; var dataset = me.getDataset(); var ilen = rects.length; From 459c81d9316e3fa7c02e8670467e7727096c3875 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 26 Aug 2017 08:01:33 +0200 Subject: [PATCH 299/685] Remove trailing `.js` in plugin names (docs) --- docs/notes/extensions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 1adc8aa7788..33e0e647e59 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -13,12 +13,12 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co ## Plugins - - chartjs-plugin-annotation.js - Draws lines and boxes on chart area. - - chartjs-plugin-datalabels.js - Displays labels on data for any type of charts. - - chartjs-plugin-deferred.js - Defers initial chart update until chart scrolls into viewport. - - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse. - - chartjs-plugin-stacked100.js - Draws 100% stacked bar chart. - - chartjs-plugin-zoom.js - Enables zooming and panning on charts. + - chartjs-plugin-annotation - Draws lines and boxes on chart area. + - chartjs-plugin-datalabels - Displays labels on data for any type of charts. + - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. + - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. + - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. + - chartjs-plugin-zoom - Enables zooming and panning on charts. In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). From 2f950e2ab3a347b182e22774819c84d6e6dcfc19 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 27 Aug 2017 22:43:16 +0200 Subject: [PATCH 300/685] Improve controller tests stability (#4698) --- test/specs/controller.bar.tests.js | 162 ++++++----------------- test/specs/controller.bubble.tests.js | 19 +-- test/specs/controller.doughnut.tests.js | 27 ++-- test/specs/controller.line.tests.js | 130 +++++++++++------- test/specs/controller.polarArea.tests.js | 27 ++-- test/specs/controller.radar.tests.js | 35 ++--- 6 files changed, 185 insertions(+), 215 deletions(-) diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index e01aecae6e4..315bdbd346b 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1,5 +1,4 @@ -// Test the bar controller -describe('Bar controller tests', function() { +describe('Chart.controllers.bar', function() { it('should be constructed', function() { var chart = window.acquireChart({ type: 'bar', @@ -36,16 +35,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - id: 'firstXScaleID', - display: false + id: 'firstXScaleID' }], yAxes: [{ - id: 'firstYScaleID', - display: false + id: 'firstYScaleID' }] } } @@ -119,16 +114,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -156,16 +147,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -216,16 +203,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -253,16 +236,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -313,16 +292,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -350,16 +325,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -437,16 +408,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -477,16 +444,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -543,16 +506,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -583,16 +542,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -649,16 +604,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: true, - display: false + stacked: true }], yAxes: [{ - stacked: true, - display: false + stacked: true }] } } @@ -689,16 +640,12 @@ describe('Bar controller tests', function() { labels: [] }, options: { - legend: false, - title: false, scales: { xAxes: [{ - stacked: false, - display: false + stacked: false }], yAxes: [{ - stacked: false, - display: false + stacked: false }] } } @@ -877,8 +824,8 @@ describe('Bar controller tests', function() { }], yAxes: [{ type: 'linear', - stacked: true, - display: false + display: false, + stacked: true }] } } @@ -936,8 +883,8 @@ describe('Bar controller tests', function() { }], yAxes: [{ type: 'linear', - stacked: true, display: false, + stacked: true, ticks: { min: 50, max: 100 @@ -995,8 +942,8 @@ describe('Bar controller tests', function() { scales: { xAxes: [{ type: 'category', - stacked: true, - display: false + display: false, + stacked: true }], yAxes: [{ type: 'linear', @@ -1058,8 +1005,8 @@ describe('Bar controller tests', function() { }], yAxes: [{ type: 'linear', - stacked: true, - display: false + display: false, + stacked: true }] } } @@ -1119,8 +1066,8 @@ describe('Bar controller tests', function() { }], yAxes: [{ type: 'linear', - stacked: true, - display: false + display: false, + stacked: true }] } } @@ -1177,9 +1124,9 @@ describe('Bar controller tests', function() { display: false }], yAxes: [{ - stacked: true, + type: 'linear', display: false, - type: 'linear' + stacked: true, }] } } @@ -1223,9 +1170,9 @@ describe('Bar controller tests', function() { display: false }], yAxes: [{ - stacked: true, + type: 'linear', display: false, - type: 'linear' + stacked: true }] } } @@ -1263,14 +1210,14 @@ describe('Bar controller tests', function() { scales: { xAxes: [{ type: 'category', - stacked: true, display: false, + stacked: true, barPercentage: 1, }], yAxes: [{ type: 'logarithmic', - stacked: true, - display: false + display: false, + stacked: true }] } } @@ -1347,8 +1294,6 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { - legend: false, - title: false, elements: { rectangle: { backgroundColor: 'rgb(255, 0, 0)', @@ -1413,8 +1358,6 @@ describe('Bar controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { - legend: false, - title: false, elements: { rectangle: { backgroundColor: 'rgb(255, 0, 0)', @@ -1506,11 +1449,8 @@ describe('Bar controller tests', function() { type: 'bar', data: this.data, options: { - legend: false, - title: false, scales: { xAxes: [{ - display: false, ticks: { min: 'March', max: 'May', @@ -1526,18 +1466,14 @@ describe('Bar controller tests', function() { type: 'bar', data: this.data, options: { - legend: false, - title: false, scales: { xAxes: [{ - display: false, ticks: { min: 'March', max: 'May', } }], yAxes: [{ - display: false, stacked: true }] } @@ -1590,8 +1526,6 @@ describe('Bar controller tests', function() { type: 'horizontalBar', data: this.data, options: { - legend: false, - title: false, scales: { yAxes: [{ ticks: { @@ -1609,15 +1543,11 @@ describe('Bar controller tests', function() { type: 'horizontalBar', data: this.data, options: { - legend: false, - title: false, scales: { xAxes: [{ - display: false, stacked: true }], yAxes: [{ - display: false, ticks: { min: 'March', max: 'May', @@ -1650,12 +1580,10 @@ describe('Bar controller tests', function() { xAxes: [{ id: 'x', type: 'category', - display: false, barThickness: barThickness }], yAxes: [{ type: 'linear', - display: false }] } } @@ -1725,7 +1653,6 @@ describe('Bar controller tests', function() { xAxes: [{ id: 'x', type: 'time', - display: false, time: { unit: 'year', parser: 'YYYY' @@ -1737,8 +1664,7 @@ describe('Bar controller tests', function() { distribution: distribution }], yAxes: [{ - type: 'linear', - display: false + type: 'linear' }] } } diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 3fa3eb883cd..4799883ddfb 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -1,5 +1,4 @@ -// Test the bubble controller -describe('Bubble controller tests', function() { +describe('Chart.controllers.bubble', function() { it('should be constructed', function() { var chart = window.acquireChart({ type: 'bubble', @@ -120,12 +119,16 @@ describe('Bubble controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { xAxes: [{ - type: 'category' + type: 'category', + display: false }], yAxes: [{ - type: 'linear' + type: 'linear', + display: false }] } } @@ -134,10 +137,10 @@ describe('Bubble controller tests', function() { var meta = chart.getDatasetMeta(0); [ - {r: 5, x: 28, y: 32}, - {r: 1, x: 183, y: 484}, - {r: 2, x: 338, y: 461}, - {r: 1, x: 492, y: 32} + {r: 5, x: 0, y: 0}, + {r: 1, x: 171, y: 512}, + {r: 2, x: 341, y: 486}, + {r: 1, x: 512, y: 0} ].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/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index f4c0c959e30..85e382ac3f9 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -1,5 +1,4 @@ -// Test the bar controller -describe('Doughnut controller tests', function() { +describe('Chart.controllers.doughnut', function() { it('should be constructed', function() { var chart = window.acquireChart({ type: 'doughnut', @@ -69,6 +68,8 @@ describe('Doughnut controller tests', function() { labels: ['label0', 'label1', 'label2', 'label3'] }, options: { + legend: false, + title: false, animation: { animateRotate: true, animateScale: false @@ -99,9 +100,9 @@ describe('Doughnut controller tests', function() { {c: 0} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); - expect(meta.data[i]._model.y).toBeCloseToPixel(272); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(239); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(179); + expect(meta.data[i]._model.y).toBeCloseToPixel(256); + expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(254); + expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(190); expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ startAngle: Math.PI * -0.5, @@ -122,9 +123,9 @@ describe('Doughnut controller tests', function() { {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); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(239); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(179); + expect(meta.data[i]._model.y).toBeCloseToPixel(256); + expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(254); + expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(190); 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); @@ -170,6 +171,8 @@ describe('Doughnut controller tests', function() { labels: ['label0', 'label1'] }, options: { + legend: false, + title: false, cutoutPercentage: 50, rotation: Math.PI, circumference: Math.PI * 0.5, @@ -192,10 +195,10 @@ describe('Doughnut controller tests', function() { {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.x).toBeCloseToPixel(510); + expect(meta.data[i]._model.y).toBeCloseToPixel(510); + expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(509); + expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(381); 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); diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index e1231dc692c..31c0763a8fe 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -1,5 +1,4 @@ -// Test the line controller -describe('Line controller tests', function() { +describe('Chart.controllers.line', function() { it('should be constructed', function() { var chart = window.acquireChart({ type: 'line', @@ -175,6 +174,8 @@ describe('Line controller tests', function() { }, options: { showLines: true, + legend: false, + title: false, elements: { point: { backgroundColor: 'red', @@ -183,10 +184,12 @@ describe('Line controller tests', function() { }, scales: { xAxes: [{ - id: 'firstXScaleID' + id: 'firstXScaleID', + display: false }], yAxes: [{ - id: 'firstYScaleID' + id: 'firstYScaleID', + display: false }] } }, @@ -203,8 +206,8 @@ describe('Line controller tests', function() { [ - {x: 33, y: 484}, - {x: 186, y: 32} + {x: 0, y: 512}, + {x: 171, y: 0} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(0); expect(meta.data[i]._index).toBe(i); @@ -234,11 +237,17 @@ describe('Line controller tests', function() { }] }, options: { + legend: false, + title: false, hover: { mode: 'single' }, scales: { + xAxes: [{ + display: false, + }], yAxes: [{ + display: false, ticks: { beginAtZero: true } @@ -250,7 +259,7 @@ describe('Line controller tests', function() { var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; - expect(point._model.x).toBeCloseToPixel(27); + expect(point._model.x).toBeCloseToPixel(0); // 2 points chart.data.labels = ['One', 'Two']; @@ -259,8 +268,8 @@ describe('Line controller tests', function() { var points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(27); - expect(points[1]._model.x).toBeCloseToPixel(498); + expect(points[0]._model.x).toBeCloseToPixel(0); + expect(points[1]._model.x).toBeCloseToPixel(512); // 3 points chart.data.labels = ['One', 'Two', 'Three']; @@ -269,9 +278,9 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(27); - expect(points[1]._model.x).toBeCloseToPixel(260); - expect(points[2]._model.x).toBeCloseToPixel(493); + expect(points[0]._model.x).toBeCloseToPixel(0); + expect(points[1]._model.x).toBeCloseToPixel(256); + expect(points[2]._model.x).toBeCloseToPixel(512); // 4 points chart.data.labels = ['One', 'Two', 'Three', 'Four']; @@ -280,10 +289,10 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(27); - expect(points[1]._model.x).toBeCloseToPixel(184); + expect(points[0]._model.x).toBeCloseToPixel(0); + expect(points[1]._model.x).toBeCloseToPixel(171); expect(points[2]._model.x).toBeCloseToPixel(340); - expect(points[3]._model.x).toBeCloseToPixel(497); + expect(points[3]._model.x).toBeCloseToPixel(512); }); it('should update elements when the y scale is stacked', function() { @@ -300,8 +309,14 @@ describe('Line controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { + xAxes: [{ + display: false, + }], yAxes: [{ + display: false, stacked: true }] } @@ -311,10 +326,10 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 28, y: 161}, - {x: 183, y: 419}, - {x: 338, y: 161}, - {x: 492, y: 419} + {x: 0, y: 146}, + {x: 171, y: 439}, + {x: 341, y: 146}, + {x: 512, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); @@ -323,10 +338,10 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 28, y: 32}, - {x: 183, y: 97}, - {x: 338, y: 161}, - {x: 492, y: 471} + {x: 0, y: 0}, + {x: 171, y: 73}, + {x: 341, y: 146}, + {x: 512, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); @@ -352,12 +367,19 @@ describe('Line controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { + xAxes: [{ + display: false, + }], yAxes: [{ + display: false, stacked: true }, { + id: 'secondAxis', type: 'linear', - id: 'secondAxis' + display: false }] } } @@ -366,10 +388,10 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 56, y: 161}, - {x: 202, y: 419}, - {x: 347, y: 161}, - {x: 492, y: 419} + {x: 0, y: 146}, + {x: 171, y: 439}, + {x: 341, y: 146}, + {x: 512, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); @@ -378,10 +400,10 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 56, y: 32}, - {x: 202, y: 97}, - {x: 347, y: 161}, - {x: 492, y: 471} + {x: 0, y: 0}, + {x: 171, y: 73}, + {x: 341, y: 146}, + {x: 512, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); @@ -427,8 +449,14 @@ describe('Line controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { + xAxes: [{ + display: false, + }], yAxes: [{ + display: false, stacked: true }] } @@ -438,10 +466,10 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 28, y: 161}, - {x: 183, y: 419}, - {x: 338, y: 161}, - {x: 492, y: 419} + {x: 0, y: 146}, + {x: 171, y: 439}, + {x: 341, y: 146}, + {x: 512, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); @@ -450,10 +478,10 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 28, y: 32}, - {x: 183, y: 97}, - {x: 338, y: 161}, - {x: 492, y: 471} + {x: 0, y: 0}, + {x: 171, y: 73}, + {x: 341, y: 146}, + {x: 512, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); @@ -475,8 +503,14 @@ describe('Line controller tests', function() { labels: ['label1', 'label2', 'label3', 'label4'] }, options: { + legend: false, + title: false, scales: { + xAxes: [{ + display: false, + }], yAxes: [{ + display: false, stacked: true }] } @@ -486,10 +520,10 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 28, y: 161}, - {x: 183, y: 419}, - {x: 338, y: 161}, - {x: 492, y: 419} + {x: 0, y: 146}, + {x: 171, y: 439}, + {x: 341, y: 146}, + {x: 512, y: 439} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); @@ -498,10 +532,10 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 28, y: 32}, - {x: 183, y: 97}, - {x: 338, y: 161}, - {x: 492, y: 471} + {x: 0, y: 0}, + {x: 171, y: 73}, + {x: 341, y: 146}, + {x: 512, y: 497} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index 636473cabf7..050bd68b80f 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -1,5 +1,4 @@ -// Test the polar area controller -describe('Polar area controller tests', function() { +describe('Chart.controllers.polarArea', function() { it('should be constructed', function() { var chart = window.acquireChart({ type: 'polarArea', @@ -82,6 +81,8 @@ describe('Polar area controller tests', function() { }, options: { showLines: true, + legend: false, + title: false, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', @@ -96,13 +97,13 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 168, s: -0.5 * Math.PI, e: 0}, - {o: 228, s: 0, e: 0.5 * Math.PI}, - {o: 48, s: 0.5 * Math.PI, e: Math.PI}, + {o: 179, s: -0.5 * Math.PI, e: 0}, + {o: 243, s: 0, e: 0.5 * Math.PI}, + {o: 51, 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); + expect(meta.data[i]._model.y).toBeCloseToPixel(256); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); expect(meta.data[i]._model.startAngle).toBe(expected.s); @@ -138,9 +139,9 @@ describe('Polar area controller tests', function() { chart.update(); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(272); + expect(meta.data[0]._model.y).toBeCloseToPixel(256); expect(meta.data[0]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(168); + expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(179); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ startAngle: -0.5 * Math.PI, endAngle: 0, @@ -163,6 +164,8 @@ describe('Polar area controller tests', function() { }, options: { showLines: true, + legend: false, + title: false, startAngle: 0, // default is -0.5 * Math.PI elements: { arc: { @@ -178,13 +181,13 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 168, s: 0, e: 0.5 * Math.PI}, - {o: 228, s: 0.5 * Math.PI, e: Math.PI}, - {o: 48, s: Math.PI, e: 1.5 * Math.PI}, + {o: 179, s: 0, e: 0.5 * Math.PI}, + {o: 243, s: 0.5 * Math.PI, e: Math.PI}, + {o: 51, 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); + expect(meta.data[i]._model.y).toBeCloseToPixel(256); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); expect(meta.data[i]._model.startAngle).toBe(expected.s); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 38959a50978..df1001420c4 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -1,5 +1,4 @@ -// Test the polar area controller -describe('Radar controller tests', function() { +describe('Chart.controllers.radar', function() { it('Should be constructed', function() { var chart = window.acquireChart({ type: 'radar', @@ -80,6 +79,8 @@ describe('Radar controller tests', function() { }, options: { showLines: true, + legend: false, + title: false, elements: { line: { backgroundColor: 'rgb(255, 0, 0)', @@ -124,10 +125,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: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, + {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, + {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, + {x: 256, y: 256, cppx: 256, cppy: 256, cpnx: 256, cpny: 256}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -151,10 +152,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: 117, cppx: 246, cppy: 117, cpnx: 272, cpny: 117}, + {x: 464, y: 256, cppx: 464, cppy: 248, cpnx: 464, cpny: 262}, + {x: 256, y: 256, cppx: 276.9, cppy: 256, cpnx: 250.4, cpny: 256}, + {x: 200, y: 256, cppx: 200, cppy: 259, cpnx: 200, cpny: 245}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -208,10 +209,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: 117}, + {x: 464, y: 256}, + {x: 256, y: 256}, + {x: 200, y: 256}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -267,11 +268,11 @@ describe('Radar controller tests', function() { })); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(133); + expect(meta.data[0]._model.y).toBeCloseToPixel(117); expect(meta.data[0]._model.controlPointPreviousX).toBeCloseToPixel(241); - expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(133); + expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(117); expect(meta.data[0]._model.controlPointNextX).toBeCloseToPixel(281); - expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(133); + expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(117); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ radius: 2.2, backgroundColor: 'rgb(0, 1, 3)', From e3b70e0420424ace8d5b45b16490cc3ffc707b38 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 28 Aug 2017 10:33:42 +0200 Subject: [PATCH 301/685] Bump version to 2.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1a78ade9c61..84901f1f739 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "main": "src/chart.js", "repository": { From c7464ebf91af37482912b434a9e5dc25cf46666e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 2 Sep 2017 11:04:10 +0200 Subject: [PATCH 302/685] Add platform basic implementation (fallback) (#4708) If `window` or `document` are `undefined`, a minimal platform implementation is used instead, which one only returns a context2d read from the given canvas/context. --- src/platforms/platform.basic.js | 15 +++++++++++++++ src/platforms/platform.dom.js | 7 +++++++ src/platforms/platform.js | 5 +++-- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/platforms/platform.basic.js diff --git a/src/platforms/platform.basic.js b/src/platforms/platform.basic.js new file mode 100644 index 00000000000..f510b3575c7 --- /dev/null +++ b/src/platforms/platform.basic.js @@ -0,0 +1,15 @@ +/** + * Platform fallback implementation (minimal). + * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 + */ + +module.exports = { + acquireContext: function(item) { + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + return item && item.getContext('2d') || null; + } +}; diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 8fe9f27073c..a14119ad425 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -305,6 +305,13 @@ function injectCSS(platform, css) { } module.exports = { + /** + * This property holds whether this platform is enabled for the current environment. + * Currently used by platform.js to select the proper implementation. + * @private + */ + _enabled: typeof window !== 'undefined' && typeof document !== 'undefined', + initialize: function() { var keyframes = 'from{opacity:0.99}to{opacity:1}'; diff --git a/src/platforms/platform.js b/src/platforms/platform.js index 8f4827732b1..c755f81fdb1 100644 --- a/src/platforms/platform.js +++ b/src/platforms/platform.js @@ -1,10 +1,11 @@ 'use strict'; var helpers = require('../helpers/index'); +var basic = require('./platform.basic'); +var dom = require('./platform.dom'); -// By default, select the browser (DOM) platform. // @TODO Make possible to select another platform at build time. -var implementation = require('./platform.dom'); +var implementation = dom._enabled ? dom : basic; /** * @namespace Chart.platform From 427aa99cb1af43208626048cb2505bf59121dd50 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 8 Sep 2017 15:25:51 -0700 Subject: [PATCH 303/685] Make major ticks optional and off by default (#4723) --- src/scales/scale.time.js | 21 ++++++++++++++------- test/specs/scale.time.tests.js | 10 ++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index dd014e1069f..27c6255fa06 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -284,8 +284,10 @@ function determineMajorUnit(unit) { * responsibility of the calling code to clamp values if needed. */ function generate(min, max, minor, major, capacity, options) { - var stepSize = helpers.valueOrDefault(options.stepSize, options.unitStepSize); - var weekday = minor === 'week' ? options.isoWeekday : false; + var timeOpts = options.time; + var stepSize = helpers.valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize); + var weekday = minor === 'week' ? timeOpts.isoWeekday : false; + var majorTicksEnabled = options.ticks.major.enabled; var interval = INTERVALS[minor]; var first = moment(min); var last = moment(max); @@ -313,7 +315,7 @@ function generate(min, max, minor, major, capacity, options) { time = moment(first); - if (major && !weekday && !options.round) { + if (majorTicksEnabled && major && !weekday && !timeOpts.round) { // Align the first tick on the previous `minor` unit aligned on the `major` unit: // we first aligned time on the previous `major` unit then add the number of full // stepSize there is between first and the previous major time. @@ -434,7 +436,11 @@ module.exports = function(Chart) { * @see https://github.com/chartjs/Chart.js/pull/4507 * @since 2.7.0 */ - source: 'auto' + source: 'auto', + + major: { + enabled: false + } } }; @@ -564,7 +570,7 @@ module.exports = function(Chart) { break; case 'auto': default: - timestamps = generate(min, max, unit, majorUnit, capacity, timeOpts); + timestamps = generate(min, max, unit, majorUnit, capacity, options); } if (options.bounds === 'ticks' && timestamps.length) { @@ -626,9 +632,10 @@ module.exports = function(Chart) { var majorUnit = me._majorUnit; var majorFormat = me._majorFormat; var majorTime = tick.clone().startOf(me._majorUnit).valueOf(); - var major = majorUnit && majorFormat && time === majorTime; + var majorTickOpts = options.ticks.major; + var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; var label = tick.format(major ? majorFormat : me._minorFormat); - var tickOpts = major ? options.ticks.major : options.ticks.minor; + var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); return formatter ? formatter(label, index, ticks) : label; diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 3c757243d56..ac79368ec51 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -94,7 +94,9 @@ describe('Time scale tests', function() { autoSkipPadding: 0, labelOffset: 0, minor: {}, - major: {}, + major: { + enabled: false + }, }, time: { parser: false, @@ -306,7 +308,7 @@ describe('Time scale tests', function() { scale.update(2500, 200); var ticks = getTicksLabels(scale); - expect(ticks).toEqual(['8PM', '9PM', '10PM', '11PM', 'Jan 2', '1AM', '2AM', '3AM', '4AM', '5AM', '6AM', '7AM', '8AM', '9AM', '10AM', '11AM', '12PM', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM']); + expect(ticks).toEqual(['8PM', '9PM', '10PM', '11PM', '12AM', '1AM', '2AM', '3AM', '4AM', '5AM', '6AM', '7AM', '8AM', '9AM', '10AM', '11AM', '12PM', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM']); }); it('build ticks honoring the minUnit', function() { @@ -324,7 +326,7 @@ describe('Time scale tests', function() { var scale = createScale(mockData, config); var ticks = getTicksLabels(scale); - expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3']); + expect(ticks).toEqual(['Jan 1', 'Jan 2', 'Jan 3']); }); it('should build ticks using the config diff', function() { @@ -345,7 +347,7 @@ describe('Time scale tests', function() { var ticks = getTicksLabels(scale); // last date is feb 15 because we round to start of week - expect(ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 2015', 'Feb 8, 2015', 'Feb 15, 2015']); + expect(ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 1, 2015', 'Feb 8, 2015', 'Feb 15, 2015']); }); describe('config step size', function() { From b2beb6f4515367428188586229591024d85476ff Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 10 Sep 2017 00:27:39 +0200 Subject: [PATCH 304/685] Update chartjs-color dependency (#4733) Enforce dependencies minor versions (tilde symbol: include everything greater than a particular version in the same minor range). --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 84901f1f739..445dc877ab0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "main": "Chart.js" }, "dependencies": { - "chartjs-color": "^2.1.0", - "moment": "^2.18.1" + "chartjs-color": "~2.2.0", + "moment": "~2.18.0" } } From fb4357ea9123df1ea1797e98291daf5073ea9d4b Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 10 Sep 2017 01:54:12 -0700 Subject: [PATCH 305/685] Add a financial time series sample (#4554) --- samples/samples.js | 3 + samples/scales/time/financial.html | 104 +++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 samples/scales/time/financial.html diff --git a/samples/samples.js b/samples/samples.js index e7147f5ef38..2d11e9d48c9 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -112,6 +112,9 @@ }, { title: 'Line (point data)', path: 'scales/time/line-point-data.html' + }, { + title: 'Time Series', + path: 'scales/time/financial.html' }, { title: 'Combo', path: 'scales/time/combo.html' diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html new file mode 100644 index 00000000000..3d91dfa56b7 --- /dev/null +++ b/samples/scales/time/financial.html @@ -0,0 +1,104 @@ + + + + + Line Chart + + + + + + + +
    + +
    +
    +
    + Chart Type: + + + + + + From 543c31d5496526a47123a923fef95c22f6980f4d Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 10 Sep 2017 14:31:59 +0200 Subject: [PATCH 306/685] Add Google Analytics to samples and update badges (#4734) Inject the GA tracking snippet for all samples, including the index page. Also update README.md badges using the shields.io service for consistency with flat-square style and cache, and add release badges to the installation documentation page. --- README.md | 4 +--- docs/README.md | 2 +- docs/getting-started/installation.md | 9 ++++++-- samples/charts/area/line-boundaries.html | 6 ++--- samples/index.html | 1 + samples/utils.js | 29 ++++++++++++++++-------- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1a6e61e6378..767950b88bc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Chart.js -[![Build Status](https://travis-ci.org/chartjs/Chart.js.svg?branch=master)](https://travis-ci.org/chartjs/Chart.js) [![Code Climate](https://codeclimate.com/github/chartjs/Chart.js/badges/gpa.svg)](https://codeclimate.com/github/chartjs/Chart.js) [![Coverage Status](https://coveralls.io/repos/github/chartjs/Chart.js/badge.svg?branch=master)](https://coveralls.io/github/chartjs/Chart.js?branch=master) - -[![Chart.js on Slack](https://img.shields.io/badge/slack-Chart.js-blue.svg)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![codeclimate](https://img.shields.io/codeclimate/github/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/docs/README.md b/docs/README.md index aa587221112..5a0e9f35579 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Chart.js -[![Chart.js on Slack](https://img.shields.io/badge/slack-Chart.js-blue.svg)](https://chart-js-automation.herokuapp.com/) +[![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=600)](https://chart-js-automation.herokuapp.com/) ## Installation diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4a3df6eab18..dd8cfa4a790 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,22 +2,27 @@ Chart.js can be installed via npm or bower. It is recommended to get Chart.js this way. ## npm +[![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) ```bash npm install chart.js --save ``` ## Bower +[![bower](https://img.shields.io/bower/v/chartjs.svg?style=flat-square&maxAge=600)](https://libraries.io/bower/chartjs) ```bash bower install chart.js --save ``` ## CDN -or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. +[![cdn](https://img.shields.io/cdnjs/v/Chart.js.svg?label=cdn&style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) +or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. ## Github +[![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) + You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). If you download or clone the repository, you must [build](../developers/contributing.md#building-chartjs) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. @@ -38,4 +43,4 @@ Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled version includes Moment.js built into the same file. This version should be used if you wish to use time axes and want a single file to include. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. \ No newline at end of file +The bundled version includes Moment.js built into the same file. This version should be used if you wish to use time axes and want a single file to include. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. diff --git a/samples/charts/area/line-boundaries.html b/samples/charts/area/line-boundaries.html index 6f1f927a920..edadc781c54 100644 --- a/samples/charts/area/line-boundaries.html +++ b/samples/charts/area/line-boundaries.html @@ -35,11 +35,11 @@ }; function generateData(config) { - return utils.numbers(utils.merge(inputs, config || {})); + return utils.numbers(Chart.helpers.merge(inputs, config || {})); } function generateLabels(config) { - return utils.months(utils.merge({ + return utils.months(Chart.helpers.merge({ count: inputs.count, section: 3 }, config || {})); @@ -85,7 +85,7 @@ fill: boundary }] }, - options: utils.merge(options, { + options: Chart.helpers.merge(options, { title: { text: 'fill: ' + boundary, display: true diff --git a/samples/index.html b/samples/index.html index d7c67934301..d855662f093 100644 --- a/samples/index.html +++ b/samples/index.html @@ -7,6 +7,7 @@ + Chart.js samples diff --git a/samples/utils.js b/samples/utils.js index 0b31703a1f1..50bb81c0c1b 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -1,5 +1,3 @@ -/* global Chart */ - 'use strict'; window.chartColors = { @@ -41,6 +39,8 @@ window.chartColors = { ]; var Samples = global.Samples || (global.Samples = {}); + var Color = global.Color; + Samples.utils = { // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ srand: function(seed) { @@ -119,18 +119,29 @@ window.chartColors = { transparentize: function(color, opacity) { var alpha = opacity === undefined ? 0.5 : 1 - opacity; - return Chart.helpers.color(color).alpha(alpha).rgbString(); - }, - - merge: Chart.helpers.configMerge + return Color(color).alpha(alpha).rgbString(); + } }; - Samples.utils.srand(Date.now()); - // DEPRECATED window.randomScalingFactor = function() { return Math.round(Samples.utils.rand(-100, 100)); }; -}(this)); + // INITIALIZATION + + Samples.utils.srand(Date.now()); + // Google Analytics + /* eslint-disable */ + if (document.location.hostname.match(/^(www\.)?chartjs\.org$/)) { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-28909194-3', 'auto'); + ga('send', 'pageview'); + } + /* eslint-enable */ + +}(this)); From ea703a54bfd63ca0b0fda9adf4aa118b299a37d8 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 10 Sep 2017 19:15:47 +0200 Subject: [PATCH 307/685] Implement scale label padding (#4646) --- docs/axes/labelling.md | 1 + src/core/core.scale.js | 47 ++++++++++++------ .../core.scale/label-offset-vertical-axes.png | Bin 4434 -> 4435 bytes test/specs/core.helpers.tests.js | 12 +---- test/specs/core.scale.tests.js | 19 +++++++ test/specs/core.tooltip.tests.js | 8 +-- test/specs/scale.category.tests.js | 18 +++---- test/specs/scale.linear.tests.js | 22 ++++---- test/specs/scale.logarithmic.tests.js | 12 ++--- test/specs/scale.radialLinear.tests.js | 6 +-- test/specs/scale.time.tests.js | 6 +-- 11 files changed, 81 insertions(+), 70 deletions(-) diff --git a/docs/axes/labelling.md b/docs/axes/labelling.md index 22fc2600463..aec8d990ff4 100644 --- a/docs/axes/labelling.md +++ b/docs/axes/labelling.md @@ -15,6 +15,7 @@ The scale label configuration is nested under the scale configuration in the `sc | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the scale title, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for scale title. | `fontStyle` | `String` | `'normal'` | Font style for the scale title, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `padding` | `Number` or `Object` | `4` | Padding to apply around scale labels. Only `top` and `bottom` are implemented. ## Creating Custom Tick Formats diff --git a/src/core/core.scale.js b/src/core/core.scale.js index bc4d1ec9c7b..f48b67a5ec1 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -36,7 +36,14 @@ defaults._set('scale', { // actual label labelString: '', - lineHeight: 1.2 + // line height + lineHeight: 1.2, + + // top/bottom padding + padding: { + top: 4, + bottom: 4 + } }, // label settings @@ -391,7 +398,6 @@ module.exports = function(Chart) { var tickFont = parseFontOptions(tickOpts); var tickMarkLength = opts.gridLines.tickMarkLength; - var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); // Width if (isHorizontal) { @@ -410,10 +416,14 @@ module.exports = function(Chart) { // Are we showing a title for the scale? if (scaleLabelOpts.display && display) { + var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); + var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); + var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; + if (isHorizontal) { - minSize.height += scaleLabelLineHeight; + minSize.height += deltaHeight; } else { - minSize.width += scaleLabelLineHeight; + minSize.width += deltaHeight; } } @@ -435,16 +445,17 @@ module.exports = function(Chart) { // TODO - improve this calculation var labelHeight = (sinRotation * largestTextWidth) + (tickFont.size * tallestLabelHeightInLines) - + (lineSpace * tallestLabelHeightInLines); + + (lineSpace * (tallestLabelHeightInLines - 1)) + + lineSpace; // padding minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); - me.ctx.font = tickFont.font; + me.ctx.font = tickFont.font; var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); - // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated - // by the font height + // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned + // which means that the right padding is dominated by the font height if (me.labelRotation !== 0) { me.paddingLeft = opts.position === 'bottom' ? (cosRotation * firstLabelWidth) + 3 : (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges me.paddingRight = opts.position === 'bottom' ? (cosRotation * lineSpace) + 3 : (cosRotation * lastLabelWidth) + 3; @@ -453,15 +464,18 @@ module.exports = function(Chart) { me.paddingRight = lastLabelWidth / 2 + 3; } } else { - // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - // Account for padding - + // A vertical axis is more constrained by the width. Labels are the + // dominant factor here, so get that length first and account for padding if (tickOpts.mirror) { largestTextWidth = 0; } else { - largestTextWidth += tickPadding; + // use lineSpace for consistency with horizontal axis + // tickPadding is not implemented for horizontal + largestTextWidth += tickPadding + lineSpace; } + minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); + me.paddingTop = tickFont.size / 2; me.paddingBottom = tickFont.size / 2; } @@ -663,6 +677,7 @@ module.exports = function(Chart) { var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); var scaleLabelFont = parseFontOptions(scaleLabel); + var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); var labelRotationRadians = helpers.toRadians(me.labelRotation); var itemsToDraw = []; @@ -840,10 +855,14 @@ module.exports = function(Chart) { if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' ? me.bottom - halfLineHeight : me.top + halfLineHeight; + scaleLabelY = options.position === 'bottom' + ? me.bottom - halfLineHeight - scaleLabelPadding.bottom + : me.top + halfLineHeight + scaleLabelPadding.top; } else { var isLeft = options.position === 'left'; - scaleLabelX = isLeft ? me.left + halfLineHeight : me.right - halfLineHeight; + scaleLabelX = isLeft + ? me.left + halfLineHeight + scaleLabelPadding.top + : me.right - halfLineHeight - scaleLabelPadding.top; scaleLabelY = me.top + ((me.bottom - me.top) / 2); rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } diff --git a/test/fixtures/core.scale/label-offset-vertical-axes.png b/test/fixtures/core.scale/label-offset-vertical-axes.png index 2f6b18c8c23939f2b79177a937ba2458298f826b..05785b8bc597181170ffe7bae4e472f35f8a0620 100644 GIT binary patch literal 4435 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 literal 4434 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 Date: Mon, 11 Sep 2017 13:33:20 -0700 Subject: [PATCH 308/685] Expand scale jsdocs (#4736) --- src/core/core.scale.js | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f48b67a5ec1..89df4304b5b 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -538,17 +538,33 @@ module.exports = function(Chart) { return rawValue; }, - // Used to get the value to display in the tooltip for the data at the given index - // function getLabelForIndex(index, datasetIndex) + /** + * Used to get the value to display in the tooltip for the data at the given index + * @param index + * @param datasetIndex + */ getLabelForIndex: helpers.noop, - // Used to get data value locations. Value can either be an index or a numerical value + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param value + * @param index + * @param datasetIndex + */ getPixelForValue: helpers.noop, - // Used to get the data value from a given pixel. This is the inverse of getPixelForValue + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param pixel + */ getValueForPixel: helpers.noop, - // Used for tick location, should + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ getPixelForTick: function(index) { var me = this; var offset = me.options.offset; @@ -569,7 +585,10 @@ module.exports = function(Chart) { return me.top + (index * (innerHeight / (me._ticks.length - 1))); }, - // Utility for getting the pixel location of a percentage of scale + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ getPixelForDecimal: function(decimal) { var me = this; if (me.isHorizontal()) { @@ -583,6 +602,10 @@ module.exports = function(Chart) { return me.top + (decimal * me.height); }, + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ getBasePixel: function() { return this.getPixelForValue(this.getBaseValue()); }, From 2b89f7a9893f22efba85e224ee68acff1cd7ee2b Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 15 Sep 2017 16:53:51 -0700 Subject: [PATCH 309/685] Consistent formatting for cartesian option docs (#4765) --- docs/axes/cartesian/category.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/axes/cartesian/category.md b/docs/axes/cartesian/category.md index 38c306712bb..4bfb6f627cc 100644 --- a/docs/axes/cartesian/category.md +++ b/docs/axes/cartesian/category.md @@ -40,7 +40,7 @@ The category scale provides the following options for configuring tick marks. Th | Name | Type | Default | Description | -----| ---- | --------| ----------- -| labels | Array[String] | - | An array of labels to display. +| `labels` | `Array[String]` | - | An array of labels to display. | `min` | `String` | | The minimum item to display. [more...](#min-max-configuration) | `max` | `String` | | The maximum item to display. [more...](#min-max-configuration) From 52145de5db5dc3bae093880429b35954f0034993 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 16 Sep 2017 08:01:26 -0400 Subject: [PATCH 310/685] Fix regression in x-axis interaction mode (#4762) --- src/core/core.interaction.js | 2 +- test/specs/global.deprecations.tests.js | 39 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index d984126ab53..be85a080f76 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -215,7 +215,7 @@ module.exports = { * @private */ 'x-axis': function(chart, e) { - return indexMode(chart, e, {intersect: true}); + return indexMode(chart, e, {intersect: false}); }, /** diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index c5bef5b20b4..f1091464d1e 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -325,6 +325,45 @@ describe('Deprecations', function() { }); }); + describe('Version 2.4.0', function() { + describe('x-axis mode', function() { + it ('behaves like index mode with intersect: false', function() { + var 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 chart = window.acquireChart({ + type: 'line', + data: data + }); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: 0, + y: 0 + }; + + var elements = Chart.Interaction.modes['x-axis'](chart, evt); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + }); + }); + describe('Version 2.1.5', function() { // https://github.com/chartjs/Chart.js/pull/2752 describe('Chart.pluginService', function() { From 8a4ac1e12ef8f266fa2dea17b263b54adfc62ee5 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 29 Sep 2017 04:06:09 -0700 Subject: [PATCH 311/685] Rename INTERVALS.major to INTERVALS.common (#4777) --- src/scales/scale.time.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 27c6255fa06..a87920c6781 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -13,47 +13,47 @@ var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; var INTERVALS = { millisecond: { - major: true, + common: true, size: 1, steps: [1, 2, 5, 10, 20, 50, 100, 250, 500] }, second: { - major: true, + common: true, size: 1000, steps: [1, 2, 5, 10, 30] }, minute: { - major: true, + common: true, size: 60000, steps: [1, 2, 5, 10, 30] }, hour: { - major: true, + common: true, size: 3600000, steps: [1, 2, 3, 6, 12] }, day: { - major: true, + common: true, size: 86400000, steps: [1, 2, 5] }, week: { - major: false, + common: false, size: 604800000, steps: [1, 2, 3, 4] }, month: { - major: true, + common: true, size: 2.628e9, steps: [1, 2, 3] }, quarter: { - major: false, + common: false, size: 7.884e9, steps: [1, 2, 3, 4] }, year: { - major: true, + common: true, size: 3.154e10 } }; @@ -261,7 +261,7 @@ function determineUnit(minUnit, min, max, capacity) { interval = INTERVALS[UNITS[i]]; factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER; - if (Math.ceil((max - min) / (factor * interval.size)) <= capacity) { + if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { return UNITS[i]; } } @@ -271,7 +271,7 @@ function determineUnit(minUnit, min, max, capacity) { function determineMajorUnit(unit) { for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { - if (INTERVALS[UNITS[i]].major) { + if (INTERVALS[UNITS[i]].common) { return UNITS[i]; } } From b94532c29f7a4526e8c48961a1451e4ae1a9460c Mon Sep 17 00:00:00 2001 From: Rydori Date: Fri, 29 Sep 2017 13:30:41 +0200 Subject: [PATCH 312/685] Error if style is null (#4781) I tested in Chrome and when style(line 50) is null, style.toString is undefined Reason: typeof null equals "object" (at least in Chrome) --- src/helpers/helpers.canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 1429a31e6e0..13b110268b9 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -47,7 +47,7 @@ var exports = module.exports = { drawPoint: function(ctx, style, radius, x, y) { var type, edgeLength, xOffset, yOffset, height, size; - if (typeof style === 'object') { + if (style && typeof style === 'object') { type = style.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { ctx.drawImage(style, x - style.width / 2, y - style.height / 2, style.width, style.height); From b4d69247b055e25c87bf67019112495c5fa982db Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 29 Sep 2017 04:52:33 -0700 Subject: [PATCH 313/685] Fix handling of null labels (#4795) --- src/core/core.scale.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 89df4304b5b..ffe13cbff82 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -662,7 +662,7 @@ module.exports = function(Chart) { // Since we always show the last tick,we need may need to hide the last shown one before shouldSkip = (skipRatio > 1 && i % skipRatio > 0) || (i % skipRatio === 0 && i + skipRatio >= tickCount); - if (shouldSkip && i !== tickCount - 1 || helpers.isNullOrUndef(tick.label)) { + if (shouldSkip && i !== tickCount - 1) { // leave tick in place but make sure it's not displayed (#4635) delete tick.label; } @@ -712,7 +712,7 @@ module.exports = function(Chart) { helpers.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) - if (tick.label === undefined) { + if (helpers.isNullOrUndef(tick.label)) { return; } From 0bd0654efb4a19f192a485d88daadbe8ee2f44c0 Mon Sep 17 00:00:00 2001 From: Rittyan Date: Tue, 3 Oct 2017 07:33:03 +0900 Subject: [PATCH 314/685] fix colour settings of BeforeLabel and BeforeBody (#4783) * fix colour settings of BeforeLabel and BeforeBody * delete redundant variable declaration * collect label colour setting. --- src/core/core.tooltip.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 048e1a08a69..2acd31e051a 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -677,6 +677,7 @@ module.exports = function(Chart) { }; // Before body lines + ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); helpers.each(vm.beforeBody, fillLineOfText); var drawColorBoxes = vm.displayColors; @@ -684,6 +685,8 @@ module.exports = function(Chart) { // Draw body lines now helpers.each(body, function(bodyItem, i) { + var textColor = mergeOpacity(vm.labelTextColors[i], opacity); + ctx.fillStyle = textColor; helpers.each(bodyItem.before, fillLineOfText); helpers.each(bodyItem.lines, function(line) { @@ -701,7 +704,6 @@ module.exports = function(Chart) { // Inner square ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); - var textColor = mergeOpacity(vm.labelTextColors[i], opacity); ctx.fillStyle = textColor; } From 73a3c3b8214047770840d603bc047731b24df1c0 Mon Sep 17 00:00:00 2001 From: Fabio Poloni Date: Fri, 6 Oct 2017 13:36:39 +0200 Subject: [PATCH 315/685] fixed typo (#4819) --- docs/configuration/tooltip.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index bb3b7a6aefb..9967bebd0a9 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -243,7 +243,7 @@ The tooltip model contains parameters that can be used to render the tooltip. // Body // The body lines that need to be rendered - // Each pbject contains 3 parameters + // Each object contains 3 parameters // before: String[] // lines of text before the line with the color square // lines: String[], // lines of text to render as the main item with color square // after: String[], // lines of text to render after the main lines From 3fe198c86098ae50d5d5e9b74d111d5f8fa74bf0 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 7 Oct 2017 17:43:09 +0200 Subject: [PATCH 316/685] Fix responsive issue when the chart is recreated (#4774) Chrome specific issue that happens when destroying a chart and re-creating it immediately (same animation frame?). The CSS animation used to detect when the canvas become visible is not re-evaluated, breaking responsiveness. Accessing the `offsetParent` property will force a reflow and re-evaluate the CSS animation. --- src/platforms/platform.dom.js | 7 +++++ test/jasmine.utils.js | 8 +++++- test/specs/core.controller.tests.js | 41 ++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index a14119ad425..a882d22a537 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -236,6 +236,13 @@ function watchForRender(node, handler) { addEventListener(node, type, proxy); }); + // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class + // is removed then added back immediately (same animation frame?). Accessing the + // `offsetParent` property will force a reflow and re-evaluate the CSS animation. + // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics + // https://github.com/chartjs/Chart.js/issues/4737 + expando.reflow = !!node.offsetParent; + node.classList.add(CSS_RENDER_MONITOR); } diff --git a/test/jasmine.utils.js b/test/jasmine.utils.js index c94249406a2..7db35642e06 100644 --- a/test/jasmine.utils.js +++ b/test/jasmine.utils.js @@ -75,7 +75,13 @@ function acquireChart(config, options) { wrapper.appendChild(canvas); window.document.body.appendChild(wrapper); - chart = new Chart(canvas.getContext('2d'), config); + try { + chart = new Chart(canvas.getContext('2d'), config); + } catch (e) { + window.document.body.removeChild(wrapper); + throw e; + } + chart.$test = { persistent: options.persistent, wrapper: wrapper diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index beac21e0daa..50a206bcfd2 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -495,6 +495,45 @@ describe('Chart', function() { }); }); }); + + // https://github.com/chartjs/Chart.js/issues/4737 + it('should resize the canvas when re-creating the chart', function(done) { + var chart = acquireChart({ + options: { + responsive: true + } + }, { + wrapper: { + style: 'width: 320px' + } + }); + + waitForResize(chart, function() { + var canvas = chart.canvas; + expect(chart).toBeChartOfSize({ + dw: 320, dh: 320, + rw: 320, rh: 320, + }); + + chart.destroy(); + chart = new Chart(canvas, { + type: 'line', + options: { + responsive: true + } + }); + + canvas.parentNode.style.width = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 455, + rw: 455, rh: 455, + }); + + done(); + }); + }); + }); }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { @@ -627,7 +666,7 @@ describe('Chart', function() { }); }); - describe('config.options.devicePixelRatio 3', function() { + describe('config.options.devicePixelRatio', function() { beforeEach(function() { this.devicePixelRatio = window.devicePixelRatio; window.devicePixelRatio = 1; From fa2ca2c1e2a6cefb4044a33d1bd1ab44aa907dfc Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 7 Oct 2017 19:18:50 -0700 Subject: [PATCH 317/685] Clarify that x/y coordinates only work with the time scale (#4826) --- docs/charts/bar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 524c795214c..cd5e8c9f391 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -152,7 +152,7 @@ The `data` property of a dataset for a bar chart is specified as a an array of n data: [20, 10] ``` -You can also specify the dataset as x/y coordinates. +You can also specify the dataset as x/y coordinates when using the [time scale](./time.md). ```javascript data: [{x:'2016-12-25', y:20}, {x:'2016-12-26', y:10}] From d2226b289236e45f277ab95686e45cbe7a64ad62 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 8 Oct 2017 05:09:39 -0700 Subject: [PATCH 318/685] Fix markdown (#4827) --- docs/configuration/title.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration/title.md b/docs/configuration/title.md index b2c04cd8fbe..837ea56eb23 100644 --- a/docs/configuration/title.md +++ b/docs/configuration/title.md @@ -11,11 +11,11 @@ The title configuration is passed into the `options.title` namespace. The global | `position` | `String` | `'top'` | Position of title. [more...](#position) | `fontSize` | `Number` | `12` | Font size | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the title text. -| `fontColor` | Color | `'#666'` | Font color +| `fontColor` | `Color` | `'#666'` | Font color | `fontStyle` | `String` | `'bold'` | Font style | `padding` | `Number` | `10` | Number of pixels to add above and below the title text. -| `lineHeight` | `Number|String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) -| `text` | `String/String[]` | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. +| `lineHeight` | Number|String | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) +| `text` | String|String[] | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. ### Position Possible title position values are: From d81afc8b5a0f4f27d9d81f1dd748a7a01e7365c0 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Mon, 9 Oct 2017 06:54:27 -0700 Subject: [PATCH 319/685] Fix choosing of formatting unit (#4779) * Don't change minorFormat when determining label capacity * Fix choosing of formatting unit --- src/scales/scale.time.js | 55 +++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index a87920c6781..27a340a3b1f 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -253,7 +253,10 @@ function determineStepSize(min, max, unit, capacity) { return factor; } -function determineUnit(minUnit, min, max, capacity) { +/** + * Figures out what unit results in an appropriate number of auto-generated ticks + */ +function determineUnitForAutoTicks(minUnit, min, max, capacity) { var ilen = UNITS.length; var i, interval, factor; @@ -269,6 +272,24 @@ function determineUnit(minUnit, min, max, capacity) { return UNITS[ilen - 1]; } +/** + * Figures out what unit to format a set of ticks with + */ +function determineUnitForFormatting(ticks, minUnit, min, max) { + var duration = moment.duration(moment(max).diff(moment(min))); + var ilen = UNITS.length; + var i, unit; + + for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { + unit = UNITS[i]; + if (INTERVALS[unit].common && duration.as(unit) >= ticks.length) { + return unit; + } + } + + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} + function determineMajorUnit(unit) { for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { if (INTERVALS[UNITS[i]].common) { @@ -283,8 +304,10 @@ function determineMajorUnit(unit) { * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. */ -function generate(min, max, minor, major, capacity, options) { +function generate(min, max, capacity, options) { var timeOpts = options.time; + var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + var major = determineMajorUnit(minor); var stepSize = helpers.valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize); var weekday = minor === 'week' ? timeOpts.isoWeekday : false; var majorTicksEnabled = options.ticks.major.enabled; @@ -553,10 +576,6 @@ module.exports = function(Chart) { var max = me.max; var options = me.options; var timeOpts = options.time; - var formats = timeOpts.displayFormats; - var capacity = me.getLabelCapacity(min); - var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, min, max, capacity); - var majorUnit = determineMajorUnit(unit); var timestamps = []; var ticks = []; var i, ilen, timestamp; @@ -570,7 +589,7 @@ module.exports = function(Chart) { break; case 'auto': default: - timestamps = generate(min, max, unit, majorUnit, capacity, options); + timestamps = generate(min, max, me.getLabelCapacity(min), options); } if (options.bounds === 'ticks' && timestamps.length) { @@ -594,14 +613,12 @@ module.exports = function(Chart) { me.max = max; // PRIVATE - me._unit = unit; - me._majorUnit = majorUnit; - me._minorFormat = formats[unit]; - me._majorFormat = formats[majorUnit]; + me._unit = timeOpts.unit || determineUnitForFormatting(ticks, timeOpts.minUnit, me.min, me.max); + me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); - return ticksFromTimestamps(ticks, majorUnit); + return ticksFromTimestamps(ticks, me._majorUnit); }, getLabelForIndex: function(index, datasetIndex) { @@ -625,16 +642,18 @@ module.exports = function(Chart) { * Function to format an individual tick mark * @private */ - tickFormatFunction: function(tick, index, ticks) { + tickFormatFunction: function(tick, index, ticks, formatOverride) { var me = this; var options = me.options; var time = tick.valueOf(); + var formats = options.time.displayFormats; + var minorFormat = formats[me._unit]; var majorUnit = me._majorUnit; - var majorFormat = me._majorFormat; - var majorTime = tick.clone().startOf(me._majorUnit).valueOf(); + var majorFormat = formats[majorUnit]; + var majorTime = tick.clone().startOf(majorUnit).valueOf(); var majorTickOpts = options.ticks.major; var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; - var label = tick.format(major ? majorFormat : me._minorFormat); + var label = tick.format(formatOverride ? formatOverride : major ? majorFormat : minorFormat); var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); @@ -720,9 +739,9 @@ module.exports = function(Chart) { getLabelCapacity: function(exampleTime) { var me = this; - me._minorFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation + var formatOverride = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation - var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []); + var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, [], formatOverride); var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; From c83b03f668a5de2cc60678220a9ea50ebd44a1af Mon Sep 17 00:00:00 2001 From: Florian Scholz Date: Wed, 11 Oct 2017 00:33:25 +0200 Subject: [PATCH 320/685] Fixes #4772: added scope for tooltip position mode call and added docs (#4784) * added scope for tooltip position mode call and added docs * added test for positioner * removed named func for lint * resolved pull-request comments --- docs/configuration/tooltip.md | 22 ++++++++++++ src/core/core.tooltip.js | 2 +- test/specs/core.tooltip.tests.js | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 9967bebd0a9..4e1bb12b159 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -51,6 +51,28 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g New modes can be defined by adding functions to the Chart.Tooltip.positioners map. +Example: +```javascript +/** + * Custom positioner + * @function Chart.Tooltip.positioners.custom + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {Point} the tooltip position + */ +Chart.Tooltip.positioners.custom = function(elements, eventPosition) { + /** @type {Chart.Tooltip} */ + var tooltip = this; + + /* ... */ + + return { + x: 0, + y: 0 + }; +} +``` + ### Sort Callback Allows sorting of [tooltip items](#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. diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 2acd31e051a..76e06d06141 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -495,7 +495,7 @@ module.exports = function(Chart) { var labelColors = []; var labelTextColors = []; - tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition); + tooltipPosition = Chart.Tooltip.positioners[opts.position].call(me, active, me._eventPosition); var tooltipItems = []; for (i = 0, len = active.length; i < len; ++i) { diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index 73d943bd853..632f8121daf 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -822,4 +822,64 @@ describe('Core.Tooltip', function() { node.dispatchEvent(firstEvent); expect(tooltip.update).not.toHaveBeenCalled(); }); + + describe('positioners', function() { + it('Should call custom positioner with correct parameters and scope', function() { + + Chart.Tooltip.positioners.test = function() { + return {x: 0, y: 0}; + }; + + spyOn(Chart.Tooltip.positioners, 'test').and.callThrough(); + + var chart = 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: 'nearest', + position: 'test' + } + } + }); + + // Trigger an event over top of the + var pointIndex = 1; + var datasetIndex = 0; + var meta = chart.getDatasetMeta(datasetIndex); + var point = meta.data[pointIndex]; + var node = 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 + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + var fn = Chart.Tooltip.positioners.test; + expect(fn.calls.count()).toBe(1); + expect(fn.calls.first().args[0] instanceof Array).toBe(true); + expect(fn.calls.first().args[1].hasOwnProperty('x')).toBe(true); + expect(fn.calls.first().args[1].hasOwnProperty('y')).toBe(true); + expect(fn.calls.first().object instanceof Chart.Tooltip).toBe(true); + }); + }); }); From 8ac0257f8da13afffc3e5d32c9b6debea8f6f6a0 Mon Sep 17 00:00:00 2001 From: JewelsJLF Date: Sat, 14 Oct 2017 15:29:35 -0600 Subject: [PATCH 321/685] Add "beforeTooltipDraw" and "afterTooltipDraw" plugin hooks (#4793) --- src/core/core.controller.js | 26 +++++++++++++++++++++++--- src/core/core.plugin.js | 21 +++++++++++++++++++++ test/specs/core.controller.tests.js | 2 ++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 03dfedb7196..241448dadd7 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -528,9 +528,7 @@ module.exports = function(Chart) { } me.drawDatasets(easingValue); - - // Finally draw the tooltip - me.tooltip.draw(); + me._drawTooltip(easingValue); plugins.notify(me, 'afterDraw', [easingValue]); }, @@ -595,6 +593,28 @@ module.exports = function(Chart) { plugins.notify(me, 'afterDatasetDraw', [args]); }, + /** + * Draws tooltip unless a plugin returns `false` to the `beforeTooltipDraw` + * hook, in which case, plugins will not be called on `afterTooltipDraw`. + * @private + */ + _drawTooltip: function(easingValue) { + var me = this; + var tooltip = me.tooltip; + var args = { + tooltip: tooltip, + easingValue: easingValue + }; + + if (plugins.notify(me, 'beforeTooltipDraw', [args]) === false) { + return; + } + + tooltip.draw(); + + plugins.notify(me, 'afterTooltipDraw', [args]); + }, + // 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) { diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index 399075b812c..0b7423a6904 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -323,6 +323,27 @@ module.exports = function(Chart) { * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. * @param {Object} options - The plugin options. */ + /** + * @method IPlugin#beforeTooltipDraw + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart tooltip drawing. + */ + /** + * @method IPlugin#afterTooltipDraw + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ /** * @method IPlugin#beforeEvent * @desc Called before processing the specified `event`. If any plugin returns `false`, diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 50a206bcfd2..fe73d02a0c2 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -884,6 +884,8 @@ describe('Chart', function() { 'beforeDatasetDraw', 'afterDatasetDraw', 'afterDatasetsDraw', + 'beforeTooltipDraw', + 'afterTooltipDraw', 'afterDraw', 'afterRender', ], From 11315fba2bef8c07591dcc21e6d2d594caf6d59b Mon Sep 17 00:00:00 2001 From: minusf Date: Tue, 17 Oct 2017 00:45:54 +0200 Subject: [PATCH 322/685] minor doc fixes (#4851) --- docs/axes/cartesian/time.md | 2 +- docs/axes/labelling.md | 6 +++--- docs/axes/radial/linear.md | 8 ++++---- docs/axes/styling.md | 10 +++++----- docs/charts/line.md | 2 +- docs/configuration/legend.md | 2 +- docs/configuration/title.md | 4 ++-- docs/configuration/tooltip.md | 12 ++++++------ 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index e087d186817..9d15b5e2674 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -34,7 +34,7 @@ The following options are provided by the time scale. You may also set options p | `time.isoWeekday` | `Boolean` | `false` | If true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. | `time.max` | [Time](#date-formats) | | If defined, this will override the data maximum | `time.min` | [Time](#date-formats) | | If defined, this will override the data minimum -| `time.parser` | `String` or `Function` | | Custom parser for dates. [more...](#parser) +| `time.parser` | `String/Function` | | Custom parser for dates. [more...](#parser) | `time.round` | `String` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#time-units) below for the allowed units. | `time.tooltipFormat` | `String` | | The moment js format string to use for the tooltip. | `time.unit` | `String` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#time-units) section below for details. diff --git a/docs/axes/labelling.md b/docs/axes/labelling.md index aec8d990ff4..2e8f28c805c 100644 --- a/docs/axes/labelling.md +++ b/docs/axes/labelling.md @@ -10,12 +10,12 @@ The scale label configuration is nested under the scale configuration in the `sc | -----| ---- | --------| ----------- | `display` | `Boolean` | `false` | If true, display the axis title. | `labelString` | `String` | `''` | The text for the title. (i.e. "# of People" or "Response Choices"). -| `lineHeight` | `Number|String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) -| `fontColor` | Color | `'#666'` | Font color for scale title. +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) +| `fontColor` | `Color` | `'#666'` | Font color for scale title. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the scale title, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for scale title. | `fontStyle` | `String` | `'normal'` | Font style for the scale title, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). -| `padding` | `Number` or `Object` | `4` | Padding to apply around scale labels. Only `top` and `bottom` are implemented. +| `padding` | `Number/Object` | `4` | Padding to apply around scale labels. Only `top` and `bottom` are implemented. ## Creating Custom Tick Formats diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index f9811c6f985..1636781a84e 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `backdropColor` | Color | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops +| `backdropColor` | `Color` | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops | `backdropPaddingX` | `Number` | `2` | Horizontal padding of label backdrop. | `backdropPaddingY` | `Number` | `2` | Vertical padding of label backdrop. | `beginAtZero` | `Boolean` | `false` | if true, scale will include 0 if it is not already included. @@ -94,7 +94,7 @@ The following options are used to configure angled lines that radiate from the c | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | if true, angle lines are shown -| `color` | Color | `rgba(0, 0, 0, 0.1)` | Color of angled lines +| `color` | `Color` | `rgba(0, 0, 0, 0.1)` | Color of angled lines | `lineWidth` | `Number` | `1` | Width of angled lines ## Point Label Options @@ -104,7 +104,7 @@ The following options are used to configure the point labels that are shown on t | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. -| `fontColor` | Color | `'#666'` | Font color for point labels. +| `fontColor` | `Color` | `'#666'` | Font color for point labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels -| `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. \ No newline at end of file +| `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 79cb4e9d66f..1de31e01f6c 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -9,10 +9,10 @@ The grid line configuration is nested under the scale configuration in the `grid | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | If false, do not display grid lines for this axis. -| `color` | Color or Color[] | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. +| `color` | `Color/Color[]` | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. | `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -| `lineWidth` | `Number or Number[]` | `1` | Stroke width of grid lines. +| `lineWidth` | `Number/Number[]` | `1` | Stroke width of grid lines. | `drawBorder` | `Boolean` | `true` | If true, draw border at the edge between the axis and the chart area. | `drawOnChartArea` | `Boolean` | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. | `drawTicks` | `Boolean` | `true` | If true, draw lines beside the ticks in the axis area beside the chart. @@ -30,7 +30,7 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). | `display` | `Boolean` | `true` | If true, show tick marks -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). @@ -44,7 +44,7 @@ The minorTick configuration is nested under the ticks configuration in the `mino | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). @@ -55,7 +55,7 @@ The majorTick configuration is nested under the ticks configuration in the `majo | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). diff --git a/docs/charts/line.md b/docs/charts/line.md index cd4141a49a0..40441b7afe3 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -50,7 +50,7 @@ All point* properties can be specified as an array. If these are set to an array | `yAxisID` | `String` | The ID of the y axis to plot this dataset on. If not specified, this defaults to the ID of the first found y axis. | `backgroundColor` | `Color` | The fill color under the line. See [Colors](../general/colors.md#colors) | `borderColor` | `Color` | The color of the line. See [Colors](../general/colors.md#colors) -| `borderWidth` | `Number/` | The width of the line in pixels. +| `borderWidth` | `Number` | The width of the line in pixels. | `borderDash` | `Number[]` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) | `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 4cad48b556e..bce69af79db 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -31,7 +31,7 @@ The legend label configuration is nested below the legend configuration using th | `boxWidth` | `Number` | `40` | width of coloured box | `fontSize` | `Number` | `12` | font size of text | `fontStyle` | `String` | `'normal'` | font style of text -| `fontColor` | Color | `'#666'` | Color of text +| `fontColor` | `Color` | `'#666'` | Color of text | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family of legend text. | `padding` | `Number` | `10` | Padding between rows of colored boxes. | `generateLabels` | `Function` | | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#legend-item-interface) for details. diff --git a/docs/configuration/title.md b/docs/configuration/title.md index 837ea56eb23..e206dfe0c55 100644 --- a/docs/configuration/title.md +++ b/docs/configuration/title.md @@ -14,8 +14,8 @@ The title configuration is passed into the `options.title` namespace. The global | `fontColor` | `Color` | `'#666'` | Font color | `fontStyle` | `String` | `'bold'` | Font style | `padding` | `Number` | `10` | Number of pixels to add above and below the title text. -| `lineHeight` | Number|String | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) -| `text` | String|String[] | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) +| `text` | `String/String[]` | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. ### Position Possible title position values are: diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 4e1bb12b159..51004843a33 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -14,22 +14,22 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `callbacks` | `Object` | | See the [callbacks section](#tooltip-callbacks) | `itemSort` | `Function` | | Sort tooltip items. [more...](#sort-callback) | `filter` | `Function` | | Filter tooltip items. [more...](#filter-callback) -| `backgroundColor` | Color | `'rgba(0,0,0,0.8)'` | Background color of the tooltip. +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.8)'` | Background color of the tooltip. | `titleFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | title font | `titleFontSize` | `Number` | `12` | Title font size | `titleFontStyle` | `String` | `'bold'` | Title font style -| `titleFontColor` | Color | `'#fff'` | Title font color +| `titleFontColor` | `Color` | `'#fff'` | Title font color | `titleSpacing` | `Number` | `2` | Spacing to add to top and bottom of each title line. | `titleMarginBottom` | `Number` | `6` | Margin to add on bottom of title section. | `bodyFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | body line font | `bodyFontSize` | `Number` | `12` | Body font size | `bodyFontStyle` | `String` | `'normal'` | Body font style -| `bodyFontColor` | Color | `'#fff'` | Body font color +| `bodyFontColor` | `Color` | `'#fff'` | Body font color | `bodySpacing` | `Number` | `2` | Spacing to add to top and bottom of each tooltip item. | `footerFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | footer font | `footerFontSize` | `Number` | `12` | Footer font size | `footerFontStyle` | `String` | `'bold'` | Footer font style -| `footerFontColor` | Color | `'#fff'` | Footer font color +| `footerFontColor` | `Color` | `'#fff'` | Footer font color | `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. | `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. | `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. @@ -37,9 +37,9 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `caretPadding` | `Number` | `2` | Extra distance to move the end of the tooltip arrow away from the tooltip point. | `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 +| `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 -| `borderColor` | Color | `'rgba(0,0,0,0)'` | Color of the border +| `borderColor` | `Color` | `'rgba(0,0,0,0)'` | Color of the border | `borderWidth` | `Number` | `0` | Size of the border ### Position Modes From b64fd5db2526b33b5da6b8182f50bba8042b82af Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Fri, 20 Oct 2017 00:03:38 -0700 Subject: [PATCH 323/685] Respect min and max when building ticks (#4860) Generate time scale ticks (`ticks.source: 'auto'`) based on the effective visualized range instead of the actual data range, meaning that the computed units and/or step size may change if the time options min and max are different from the data min and max. --- src/scales/scale.time.js | 7 +++++-- test/specs/scale.time.tests.js | 25 +++++++++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 27a340a3b1f..4fe39b81082 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -504,8 +504,8 @@ module.exports = function(Chart) { var me = this; var chart = me.chart; var timeOpts = me.options.time; - var min = parse(timeOpts.min, me) || MAX_INTEGER; - var max = parse(timeOpts.max, me) || MIN_INTEGER; + var min = MAX_INTEGER; + var max = MIN_INTEGER; var timestamps = []; var datasets = []; var labels = []; @@ -552,6 +552,9 @@ module.exports = function(Chart) { max = Math.max(max, timestamps[timestamps.length - 1]); } + min = parse(timeOpts.min, me) || min; + max = parse(timeOpts.max, me) || max; + // In case there is no valid min/max, let's use today limits min = min === MAX_INTEGER ? +moment().startOf('day') : min; max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max; diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 98dd774b6be..19189040ee4 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -376,23 +376,36 @@ describe('Time scale tests', function() { var config; beforeEach(function() { config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); + config.ticks.source = 'labels'; + config.time.unit = 'day'; }); - it('should use the min option', function() { - config.time.unit = 'day'; + it('should use the min option when less than first label for building ticks', function() { config.time.min = '2014-12-29T04:00:00'; var scale = createScale(mockData, config); - expect(scale.ticks[0]).toEqual('Dec 31'); + expect(scale.ticks[0]).toEqual('Jan 1'); }); - it('should use the max option', function() { - config.time.unit = 'day'; + it('should use the min option when greater than first label for building ticks', function() { + config.time.min = '2015-01-02T04:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[0]).toEqual('Jan 2'); + }); + + it('should use the max option when greater than last label for building ticks', function() { config.time.max = '2015-01-05T06:00:00'; var scale = createScale(mockData, config); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 3'); + }); - expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5'); + it('should use the max option when less than last label for building ticks', function() { + config.time.max = '2015-01-02T23:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 2'); }); }); From ee6432b4cbb5a2bf191d5ff5bb890136a2a279f5 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 21 Oct 2017 08:43:31 -0400 Subject: [PATCH 324/685] Update supported browser section (#4818) --- docs/developers/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/developers/README.md b/docs/developers/README.md index d2b0635b095..3826230a699 100644 --- a/docs/developers/README.md +++ b/docs/developers/README.md @@ -22,7 +22,12 @@ Latest builds are available for testing at: # Browser support -Chart.js offers support for all browsers where canvas is supported. +Chart.js offers support for the following browsers: +* Chrome 50+ +* Firefox 45+ +* Internet Explorer 11 +* Edge 14+ +* Safari 9+ Browser support for the canvas element is available in all modern & major mobile browsers. [CanIUse](http://caniuse.com/#feat=canvas) From 9f32f0775a38088a829cad6e920989ee98e3c86d Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 22 Oct 2017 08:32:39 -0700 Subject: [PATCH 325/685] Attempt to fix test flakiness (Firefox) (#4880) These settings deal with browser disconnects. We had seen test flakiness from Firefox: [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms --- karma.conf.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index 3ef7f49bfa7..5601cbd7212 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,7 +14,13 @@ module.exports = function(karma) { browserify: { debug: true - } + }, + + // These settings deal with browser disconnects. We had seen test flakiness from Firefox + // [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms. + // https://github.com/jasmine/jasmine/issues/1327#issuecomment-332939551 + browserNoActivityTimeout: 60000, + browserDisconnectTolerance: 3 }; // https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647 From c81a55fed1d9da9f97cc26391083876e5471e84f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 22 Oct 2017 19:18:24 +0200 Subject: [PATCH 326/685] Add jsDelivr as CDN install option (#4881) --- docs/getting-started/installation.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index dd8cfa4a790..c8291d260c2 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -3,6 +3,7 @@ Chart.js can be installed via npm or bower. It is recommended to get Chart.js th ## npm [![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) +[![npm](https://img.shields.io/npm/dm/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) ```bash npm install chart.js --save @@ -16,9 +17,19 @@ bower install chart.js --save ``` ## CDN -[![cdn](https://img.shields.io/cdnjs/v/Chart.js.svg?label=cdn&style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) +### CDNJS +[![cdnjs](https://img.shields.io/cdnjs/v/Chart.js.svg?style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) -or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. +Chart.js built files are available on [CDNJS](https://cdnjs.com/): + +https://cdnjs.com/libraries/Chart.js + +### jsDelivr +[![jsdelivr](https://img.shields.io/npm/v/chart.js.svg?label=jsdelivr&style=flat-square&maxAge=600)](https://cdn.jsdelivr.net/npm/chart.js@latest/dist/) [![jsdelivr hits](https://data.jsdelivr.com/v1/package/npm/chart.js/badge)](https://www.jsdelivr.com/package/npm/chart.js) + +Chart.js built files are also available through [jsDelivr](http://www.jsdelivr.com/): + +https://www.jsdelivr.com/package/npm/chart.js?path=dist ## Github [![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) From 26c1936dee7dcc2f00b63af9bb04e9af1703e461 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 24 Oct 2017 19:11:40 +0200 Subject: [PATCH 327/685] Move extend and inherits helpers in helpers.core.js (#4878) Fix Rollup issue caused by early access of the `extend` and `inherits` helpers not yet part of the `helpers/index` import. Also added (basic) unit tests for whose methods. --- src/core/core.helpers.js | 32 ------------------- src/helpers/helpers.core.js | 42 +++++++++++++++++++++++++ test/specs/core.helpers.tests.js | 20 ------------ test/specs/helpers.core.tests.js | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 52 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 0b9cea52eb3..713362cd8d1 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -10,16 +10,6 @@ module.exports = function(Chart) { // -- Basic js utility methods - helpers.extend = function(base) { - var setFn = function(value, key) { - base[key] = value; - }; - for (var i = 1, ilen = arguments.length; i < ilen; i++) { - helpers.each(arguments[i], setFn); - } - return base; - }; - helpers.configMerge = function(/* objects ... */) { return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), { merger: function(key, target, source, options) { @@ -125,29 +115,7 @@ module.exports = function(Chart) { } } }; - helpers.inherits = function(extensions) { - // Basic javascript inheritance based on the model created in Backbone.js - var me = this; - var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { - return me.apply(this, arguments); - }; - - var Surrogate = function() { - this.constructor = ChartElement; - }; - Surrogate.prototype = me.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = helpers.inherits; - - if (extensions) { - helpers.extend(ChartElement.prototype, extensions); - } - - ChartElement.__super__ = me.prototype; - return ChartElement; - }; // -- Math methods helpers.isNumber = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 1ada7a754b1..2a4b0098ccf 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -250,6 +250,48 @@ var helpers = { */ mergeIf: function(target, source) { return helpers.merge(target, source, {merger: helpers._mergerIf}); + }, + + /** + * Applies the contents of two or more objects together into the first object. + * @param {Object} target - The target object in which all objects are merged into. + * @param {Object} arg1 - Object containing additional properties to merge in target. + * @param {Object} argN - Additional objects containing properties to merge in target. + * @returns {Object} The `target` object. + */ + extend: function(target) { + var setFn = function(value, key) { + target[key] = value; + }; + for (var i = 1, ilen = arguments.length; i < ilen; ++i) { + helpers.each(arguments[i], setFn); + } + return target; + }, + + /** + * Basic javascript inheritance based on the model created in Backbone.js + */ + inherits: function(extensions) { + var me = this; + var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { + return me.apply(this, arguments); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + + Surrogate.prototype = me.prototype; + ChartElement.prototype = new Surrogate(); + ChartElement.extend = helpers.inherits; + + if (extensions) { + helpers.extend(ChartElement.prototype, extensions); + } + + ChartElement.__super__ = me.prototype; + return ChartElement; } }; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index dc865fb01b2..419428bf1d6 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -6,26 +6,6 @@ describe('Core helper tests', function() { helpers = window.Chart.helpers; }); - it('should extend an object', function() { - var original = { - myProp1: 'abc', - myProp2: 56 - }; - - var extension = { - myProp3: [2, 5, 6], - myProp2: 0 - }; - - helpers.extend(original, extension); - - expect(original).toEqual({ - myProp1: 'abc', - myProp2: 0, - myProp3: [2, 5, 6], - }); - }); - it('should merge a normal config without scales', function() { var baseConfig = { valueProp: 5, diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js index b13b7cad2af..80b0640b2cc 100644 --- a/test/specs/helpers.core.tests.js +++ b/test/specs/helpers.core.tests.js @@ -369,4 +369,57 @@ describe('Chart.helpers.core', function() { expect(output.o.a).not.toBe(a1); }); }); + + describe('extend', function() { + it('should merge object properties in target and return target', function() { + var target = {a: 'abc', b: 56}; + var object = {b: 0, c: [2, 5, 6]}; + var result = helpers.extend(target, object); + + expect(result).toBe(target); + expect(target).toEqual({a: 'abc', b: 0, c: [2, 5, 6]}); + }); + it('should merge multiple objects properties in target', function() { + var target = {a: 0, b: 1}; + var o0 = {a: 2, c: 3, d: 4}; + var o1 = {a: 5, c: 6}; + var o2 = {a: 7, e: 8}; + + helpers.extend(target, o0, o1, o2); + + expect(target).toEqual({a: 7, b: 1, c: 6, d: 4, e: 8}); + }); + it('should not deeply merge object properties in target', function() { + var target = {a: {b: 0, c: 1}}; + var object = {a: {b: 2, d: 3}}; + + helpers.extend(target, object); + + expect(target).toEqual({a: {b: 2, d: 3}}); + expect(target.a).toBe(object.a); + }); + }); + + describe('inherits', function() { + it('should return a derived class', function() { + var A = function() {}; + A.prototype.p0 = 41; + A.prototype.p1 = function() { + return '42'; + }; + + A.inherits = helpers.inherits; + var B = A.inherits({p0: 43, p2: [44]}); + var C = A.inherits({p3: 45, p4: [46]}); + var b = new B(); + + expect(b instanceof A).toBeTruthy(); + expect(b instanceof B).toBeTruthy(); + expect(b instanceof C).toBeFalsy(); + + expect(b.p0).toBe(43); + expect(b.p1()).toBe('42'); + expect(b.p2).toEqual([44]); + }); + }); }); From 13e9676625723a4c6d4e8232b2fe3fa02421d38b Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 28 Oct 2017 04:20:34 -0400 Subject: [PATCH 328/685] Reset tooltip when calling Chart.update (#4840) --- src/core/core.controller.js | 8 +++++ src/core/core.tooltip.js | 1 + test/specs/core.controller.tests.js | 48 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 241448dadd7..9e4984a9af6 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -371,6 +371,14 @@ module.exports = function(Chart) { me.updateDatasets(); + // Need to reset tooltip in case it is displayed with elements that are removed + // after update. + me.tooltip.initialize(); + + // Last active contains items that were previously in the tooltip. + // When we reset the tooltip, we need to clear it + me.lastActive = []; + // Do this before render so that any plugins that need final scale updates can use it plugins.notify(me, 'afterUpdate'); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 76e06d06141..73460f8d106 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -384,6 +384,7 @@ module.exports = function(Chart) { Chart.Tooltip = Element.extend({ initialize: function() { this._model = getBaseModel(this._options); + this._lastActive = []; }, // Get the title diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index fe73d02a0c2..3ec8da50b76 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -822,6 +822,54 @@ describe('Chart', function() { expect(chart.tooltip._options).toEqual(jasmine.objectContaining(newTooltipConfig)); }); + it ('should reset the tooltip on update', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + tooltip: { + mode: 'nearest' + } + } + }); + + // Trigger an event over top of a point to + // put an item into the tooltip + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + var node = 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 + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + + expect(chart.lastActive).toEqual([point]); + expect(tooltip._lastActive).toEqual([]); + + // Update and confirm tooltip is reset + chart.update(); + expect(chart.lastActive).toEqual([]); + expect(tooltip._lastActive).toEqual([]); + }); + it ('should update the metadata', function() { var cfg = { data: { From 8a6d58d2d907fcf608b7c509d19eee929e5bd1bc Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 28 Oct 2017 01:22:20 -0700 Subject: [PATCH 329/685] Bump version to 2.7.1 (#4877) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 445dc877ab0..a7a504887fd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.0", + "version": "2.7.1", "license": "MIT", "main": "src/chart.js", "repository": { From ffbdb483105c750c836fde32501f65d9577ddc83 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Wed, 1 Nov 2017 08:00:10 -0700 Subject: [PATCH 330/685] Upgrade dependencies (incl. ESLint 4) (#4738) --- .codeclimate.yml | 2 +- .eslintrc | 8 ++------ package.json | 27 ++++++++++++++------------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index fcc885c8200..bd32376436b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -6,7 +6,7 @@ engines: - javascript eslint: enabled: true - channel: "eslint-3" + channel: "eslint-4" fixme: enabled: true ratings: diff --git a/.eslintrc b/.eslintrc index 6a31e4b6ec2..c0e1b26ce6f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,3 @@ -ecmaFeatures: - modules: true - jsx: true - env: amd: true browser: true @@ -72,7 +68,7 @@ rules: no-lone-blocks: 2 no-loop-func: 2 no-magic-number: 0 - no-multi-spaces: 2 + no-multi-spaces: [2, {ignoreEOLComments: true}] no-multi-str: 2 no-native-reassign: 2 no-new-func: 2 @@ -141,7 +137,7 @@ rules: func-style: 0 id-length: 0 id-match: 0 - indent: [2, tab] + indent: [2, tab, {flatTernaryExpressions: true}] jsx-quotes: 0 key-spacing: 2 keyword-spacing: 2 diff --git a/package.json b/package.json index a7a504887fd..83987fd129e 100644 --- a/package.json +++ b/package.json @@ -10,30 +10,31 @@ "url": "https://github.com/chartjs/Chart.js.git" }, "devDependencies": { - "browserify": "^14.3.0", - "browserify-istanbul": "^2.0.0", - "bundle-collapser": "^1.2.1", + "browserify": "^14.5.0", + "browserify-istanbul": "^3.0.1", + "bundle-collapser": "^1.3.0", "child-process-promise": "^2.2.1", - "coveralls": "^2.13.1", - "gitbook-cli": "^2.3.0", + "coveralls": "^3.0.0", + "eslint": "^4.9.0", + "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", "gulp-html-validator": "^0.0.5", "gulp-insert": "~0.5.0", - "gulp-replace": "^0.5.4", + "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", "gulp-streamify": "^1.0.2", "gulp-uglify": "~3.0.x", "gulp-util": "~3.0.x", "gulp-zip": "~4.0.0", - "jasmine": "^2.6.0", - "jasmine-core": "^2.6.2", - "karma": "^1.7.0", + "jasmine": "^2.8.0", + "jasmine-core": "^2.8.0", + "karma": "^1.7.1", "karma-browserify": "^5.1.1", - "karma-chrome-launcher": "^2.1.1", + "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", @@ -42,13 +43,13 @@ "pixelmatch": "^4.0.2", "vinyl-source-stream": "^1.1.0", "watchify": "^3.9.0", - "yargs": "^8.0.1" + "yargs": "^9.0.1" }, "spm": { "main": "Chart.js" }, "dependencies": { "chartjs-color": "~2.2.0", - "moment": "~2.18.0" + "moment": "~2.19.1" } } From 7f751c8d8082e204269945958e41d72c5d2f7d52 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 1 Nov 2017 16:00:41 +0100 Subject: [PATCH 331/685] Suppress coveralls errors if run from fork (#4699) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 862d8b0445a..9b38ab3c146 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: - gulp docs - gulp package - gulp bower - - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls + - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls || true notifications: slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA From 34709826cd05cb6bacd2150726768bd619b7c765 Mon Sep 17 00:00:00 2001 From: Aspaldiko Date: Thu, 2 Nov 2017 16:51:36 +0400 Subject: [PATCH 332/685] Fix incorrect samples titles (#4914) --- samples/scales/gridlines-display.html | 2 +- samples/scales/gridlines-style.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index 0c2dc114e6f..c21469329b8 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,7 +2,7 @@ - Suggested Min/Max Settings + Grid Lines Display Settings + + + +
    + +
    + + + + + diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 5fd4b2d6aa6..17e6c3bfa67 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -45,17 +45,21 @@ module.exports = function(Chart) { function updateConfig(chart) { var newOptions = chart.options; - // Update Scale(s) with options - if (newOptions.scale) { - chart.scale.options = newOptions.scale; - } else if (newOptions.scales) { - newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) { - chart.scales[scaleOptions.id].options = scaleOptions; - }); - } - + helpers.each(chart.scales, function(scale) { + Chart.layoutService.removeBox(chart, scale); + }); + + newOptions = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[chart.config.type], + newOptions); + + chart.options = chart.config.options = newOptions; + chart.ensureScalesHaveIDs(); + chart.buildOrUpdateScales(); // Tooltip chart.tooltip._options = newOptions.tooltips; + chart.tooltip.initialize(); } function positionIsHorizontal(position) { @@ -143,7 +147,7 @@ module.exports = function(Chart) { // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildScales(); + me.buildOrUpdateScales(); me.initToolTip(); // After init plugin notification @@ -223,11 +227,15 @@ module.exports = function(Chart) { /** * Builds a map of scale ID to scale object for future lookup. */ - buildScales: function() { + buildOrUpdateScales: function() { var me = this; var options = me.options; - var scales = me.scales = {}; + var scales = me.scales || {}; var items = []; + var updated = Object.keys(scales).reduce(function(obj, id) { + obj[id] = false; + return obj; + }, {}); if (options.scales) { items = items.concat( @@ -251,24 +259,35 @@ module.exports = function(Chart) { helpers.each(items, function(item) { var scaleOptions = item.options; + var id = scaleOptions.id; var scaleType = helpers.valueOrDefault(scaleOptions.type, item.dtype); - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); - if (!scaleClass) { - return; - } if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } - var scale = new scaleClass({ - id: scaleOptions.id, - options: scaleOptions, - ctx: me.ctx, - chart: me - }); + updated[id] = true; + var scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + scale.options = scaleOptions; + scale.ctx = me.ctx; + scale.chart = me; + } else { + var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + scale = new scaleClass({ + id: id, + type: scaleType, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } - scales[scale.id] = scale; scale.mergeTicksOptions(); // TODO(SB): I think we should be able to remove this custom case (options.scale) @@ -278,6 +297,14 @@ module.exports = function(Chart) { me.scale = scale; } }); + // clear up discarded scales + helpers.each(updated, function(hasUpdated, id) { + if (!hasUpdated) { + delete scales[id]; + } + }); + + me.scales = scales; Chart.scaleService.addScalesToLayout(this); }, @@ -301,6 +328,7 @@ module.exports = function(Chart) { if (meta.controller) { meta.controller.updateIndex(datasetIndex); + meta.controller.linkScales(); } else { var ControllerClass = Chart.controllers[meta.type]; if (ControllerClass === undefined) { diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 67dbe27f200..ee6158c35b1 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -111,10 +111,10 @@ module.exports = function(Chart) { var meta = me.getMeta(); var dataset = me.getDataset(); - if (meta.xAxisID === null) { + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; } - if (meta.yAxisID === null) { + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; } }, diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index b2615f14eec..1ca699bc132 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -775,6 +775,38 @@ describe('Chart', function() { }); describe('config update', function() { + it ('should update options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options = { + responsive: false, + scales: { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + } + }; + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + it ('should update scales options', function() { var chart = acquireChart({ type: 'line', @@ -798,6 +830,79 @@ describe('Chart', function() { expect(yScale.options.ticks.max).toBe(10); }); + it ('should update scales options from new object', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + + it ('should remove discarded scale', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + scales: { + yAxes: [{ + id: 'yAxis0', + ticks: { + min: 0, + max: 10 + } + }] + } + } + }); + + var newScalesConfig = { + yAxes: [{ + ticks: { + min: 0, + max: 10 + } + }] + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales.yAxis0; + expect(yScale).toBeUndefined(); + var newyScale = chart.scales['y-axis-0']; + expect(newyScale.options.ticks.min).toBe(0); + expect(newyScale.options.ticks.max).toBe(10); + }); + it ('should update tooltip options', function() { var chart = acquireChart({ type: 'line', From 9a7182ba36d3a9f1e73f2d2cdc0011ae7837d672 Mon Sep 17 00:00:00 2001 From: beiz23 Date: Thu, 30 Nov 2017 22:41:32 +0900 Subject: [PATCH 346/685] Fix typos and broken links in the docs (#5010) --- docs/SUMMARY.md | 1 + docs/axes/cartesian/README.md | 6 +++--- docs/axes/cartesian/linear.md | 4 ++-- docs/axes/radial/linear.md | 4 ++-- docs/axes/styling.md | 4 ++-- docs/charts/line.md | 2 +- docs/configuration/tooltip.md | 2 +- docs/developers/axes.md | 4 ++-- docs/getting-started/README.md | 2 +- docs/getting-started/installation.md | 2 +- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 60630090003..8591dad86ae 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,6 +7,7 @@ * [Usage](getting-started/usage.md) * [General](general/README.md) * [Responsive](general/responsive.md) + * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) * [Events](general/interactions/events.md) * [Modes](general/interactions/modes.md) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 64ce32073c2..8518da9f250 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -75,13 +75,13 @@ var myChart = new Chart(ctx, { data: { datasets: [{ data: [20, 50, 100, 75, 25, 0], - label: 'Left dataset' + label: 'Left dataset', // This binds the dataset to the left y axis yAxisID: 'left-y-axis' }, { - data: [0.1, 0.5, 1.0, 2.0, 1.5, 0] - label: 'Right dataset' + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], + label: 'Right dataset', // This binds the dataset to the right y axis yAxisID: 'right-y-axis', diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md index 00f7aced1fe..1d0292074a8 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -43,7 +43,7 @@ let chart = new Chart(ctx, { scales: { yAxes: [{ ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } }] diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 1636781a84e..adebe77cc61 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -36,7 +36,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -58,7 +58,7 @@ let chart = new Chart(ctx, { options: { scale: { ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } } diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 1de31e01f6c..f60afd9bb68 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -35,8 +35,8 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. -| `minor` | `object` | `{}` | Minor ticks configuration. Ommited options are inherited from options above. -| `major` | `object` | `{}` | Major ticks configuration. Ommited options are inherited from options above. +| `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. +| `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. ## Minor Tick Configuration The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. diff --git a/docs/charts/line.md b/docs/charts/line.md index 40441b7afe3..90471e462dd 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -119,7 +119,7 @@ The `data` property of a dataset for a line chart can be passed in two formats. data: [20, 10] ``` -When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#category-cartesian-axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. ### Point[] diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 51004843a33..1be2c26bc0e 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -30,7 +30,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `footerFontSize` | `Number` | `12` | Footer font size | `footerFontStyle` | `String` | `'bold'` | Footer font style | `footerFontColor` | `Color` | `'#fff'` | Footer font color -| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. +| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. | `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. | `yPadding` | `Number` | `6` | Padding to add on top and bottom of tooltip. diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 8d120195ae9..7d6c74e32c9 100644 --- a/docs/developers/axes.md +++ b/docs/developers/axes.md @@ -78,14 +78,14 @@ To work with Chart.js, custom scale types must implement the following interface // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array - // @param includeOffset: if true, get the pixel halway between the given tick and the next + // @param includeOffset: if true, get the pixel halfway between the given tick and the next getPixelForTick: function(index, includeOffset) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param value : the value to get the pixel for // @param index : index into the data array of the value // @param datasetIndex : index of the dataset the value comes from - // @param includeOffset : if true, get the pixel halway between the given tick and the next + // @param includeOffset : if true, get the pixel halfway between the given tick and the next getPixelForValue: function(value, index, datasetIndex, includeOffset) {} // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 9266b309938..bff52c72dd6 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -40,4 +40,4 @@ var chart = new Chart(ctx, { It's that easy to get started using Chart.js! From here you can explore the many options that can help you customise your charts with scales, tooltips, labels, colors, custom actions, and much more. -There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attatched to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file +There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attached to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index c8291d260c2..613d7aa7a6c 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -36,7 +36,7 @@ https://www.jsdelivr.com/package/npm/chart.js?path=dist You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). -If you download or clone the repository, you must [build](../developers/contributing.md#building-chartjs) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. +If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. # Selecting the Correct Build From b835df02cd05830fb734806f3af27fa43cc5674c Mon Sep 17 00:00:00 2001 From: JohnShaft Date: Fri, 1 Dec 2017 18:56:49 +0100 Subject: [PATCH 347/685] Add Angular2+ libraries for Chart.js in docs (#5006) --- docs/notes/extensions.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 9b17418e2a5..0998641bacc 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -25,7 +25,12 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ## Integrations -### Angular +### Angular (v2+) + + - emn178/angular2-chartjs + - valor-software/ng2-charts + +### Angular (v1) - angular-chart.js - tc-angular-chartjs - angular-chartjs From 15d1056b537a07b613f6dfe56fc920390c1dd267 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 2 Dec 2017 12:38:36 +0100 Subject: [PATCH 348/685] Implement equally sized bars (#4994) When `barThickness: undefined|null` (default), we compute an optimal sample size based on the smallest tick interval reduced to prevent any bar to overlap (bar equally sized). Also added support for a special `barThickness: 'flex'` value (previous default) that globally arranges bars side by side to prevent any gap when percentage options are 1 (variable bar sizes). --- .gitignore | 1 + src/controllers/controller.bar.js | 150 +++++++++++++----- test/.eslintrc | 1 + .../bar-thickness-absolute.json | 42 +++++ .../controller.bar/bar-thickness-absolute.png | Bin 0 -> 5055 bytes .../bar-thickness-flex-offset.json | 42 +++++ .../bar-thickness-flex-offset.png | Bin 0 -> 4583 bytes .../controller.bar/bar-thickness-flex.json | 41 +++++ .../controller.bar/bar-thickness-flex.png | Bin 0 -> 5095 bytes .../controller.bar/bar-thickness-max.json | 41 +++++ .../controller.bar/bar-thickness-max.png | Bin 0 -> 4421 bytes .../bar-thickness-min-interval.json | 40 +++++ .../bar-thickness-min-interval.png | Bin 0 -> 5180 bytes .../bar-thickness-multiple.json | 46 ++++++ .../controller.bar/bar-thickness-multiple.png | Bin 0 -> 5847 bytes .../bar-thickness-no-overlap.json | 46 ++++++ .../bar-thickness-no-overlap.png | Bin 0 -> 4211 bytes .../controller.bar/bar-thickness-offset.json | 47 ++++++ .../controller.bar/bar-thickness-offset.png | Bin 0 -> 6577 bytes .../bar-thickness-single-xy.json | 40 +++++ .../bar-thickness-single-xy.png | Bin 0 -> 4514 bytes .../controller.bar/bar-thickness-single.json | 43 +++++ .../controller.bar/bar-thickness-single.png | Bin 0 -> 4374 bytes .../controller.bar/bar-thickness-stacked.json | 48 ++++++ .../controller.bar/bar-thickness-stacked.png | Bin 0 -> 5586 bytes test/jasmine.utils.js | 3 - test/specs/controller.bar.tests.js | 82 +--------- 27 files changed, 586 insertions(+), 127 deletions(-) create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.json create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.png create mode 100644 test/fixtures/controller.bar/bar-thickness-max.json create mode 100644 test/fixtures/controller.bar/bar-thickness-max.png create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.json create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.png create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.json create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.png create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.json create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.png create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single.png create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.json create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.png diff --git a/.gitignore b/.gitignore index 53ce8fedb1c..cfb878a6dee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ bower.json *.log *.swp +*.stackdump diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index b811c6f05b1..ff2b56ae5a0 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -95,6 +95,93 @@ defaults._set('horizontalBar', { } }); +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, pixels[i] - pixels[i - 1]); + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var size, ratio; + + if (helpers.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale end extremity. + prev = curr - (next === null ? ruler.end - curr : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - ((curr - prev) / 2) * percent; + size = ((next - prev) / 2) * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + module.exports = function(Chart) { Chart.controllers.bar = Chart.DatasetController.extend({ @@ -262,17 +349,22 @@ module.exports = function(Chart) { var scale = me.getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var pixels = []; var isHorizontal = scale.isHorizontal(); var start = isHorizontal ? scale.left : scale.top; var end = start + (isHorizontal ? scale.width : scale.height); - var i, ilen; + var pixels = []; + var i, ilen, min; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { pixels.push(scale.getPixelForValue(null, i, datasetIndex)); } + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + return { + min: min, pixels: pixels, start: start, end: end, @@ -332,51 +424,21 @@ module.exports = function(Chart) { calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; var options = ruler.scale.options; - var meta = me.getMeta(); - var stackIndex = me.getStackIndex(datasetIndex, meta.stack); - var pixels = ruler.pixels; - var base = pixels[index]; - var length = pixels.length; - var start = ruler.start; - var end = ruler.end; - var leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, size; - - if (length === 1) { - leftSampleSize = base > start ? base - start : end - base; - rightSampleSize = base < end ? end - base : base - start; - } else { - if (index > 0) { - leftSampleSize = (base - pixels[index - 1]) / 2; - if (index === length - 1) { - rightSampleSize = leftSampleSize; - } - } - if (index < length - 1) { - rightSampleSize = (pixels[index + 1] - base) / 2; - if (index === 0) { - leftSampleSize = rightSampleSize; - } - } - } - - leftCategorySize = leftSampleSize * options.categoryPercentage; - rightCategorySize = rightSampleSize * options.categoryPercentage; - fullBarSize = (leftCategorySize + rightCategorySize) / ruler.stackCount; - size = fullBarSize * options.barPercentage; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); - size = Math.min( - helpers.valueOrDefault(options.barThickness, size), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); - - base -= leftCategorySize; - base += fullBarSize * stackIndex; - base += (fullBarSize - size) / 2; + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); return { - size: size, - base: base, - head: base + size, - center: base + size / 2 + base: center - size / 2, + head: center + size / 2, + center: center, + size: size }; }, diff --git a/test/.eslintrc b/test/.eslintrc index 8e8f899bffd..9d98c452878 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -11,3 +11,4 @@ globals: rules: # Best Practices complexity: 0 + max-statements: 0 diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.json b/test/fixtures/controller.bar/bar-thickness-absolute.json new file mode 100644 index 00000000000..599b090d6d1 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-absolute.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2019", "2024", "2025"], + "datasets": [{ + "backgroundColor": "rgba(255, 99, 132, 0.5)", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": 128, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.png b/test/fixtures/controller.bar/bar-thickness-absolute.png new file mode 100644 index 0000000000000000000000000000000000000000..40172b39241f2a6e13d201f7a47509137acc468e GIT binary patch literal 5055 zcmeHLe^gU-8-MQQA{Pi8>=>pT*g%F!uR}A%6F+XTcSFf(?|F;fw^xU!Q)HUUOg4nU zm$D{epsxp)S!~)lQc$2$NV3L`l|UHlgi1Na29$^}_XBYZ*w%N}@f^!PJN?uCxaZ#I zJkR&@e7?_lew|yfK`e?0TMz~S5o>Y;{{kTKCjs;v{QCMpPXPe?tu=zj{w?2RHD*-3 zTrh9XV6On}+1nL+73+n`aTzjy^9*HdyPx`SC;)5HD z={wm`ezzq=nl^~C*y)FEyRw8cx}ZCb2nj^mfJjRx!*4U0A`f7(^Wjxd7?;t<1l$IK z{AZ?=d|r9Hx7!h;)?HWjxRhC6m@;R*C0j#QmX=PBSV!G$#p@Hm#k=m};$)TaQXTD8 zYN@)vcDuU&0n;O;ritD&lx4w_yMz5jvxKB9Pqj9>l%e{`<&z0GVJ%4%tWs|v6k?IHDIupoPKSQA8i*#(-Pa^e7MC;b5e5=ToBsk z+;tQy7y6Ya*CP0s=T`rl^`P4u{TOZ!hCQb;q`I)e_V`HQj8q-tx5fc}Co~x~!wOx^~4+RjEEF z@in$!H4a;n5921q zNS3G_<8wgvb&*aYgdWw86{~oV9BGSdV58ybZHE+Bxkf%@ehkF11Ma7jVWfM!aUmPE z4VAd~iC`1TZ%7#EdEeaCk_^e=;{#*SsFC_0)J zfep#tj8B5R9LEYN2kI)WFRP439j_?Hq)8x6k{whhLQJSL*1&<0&e6@!MkD1hMK7NR z(sPCH$0tBvmy_aiA(*pxl?df8$4U+Tqy)WgxXgsoYw1H-}q<-`n&Fs6`eU*h&JI;=)6F~KLi(@mTSyp2aQyh9G_ybxW&e`a$ zW}%e2XI(4F4KZs9x#WSLJ*T!%mgn{?^{UyZGNgYpo&2swRI-p{iM-80p-f(!<7JwO zbvqgIQci$wH0kLq--#bN*aT<*&vxRGGww zm5bU~M5i-OEriSAmL{lQz(D;_CqEl6JIdr}G-&tCV(LXJ7D#I+^xyBo@a?!j=*A1`Q*2oMZJ@Ykdc zoVE3Y?(+Zf QFFJrV{}c-vAJ2dN7yOH(lK=n! literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.json b/test/fixtures/controller.bar/bar-thickness-flex-offset.json new file mode 100644 index 00000000000..1776b07be16 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-offset.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.png b/test/fixtures/controller.bar/bar-thickness-flex-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..e20cc4eb450c8f90b94da145252619ba60cec024 GIT binary patch literal 4583 zcmeHK>r)d~6hC{j;0A-)Xtj!nOAwV7LA2JOJQfI`#p3(XN`)#dK1oEyAWC*)%R_C{ zX~$@dQroe9utht>X2%I@XKM#PQNCD%Izl(dEc>tQKnfjy!re&S2G0tx`jY@xf;Lg5h ziy|8?{d8f}X(nc^n5?a6xNyVo^736ZpG?nDSG(R_d3l|&Ew{9+tJ|)gaQ?@xVrRYX z%ouk`x0*1VE0Gu~`|h1^w6xq8I6>-Bww_45o=mf*Xo_+zo0m`~^(q;mdwMm9HVq5W zDSzx__mAV?_o1<@*ugMD#}ZFCU6N0TeE&_^tr@Q6pfp$KCsytk;CTh**+M(@JlL=7 zTdFZx1u$7F`-8*`G-bQ7N=5o@0=RgVmj@6ct>C{%;bCLe(j8I?+6&c==H*E=Eq$;c z$Rc&4(5>30AbFu2IG0~N#02v^1jFsw+{9!ac9k3b$w1Vx_)X=EDvC0y^6D*81V%W2 zWx*H{jk}gvuG=_cAes&e`IeXCQM-72uZn00;k3WZqVpXG!KM0-1oI*u+JG=yXYpWC zGaH%bLj>!Z42@|94{`dt3MQ*mfYZC)N?}ch5!J&-47?nP3B5Dx%n(t5St~e?l1@WG zqjB;%q5w-y%Ko$!VX*%8a51a%Lx=+2w(8|@+?6r$ufcRx5)WDDzZ%Nam7zs-cx4iY z!0Pn}D}?$1(nub}-AV?!C~**FCe7>DMG!SoqpW-@h)pckbM7jJTpPl|waHHd8T-cq z=ypbI)*1mFL2jZbF%&gXdu{|1iOK_qwF*)UdXBMs1IRIsgUlmW_A+%Jp=V`UwndtP zk*_gT6VZ*@T}SRtX7spHzH+k0P$Vq7SIrqmV1XyS z7MLU#p-@!%10|k98+kGJGU-9+Xpr!596KCwNYm>Sq8}kS4VICt4&l_5p2|*=U%{v% z-iRG6+HhBN!-EX=v_Ga0xO)-j3PFqUpKbLQF&9>|ZY=2w0RvEU!dMRK2GG;!7fQad z<|q?4hJz686N-*PLT9BN4Q1kHW8JPbBR7YkjmWQCD+US{-28L>qaAoWXs&;#HP$MHLyI{m%-R^Of%n||?qd^a6pD}wchMGUe$I$ytn>iO= zJ%wW;Un(6Za*O9y_KQ2rY9cdNOTsdE$h|qTEer>n-IEQ|qt4nGp{lrP1723G1=Puo zFN-VS!C6OKIu8%Wzv@%);qd?BgS?P3)?zn#nJtFOZM|1I@rD@ucxj1345p|(^Ye{1 Z$qf1fHLEYRwyyyEW~OH9tCDjpe*@UW)UW^m literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex.json b/test/fixtures/controller.bar/bar-thickness-flex.json new file mode 100644 index 00000000000..0bef9db1828 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex.png b/test/fixtures/controller.bar/bar-thickness-flex.png new file mode 100644 index 0000000000000000000000000000000000000000..791a29d25d380f64f8c33fc3a928cebc531348cb GIT binary patch literal 5095 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.json b/test/fixtures/controller.bar/bar-thickness-min-interval.json new file mode 100644 index 00000000000..e5861685cd4 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.png b/test/fixtures/controller.bar/bar-thickness-min-interval.png new file mode 100644 index 0000000000000000000000000000000000000000..ae01a9b945c4ef691bd0a861cdce518f6c23e872 GIT binary patch literal 5180 zcmeHLT}V@57=FKRCpl-fJT8Mu%42Q1ewDh=Na{G1wlp0gSW(ML`nhSNprqzHvmYzS zriLYy*bf*5DNz}Px{At}7}mvtEuAS=gtgR`ZEoxANcKZFK{vsbj~rCW)3z zeO&CWIR*(gX|d zW^_9}bzk71gxr=Z0C&+eq?ob{jIb_h5@E|o9>*CE zad4-1!F~x(N75xK24@h0ZR&~*vvIZzggkV|xo}z1#W1jI^hEb~7qg`# zS6(zQUNr(Ce1l&j-<`<1 zuW&%RFvHX?M5yVoi=tEKGk}|c)FfoHurPifCMx&=wJic%^;AwncKdm-T*8wJjuZ4-Urd95b&9nFGPT zq@It%PWvR*C(HK&GywdxOA78D7HK1;By2o)6&6%Z8= z6%Z8=6%ZBpYXwY|$m8uzYFmGB^qv2G!^*(>5rcV6TOvGn{OmhyXJ$Z;$$vZOx%9>1 zXlpV1`r;~r1BKqLm?=&E&5Mk@*a$gFlziMglaCDwJ~m8b38Xu$|AVE$^(4hz?hio> z?`ux;zGlffjEg9Wo1=iLDp2vZNkx4R8*<3ffI^9MyaCcpY*+y&te{zq>ys(2K3r3X zDjJB1M^>J3vmDaHE~;9|K#dhtYRt@|xCaU-`=vR8&6mRw9;lqKTjRYcKw1st>IwTs zu+ehRYQ=^{g}*0faU}lrp&J<*@&%qaQcIhcB6>xO@E37sPv$~s?}p~r1>W`$e=7*u LtlZ4&>Y}o5GpccS literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.json b/test/fixtures/controller.bar/bar-thickness-multiple.json new file mode 100644 index 00000000000..fc39849ae86 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-multiple.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.png b/test/fixtures/controller.bar/bar-thickness-multiple.png new file mode 100644 index 0000000000000000000000000000000000000000..d38405292c3b496ae1414813c6bfd120f83ffc8a GIT binary patch literal 5847 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-offset.json b/test/fixtures/controller.bar/bar-thickness-offset.json new file mode 100644 index 00000000000..b31eb739d2b --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-offset.json @@ -0,0 +1,47 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcecac88a409dff7867684f0987662f1cc4f46e GIT binary patch literal 6577 zcmeI1dr(tX9>>qU1PCCT2rdYLP1YAwQ1A(uLZWCXB4|sisJwiEyU1H4kPw2dB4Y(B zAdN(VrE80{pdvvGk|-h~V0<P+BsAGF;?NZln|8v(uX9xG*@5C3hE3~{-J6a3a+92_J`hGf# z*|}EDqvA<5x~XMNw^ke;J~Cd^TwKr9Pz1{30{S7nKz1scEzii3Ue=t?%ECbKTey2V zVO^tGWyCcSSwusRYy5n!VAJ<5^Yx*jfc4VQe+(u}XPpN{Ml_mXZ0~yDH5d5gJ2bRk zHD4#*+y*AcR?z0bs09#&&6I1d0Po<_!29;@2nPQe1H>#D>avDw)pOulRQfUq?uFq* zW`?UPn(<^6n&H&mfs-^# zL(jg16E|O@2nuca()x>K@BRknpFP}=g zLIj3olwgSJG_x6&47>0+8aqbH+gpVdg}b14b%uQN^gw|Xw!IFZuXvocMv=E9)h_x_ z=$$FnWU$T+2otoo3TNMLHB9y2o^2&gG=UR@Y9X3&sbjc=pJpY&hv>@&m6QlobFA)1 z9vs+Ghcs)RSKC^XM{}03T!8S7Pl6|@4%}ELdK>_56K8D*{H)b3Sg?Xwbe4he1Jm?J z0R8Y^<1YoUZ`eMBUFB}xb_kIS?{-00hCKuHK8_)SyZdc6!w9-a?b`vW7PfS2nFLEF z{Ljl__%FaCfj8s8%ic4Q9L%C>q$S)VIM6gS(=QjK85>I2$^?@IsE~ESq1L5v!)O*)fitVLD+)`m?FUb zm_d#m}9b--exEpVikI*fd)V33#&Lc6MfZI86^41(R3Pl#svzh^VwKbWs7_9V# zuvE9P{}={L(5!sqj0M5^o_?6{XdZyhf%{0r$pf`s1dy*OueK)FeuNMfIFAUI7jaa!}IN}1L}K+@gk zc-FYj9@^?rLswtD$c+t^EQ4Xm#LRIlB8w1AAkSkG%<>3oHj1>nf6f5^`m#8fuOsx|NhUb2S{7!XxV@MZQuUgqy)Yc6Rhkh zaWEVE92ITaD!TsF-MGOcG~+8d+syBaE3U$L2}B=fBKl??iDqo|3oaaVRXn)Vm@#`L zI6E*`YWkXcB)>;#C@(uS_`!;5?@GtR@VJ}3Yfz#E{%$}(dp(qHdMp@z{{)4oKa;*N zUzTHDp%Uv;iK4@Sc~j3z1T8}1A3l6aiz>LmRpe2|mO|L|9a*w~)~2CZ<-t+=;)H*S zwH-B*081*8O!ktB zZDP_Wj?sreB0fs{E3GwxhmvAUa}zBJ%A!#%qWb^LwkTEujvN6PZZ-(xhmWecmG^2T zbmk-a%xro>zKP=}s*H@2s`pSwGp6ns7v{>3muivi5LQ3c7t0 z8N_<>urPrVhFCG34WU!z_-oG;VC9~>jaFI;s=lMN{bXYa+%2S%E)2eSMeDKsmdZxBxZRYclMZED0)lfrdm0Wc` zg@C*O@~6T7q90hf2|AN!$LQ-F|Ce7JZNY-7ps9{GQu2tr42drlgYVK`A0>|JEQ zI{#Opt)DIb#f~7Z;n==uNAfI=(!$#}p-j7=b>wpwrwT6fU$b#qv5qPKJC1aH_G=MK fy92n%3ZcHs(Z24L4=Kp67!bZGYGdPu#H0TJN7H47 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.json b/test/fixtures/controller.bar/bar-thickness-single-xy.json new file mode 100644 index 00000000000..76caa37fa1a --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single-xy.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [{"x": "2022", "y": 42}] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.png b/test/fixtures/controller.bar/bar-thickness-single-xy.png new file mode 100644 index 0000000000000000000000000000000000000000..26171e531a2b87dd1fcfb8014cb437a531fbe1c4 GIT binary patch literal 4514 zcmeI0`%e^C6vxlK49iGanFX=RW0_r~+7*z{ip63aV0jxvx*%;qC)NfVP$FmwvC6EZ z3L>~z*GD3SL`gw;Y}1w$f^@ID?9<=Qb;#mn#} zAZ3ahH+uH;xrIhzV^=Lxloe`-T! zy#*+GnXSh>PVz{v&^XpsJpbqNsj-oq$+8qy}0O1*@dHpJ+~Iv^*{8~9>) zg5-iJALB|Sw{Iw~lai(FEYq~J?S zwMo}u8p%VpP+ExO56(CO$q^XmSmBRJPh%n$=IiWJtZ)rTRnuurByzlt>yEB~-EK_N z(UOB!HP-3ZR{cIYfaJyI1m+fsp*zjpp=iSVcjZK=a*;ghZ@$fzF=Gb=@L#%-4MUOD zwA=J>6B{r`W1o>0j50@34qxFqkb#y!Z6vDhO193l3icxTo04j)KaxjVVw0^Q)@bHG z#XR}iE84AZ1P79nrZ{5^p0Fd!*h}spxxt*yjAH)ZB~8UpHR;mG(v_tvOIN;i)GNYo^I(l4m zT{D(7UJz`k#Z_D@Zp`}?Dq**vLdbO54PlvV3OW$1mnwAsL-x&&P4=lD-n8j?-{*Os z-~YL!$EaSmBt-CnfDjU*RAlB5LgOn%0w>|)d(-7Lga~_-nXl$mti0>G@Wtw%LwL@} zJvX}rBN->}Ww48Os16UNEAkUgFP9$us{xnMu?+KeSMNK{pm~X)^seCb8fBI?V@;IWm954q8B|@o{@U}lr-?yLG zV()y+3lz$NWF-28+#qJVrV-OPtm*^usPJUCMJ#~{nIFtFvYl8^CIg5oq#cS z@!?b;?tIyi8KR?k*5~h58 z-a>c9Y@%>S5!kfnrT5x=1>p%{bdDQK;PV6v<14DR&LbTGN21`P6t{|eSe_dWpfOV= zOrMFRH_Dp29RkL9X%?<+3zm`FL36M+&&2*L8V~Mv&Ei7UMkDG}L|yN`DCHOb(36M# z35dF8@-Hj1skLEexI8i9#45+W4LQDo7@H(X(+tgKSY|l`&XMq<4PpN0#w3u!J%UgTD|In&k*KqpwzW-YeLK&EFPq8QXteC4Oei>7^-4T zT9T5geJ_QplMY1PPaXd?m$%68|D{R zGFKsvvzi6O@%0V>bxDyB(@}b%+cj@$DA{)lGZE${piC>_)`XZ!1`lSuY(uCW4n$oM zYbEE7TIy`%;wdv%C;bq2gmn$Zk+O^kp@fmV!BC2&?opMmARN8Kc5r~}Fl!-qSzCX^ zBlG~tAjBztjR8b`iUvT-L`aBG^b12<{9y;#b!|GTojC)PmS%2EhzY57T??I&gHTs% z5cRn*HMvpHxip8A{;uNc`fHTRgMB3^!apvanVbG`;U5?Ne;3dG&GL^6|G4=7#)b3Y9D-L8dmEzmI>>Vurr}-0 d)*jl+zg+8GC)Z!M<9!EFE?$=TO-Au2{{nTa>rMaw literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.json b/test/fixtures/controller.bar/bar-thickness-stacked.json new file mode 100644 index 00000000000..aa2d825533f --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-stacked.json @@ -0,0 +1,48 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "stacked": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "stacked": true, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.png b/test/fixtures/controller.bar/bar-thickness-stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..696829ee39b400f7bd77dbc1b930784d3cf42542 GIT binary patch literal 5586 zcmeI0dr(tn7QoNFbD=k&B?Jq!IBS4_Aj(5$&~7QYAYcMnL+g%itJ`2@mSvYUUF{6C zJaWTQ9#u3vEViIQZO83)6_qNW0>Lh8k@`s0;v)oHr6`mTKn)O*>`APYcK_W!c02ha zljJ++e&5MC=lh*=QkJ@Q&3v~P+yG#{Sd{Q80Er(7pk48;Q*|i=;JFXQ330#9d1K^O z$7he;i90{Gy=q?QIq&tUM{bD&CKfbiX_;le(_VO;^IpK`Sy9yH{`lX{d*~sd55DA* z-B`NwN}{_qldPcs?(KO`K*cNftSmk?FqnUOh3t~f{LzRdX0UE_*j!t(axgaA$L7gr zSRhFGXBvyl^0>RkY_8FNv$AEpbM&aL*6WFIgB#6;b$rF);#bWUE*Ey+@!qGzrzm<; zC?AeeD;7I8*&W9~O!3CIT~Wa0zx4?V&AhMb9C|UX1Yz{us(l^>xXB)p>`#u|?$9&( zQj}&(b7IEN@vyDS!O%W-V0W*#p|`PnzqEZh&uTDw=5P@47AYREZNB7Jx4G~|438Z}Z| zlhK>Q_^cSR4s|C!ap^9M%DJU*suFG_k@db9y0=6a`ri}B_E4H=9&~9o{R>| z9bv(}n=e`U@w1n7E_ z&;JlAcCY`KFFbW$)9!ix(^B!vAriiuo~ zgxJdheT_^DbP%Gjf{R94ruXx?Pf~Q%s)6y`mvXEiihhWWQI}MFCQ1*DK|JWWpj2`07RpQ zlSrrM{?GEuG9i52D?e-WlG&Of^G{SMCaqH2H6(^E;^o)aZN@X*!!oncyg%&f;j=Ba zUTOY!p=#ki%NpC=lIS zKu7YUdKLGoOqr9_Yx-=L)h*_k#cdPSoI1-LYyy1U*iQuztnF-oLhZmdPv{T>C8!GG z!v|Mtg+8!O43YqXTdJ?4SqK8MvJ3l!6s+q9b^t9YaN(*bCY}dnUH41eX?DD5(lu=f zhZW!RV;Ovv3Lv2RrU!y!V2V4t7sVv-!1ICxVjez^tEo>;aIXH(`oY+vpES#@+P<02 zGv6A^JPKx{)_#k$F6|dKYoPk9`q1H6E=Vn$oRmunn^xMI*V3+du_Drs(?N#z02m5wzn2peY=h;W9A2KbRQR}R)G}$7O=`MKd52qj+~s-ZlTyWS_ho&i+H4%mw-{C| zQW%W6ZFlu+p(xDDadUAK-7Og?EP8eRqb-YOF8cUzn@e-0G?VI^M@dM}|(h{*(OaTm^XQ#oWnR88RySo|AA~lbUo!H7tv)e%czLx`W~z#9~oC903IqLx@H;2%A?h>R2c0A{DbvmRD%oX6T+FB Date: Sat, 2 Dec 2017 15:24:57 +0100 Subject: [PATCH 349/685] Fix issue #4928: linear tick generator doesn't round values to needed precision. (#4943) * Fix issue 4928 - linear tick generator doesn't round values to needed precision. * Improve: replace toPrecision() in toString() to improve readability. * Fix: logarithmic tick generator doesn't round values to needed precision. * Fix: rounding tick values didn't work for negative values. * Add: Core ticks tests --- src/core/core.ticks.js | 13 +++-- test/specs/core.ticks.tests.js | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 test/specs/core.ticks.tests.js diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 2dbc27474d4..655369a17c8 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -80,10 +80,15 @@ module.exports = { numSpaces = Math.ceil(numSpaces); } - // Put the values into the ticks array + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); } ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); @@ -121,6 +126,7 @@ module.exports = { exp = Math.floor(helpers.log10(tickVal)); significand = Math.floor(tickVal / Math.pow(10, exp)); } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; do { ticks.push(tickVal); @@ -129,9 +135,10 @@ module.exports = { if (significand === 10) { significand = 1; ++exp; + precision = exp >= 0 ? 1 : precision; } - tickVal = significand * Math.pow(10, exp); + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; } while (exp < endExp || (exp === endExp && significand < endSignificand)); var lastTick = valueOrDefault(generationOptions.max, tickVal); diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js new file mode 100644 index 00000000000..6d7c2b3c111 --- /dev/null +++ b/test/specs/core.ticks.tests.js @@ -0,0 +1,87 @@ +describe('Test tick generators', function() { + it('Should generate linear spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'linear', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['-1', '-0.8', '-0.6', '-0.4', '-0.2', '0', '0.2', '0.4', '0.6', '0.8', '1']); + expect(yAxis.ticks).toEqual(['1', '0.8', '0.6', '0.4', '0.2', '0', '-0.2', '-0.4', '-0.6', '-0.8', '-1']); + }); + + it('Should generate logarithmic spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + legend: { + display: false, + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }], + yAxes: [{ + type: 'logarithmic', + ticks: { + min: 0.1, + max: 1, + callback: function(value) { + return value.toString(); + } + } + }] + } + } + }); + + var xAxis = chart.scales['x-axis-0']; + var yAxis = chart.scales['y-axis-0']; + + expect(xAxis.ticks).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + expect(yAxis.ticks).toEqual(['1', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1']); + }); +}); From 4e47c178e42fe84e40d39f9e2b7e26030cc0fc23 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 13 Dec 2017 00:43:17 +0100 Subject: [PATCH 350/685] Fix tooltip animation when target changes while animating (#5005) * Fix issue #4989 - tooltip in 'index' mode doesn't animate smoothly. * Change: different approach for smooth tooltip animation in 'index' mode, when target doesn't change. * Fix: jslint error * Fix: remove spyOn pivot from test * Add: setAnimating-flag in transition used to set on tooltip.transition to keep track of tooltip animation. * Decrease code complexity * Revert transition and complexity changes Add: use 'tooltip._start' as workaround check for tooltip animation status --- src/core/core.controller.js | 10 +++++++++- src/core/core.tooltip.js | 32 +++++++++++++------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 17e6c3bfa67..6026e8620fb 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -849,7 +849,15 @@ module.exports = function(Chart) { me._bufferedRequest = null; var changed = me.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // for smooth tooltip animations issue #4989 + // the tooltip should be the source of change + // Animation check workaround: + // tooltip._start will be null when tooltip isn't animating + if (tooltip) { + changed = tooltip._start + ? tooltip.handleEvent(e) + : changed | tooltip.handleEvent(e); + } plugins.notify(me, 'afterEvent', [e]); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 0072580dfbf..9b09d760443 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -852,25 +852,19 @@ module.exports = function(Chart) { // Remember Last Actives changed = !helpers.arrayEquals(me._active, me._lastActive); - // If tooltip didn't change, do not handle the target event - if (!changed) { - return false; - } - - me._lastActive = me._active; - - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; - - var model = me._model; - me.update(true); - me.pivot(); - - // See if our tooltip position changed - changed |= (model.x !== me._model.x) || (model.y !== me._model.y); + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; + + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; + + me.update(true); + me.pivot(); + } } return changed; From f0bf3954fe363eda86a3db170063aba34f1be08e Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 13 Dec 2017 00:43:51 +0100 Subject: [PATCH 351/685] Fix issues #4572 and #4703 (#4959) - issue #4572: logarithmic type if all numbers are zero browser crashes "Out of memory" - issue #4703: [BUG] Browser unresponsive on bubble chart with logarithmic axes --- src/scales/scale.logarithmic.js | 59 ++- test/specs/scale.logarithmic.tests.js | 497 ++++++++++++++++++++------ 2 files changed, 435 insertions(+), 121 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 09296c19568..87fe9b1af2b 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -18,11 +18,9 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var opts = me.options; - var tickOpts = opts.ticks; var chart = me.chart; var data = chart.data; var datasets = data.datasets; - var valueOrDefault = helpers.valueOrDefault; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; @@ -68,27 +66,23 @@ module.exports = function(Chart) { helpers.each(dataset.data, function(rawValue, index) { var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } - values[index] = values[index] || 0; - - if (opts.relativePoints) { - values[index] = 100; - } else { - // Don't need to split positive and negative since the log scale can't handle a 0 crossing - values[index] += value; - } + values[index] += value; }); } }); helpers.each(valuesPerStack, function(valuesForType) { - var minVal = helpers.min(valuesForType); - var maxVal = helpers.max(valuesForType); - me.min = me.min === null ? minVal : Math.min(me.min, minVal); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + if (valuesForType.length > 0) { + var minVal = helpers.min(valuesForType); + var maxVal = helpers.max(valuesForType); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + } }); } else { @@ -97,7 +91,8 @@ module.exports = function(Chart) { if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } @@ -121,6 +116,17 @@ module.exports = function(Chart) { }); } + // Common base implementation to handle ticks.min, ticks.max + this.handleTickRangeOptions(); + }, + handleTickRangeOptions: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + var valueOrDefault = helpers.valueOrDefault; + var DEFAULT_MIN = 1; + var DEFAULT_MAX = 10; + me.min = valueOrDefault(tickOpts.min, me.min); me.max = valueOrDefault(tickOpts.max, me.max); @@ -129,8 +135,25 @@ module.exports = function(Chart) { me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1); me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1); } else { - me.min = 1; - me.max = 10; + me.min = DEFAULT_MIN; + me.max = DEFAULT_MAX; + } + } + if (me.min === null) { + me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1); + } + if (me.max === null) { + me.max = me.min !== 0 + ? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1) + : DEFAULT_MAX; + } + if (me.minNotZero === null) { + if (me.min > 0) { + me.minNotZero = me.min; + } else if (me.max < 1) { + me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max))); + } else { + me.minNotZero = DEFAULT_MIN; } } }, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 3fd92197763..3d79e6cfc37 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -690,142 +690,433 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1.getLabelForIndex(0, 2)).toBe(150); }); - it('should get the correct pixel value for a point', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale', // for the horizontal scale - yAxisID: 'yScale', - data: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}] - }], + describe('when', function() { + var data = [ + { + data: [1, 39], + stack: 'stack' }, - options: { - scales: { - xAxes: [{ - id: 'xScale', - type: 'logarithmic' - }], + { + data: [1, 39], + stack: 'stack' + }, + ]; + var dataWithEmptyStacks = [ + { + data: [] + }, + { + data: [] + } + ].concat(data); + var config = [ + { + axis: 'y', + firstTick: 1, // start of the axis (minimum) + describe: 'all stacks are defined' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'y', + scale: { yAxes: [{ - id: 'yScale', - type: 'logarithmic' + ticks: { + min: 0 + } }] - } + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + scale: { + yAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + firstTick: 1, + describe: 'all stacks are defined' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'x', + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not all stacks are defined and ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var scaleConfig = {}; + var type, chartStart, chartEnd; + + if (setup.axis === 'x') { + type = 'horizontalBar'; + chartStart = 'left'; + chartEnd = 'right'; + } else { + type = 'bar'; + chartStart = 'bottom'; + chartEnd = 'top'; } - }); + scaleConfig[setup.axis + 'Axes'] = [{ + type: 'logarithmic' + }]; + Chart.helpers.extend(scaleConfig, setup.scale); + var description = 'dataset has stack option and ' + setup.describe + + ' and axis is "' + setup.axis + '";'; + describe(description, function() { + it('should define the correct axis limits', function() { + var chart = window.acquireChart({ + type: type, + data: { + labels: ['category 1', 'category 2'], + datasets: setup.data || data, + }, + options: { + scales: scaleConfig + } + }); - var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37 + 6); // left + paddingLeft + lineSpace - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(278 + 6 / 2); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37 + 6); // 0 is invalid, put it on the left. + var axisID = setup.axis + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = 80; // last tick (should be first available tick after: 2 * 39) + var start = chart.chartArea[chartStart]; + var end = chart.chartArea[chartEnd]; - expect(xScale.getValueForPixel(495)).toBeCloseToPixel(80); - expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(278)).toBeCloseTo(10, 1e-4); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // 0 is invalid. force it on bottom - - expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); - expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); - expect(yScale.getValueForPixel(246)).toBeCloseTo(10, 1e-4); + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[chartEnd]; + end = chart.chartArea[chartStart]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); }); - it('should get the correct pixel value for a point when 0 values are present or min: 0', function() { + describe('when', function() { var config = [ { - dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + dataset: [], firstTick: 1, // value of the first tick - lastTick: 80 + lastTick: 10, // value of the last tick + describe: 'empty dataset, without ticks.min/max' + }, + { + dataset: [], + scale: {stacked: true}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, without ticks.min/max, with stacked: true' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'bar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'horizontalBar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + dataset: [], + scale: {ticks: {min: 1}}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, ticks.min: 1, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.max: 80, without ticks.min' + }, + { + dataset: [], + scale: {ticks: {max: 0.8}}, + firstTick: 0.01, + lastTick: 0.8, + describe: 'empty dataset, ticks.max: 0.8, without ticks.min' + }, + { + dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); // 0 is invalid, put it at the start. + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], + scale: {ticks: {min: 0}}, + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, ticks.min: 0, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.min: 0, ticks.max: 80' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 0.8}}, + firstTick: 0.1, + lastTick: 0.8, + describe: 'empty dataset, ticks.min: 0, ticks.max: 0.8' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 1.2, y: 1.2}' }, { dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], firstTick: 6, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 6.3, y: 6.3}' }, { dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {ticks: {min: 0}}, firstTick: 1, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, ticks.min: 0' }, { dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {ticks: {min: 0}}, firstTick: 6, - lastTick: 80 + lastTick: 80, + describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, ticks.min: 0' }, ]; - Chart.helpers.each(config, function(setup) { - var xScaleConfig = { - type: 'logarithmic' - }; - var yScaleConfig = { - type: 'logarithmic' - }; - Chart.helpers.extend(xScaleConfig, setup.scale); - Chart.helpers.extend(yScaleConfig, setup.scale); - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - data: setup.dataset - }], - }, - options: { - scales: { - xAxes: [xScaleConfig], - yAxes: [yScaleConfig] - } - } - }); - - var chartArea = chart.chartArea; - var expectations = [ + config.forEach(function(setup) { + var axes = [ { - id: 'x-axis-0', // horizontal scale - axis: 'xAxes', - start: chartArea.left, - end: chartArea.right + id: 'x', // horizontal scale + start: 'left', + end: 'right' }, { - id: 'y-axis-0', // vertical scale - axis: 'yAxes', - start: chartArea.bottom, - end: chartArea.top + id: 'y', // vertical scale + start: 'bottom', + end: 'top' } ]; - Chart.helpers.each(expectations, function(expectation) { - var scale = chart.scales[expectation.id]; - var firstTick = setup.firstTick; - var lastTick = setup.lastTick; - var fontSize = chart.options.defaultFontSize; - var start = expectation.start; - var end = expectation.end; - var sign = scale.isHorizontal() ? 1 : -1; - - expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start + sign * fontSize); - - expect(scale.getValueForPixel(start)).toBeCloseTo(0); - expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick); - expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick); - - chart.options.scales[expectation.axis][0].ticks.reverse = true; // Reverse mode - chart.update(); - - expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(end); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(end - sign * fontSize); - - expect(scale.getValueForPixel(end)).toBeCloseTo(0); - expect(scale.getValueForPixel(start)).toBeCloseTo(lastTick); - expect(scale.getValueForPixel(end - sign * fontSize)).toBeCloseTo(firstTick); + axes.forEach(function(axis) { + var expectation = 'min = 0, max = ' + setup.lastTick + ', first tick = ' + setup.firstTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var fontSize = chart.options.defaultFontSize; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + var sign = scale.isHorizontal() ? 1 : -1; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start + sign * fontSize); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start - sign * fontSize, 4); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start - sign * fontSize)).toBeCloseTo(firstTick, 4); + }); + }); }); }); }); From f9beedba40217f07169ce9764f4371d40ee72367 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Thu, 14 Dec 2017 16:03:07 +0100 Subject: [PATCH 352/685] Fix issue #5029 (#5041) - infinite loop in generating time axis, due to insufficient bounds checking. --- src/scales/scale.time.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4fe39b81082..cfa9f829aa9 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ function determineStepSize(min, max, unit, capacity) { var i, ilen, factor; if (!steps) { - return Math.ceil(range / ((capacity || 1) * milliseconds)); + return Math.ceil(range / (capacity * milliseconds)); } for (i = 0, ilen = steps.length; i < ilen; ++i) { @@ -748,7 +748,8 @@ module.exports = function(Chart) { var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; - return Math.floor(innerWidth / tickLabelWidth); + var capacity = Math.floor(innerWidth / tickLabelWidth); + return capacity > 0 ? capacity : 1; } }); From b04ce949d12870e57e51ea998f5f32a1a94f5f8e Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Fri, 15 Dec 2017 18:10:30 +0100 Subject: [PATCH 353/685] Use time.unit option to create default min/max for empty chart (#5045) --- src/scales/scale.time.js | 7 ++++--- test/specs/scale.time.tests.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index cfa9f829aa9..90904f9e6f0 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -504,6 +504,7 @@ module.exports = function(Chart) { var me = this; var chart = me.chart; var timeOpts = me.options.time; + var unit = timeOpts.unit || 'day'; var min = MAX_INTEGER; var max = MIN_INTEGER; var timestamps = []; @@ -555,9 +556,9 @@ module.exports = function(Chart) { min = parse(timeOpts.min, me) || min; max = parse(timeOpts.max, me) || max; - // In case there is no valid min/max, let's use today limits - min = min === MAX_INTEGER ? +moment().startOf('day') : min; - max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max; + // In case there is no valid min/max, set limits based on unit time option + min = min === MAX_INTEGER ? +moment().startOf(unit) : min; + max = max === MIN_INTEGER ? +moment().endOf(unit) + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 19189040ee4..e4c1739e6c5 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -703,7 +703,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -714,6 +714,19 @@ describe('Time scale tests', function() { expect(scale.max).toEqual(+moment().endOf('day') + 1); expect(getTicksLabels(scale)).toEqual([]); }); + it ('should correctly handle empty `data.labels` using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); describe('is "data"', function() { @@ -784,7 +797,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -796,6 +809,21 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2018', '2020', '2043']); }); + it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + var meta = chart.getDatasetMeta(1); + meta.hidden = true; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); }); From ce1fc28b743d518add5c89653108a287f6aa911d Mon Sep 17 00:00:00 2001 From: Boyi C Date: Tue, 19 Dec 2017 03:24:02 +0800 Subject: [PATCH 354/685] Improve point.xRange and point.yRange performance (#5062) --- src/elements/element.point.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/elements/element.point.js b/src/elements/element.point.js index d5116a259c5..eab5b31d453 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -24,12 +24,12 @@ defaults._set('global', { function xRange(mouseX) { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; } function yRange(mouseY) { var vm = this._view; - return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; } module.exports = Element.extend({ From 92d033beb2d29fc296aee23b9e9d9c4be1fadd15 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Sat, 23 Dec 2017 14:34:55 +0100 Subject: [PATCH 355/685] Optimization: prevent double ticks array reverse for vertical logarithmic axis (#5076) with ticks option 'reverse: true'. --- src/scales/scale.logarithmic.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 87fe9b1af2b..372efe7adab 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -161,6 +161,7 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; + var reverse = !me.isHorizontal(); var generationOptions = { min: tickOpts.min, @@ -168,25 +169,22 @@ module.exports = function(Chart) { }; var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); - if (!me.isHorizontal()) { - // We are in a vertical orientation. The top value is the highest. So reverse the array - ticks.reverse(); - } - // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale me.max = helpers.max(ticks); me.min = helpers.min(ticks); if (tickOpts.reverse) { - ticks.reverse(); - + reverse = !reverse; me.start = me.max; me.end = me.min; } else { me.start = me.min; me.end = me.max; } + if (reverse) { + ticks.reverse(); + } }, convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); From 9874a754e0324cf49df9b2e2a485e44c729eeec3 Mon Sep 17 00:00:00 2001 From: Cameron Childress Date: Fri, 29 Dec 2017 02:52:17 -0500 Subject: [PATCH 356/685] Adding helpful note about legendCallback (#5094) I didn't realize you had to call generateLegend() manually and a quick search tells me that lots of other people have run into this too. --- docs/configuration/legend.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index bce69af79db..52bee8687c0 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -164,3 +164,7 @@ var chart = new Chart(ctx, { } }); ``` + +Note that legendCallback is not called automatically and you must call `generateLegend()` yourself in code when creating a legend using this method. + + From fcd463354bae115b4847272ba44f23c57cc0d6ed Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 31 Dec 2017 15:17:22 +0100 Subject: [PATCH 357/685] Update license year and copyright holders (#5053) --- LICENSE.md | 2 +- gulpfile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 620db307e3c..29c941dcccf 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Nick Downie +Copyright (c) 2018 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/gulpfile.js b/gulpfile.js index bb42ea9d3b8..5a3093e444e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -29,7 +29,7 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2017 Nick Downie\n" + + " * Copyright " + (new Date().getFullYear()) + " Chart.js Contributors\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; From 33c7d941f7caa5363f68ac2135a66d7c2328e196 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 6 Jan 2018 09:59:47 -0800 Subject: [PATCH 358/685] Add back Chart.Ticks.formatters (#5088) --- src/chart.js | 1 + src/core/core.ticks.js | 141 -------------------------------- src/scales/scale.linearbase.js | 58 ++++++++++++- src/scales/scale.logarithmic.js | 54 +++++++++++- test/specs/core.ticks.tests.js | 9 ++ 5 files changed, 119 insertions(+), 144 deletions(-) diff --git a/src/chart.js b/src/chart.js index df17bb92c3f..8bd49761461 100644 --- a/src/chart.js +++ b/src/chart.js @@ -13,6 +13,7 @@ Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); Chart.platform = require('./platforms/platform'); +Chart.Ticks = require('./core/core.ticks'); require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 655369a17c8..3898842a49a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -7,147 +7,6 @@ var helpers = require('../helpers/index'); * @namespace Chart.Ticks */ module.exports = { - /** - * 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; - - // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - // If very close to our whole number, use it. - if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { - 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); - } else { - numSpaces = Math.ceil(numSpaces); - } - - var precision = 1; - if (spacing < 1) { - precision = Math.pow(10, spacing.toString().length - 2); - niceMin = Math.round(niceMin * precision) / precision; - niceMax = Math.round(niceMax * precision) / precision; - } - ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); - } - 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 valueOrDefault = helpers.valueOrDefault; - - // 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 = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - - var endExp = Math.floor(helpers.log10(dataRange.max)); - var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); - var exp, significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - - ticks.push(tickVal); - tickVal = significand * Math.pow(10, exp); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)); - } - var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; - - do { - ticks.push(tickVal); - - ++significand; - if (significand === 10) { - significand = 1; - ++exp; - precision = exp >= 0 ? 1 : precision; - } - - tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; - } while (exp < endExp || (exp === endExp && significand < endSignificand)); - - var lastTick = valueOrDefault(generationOptions.max, tickVal); - ticks.push(lastTick); - - return ticks; - } - }, - /** * Namespace to hold formatters for different types of ticks * @namespace Chart.Ticks.formatters diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 0d9a33bf2a3..3e4b5c083f3 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,7 +1,61 @@ 'use strict'; var helpers = require('../helpers/index'); -var Ticks = require('../core/core.ticks'); + +/** + * Generate a set of linear ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(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; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + // If very close to our whole number, use it. + if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { + 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); + } else { + numSpaces = Math.ceil(numSpaces); + } + + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; +} + module.exports = function(Chart) { @@ -102,7 +156,7 @@ module.exports = function(Chart) { max: tickOpts.max, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; - var ticks = me.ticks = Ticks.generators.linear(numericGeneratorOptions, me); + var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 372efe7adab..74a210e4473 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -3,6 +3,58 @@ var helpers = require('../helpers/index'); var Ticks = require('../core/core.ticks'); +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + var valueOrDefault = helpers.valueOrDefault; + + // 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 = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp, significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); + + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + + do { + ticks.push(tickVal); + + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + + var lastTick = valueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; +} + + module.exports = function(Chart) { var defaultConfig = { @@ -167,7 +219,7 @@ module.exports = function(Chart) { min: tickOpts.min, max: tickOpts.max }; - var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); + var ticks = me.ticks = generateTicks(generationOptions, me); // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js index 6d7c2b3c111..01cecc5462e 100644 --- a/test/specs/core.ticks.tests.js +++ b/test/specs/core.ticks.tests.js @@ -1,4 +1,13 @@ describe('Test tick generators', function() { + // formatters are used as default config values so users want to be able to reference them + it('Should expose formatters api', function() { + expect(typeof Chart.Ticks).toBeDefined(); + expect(typeof Chart.Ticks.formatters).toBeDefined(); + expect(typeof Chart.Ticks.formatters.values).toBe('function'); + expect(typeof Chart.Ticks.formatters.linear).toBe('function'); + expect(typeof Chart.Ticks.formatters.logarithmic).toBe('function'); + }); + it('Should generate linear spaced ticks with correct precision', function() { var chart = window.acquireChart({ type: 'line', From 9ddb449ad1abae8dd95639a60311fb8bfc17e572 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 6 Jan 2018 23:59:13 +0100 Subject: [PATCH 359/685] Use the Chart.js shared ESLint config (#5112) An ESLint shareable config has been created (from this repository) in the attempt to homogenize Chart.js hosted projects and plugins style. Rename `.eslintrc` files to `.eslintrc.yml` since the name has been deprecated. Fix the CC badge (maintainability) and disable CodeClimate ESLint plugin because it doesn't support custom shareable config but also because it already executes relevant checks as part of the regular process. --- .codeclimate.yml | 32 ++--- .eslintrc | 220 ------------------------------ .eslintrc.yml | 5 + README.md | 2 +- package.json | 1 + test/{.eslintrc => .eslintrc.yml} | 0 6 files changed, 20 insertions(+), 240 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.yml rename test/{.eslintrc => .eslintrc.yml} (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index bd32376436b..0b8340feb58 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,25 +1,19 @@ -engines: +version: "2" +plugins: duplication: enabled: true config: languages: - - javascript - eslint: - enabled: true - channel: "eslint-4" + - javascript fixme: enabled: true -ratings: - paths: - - "src/**/*.js" -exclude_paths: -- '.github/' -- 'dist/' -- 'test/' -- 'docs/' -- 'samples/' -- 'scripts/' -- '**.md' -- '**.json' -- 'gulpfile.js' -- 'karma.conf.js' +exclude_patterns: + - "dist/" + - "docs/" + - "samples/" + - "scripts/" + - "test/" + - "*.js" + - "*.json" + - "*.md" + - ".*" diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c0e1b26ce6f..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,220 +0,0 @@ -env: - amd: true - browser: true - es6: true - jquery: true - node: true - -# http://eslint.org/docs/rules/ -rules: - # Possible Errors - no-cond-assign: 2 - no-console: [2, {allow: [warn, error]}] - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-dupe-args: 2 - no-dupe-keys: 2 - no-duplicate-case: 2 - no-empty: 2 - no-empty-character-class: 2 - no-ex-assign: 2 - no-extra-boolean-cast: 2 - no-extra-parens: [2, functions] - no-extra-semi: 2 - no-func-assign: 2 - no-inner-declarations: [2, functions] - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-negated-in-lhs: 2 - no-obj-calls: 2 - no-regex-spaces: 2 - no-sparse-arrays: 2 - no-unexpected-multiline: 2 - no-unreachable: 2 - use-isnan: 2 - valid-jsdoc: 0 - valid-typeof: 2 - - # Best Practices - accessor-pairs: 2 - array-callback-return: 0 - block-scoped-var: 0 - complexity: [2, 10] - consistent-return: 0 - curly: [2, all] - default-case: 2 - dot-location: 0 - dot-notation: 2 - eqeqeq: 2 - guard-for-in: 2 - no-alert: 2 - no-caller: 2 - no-case-declarations: 2 - no-div-regex: 2 - no-else-return: 2 - no-empty-pattern: 2 - no-eq-null: 2 - no-eval: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-fallthrough: 2 - no-floating-decimal: 2 - no-implicit-coercion: 0 - no-implied-eval: 2 - no-invalid-this: 0 - no-iterator: 2 - no-labels: 2 - no-lone-blocks: 2 - no-loop-func: 2 - no-magic-number: 0 - no-multi-spaces: [2, {ignoreEOLComments: true}] - no-multi-str: 2 - no-native-reassign: 2 - no-new-func: 2 - no-new-wrappers: 2 - no-new: 2 - no-octal-escape: 2 - no-octal: 2 - no-proto: 2 - no-redeclare: 2 - no-return-assign: 2 - no-script-url: 2 - no-self-compare: 2 - no-sequences: 2 - no-throw-literal: 0 - no-unused-expressions: 2 - no-useless-call: 2 - no-useless-concat: 2 - no-void: 2 - no-warning-comments: 0 - no-with: 2 - radix: 2 - vars-on-top: 0 - wrap-iife: 2 - yoda: [1, never] - - # Strict - strict: 0 - - # Variables - init-declarations: 0 - no-catch-shadow: 2 - no-delete-var: 2 - no-label-var: 2 - no-shadow-restricted-names: 2 - no-shadow: 2 - no-undef-init: 2 - no-undef: 2 - no-undefined: 0 - no-unused-vars: 2 - no-use-before-define: 2 - - # Node.js and CommonJS - callback-return: 2 - global-require: 2 - handle-callback-err: 2 - no-mixed-requires: 0 - no-new-require: 0 - no-path-concat: 2 - no-process-exit: 2 - no-restricted-modules: 0 - no-sync: 0 - - # Stylistic Issues - array-bracket-spacing: [2, never] - block-spacing: 0 - brace-style: [2, 1tbs] - camelcase: 2 - comma-dangle: [2, only-multiline] - comma-spacing: 2 - comma-style: [2, last] - computed-property-spacing: [2, never] - consistent-this: [2, me] - eol-last: 2 - func-call-spacing: 0 - func-names: [2, never] - func-style: 0 - id-length: 0 - id-match: 0 - indent: [2, tab, {flatTernaryExpressions: true}] - jsx-quotes: 0 - key-spacing: 2 - keyword-spacing: 2 - linebreak-style: 0 - lines-around-comment: 0 - max-depth: 0 - max-len: 0 - max-lines: 0 - max-nested-callbacks: 0 - max-params: 0 - max-statements-per-line: 0 - max-statements: [2, 30] - multiline-ternary: 0 - new-cap: 0 - new-parens: 2 - newline-after-var: 0 - newline-before-return: 0 - newline-per-chained-call: 0 - no-array-constructor: 0 - no-bitwise: 0 - no-continue: 0 - no-inline-comments: 0 - no-lonely-if: 2 - no-mixed-operators: 0 - no-mixed-spaces-and-tabs: 2 - no-multiple-empty-lines: [2, {max: 2}] - no-negated-condition: 0 - no-nested-ternary: 0 - no-new-object: 0 - no-plusplus: 0 - no-restricted-syntax: 0 - no-spaced-func: 0 - no-ternary: 0 - no-trailing-spaces: 2 - no-underscore-dangle: 0 - no-unneeded-ternary: 0 - no-whitespace-before-property: 2 - object-curly-newline: 0 - object-curly-spacing: [2, never] - object-property-newline: 0 - one-var-declaration-per-line: 2 - one-var: [2, {initialized: never}] - operator-assignment: 0 - operator-linebreak: 0 - padded-blocks: 0 - quote-props: [2, as-needed] - quotes: [2, single, {avoidEscape: true}] - require-jsdoc: 0 - semi-spacing: 2 - semi: [2, always] - sort-keys: 0 - sort-vars: 0 - space-before-blocks: [2, always] - space-before-function-paren: [2, never] - space-in-parens: [2, never] - space-infix-ops: 2 - space-unary-ops: [2, {words: true, nonwords: false}] - spaced-comment: [2, always] - unicode-bom: 0 - wrap-regex: 2 - - # ECMAScript 6 - arrow-body-style: 0 - arrow-parens: 0 - arrow-spacing: 0 - constructor-super: 0 - generator-star-spacing: 0 - no-arrow-condition: 0 - no-class-assign: 0 - no-const-assign: 0 - no-dupe-class-members: 0 - no-this-before-super: 0 - no-var: 0 - object-shorthand: 0 - prefer-arrow-callback: 0 - prefer-const: 0 - prefer-reflect: 0 - prefer-spread: 0 - prefer-template: 0 - require-yield: 0 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..8f9b4af3023 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,5 @@ +extends: chartjs + +env: + browser: true + node: true diff --git a/README.md b/README.md index 767950b88bc..10d2c1d8d7d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![codeclimate](https://img.shields.io/codeclimate/github/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/package.json b/package.json index 3ebc721f95a..510c141cd88 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "child-process-promise": "^2.2.1", "coveralls": "^3.0.0", "eslint": "^4.9.0", + "eslint-config-chartjs": "git+https://github.com/chartjs/eslint-config-chartjs.git", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", diff --git a/test/.eslintrc b/test/.eslintrc.yml similarity index 100% rename from test/.eslintrc rename to test/.eslintrc.yml From ce27fe5ea6040523a61cef198ccc1e866d07ad82 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 7 Jan 2018 23:38:26 +0100 Subject: [PATCH 360/685] Make `Chart.layout(Service)` importable (#5113) Rename (and deprecate) `Chart.layoutService` to `Chart.layout` and make it importable. --- src/chart.js | 11 +- src/core/core.controller.js | 5 +- src/core/core.layout.js | 419 +++++++++++++++++++++++ src/core/core.layoutService.js | 422 ------------------------ src/core/core.scaleService.js | 3 +- src/plugins/plugin.legend.js | 2 +- src/plugins/plugin.title.js | 4 +- test/specs/global.deprecations.tests.js | 13 +- 8 files changed, 448 insertions(+), 431 deletions(-) create mode 100644 src/core/core.layout.js delete mode 100644 src/core/core.layoutService.js diff --git a/src/chart.js b/src/chart.js index 8bd49761461..b46e66b9e6f 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,6 +12,7 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); +Chart.layout = require('./core/core.layout'); Chart.platform = require('./platforms/platform'); Chart.Ticks = require('./core/core.ticks'); @@ -19,7 +20,6 @@ require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); -require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.scale')(Chart); require('./core/core.tooltip')(Chart); @@ -77,3 +77,12 @@ if (typeof window !== 'undefined') { * @private */ Chart.canvasHelpers = Chart.helpers.canvas; + +/** + * Provided for backward compatibility, use Chart.layout instead. + * @namespace Chart.layoutService + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +Chart.layoutService = Chart.layout; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 6026e8620fb..2d143f6e009 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); +var layout = require('./core.layout'); var platform = require('../platforms/platform'); module.exports = function(Chart) { @@ -46,7 +47,7 @@ module.exports = function(Chart) { var newOptions = chart.options; helpers.each(chart.scales, function(scale) { - Chart.layoutService.removeBox(chart, scale); + layout.removeBox(chart, scale); }); newOptions = helpers.configMerge( @@ -435,7 +436,7 @@ module.exports = function(Chart) { return; } - Chart.layoutService.update(this, this.width, this.height); + layout.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. diff --git a/src/core/core.layout.js b/src/core/core.layout.js new file mode 100644 index 00000000000..b99612bbebd --- /dev/null +++ b/src/core/core.layout.js @@ -0,0 +1,419 @@ +'use strict'; + +var helpers = require('../helpers/index'); + +function filterByPosition(array, position) { + return helpers.where(array, function(v) { + return v.position === position; + }); +} + +function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); +} + +/** + * @interface ILayoutItem + * @prop {String} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {Function} update - Takes two parameters: width and height. Returns size of item + * @prop {Function} getPadding - Returns an object with padding on the edges + * @prop {Number} width - Width of item. Must be valid after update() + * @prop {Number} height - Height of item. Must be valid after update() + * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +module.exports = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {Object} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {Object} item - the item to configure with the given options + * @param {Object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {Number} width - the width to fit into + * @param {Number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers.options.toPadding(layoutOptions.padding); + var leftPadding = padding.left; + var rightPadding = padding.right; + var topPadding = padding.top; + var bottomPadding = padding.bottom; + + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each layout the maximum size it can be. The layout will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + // 9. Tell any axes that overlay the chart area the positions of the chart area + + // Step 1 + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; + var chartAreaWidth = chartWidth / 2; // min 50% + var chartAreaHeight = chartHeight / 2; // min 50% + + // Step 2 + var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); + + // Step 3 + var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); + + // Step 4 + var maxChartAreaWidth = chartWidth; + var maxChartAreaHeight = chartHeight; + var minBoxSizes = []; + + function getMinimumBoxSize(box) { + var minSize; + var isHorizontal = box.isHorizontal(); + + if (isHorizontal) { + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); + maxChartAreaHeight -= minSize.height; + } else { + minSize = box.update(verticalBoxWidth, maxChartAreaHeight); + maxChartAreaWidth -= minSize.width; + } + + minBoxSizes.push({ + horizontal: isHorizontal, + minSize: minSize, + box: box, + }); + } + + helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 0; + var maxVerticalTopPadding = 0; + var maxVerticalBottomPadding = 0; + + helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { + if (horizontalBox.getPadding) { + var boxPadding = horizontalBox.getPadding(); + maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); + maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); + } + }); + + helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { + if (verticalBox.getPadding) { + var boxPadding = verticalBox.getPadding(); + maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); + maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); + } + }); + + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + // Steps 5 & 6 + var totalLeftBoxesWidth = leftPadding; + var totalRightBoxesWidth = rightPadding; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; + + // Function to fit a box + function fitBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { + return minBox.box === box; + }); + + if (minBoxSize) { + if (box.isHorizontal()) { + var scaleMargin = { + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), + top: 0, + bottom: 0 + }; + + // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends + // on the margin. Sometimes they need to increase in size slightly + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + } else { + box.update(minBoxSize.minSize.width, maxChartAreaHeight); + } + } + } + + // Update, and calculate the left and right margins for the horizontal boxes + helpers.each(leftBoxes.concat(rightBoxes), fitBox); + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + // Set the Left and Right margins for the horizontal boxes + helpers.each(topBoxes.concat(bottomBoxes), fitBox); + + // Figure out how much margin is on the top and bottom of the vertical boxes + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + function finalFitVerticalBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { + return minSize.box === box; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopBoxesHeight, + bottom: totalBottomBoxesHeight + }; + + if (minBoxSize) { + box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); + } + } + + // Let the left layout know the final margin + 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 = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); + totalTopBoxesHeight += topPaddingAddition; + totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); + + // Figure out if our chart area changed. This would occur if the dataset layout label rotation + // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do + // without calling `fit` again + var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; + var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; + + if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { + helpers.each(leftBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(rightBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(topBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + helpers.each(bottomBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + maxChartAreaHeight = newMaxChartAreaHeight; + maxChartAreaWidth = newMaxChartAreaWidth; + } + + // Step 7 - Position the boxes + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; + + function placeBox(box) { + if (box.isHorizontal()) { + box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.top = top; + box.bottom = top + box.height; + + // Move to next point + top = box.bottom; + + } else { + + box.left = left; + box.right = left + box.width; + box.top = totalTopBoxesHeight; + box.bottom = totalTopBoxesHeight + maxChartAreaHeight; + + // Move to next point + left = box.right; + } + } + + helpers.each(leftBoxes.concat(topBoxes), placeBox); + + // Account for chart width and height + left += maxChartAreaWidth; + top += maxChartAreaHeight; + + helpers.each(rightBoxes, placeBox); + helpers.each(bottomBoxes, placeBox); + + // Step 8 + chart.chartArea = { + left: totalLeftBoxesWidth, + top: totalTopBoxesHeight, + right: totalLeftBoxesWidth + maxChartAreaWidth, + bottom: totalTopBoxesHeight + maxChartAreaHeight + }; + + // Step 9 + helpers.each(chartAreaBoxes, function(box) { + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; + + box.update(maxChartAreaWidth, maxChartAreaHeight); + }); + } +}; diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js deleted file mode 100644 index df28f974eb1..00000000000 --- a/src/core/core.layoutService.js +++ /dev/null @@ -1,422 +0,0 @@ -'use strict'; - -var helpers = require('../helpers/index'); - -module.exports = function(Chart) { - - function filterByPosition(array, position) { - return helpers.where(array, function(v) { - return v.position === position; - }); - } - - function sortByWeight(array, reverse) { - array.forEach(function(v, i) { - v._tmpIndex_ = i; - return v; - }); - array.sort(function(a, b) { - var v0 = reverse ? b : a; - var v1 = reverse ? a : b; - return v0.weight === v1.weight ? - v0._tmpIndex_ - v1._tmpIndex_ : - v0.weight - v1.weight; - }); - array.forEach(function(v) { - delete v._tmpIndex_; - }); - } - - /** - * @interface ILayoutItem - * @prop {String} position - The position of the item in the chart layout. Possible values are - * 'left', 'top', 'right', 'bottom', and 'chartArea' - * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area - * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down - * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) - * @prop {Function} update - Takes two parameters: width and height. Returns size of item - * @prop {Function} getPadding - Returns an object with padding on the edges - * @prop {Number} width - Width of item. Must be valid after update() - * @prop {Number} height - Height of item. Must be valid after update() - * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update - * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update - * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update - * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update - */ - - // The layout service is very self explanatory. It's responsible for the layout within a chart. - // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need - // It is this service's responsibility of carrying out that layout. - Chart.layoutService = { - defaults: {}, - - /** - * Register a box to a chart. - * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. - * @param {Chart} chart - the chart to use - * @param {ILayoutItem} item - the item to add to be layed out - */ - addBox: function(chart, item) { - if (!chart.boxes) { - chart.boxes = []; - } - - // initialize item with default values - item.fullWidth = item.fullWidth || false; - item.position = item.position || 'top'; - item.weight = item.weight || 0; - - chart.boxes.push(item); - }, - - /** - * Remove a layoutItem from a chart - * @param {Chart} chart - the chart to remove the box from - * @param {Object} layoutItem - the item to remove from the layout - */ - removeBox: function(chart, layoutItem) { - var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; - if (index !== -1) { - chart.boxes.splice(index, 1); - } - }, - - /** - * Sets (or updates) options on the given `item`. - * @param {Chart} chart - the chart in which the item lives (or will be added to) - * @param {Object} item - the item to configure with the given options - * @param {Object} options - the new item options. - */ - configure: function(chart, item, options) { - var props = ['fullWidth', 'position', 'weight']; - var ilen = props.length; - var i = 0; - var prop; - - for (; i < ilen; ++i) { - prop = props[i]; - if (options.hasOwnProperty(prop)) { - item[prop] = options[prop]; - } - } - }, - - /** - * Fits boxes of the given chart into the given size by having each box measure itself - * then running a fitting algorithm - * @param {Chart} chart - the chart - * @param {Number} width - the width to fit into - * @param {Number} height - the height to fit into - */ - update: function(chart, width, height) { - if (!chart) { - return; - } - - var layoutOptions = chart.options.layout || {}; - var padding = helpers.options.toPadding(layoutOptions.padding); - var leftPadding = padding.left; - var rightPadding = padding.right; - var topPadding = padding.top; - var bottomPadding = padding.bottom; - - var leftBoxes = filterByPosition(chart.boxes, 'left'); - var rightBoxes = filterByPosition(chart.boxes, 'right'); - var topBoxes = filterByPosition(chart.boxes, 'top'); - var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); - var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); - - // Sort boxes by weight. A higher weight is further away from the chart area - sortByWeight(leftBoxes, true); - sortByWeight(rightBoxes, false); - sortByWeight(topBoxes, true); - sortByWeight(bottomBoxes, false); - - // Essentially we now have any number of boxes on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays - // These locations are single-box locations only, when trying to register a chartArea location that is already taken, - // an error will be thrown. - // - // |----------------------------------------------------| - // | T1 (Full Width) | - // |----------------------------------------------------| - // | | | T2 | | - // | |----|-------------------------------------|----| - // | | | C1 | | C2 | | - // | | |----| |----| | - // | | | | | - // | L1 | L2 | ChartArea (C0) | R1 | - // | | | | | - // | | |----| |----| | - // | | | C3 | | C4 | | - // | |----|-------------------------------------|----| - // | | | B1 | | - // |----------------------------------------------------| - // | B2 (Full Width) | - // |----------------------------------------------------| - // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - - // Step 1 - var chartWidth = width - leftPadding - rightPadding; - var chartHeight = height - topPadding - bottomPadding; - var chartAreaWidth = chartWidth / 2; // min 50% - var chartAreaHeight = chartHeight / 2; // min 50% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); - - // Step 3 - var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); - - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var minBoxSizes = []; - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, maxChartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - minSize: minSize, - box: box, - }); - } - - helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); - - // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) - var maxHorizontalLeftPadding = 0; - var maxHorizontalRightPadding = 0; - var maxVerticalTopPadding = 0; - var maxVerticalBottomPadding = 0; - - helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { - if (horizontalBox.getPadding) { - var boxPadding = horizontalBox.getPadding(); - maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); - maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); - } - }); - - helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { - if (verticalBox.getPadding) { - var boxPadding = verticalBox.getPadding(); - maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); - maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); - } - }); - - // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 - var totalLeftBoxesWidth = leftPadding; - var totalRightBoxesWidth = rightPadding; - var totalTopBoxesHeight = topPadding; - var totalBottomBoxesHeight = bottomPadding; - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { - return minBox.box === box; - }); - - if (minBoxSize) { - if (box.isHorizontal()) { - var scaleMargin = { - left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), - right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.minSize.width, maxChartAreaHeight); - } - } - } - - // Update, and calculate the left and right margins for the horizontal boxes - helpers.each(leftBoxes.concat(rightBoxes), fitBox); - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - // Set the Left and Right margins for the horizontal boxes - helpers.each(topBoxes.concat(bottomBoxes), fitBox); - - // Figure out how much margin is on the top and bottom of the vertical boxes - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { - return minSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopBoxesHeight, - bottom: totalBottomBoxesHeight - }; - - if (minBoxSize) { - box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Let the left layout know the final margin - 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 = leftPadding; - totalRightBoxesWidth = rightPadding; - totalTopBoxesHeight = topPadding; - totalBottomBoxesHeight = bottomPadding; - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - // We may be adding some padding to account for rotated x axis labels - var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); - totalLeftBoxesWidth += leftPaddingAddition; - totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); - - var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); - totalTopBoxesHeight += topPaddingAddition; - totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); - - // Figure out if our chart area changed. This would occur if the dataset layout label rotation - // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do - // without calling `fit` again - var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; - var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers.each(leftBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(rightBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(topBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - helpers.each(bottomBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; - box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = totalTopBoxesHeight; - box.bottom = totalTopBoxesHeight + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - helpers.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers.each(rightBoxes, placeBox); - helpers.each(bottomBoxes, placeBox); - - // Step 8 - chart.chartArea = { - left: totalLeftBoxesWidth, - top: totalTopBoxesHeight, - right: totalLeftBoxesWidth + maxChartAreaWidth, - bottom: totalTopBoxesHeight + maxChartAreaHeight - }; - - // Step 9 - helpers.each(chartAreaBoxes, function(box) { - box.left = chart.chartArea.left; - box.top = chart.chartArea.top; - box.right = chart.chartArea.right; - box.bottom = chart.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); - }); - } - }; -}; diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 23cabe610d2..bfbf832094a 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,6 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var layout = require('./core.layout'); module.exports = function(Chart) { @@ -38,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - Chart.layoutService.addBox(chart, scale); + layout.addBox(chart, scale); }); } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 71e0e4110e4..7e0e429d0ce 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,6 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layout = require('../core/core.layout'); defaults._set('global', { legend: { @@ -81,7 +82,6 @@ defaults._set('global', { module.exports = function(Chart) { - var layout = Chart.layoutService; var noop = helpers.noop; /** diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index ebefed294ff..8eac103f542 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,6 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layout = require('../core/core.layout'); defaults._set('global', { title: { @@ -19,7 +20,6 @@ defaults._set('global', { module.exports = function(Chart) { - var layout = Chart.layoutService; var noop = helpers.noop; Chart.Title = Element.extend({ @@ -235,7 +235,7 @@ module.exports = function(Chart) { createNewTitleBlockAndAttach(chart, titleOpts); } } else if (titleBlock) { - Chart.layoutService.removeBox(chart, titleBlock); + layout.removeBox(chart, titleBlock); delete chart.titleBlock; } } diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index f1091464d1e..fee37288df9 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,4 +1,13 @@ describe('Deprecations', function() { + describe('Version 2.8.0', function() { + describe('Chart.layoutService', function() { + it('should be defined and an alias of Chart.layout', function() { + expect(Chart.layoutService).toBeDefined(); + expect(Chart.layoutService).toBe(Chart.layout); + }); + }); + }); + describe('Version 2.7.0', function() { describe('Chart.Controller.update(duration, lazy)', function() { it('should add an animation with the provided options', function() { @@ -302,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layoutService.update; - Chart.layoutService.update = function() { + var override = Chart.layout.update; + Chart.layout.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; From fb3ea03440769a267880ba8721d14a3939792718 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 8 Jan 2018 11:48:59 +0100 Subject: [PATCH 361/685] Make `Chart.plugins` importable (#5114) Explicitly deprecate (since 2.1.5) `Chart.Legend` and `Chart.Title`. --- src/chart.js | 54 +- src/core/core.controller.js | 2 +- src/core/core.plugin.js | 395 ----------- src/core/core.plugins.js | 372 +++++++++++ src/plugins/index.js | 6 + src/plugins/plugin.filler.js | 485 +++++++------- src/plugins/plugin.legend.js | 841 ++++++++++++------------ src/plugins/plugin.title.js | 409 ++++++------ test/specs/global.deprecations.tests.js | 18 + test/specs/plugin.legend.tests.js | 5 - test/specs/plugin.title.tests.js | 5 - 11 files changed, 1316 insertions(+), 1276 deletions(-) delete mode 100644 src/core/core.plugin.js create mode 100644 src/core/core.plugins.js create mode 100644 src/plugins/index.js diff --git a/src/chart.js b/src/chart.js index b46e66b9e6f..70d70a66995 100644 --- a/src/chart.js +++ b/src/chart.js @@ -14,9 +14,9 @@ Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); Chart.layout = require('./core/core.layout'); Chart.platform = require('./platforms/platform'); +Chart.plugins = require('./core/core.plugins'); Chart.Ticks = require('./core/core.ticks'); -require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); @@ -50,15 +50,12 @@ require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); // Loading built-it plugins -var plugins = []; - -plugins.push( - require('./plugins/plugin.filler')(Chart), - require('./plugins/plugin.legend')(Chart), - require('./plugins/plugin.title')(Chart) -); - -Chart.plugins.register(plugins); +var plugins = require('./plugins'); +for (var k in plugins) { + if (plugins.hasOwnProperty(k)) { + Chart.plugins.register(plugins[k]); + } +} Chart.platform.initialize(); @@ -69,6 +66,43 @@ if (typeof window !== 'undefined') { // DEPRECATIONS +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Legend + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Legend = plugins.legend._element; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Title + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Title = plugins.title._element; + +/** + * Provided for backward compatibility, use Chart.plugins instead + * @namespace Chart.pluginService + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.pluginService = Chart.plugins; + +/** + * Provided for backward compatibility, inheriting from Chart.PlugingBase has no + * effect, instead simply create/register plugins via plain JavaScript objects. + * @interface Chart.PluginBase + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ +Chart.PluginBase = Chart.Element.extend({}); + /** * Provided for backward compatibility, use Chart.helpers.canvas instead. * @namespace Chart.canvasHelpers diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 2d143f6e009..157bc50a423 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -5,9 +5,9 @@ var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); var layout = require('./core.layout'); var platform = require('../platforms/platform'); +var plugins = require('./core.plugins'); module.exports = function(Chart) { - var plugins = Chart.plugins; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js deleted file mode 100644 index 0b7423a6904..00000000000 --- a/src/core/core.plugin.js +++ /dev/null @@ -1,395 +0,0 @@ -'use strict'; - -var defaults = require('./core.defaults'); -var Element = require('./core.element'); -var helpers = require('../helpers/index'); - -defaults._set('global', { - plugins: {} -}); - -module.exports = function(Chart) { - - /** - * The plugin service singleton - * @namespace Chart.plugins - * @since 2.1.0 - */ - Chart.plugins = { - /** - * Globally registered plugins. - * @private - */ - _plugins: [], - - /** - * This identifier is used to invalidate the descriptors cache attached to each chart - * when a global plugin is registered or unregistered. In this case, the cache ID is - * incremented and descriptors are regenerated during following API calls. - * @private - */ - _cacheId: 0, - - /** - * Registers the given plugin(s) if not already registered. - * @param {Array|Object} plugins plugin instance(s). - */ - register: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - if (p.indexOf(plugin) === -1) { - p.push(plugin); - } - }); - - this._cacheId++; - }, - - /** - * Unregisters the given plugin(s) only if registered. - * @param {Array|Object} plugins plugin instance(s). - */ - unregister: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - var idx = p.indexOf(plugin); - if (idx !== -1) { - p.splice(idx, 1); - } - }); - - this._cacheId++; - }, - - /** - * Remove all registered plugins. - * @since 2.1.5 - */ - clear: function() { - this._plugins = []; - this._cacheId++; - }, - - /** - * Returns the number of registered plugins? - * @returns {Number} - * @since 2.1.5 - */ - count: function() { - return this._plugins.length; - }, - - /** - * Returns all registered plugin instances. - * @returns {Array} array of plugin objects. - * @since 2.1.5 - */ - getAll: function() { - return this._plugins; - }, - - /** - * Calls enabled plugins for `chart` on the specified hook and with the given args. - * This method immediately returns as soon as a plugin explicitly returns false. The - * returned value can be used, for instance, to interrupt the current action. - * @param {Object} chart - The chart instance for which plugins should be called. - * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] - Extra arguments to apply to the hook call. - * @returns {Boolean} false if any of the plugins return false, else returns true. - */ - notify: function(chart, hook, args) { - var descriptors = this.descriptors(chart); - var ilen = descriptors.length; - var i, descriptor, plugin, params, method; - - for (i = 0; i < ilen; ++i) { - descriptor = descriptors[i]; - plugin = descriptor.plugin; - method = plugin[hook]; - if (typeof method === 'function') { - params = [chart].concat(args || []); - params.push(descriptor.options); - if (method.apply(plugin, params) === false) { - return false; - } - } - } - - return true; - }, - - /** - * Returns descriptors of enabled plugins for the given chart. - * @returns {Array} [{ plugin, options }] - * @private - */ - descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); - if (cache.id === this._cacheId) { - return cache.descriptors; - } - - var plugins = []; - var descriptors = []; - var config = (chart && chart.config) || {}; - var options = (config.options && config.options.plugins) || {}; - - this._plugins.concat(config.plugins || []).forEach(function(plugin) { - var idx = plugins.indexOf(plugin); - if (idx !== -1) { - return; - } - - var id = plugin.id; - var opts = options[id]; - if (opts === false) { - return; - } - - if (opts === true) { - opts = helpers.clone(defaults.global.plugins[id]); - } - - plugins.push(plugin); - descriptors.push({ - plugin: plugin, - options: opts || {} - }); - }); - - cache.descriptors = descriptors; - cache.id = this._cacheId; - return descriptors; - } - }; - - /** - * Plugin extension hooks. - * @interface IPlugin - * @since 2.1.0 - */ - /** - * @method IPlugin#beforeInit - * @desc Called before initializing `chart`. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterInit - * @desc Called after `chart` has been initialized and before the first update. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeUpdate - * @desc Called before updating `chart`. If any plugin returns `false`, the update - * is cancelled (and thus subsequent render(s)) until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart update. - */ - /** - * @method IPlugin#afterUpdate - * @desc Called after `chart` has been updated and before rendering. Note that this - * hook will not be called if the chart update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsUpdate - * @desc Called before updating the `chart` datasets. If any plugin returns `false`, - * the datasets update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} false to cancel the datasets update. - * @since version 2.1.5 - */ - /** - * @method IPlugin#afterDatasetsUpdate - * @desc Called after the `chart` datasets have been updated. Note that this hook - * will not be called if the datasets update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @since version 2.1.5 - */ - /** - * @method IPlugin#beforeDatasetUpdate - * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin - * returns `false`, the datasets update is cancelled until another `update` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetUpdate - * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note - * that this hook will not be called if the datasets update has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeLayout - * @desc Called before laying out `chart`. If any plugin returns `false`, - * the layout update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart layout. - */ - /** - * @method IPlugin#afterLayout - * @desc Called after the `chart` has been layed out. Note that this hook will not - * be called if the layout update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeRender - * @desc Called before rendering `chart`. If any plugin returns `false`, - * the rendering is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart rendering. - */ - /** - * @method IPlugin#afterRender - * @desc Called after the `chart` has been fully rendered (and animation completed). Note - * that this hook will not be called if the rendering has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDraw - * @desc Called before drawing `chart` at every animation frame specified by the given - * easing value. If any plugin returns `false`, the frame drawing is cancelled until - * another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart drawing. - */ - /** - * @method IPlugin#afterDraw - * @desc Called after the `chart` has been drawn for the specific easing value. Note - * that this hook will not be called if the drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsDraw - * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, - * the datasets drawing is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetsDraw - * @desc Called after the `chart` datasets have been drawn. Note that this hook - * will not be called if the datasets drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetDraw - * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets - * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing - * is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetDraw - * @desc Called after the `chart` datasets at the given `args.index` have been drawn - * (datasets are drawn in the reverse order). Note that this hook will not be called - * if the datasets drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeTooltipDraw - * @desc Called before drawing the `tooltip`. If any plugin returns `false`, - * the tooltip drawing is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart tooltip drawing. - */ - /** - * @method IPlugin#afterTooltipDraw - * @desc Called after drawing the `tooltip`. Note that this hook will not - * be called if the tooltip drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeEvent - * @desc Called before processing the specified `event`. If any plugin returns `false`, - * the event will be discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterEvent - * @desc Called after the `event` has been consumed. Note that this hook - * will not be called if the `event` has been previously discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#resize - * @desc Called after the chart as been resized. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} size - The new canvas display size (eq. canvas.style width & height). - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#destroy - * @desc Called after the chart as been destroyed. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - - /** - * Provided for backward compatibility, use Chart.plugins instead - * @namespace Chart.pluginService - * @deprecated since version 2.1.5 - * @todo remove at version 3 - * @private - */ - Chart.pluginService = Chart.plugins; - - /** - * Provided for backward compatibility, inheriting from Chart.PlugingBase has no - * effect, instead simply create/register plugins via plain JavaScript objects. - * @interface Chart.PluginBase - * @deprecated since version 2.5.0 - * @todo remove at version 3 - * @private - */ - Chart.PluginBase = Element.extend({}); -}; diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js new file mode 100644 index 00000000000..f5e8d10d8ac --- /dev/null +++ b/src/core/core.plugins.js @@ -0,0 +1,372 @@ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + plugins: {} +}); + +/** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ +module.exports = { + /** + * Globally registered plugins. + * @private + */ + _plugins: [], + + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + + /** + * Registers the given plugin(s) if not already registered. + * @param {Array|Object} plugins plugin instance(s). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); + + this._cacheId++; + }, + + /** + * Unregisters the given plugin(s) only if registered. + * @param {Array|Object} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + + this._cacheId++; + }, + + /** + * Remove all registered plugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; + this._cacheId++; + }, + + /** + * Returns the number of registered plugins? + * @returns {Number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin instances. + * @returns {Array} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. + * @returns {Boolean} false if any of the plugins return false, else returns true. + */ + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; + + for (i = 0; i < ilen; ++i) { + descriptor = descriptors[i]; + plugin = descriptor.plugin; + method = plugin[hook]; + if (typeof method === 'function') { + params = [chart].concat(args || []); + params.push(descriptor.options); + if (method.apply(plugin, params) === false) { + return false; + } + } + } + + return true; + }, + + /** + * Returns descriptors of enabled plugins for the given chart. + * @returns {Array} [{ plugin, options }] + * @private + */ + descriptors: function(chart) { + var cache = chart._plugins || (chart._plugins = {}); + if (cache.id === this._cacheId) { + return cache.descriptors; + } + + var plugins = []; + var descriptors = []; + var config = (chart && chart.config) || {}; + var options = (config.options && config.options.plugins) || {}; + + this._plugins.concat(config.plugins || []).forEach(function(plugin) { + var idx = plugins.indexOf(plugin); + if (idx !== -1) { + return; + } + + var id = plugin.id; + var opts = options[id]; + if (opts === false) { + return; + } + + if (opts === true) { + opts = helpers.clone(defaults.global.plugins[id]); + } + + plugins.push(plugin); + descriptors.push({ + plugin: plugin, + options: opts || {} + }); + }); + + cache.descriptors = descriptors; + cache.id = this._cacheId; + return descriptors; + } +}; + +/** + * Plugin extension hooks. + * @interface IPlugin + * @since 2.1.0 + */ +/** + * @method IPlugin#beforeInit + * @desc Called before initializing `chart`. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterInit + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeUpdate + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart update. + */ +/** + * @method IPlugin#afterUpdate + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsUpdate + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} false to cancel the datasets update. + * @since version 2.1.5 +*/ +/** + * @method IPlugin#afterDatasetsUpdate + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @since version 2.1.5 + */ +/** + * @method IPlugin#beforeDatasetUpdate + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetUpdate + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeLayout + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart layout. + */ +/** + * @method IPlugin#afterLayout + * @desc Called after the `chart` has been layed out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeRender + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart rendering. + */ +/** + * @method IPlugin#afterRender + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDraw + * @desc Called before drawing `chart` at every animation frame specified by the given + * easing value. If any plugin returns `false`, the frame drawing is cancelled until + * another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart drawing. + */ +/** + * @method IPlugin#afterDraw + * @desc Called after the `chart` has been drawn for the specific easing value. Note + * that this hook will not be called if the drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsDraw + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetsDraw + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetDraw + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetDraw + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeTooltipDraw + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart tooltip drawing. + */ +/** + * @method IPlugin#afterTooltipDraw + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeEvent + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterEvent + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#resize + * @desc Called after the chart as been resized. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} size - The new canvas display size (eq. canvas.style width & height). + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#destroy + * @desc Called after the chart as been destroyed. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 00000000000..1cd98151629 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = {}; +module.exports.filler = require('./plugin.filler'); +module.exports.legend = require('./plugin.legend'); +module.exports.title = require('./plugin.title'); diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index cf022654284..eb8dad4c3b0 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -18,304 +18,301 @@ defaults._set('global', { } }); -module.exports = function() { - - var mappers = { - dataset: function(source) { - var index = source.fill; - var chart = source.chart; - var meta = chart.getDatasetMeta(index); - var visible = meta && chart.isDatasetVisible(index); - var points = (visible && meta.dataset._children) || []; - var length = points.length || 0; - - return !length ? null : function(point, i) { - return (i < length && points[i]._view) || null; +var mappers = { + dataset: function(source) { + var index = source.fill; + var chart = source.chart; + var meta = chart.getDatasetMeta(index); + var visible = meta && chart.isDatasetVisible(index); + var points = (visible && meta.dataset._children) || []; + var length = points.length || 0; + + return !length ? null : function(point, i) { + return (i < length && points[i]._view) || null; + }; + }, + + boundary: function(source) { + var boundary = source.boundary; + var x = boundary ? boundary.x : null; + var y = boundary ? boundary.y : null; + + return function(point) { + return { + x: x === null ? point.x : x, + y: y === null ? point.y : y, }; - }, + }; + } +}; - boundary: function(source) { - var boundary = source.boundary; - var x = boundary ? boundary.x : null; - var y = boundary ? boundary.y : null; +// @todo if (fill[0] === '#') +function decodeFill(el, index, count) { + var model = el._model || {}; + var fill = model.fill; + var target; - return function(point) { - return { - x: x === null ? point.x : x, - y: y === null ? point.y : y, - }; - }; - } - }; + if (fill === undefined) { + fill = !!model.backgroundColor; + } + + if (fill === false || fill === null) { + return false; + } - // @todo if (fill[0] === '#') - function decodeFill(el, index, count) { - var model = el._model || {}; - var fill = model.fill; - var target; + if (fill === true) { + return 'origin'; + } - if (fill === undefined) { - fill = !!model.backgroundColor; + target = parseFloat(fill, 10); + if (isFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; } - if (fill === false || fill === null) { + if (target === index || target < 0 || target >= count) { return false; } - if (fill === true) { - return 'origin'; - } + return target; + } - target = parseFloat(fill, 10); - if (isFinite(target) && Math.floor(target) === target) { - if (fill[0] === '-' || fill[0] === '+') { - target = index + target; - } + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } +} - if (target === index || target < 0 || target >= count) { - return false; - } +function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; - return target; - } - - switch (fill) { - // compatibility - case 'bottom': - return 'start'; - case 'top': - return 'end'; - case 'zero': - return 'origin'; - // supported boundaries - case 'origin': - case 'start': - case 'end': - return fill; - // invalid fill values - default: - return false; - } + if (isFinite(fill)) { + return null; } - function computeBoundary(source) { - var model = source.el._model || {}; - var scale = source.el._scale || {}; - var fill = source.fill; - var target = null; - var horizontal; + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined ? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } - if (isFinite(fill)) { - return null; + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; } - // Backward compatibility: until v3, we still need to support boundary values set on - // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and - // controllers might still use it (e.g. the Smith chart). - - if (fill === 'start') { - target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; - } else if (fill === 'end') { - target = model.scaleTop === undefined ? scale.top : model.scaleTop; - } else if (model.scaleZero !== undefined) { - target = model.scaleZero; - } else if (scale.getBasePosition) { - target = scale.getBasePosition(); - } else if (scale.getBasePixel) { - target = scale.getBasePixel(); + if (typeof target === 'number' && isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; } + } - if (target !== undefined && target !== null) { - if (target.x !== undefined && target.y !== undefined) { - return target; - } + return null; +} - if (typeof target === 'number' && isFinite(target)) { - horizontal = scale.isHorizontal(); - return { - x: horizontal ? target : null, - y: horizontal ? null : target - }; - } - } +function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; - return null; + if (!propagate) { + return fill; } - function resolveTarget(sources, index, propagate) { - var source = sources[index]; - var fill = source.fill; - var visited = [index]; - var target; - - if (!propagate) { + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isFinite(fill)) { return fill; } - while (fill !== false && visited.indexOf(fill) === -1) { - if (!isFinite(fill)) { - return fill; - } - - target = sources[fill]; - if (!target) { - return false; - } - - if (target.visible) { - return fill; - } + target = sources[fill]; + if (!target) { + return false; + } - visited.push(fill); - fill = target.fill; + if (target.visible) { + return fill; } - return false; + visited.push(fill); + fill = target.fill; } - function createMapper(source) { - var fill = source.fill; - var type = 'dataset'; - - if (fill === false) { - return null; - } + return false; +} - if (!isFinite(fill)) { - type = 'boundary'; - } +function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; - return mappers[type](source); + if (fill === false) { + return null; } - function isDrawable(point) { - return point && !point.skip; + if (!isFinite(fill)) { + type = 'boundary'; } - function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + return mappers[type](source); +} - if (!len0 || !len1) { - return; - } +function isDrawable(point) { + return point && !point.skip; +} - // building first area curve (normal) - ctx.moveTo(curve0[0].x, curve0[0].y); - for (i = 1; i < len0; ++i) { - helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); - } +function drawArea(ctx, curve0, curve1, len0, len1) { + var i; - // joining the two area curves - ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + if (!len0 || !len1) { + return; + } - // building opposite area curve (reverse) - for (i = len1 - 1; i > 0; --i) { - helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); - } + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i = 1; i < len0; ++i) { + helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); } - function doFill(ctx, points, mapper, view, color, loop) { - var count = points.length; - var span = view.spanGaps; - var curve0 = []; - var curve1 = []; - var len0 = 0; - var len1 = 0; - var i, ilen, index, p0, p1, d0, d1; - - ctx.beginPath(); - - for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { - index = i % count; - p0 = points[index]._view; - p1 = mapper(p0, index, view); - d0 = isDrawable(p0); - d1 = isDrawable(p1); - - if (d0 && d1) { - len0 = curve0.push(p0); - len1 = curve1.push(p1); - } else if (len0 && len1) { - if (!span) { - drawArea(ctx, curve0, curve1, len0, len1); - len0 = len1 = 0; - curve0 = []; - curve1 = []; - } else { - if (d0) { - curve0.push(p0); - } - if (d1) { - curve1.push(p1); - } + // joining the two area curves + ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + + // building opposite area curve (reverse) + for (i = len1 - 1; i > 0; --i) { + helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + } +} + +function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i % count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); } } } - - drawArea(ctx, curve0, curve1, len0, len1); - - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); } - return { - id: 'filler', - - afterDatasetsUpdate: function(chart, options) { - var count = (chart.data.datasets || []).length; - var propagate = options.propagate; - var sources = []; - var meta, i, el, source; - - for (i = 0; i < count; ++i) { - meta = chart.getDatasetMeta(i); - el = meta.dataset; - source = null; - - if (el && el._model && el instanceof elements.Line) { - source = { - visible: chart.isDatasetVisible(i), - fill: decodeFill(el, i, count), - chart: chart, - el: el - }; - } - - meta.$filler = source; - sources.push(source); + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +module.exports = { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; } - for (i = 0; i < count; ++i) { - source = sources[i]; - if (!source) { - continue; - } + meta.$filler = source; + sources.push(source); + } - source.fill = resolveTarget(sources, i, propagate); - source.boundary = computeBoundary(source); - source.mapper = createMapper(source); + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source) { + continue; } - }, - beforeDatasetDraw: function(chart, args) { - var meta = args.meta.$filler; - if (!meta) { - return; - } + source.fill = resolveTarget(sources, i, propagate); + source.boundary = computeBoundary(source); + source.mapper = createMapper(source); + } + }, - var ctx = chart.ctx; - var el = meta.el; - var view = el._view; - var points = el._children || []; - var mapper = meta.mapper; - var color = view.backgroundColor || defaults.global.defaultColor; - - if (mapper && color && points.length) { - helpers.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, view, color, el._loop); - helpers.canvas.unclipArea(ctx); - } + beforeDatasetDraw: function(chart, args) { + var meta = args.meta.$filler; + if (!meta) { + return; + } + + var ctx = chart.ctx; + var el = meta.el; + var view = el._view; + var points = el._children || []; + var mapper = meta.mapper; + var color = view.backgroundColor || defaults.global.defaultColor; + + if (mapper && color && points.length) { + helpers.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers.canvas.unclipArea(ctx); } - }; + } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 7e0e429d0ce..3715ea3d5f7 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -5,6 +5,8 @@ var Element = require('../core/core.element'); var helpers = require('../helpers/index'); var layout = require('../core/core.layout'); +var noop = helpers.noop; + defaults._set('global', { legend: { display: true, @@ -80,488 +82,495 @@ defaults._set('global', { } }); -module.exports = function(Chart) { - - var noop = helpers.noop; - - /** - * 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 = Element.extend({ - - initialize: function(config) { - helpers.extend(this, config); - - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; - - // Are we in doughnut mode which has a different data type - this.doughnutMode = false; - }, - - // These methods are ordered by lifecycle. Utilities then follow. - // Any function defined here is inherited by all legend types. - // Any function can be extended by the legend type - - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - }, - afterUpdate: noop, +/** + * 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; +} + +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Legend = Element.extend({ + + initialize: function(config) { + helpers.extend(this, config); + + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; + + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + }, + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all legend types. + // Any function can be extended by the legend type + + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + return me.minSize; + }, + afterUpdate: noop, + + // + + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + // - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + beforeBuildLabels: noop, + buildLabels: function() { + var me = this; + var labelOpts = me.options.labels || {}; + var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; - // + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } - beforeBuildLabels: noop, - buildLabels: function() { - var me = this; - var labelOpts = me.options.labels || {}; - var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + if (me.options.reverse) { + legendItems.reverse(); + } - if (labelOpts.filter) { - legendItems = legendItems.filter(function(item) { - return labelOpts.filter(item, me.chart.data); - }); - } + me.legendItems = legendItems; + }, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var display = opts.display; + + var ctx = me.ctx; + + var globalDefault = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + + // Reset hit boxes + var hitboxes = me.legendHitBoxes = []; + + var minSize = me.minSize; + var isHorizontal = me.isHorizontal(); + + if (isHorizontal) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = display ? 10 : 0; + } else { + minSize.width = display ? 10 : 0; + minSize.height = me.maxHeight; // fill all the height + } - if (me.options.reverse) { - legendItems.reverse(); - } + // Increase sizes here + if (display) { + ctx.font = labelFont; - me.legendItems = legendItems; - }, - afterBuildLabels: noop, + if (isHorizontal) { + // Labels - // + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; - beforeFit: noop, - fit: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var display = opts.display; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; - var ctx = me.ctx; + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - var globalDefault = defaults.global; - var valueOrDefault = helpers.valueOrDefault; - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { + totalHeight += fontSize + (labelOpts.padding); + lineWidths[lineWidths.length] = me.left; + } - // Reset hit boxes - var hitboxes = me.legendHitBoxes = []; + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; - var minSize = me.minSize; - var isHorizontal = me.isHorizontal(); + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; + }); - if (isHorizontal) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = display ? 10 : 0; - } else { - minSize.width = display ? 10 : 0; - minSize.height = me.maxHeight; // fill all the height - } + minSize.height += totalHeight; - // Increase sizes here - if (display) { - ctx.font = labelFont; + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; + var itemHeight = fontSize + vPadding; - if (isHorizontal) { - // Labels + helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(labelOpts, fontSize); + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - var lineWidths = me.lineWidths = [0]; - var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; + // If too tall, go to new column + if (currentColHeight + itemHeight > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + currentColWidth = 0; + currentColHeight = 0; + } - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight; - if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { - totalHeight += fontSize + (labelOpts.padding); - lineWidths[lineWidths.length] = me.left; - } + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + minSize.width += totalWidth; + } + } - lineWidths[lineWidths.length - 1] += width + labelOpts.padding; - }); + me.width = minSize.width; + me.height = minSize.height; + }, + afterFit: noop, - minSize.height += totalHeight; + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, - } else { - var vPadding = labelOpts.padding; - var columnWidths = me.columnWidths = []; - var totalWidth = labelOpts.padding; - var currentColWidth = 0; - var currentColHeight = 0; - var itemHeight = fontSize + vPadding; - - helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - // If too tall, go to new column - if (currentColHeight + itemHeight > minSize.height) { - totalWidth += currentColWidth + labelOpts.padding; - columnWidths.push(currentColWidth); // previous column width - - currentColWidth = 0; - currentColHeight = 0; - } - - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); - - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; + // Actually draw the legend on the canvas + draw: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var globalDefault = defaults.global; + var lineDefault = globalDefault.elements.line; + var legendWidth = me.width; + var lineWidths = me.lineWidths; + + if (opts.display) { + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var cursor; + + // Canvas setup + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont; + + var boxWidth = getBoxWidth(labelOpts, fontSize); + var hitboxes = me.legendHitBoxes; + + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; } - } - - me.width = minSize.width; - me.height = minSize.height; - }, - afterFit: noop, - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, + // Set the ctx for the box + ctx.save(); - // Actually draw the legend on the canvas - draw: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var globalDefault = defaults.global; - var lineDefault = globalDefault.elements.line; - var legendWidth = me.width; - var lineWidths = me.lineWidths; - - if (opts.display) { - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var cursor; - - // Canvas setup - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; - - var boxWidth = getBoxWidth(labelOpts, fontSize); - var hitboxes = me.legendHitBoxes; - - // current position - var drawLegendBox = function(x, y, legendItem) { - if (isNaN(boxWidth) || boxWidth <= 0) { - return; - } + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); + var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); - // Set the ctx for the box - ctx.save(); + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); + } - ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); - ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); - ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); - ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); - ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); - var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); + if (opts.labels && opts.labels.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = fontSize * Math.SQRT2 / 2; + var offSet = radius / Math.SQRT2; + var centerX = x + offSet; + var centerY = y + offSet; - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); + // Draw pointStyle as legend symbol + helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } else { + // Draw box as legend symbol + if (!isLineWidthZero) { + ctx.strokeRect(x, y, boxWidth, fontSize); } + ctx.fillRect(x, y, boxWidth, fontSize); + } - if (opts.labels && opts.labels.usePointStyle) { - // Recalculate x and y for drawPoint() because its expecting - // x and y to be center of figure (instead of top left) - var radius = fontSize * Math.SQRT2 / 2; - var offSet = radius / Math.SQRT2; - var centerX = x + offSet; - var centerY = y + offSet; - - // Draw pointStyle as legend symbol - helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); - } else { - // Draw box as legend symbol - if (!isLineWidthZero) { - ctx.strokeRect(x, y, boxWidth, fontSize); - } - ctx.fillRect(x, y, boxWidth, fontSize); - } + ctx.restore(); + }; + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = boxWidth + halfFontSize + x; + var yMiddle = y + halfFontSize; + + ctx.fillText(legendItem.text, xLeft, yMiddle); + + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(xLeft + textWidth, yMiddle); + ctx.stroke(); + } + }; - ctx.restore(); + // Horizontal + var isHorizontal = me.isHorizontal(); + if (isHorizontal) { + cursor = { + x: me.left + ((legendWidth - lineWidths[0]) / 2), + y: me.top + labelOpts.padding, + line: 0 }; - var fillText = function(x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; - - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } + } else { + cursor = { + x: me.left + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 }; + } - // Horizontal - var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2), - y: me.top + labelOpts.padding, - line: 0 - }; - } else { - cursor = { - x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; - } + var itemHeight = fontSize + labelOpts.padding; + helpers.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; - var itemHeight = fontSize + labelOpts.padding; - helpers.each(me.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = boxWidth + (fontSize / 2) + textWidth; - var x = cursor.x; - var y = cursor.y; - - if (isHorizontal) { - if (x + width >= legendWidth) { - y = cursor.y += itemHeight; - cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); - } - } else if (y + itemHeight > me.bottom) { - x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; + if (isHorizontal) { + if (x + width >= legendWidth) { + y = cursor.y += itemHeight; cursor.line++; + x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); } + } else if (y + itemHeight > me.bottom) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + y = cursor.y = me.top + labelOpts.padding; + cursor.line++; + } - drawLegendBox(x, y, legendItem); + drawLegendBox(x, y, legendItem); - hitboxes[i].left = x; - hitboxes[i].top = y; + hitboxes[i].left = x; + hitboxes[i].top = y; - // Fill the actual label - fillText(x, y, legendItem, textWidth); + // Fill the actual label + fillText(x, y, legendItem, textWidth); - if (isHorizontal) { - cursor.x += width + (labelOpts.padding); - } else { - cursor.y += itemHeight; - } + if (isHorizontal) { + cursor.x += width + (labelOpts.padding); + } else { + cursor.y += itemHeight; + } - }); - } - }, + }); + } + }, - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @return {Boolean} true if a change occured - */ - handleEvent: function(e) { - var me = this; - var opts = me.options; - var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; - - if (type === 'mousemove') { - if (!opts.onHover) { - return; - } - } else if (type === 'click') { - if (!opts.onClick) { - return; - } - } else { + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @return {Boolean} true if a change occured + */ + handleEvent: function(e) { + var me = this; + var opts = me.options; + var type = e.type === 'mouseup' ? 'click' : e.type; + var changed = false; + + if (type === 'mousemove') { + if (!opts.onHover) { + return; + } + } else if (type === 'click') { + if (!opts.onClick) { return; } + } else { + return; + } - // Chart event already has relative position in it - var x = e.x; - var y = e.y; - - if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { - // See if we are touching one of the dataset boxes - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; - - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + // Chart event already has relative position in it + var x = e.x; + var y = e.y; + + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + var lh = me.legendHitBoxes; + for (var i = 0; i < lh.length; ++i) { + var hitBox = lh[i]; + + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + if (type === 'click') { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); + changed = true; + break; + } else if (type === 'mousemove') { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); + changed = true; + break; } } } - - return changed; } - }); - function createNewLegendAndAttach(chart, legendOpts) { - var legend = new Chart.Legend({ - ctx: chart.ctx, - options: legendOpts, - chart: chart - }); - - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); - chart.legend = legend; + return changed; } +}); - return { - id: 'legend', +function createNewLegendAndAttach(chart, legendOpts) { + var legend = new Legend({ + ctx: chart.ctx, + options: legendOpts, + chart: chart + }); - beforeInit: function(chart) { - var legendOpts = chart.options.legend; + layout.configure(chart, legend, legendOpts); + layout.addBox(chart, legend); + chart.legend = legend; +} - if (legendOpts) { - createNewLegendAndAttach(chart, legendOpts); - } - }, +module.exports = { + id: 'legend', - beforeUpdate: function(chart) { - var legendOpts = chart.options.legend; - var legend = chart.legend; + /** + * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making + * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Legend, - if (legendOpts) { - helpers.mergeIf(legendOpts, defaults.global.legend); + beforeInit: function(chart) { + var legendOpts = chart.options.legend; - if (legend) { - layout.configure(chart, legend, legendOpts); - legend.options = legendOpts; - } else { - createNewLegendAndAttach(chart, legendOpts); - } - } else if (legend) { - layout.removeBox(chart, legend); - delete chart.legend; - } - }, + if (legendOpts) { + createNewLegendAndAttach(chart, legendOpts); + } + }, + + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; + var legend = chart.legend; + + if (legendOpts) { + helpers.mergeIf(legendOpts, defaults.global.legend); - afterEvent: function(chart, e) { - var legend = chart.legend; if (legend) { - legend.handleEvent(e); + layout.configure(chart, legend, legendOpts); + legend.options = legendOpts; + } else { + createNewLegendAndAttach(chart, legendOpts); } + } else if (legend) { + layout.removeBox(chart, legend); + delete chart.legend; } - }; + }, + + afterEvent: function(chart, e) { + var legend = chart.legend; + if (legend) { + legend.handleEvent(e); + } + } }; diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 8eac103f542..0a233f9bca4 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -5,6 +5,8 @@ var Element = require('../core/core.element'); var helpers = require('../helpers/index'); var layout = require('../core/core.layout'); +var noop = helpers.noop; + defaults._set('global', { title: { display: false, @@ -18,226 +20,233 @@ defaults._set('global', { } }); -module.exports = function(Chart) { - - var noop = helpers.noop; - - Chart.Title = Element.extend({ - initialize: function(config) { - var me = this; - helpers.extend(me, config); - - // Contains hit boxes for each dataset (in dataset order) - me.legendHitBoxes = []; - }, - - // These methods are ordered by lifecycle. Utilities then follow. - - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - - }, - afterUpdate: noop, - - // - - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; - - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } - - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; - - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, - +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Title = Element.extend({ + initialize: function(config) { + var me = this; + helpers.extend(me, config); + + // Contains hit boxes for each dataset (in dataset order) + me.legendHitBoxes = []; + }, + + // These methods are ordered by lifecycle. Utilities then follow. + + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); + + return me.minSize; + + }, + afterUpdate: noop, + + // + + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - beforeBuildLabels: noop, - buildLabels: noop, - afterBuildLabels: noop, - - // + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, + + // + + beforeBuildLabels: noop, + buildLabels: noop, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var display = opts.display; + var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); + var minSize = me.minSize; + var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; + + if (me.isHorizontal()) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = textSize; + } else { + minSize.width = textSize; + minSize.height = me.maxHeight; // fill all the height + } - beforeFit: noop, - fit: function() { - var me = this; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var display = opts.display; - var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); - var minSize = me.minSize; - var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + me.width = minSize.width; + me.height = minSize.height; + + }, + afterFit: noop, + + // Shared Methods + isHorizontal: function() { + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + }, + + // Actually draw the title block on the canvas + draw: function() { + var me = this; + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var globalDefaults = defaults.global; + + if (opts.display) { + var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); + var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); + var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; - + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; + + ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour + ctx.font = titleFont; + + // Horizontal if (me.isHorizontal()) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = textSize; + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; } else { - minSize.width = textSize; - minSize.height = me.maxHeight; // fill all the height + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); } - me.width = minSize.width; - me.height = minSize.height; - - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - var pos = this.options.position; - return pos === 'top' || pos === 'bottom'; - }, - - // Actually draw the title block on the canvas - draw: function() { - var me = this; - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var globalDefaults = defaults.global; - - if (opts.display) { - var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); - var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var offset = lineHeight / 2 + opts.padding; - var rotation = 0; - var top = me.top; - var left = me.left; - var bottom = me.bottom; - var right = me.right; - var maxWidth, titleX, titleY; - - ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour - ctx.font = titleFont; - - // Horizontal - if (me.isHorizontal()) { - titleX = left + ((right - left) / 2); // midpoint of the width - titleY = top + offset; - maxWidth = right - left; - } else { - titleX = opts.position === 'left' ? left + offset : right - offset; - titleY = top + ((bottom - top) / 2); - maxWidth = bottom - top; - rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + var text = opts.text; + if (helpers.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; } - - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - var text = opts.text; - if (helpers.isArray(text)) { - var y = 0; - for (var i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } - - ctx.restore(); + } else { + ctx.fillText(text, 0, 0, maxWidth); } + + ctx.restore(); } + } +}); + +function createNewTitleBlockAndAttach(chart, titleOpts) { + var title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart: chart }); - function createNewTitleBlockAndAttach(chart, titleOpts) { - var title = new Chart.Title({ - ctx: chart.ctx, - options: titleOpts, - chart: chart - }); + layout.configure(chart, title, titleOpts); + layout.addBox(chart, title); + chart.titleBlock = title; +} - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); - chart.titleBlock = title; - } +module.exports = { + id: 'title', - return { - id: 'title', + /** + * Backward compatibility: since 2.1.5, the title is registered as a plugin, making + * Chart.Title obsolete. To avoid a breaking change, we export the Title as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Title, - beforeInit: function(chart) { - var titleOpts = chart.options.title; + beforeInit: function(chart) { + var titleOpts = chart.options.title; - if (titleOpts) { - createNewTitleBlockAndAttach(chart, titleOpts); - } - }, + if (titleOpts) { + createNewTitleBlockAndAttach(chart, titleOpts); + } + }, - beforeUpdate: function(chart) { - var titleOpts = chart.options.title; - var titleBlock = chart.titleBlock; + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; + var titleBlock = chart.titleBlock; - if (titleOpts) { - helpers.mergeIf(titleOpts, defaults.global.title); + if (titleOpts) { + helpers.mergeIf(titleOpts, defaults.global.title); - if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); - titleBlock.options = titleOpts; - } else { - createNewTitleBlockAndAttach(chart, titleOpts); - } - } else if (titleBlock) { - layout.removeBox(chart, titleBlock); - delete chart.titleBlock; + if (titleBlock) { + layout.configure(chart, titleBlock, titleOpts); + titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chart, titleOpts); } + } else if (titleBlock) { + layout.removeBox(chart, titleBlock); + delete chart.titleBlock; } - }; + } }; diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index fee37288df9..08d5e79dbab 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -381,5 +381,23 @@ describe('Deprecations', function() { expect(Chart.pluginService).toBe(Chart.plugins); }); }); + + describe('Chart.Legend', function() { + it('should be defined and an instance of Chart.Element', function() { + var legend = new Chart.Legend({}); + expect(Chart.Legend).toBeDefined(); + expect(legend).not.toBe(undefined); + expect(legend instanceof Chart.Element).toBeTruthy(); + }); + }); + + describe('Chart.Title', function() { + it('should be defined and an instance of Chart.Element', function() { + var title = new Chart.Title({}); + expect(Chart.Title).toBeDefined(); + expect(title).not.toBe(undefined); + expect(title instanceof Chart.Element).toBeTruthy(); + }); + }); }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index b950c360160..5b75069aaea 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -1,10 +1,5 @@ // Test the rectangle element describe('Legend block tests', function() { - it('Should be constructed', function() { - var legend = new Chart.Legend({}); - expect(legend).not.toBe(undefined); - }); - it('should have the correct default config', function() { expect(Chart.defaults.global.legend).toEqual({ display: true, diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index edeb32a8b47..28786f05402 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -1,11 +1,6 @@ // Test the rectangle element describe('Title block tests', function() { - it('Should be constructed', function() { - var title = new Chart.Title({}); - expect(title).not.toBe(undefined); - }); - it('Should have the correct default config', function() { expect(Chart.defaults.global.title).toEqual({ display: false, From 6bea15e7cf89003e3a5945a20cf1d2cc5096728e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 9 Jan 2018 14:12:40 +0100 Subject: [PATCH 362/685] Rename Chart.layout to Chart.layouts (#5118) Chart.layouts seems more consistent with other service names (Chart.plugins, Chart.scales, etc.) but also more inline with the service which handle many layout (one per charts). --- src/chart.js | 6 +++--- src/core/core.controller.js | 6 +++--- src/core/{core.layout.js => core.layouts.js} | 0 src/core/core.scaleService.js | 4 ++-- src/plugins/plugin.legend.js | 10 +++++----- src/plugins/plugin.title.js | 10 +++++----- ...layoutService.tests.js => core.layouts.tests.js} | 13 +++++++++++-- test/specs/global.deprecations.tests.js | 8 ++++---- 8 files changed, 33 insertions(+), 24 deletions(-) rename src/core/{core.layout.js => core.layouts.js} (100%) rename test/specs/{core.layoutService.tests.js => core.layouts.tests.js} (97%) diff --git a/src/chart.js b/src/chart.js index 70d70a66995..a958e343ff8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,7 +12,7 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); -Chart.layout = require('./core/core.layout'); +Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); Chart.plugins = require('./core/core.plugins'); Chart.Ticks = require('./core/core.ticks'); @@ -113,10 +113,10 @@ Chart.PluginBase = Chart.Element.extend({}); Chart.canvasHelpers = Chart.helpers.canvas; /** - * Provided for backward compatibility, use Chart.layout instead. + * Provided for backward compatibility, use Chart.layouts instead. * @namespace Chart.layoutService * @deprecated since version 2.8.0 * @todo remove at version 3 * @private */ -Chart.layoutService = Chart.layout; +Chart.layoutService = Chart.layouts; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 157bc50a423..e32255edcea 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,7 +3,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); -var layout = require('./core.layout'); +var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); var plugins = require('./core.plugins'); @@ -47,7 +47,7 @@ module.exports = function(Chart) { var newOptions = chart.options; helpers.each(chart.scales, function(scale) { - layout.removeBox(chart, scale); + layouts.removeBox(chart, scale); }); newOptions = helpers.configMerge( @@ -436,7 +436,7 @@ module.exports = function(Chart) { return; } - layout.update(this, this.width, this.height); + layouts.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. diff --git a/src/core/core.layout.js b/src/core/core.layouts.js similarity index 100% rename from src/core/core.layout.js rename to src/core/core.layouts.js diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index bfbf832094a..f2ea01d329a 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,7 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); -var layout = require('./core.layout'); +var layouts = require('./core.layouts'); module.exports = function(Chart) { @@ -39,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - layout.addBox(chart, scale); + layouts.addBox(chart, scale); }); } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 3715ea3d5f7..3f1559c3003 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,7 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); -var layout = require('../core/core.layout'); +var layouts = require('../core/core.layouts'); var noop = helpers.noop; @@ -523,8 +523,8 @@ function createNewLegendAndAttach(chart, legendOpts) { chart: chart }); - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); + layouts.configure(chart, legend, legendOpts); + layouts.addBox(chart, legend); chart.legend = legend; } @@ -556,13 +556,13 @@ module.exports = { helpers.mergeIf(legendOpts, defaults.global.legend); if (legend) { - layout.configure(chart, legend, legendOpts); + layouts.configure(chart, legend, legendOpts); legend.options = legendOpts; } else { createNewLegendAndAttach(chart, legendOpts); } } else if (legend) { - layout.removeBox(chart, legend); + layouts.removeBox(chart, legend); delete chart.legend; } }, diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index 0a233f9bca4..47588844d4c 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,7 +3,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); -var layout = require('../core/core.layout'); +var layouts = require('../core/core.layouts'); var noop = helpers.noop; @@ -206,8 +206,8 @@ function createNewTitleBlockAndAttach(chart, titleOpts) { chart: chart }); - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); chart.titleBlock = title; } @@ -239,13 +239,13 @@ module.exports = { helpers.mergeIf(titleOpts, defaults.global.title); if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); + layouts.configure(chart, titleBlock, titleOpts); titleBlock.options = titleOpts; } else { createNewTitleBlockAndAttach(chart, titleOpts); } } else if (titleBlock) { - layout.removeBox(chart, titleBlock); + layouts.removeBox(chart, titleBlock); delete chart.titleBlock; } } diff --git a/test/specs/core.layoutService.tests.js b/test/specs/core.layouts.tests.js similarity index 97% rename from test/specs/core.layoutService.tests.js rename to test/specs/core.layouts.tests.js index a8673971b06..19b3a14b240 100644 --- a/test/specs/core.layoutService.tests.js +++ b/test/specs/core.layouts.tests.js @@ -1,5 +1,14 @@ -// Tests of the scale service -describe('Test the layout service', function() { +describe('Chart.layouts', function() { + it('should be exposed through Chart.layouts', function() { + expect(Chart.layouts).toBeDefined(); + expect(typeof Chart.layouts).toBe('object'); + expect(Chart.layouts.defaults).toBeDefined(); + expect(Chart.layouts.addBox).toBeDefined(); + expect(Chart.layouts.removeBox).toBeDefined(); + expect(Chart.layouts.configure).toBeDefined(); + expect(Chart.layouts.update).toBeDefined(); + }); + // 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 diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 08d5e79dbab..535b9af3307 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,9 +1,9 @@ describe('Deprecations', function() { describe('Version 2.8.0', function() { describe('Chart.layoutService', function() { - it('should be defined and an alias of Chart.layout', function() { + it('should be defined and an alias of Chart.layouts', function() { expect(Chart.layoutService).toBeDefined(); - expect(Chart.layoutService).toBe(Chart.layout); + expect(Chart.layoutService).toBe(Chart.layouts); }); }); }); @@ -311,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layout.update; - Chart.layout.update = function() { + var override = Chart.layouts.update; + Chart.layouts.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; From 1d5619d6d42867f4380527901f4a39a17095d98e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 11 Jan 2018 09:03:16 +0100 Subject: [PATCH 363/685] Fix GitBook error with the shared ESLint config (#5133) `gitbook-cli install` failed when trying to fetch eslint-config-chartjs because of the way it was installed (ie. using the GitHub repository URL). The shared config is now published on npmjs: https://www.npmjs.com/package/eslint-config-chartjs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 510c141cd88..c7d21043b1a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "child-process-promise": "^2.2.1", "coveralls": "^3.0.0", "eslint": "^4.9.0", - "eslint-config-chartjs": "git+https://github.com/chartjs/eslint-config-chartjs.git", + "eslint-config-chartjs": "^0.1.0", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", From 2f5a3e171be30465f107e78c4283a2111ed3d5b4 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 11 Jan 2018 12:51:03 +0100 Subject: [PATCH 364/685] Ignore package-lock.json (#5138) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cfb878a6dee..0a65be9b282 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /gh-pages /jsdoc /node_modules +/package-lock.json .DS_Store .idea .vscode From 2d7f0a46c3f58278f883cb829d9e8a109151559d Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 13 Jan 2018 14:23:50 +0100 Subject: [PATCH 365/685] Fix updating plugin options (#5144) Cached plugin descriptors hold a reference on the plugin options, which break if the plugin options object is replaced. That case happens when the user updates the plugin options with a new object, but also since the new config update logic (#4198) that now always clones the plugin options. The fix consists in explicitly invalidating that cache before updating the chart. --- src/core/core.controller.js | 4 ++++ src/core/core.plugins.js | 12 +++++++++++- test/specs/core.plugin.tests.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e32255edcea..e29a5b0769c 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -376,6 +376,10 @@ module.exports = function(Chart) { updateConfig(me); + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + plugins._invalidate(me); + if (plugins.notify(me, 'beforeUpdate') === false) { return; } diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index f5e8d10d8ac..f2fbcadec31 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -121,7 +121,7 @@ module.exports = { * @private */ descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); + var cache = chart.$plugins || (chart.$plugins = {}); if (cache.id === this._cacheId) { return cache.descriptors; } @@ -157,6 +157,16 @@ module.exports = { cache.descriptors = descriptors; cache.id = this._cacheId; return descriptors; + }, + + /** + * Invalidates cache for the given chart: descriptors hold a reference on plugin option, + * but in some cases, this reference can be changed by the user when updating options. + * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + * @private + */ + _invalidate: function(chart) { + delete chart.$plugins; } }; diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index 387d78808c7..3a9e908a38d 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -339,6 +339,39 @@ describe('Chart.plugins', function() { expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'}); + + delete Chart.defaults.global.plugins.a; + }); + + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + it('should invalidate cache when update plugin options', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + foo: 'foo' + } + } + }, + }); + + spyOn(plugin, 'hook'); + + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({foo: 'foo'}); + + chart.options.plugins.a = {bar: 'bar'}; + chart.update(); + + plugin.hook.calls.reset(); + Chart.plugins.notify(chart, 'hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[1]).toEqual({bar: 'bar'}); }); }); }); From 37ec8384d7f6ce9b07a822626d787d1327a4a235 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 13 Jan 2018 07:39:17 -0800 Subject: [PATCH 366/685] Format the label in the time scale tooltip (#5095) --- src/scales/scale.time.js | 29 ++++++++- test/specs/scale.time.tests.js | 109 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 90904f9e6f0..4892eea9996 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -403,6 +403,27 @@ function ticksFromTimestamps(values, majorUnit) { return ticks; } +function determineLabelFormat(data, timeOpts) { + var i, momentDate, hasTime; + var ilen = data.length; + + // find the label with the most parts (milliseconds, minutes, etc.) + // format all labels with the same level of detail as the most specific label + for (i = 0; i < ilen; i++) { + momentDate = momentify(data[i], timeOpts); + if (momentDate.millisecond() !== 0) { + return 'MMM D, YYYY h:mm:ss.SSS a'; + } + if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { + hasTime = true; + } + } + if (hasTime) { + return 'MMM D, YYYY h:mm:ss a'; + } + return 'MMM D, YYYY'; +} + module.exports = function(Chart) { var defaultConfig = { @@ -621,6 +642,7 @@ module.exports = function(Chart) { me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); + me._labelFormat = determineLabelFormat(me._timestamps.data, timeOpts); return ticksFromTimestamps(ticks, me._majorUnit); }, @@ -636,10 +658,13 @@ module.exports = function(Chart) { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - label = momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + } + if (typeof label === 'string') { + return label; } - return label; + return momentify(label, timeOpts).format(me._labelFormat); }, /** diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index e4c1739e6c5..964e25e080a 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -584,6 +584,115 @@ describe('Time scale tests', function() { expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); }); + it('should get the correct label when time is specified as a string', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{t: '2015-01-01T20:00:00', y: 10}, {t: '2015-01-02T21:00:00', y: 3}] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); + expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + }); + + it('should get the correct label for a timestamp with milliseconds', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23.234'), y: 10}, + {t: +new Date('2018-01-09 06:17:43.426'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23.234 am'); + }); + + it('should get the correct label for a timestamp with time', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23'), y: 10}, + {t: +new Date('2018-01-09 06:17:43'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23 am'); + }); + + it('should get the correct label for a timestamp representing a date', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 00:00:00'), y: 10}, + {t: +new Date('2018-01-09 00:00:00'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018'); + }); + it('should get the correct pixel for only one data in the dataset', function() { var chart = window.acquireChart({ type: 'line', From e585c7505f33e7115a0d75b7d058e3c0d37a6952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Sat, 13 Jan 2018 17:17:38 +0000 Subject: [PATCH 367/685] Log gulp error to Chart.js (#5143) * Log errors and skip for buildTask * Write gulp error to Chart.js + Add intentional error to core to check if travis fails * Remove unused require * Remove error + Proper require fs * Fix newline * Refactor * Put back browser errors * Use options * Fix intentional error * Use yargs + Refactor * remove space * Fefactor * Use booleans --- gulpfile.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 5a3093e444e..09165c8b041 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,10 +17,17 @@ 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 yargs = require('yargs'); var path = require('path'); +var fs = require('fs'); var package = require('./package.json'); +var argv = yargs + .option('force-output', {default: false}) + .option('silent-errors', {default: false}) + .option('verbose', {default: false}) + .argv + var srcDir = './src/'; var outDir = './dist/'; @@ -34,6 +41,10 @@ var header = "/*!\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; +if (argv.verbose) { + util.log("Gulp running with options: " + JSON.stringify(argv, null, 2)); +} + gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); @@ -79,9 +90,25 @@ function bowerTask() { function buildTask() { + var errorHandler = function (err) { + if(argv.forceOutput) { + var browserError = 'console.error("Gulp: ' + err.toString() + '")'; + ['Chart', 'Chart.min', 'Chart.bundle', 'Chart.bundle.min'].forEach(function(fileName) { + fs.writeFileSync(outDir+fileName+'.js', browserError); + }); + } + if(argv.silentErrors) { + util.log(util.colors.red('[Error]'), err.toString()); + this.emit('end'); + } else { + throw err; + } + } + var bundled = browserify('./src/chart.js', { standalone: 'Chart' }) .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) @@ -96,6 +123,7 @@ function buildTask() { .ignore('moment') .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) From 6d58a6a8a8af64373605c86fe380a61250411e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Tue, 16 Jan 2018 09:29:49 +0000 Subject: [PATCH 368/685] Add tests related to showLines for controller.scatter (#5150) --- test/specs/controller.scatter.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/specs/controller.scatter.test.js diff --git a/test/specs/controller.scatter.test.js b/test/specs/controller.scatter.test.js new file mode 100644 index 00000000000..435750928d9 --- /dev/null +++ b/test/specs/controller.scatter.test.js @@ -0,0 +1,25 @@ +describe('Chart.controllers.scatter', function() { + describe('showLines option', function() { + it('should not draw a line if undefined', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}], + label: 'dataset1' + }], + }, + options: {} + }); + + var meta = chart.getDatasetMeta(0); + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + + chart.update(); + + expect(meta.dataset.draw.calls.count()).toBe(0); + expect(meta.data[0].draw.calls.count()).toBe(1); + }); + }); +}); From 26c44cf7abfce2df221f9c9b568e1c5eae893bfb Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 12:12:33 -0500 Subject: [PATCH 369/685] Treat negative values in doughnut charts as positive (#5165) --- src/controllers/controller.doughnut.js | 2 +- test/specs/controller.doughnut.tests.js | 40 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 6028602c472..f7b36e989a9 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -273,7 +273,7 @@ module.exports = function(Chart) { calculateCircumference: function(value) { var total = this.getMeta().total; if (total > 0 && !isNaN(value)) { - return (Math.PI * 2.0) * (value / total); + return (Math.PI * 2.0) * (Math.abs(value) / total); } return 0; }, diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 85e382ac3f9..a4d6526c912 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -205,6 +205,46 @@ describe('Chart.controllers.doughnut', function() { }); }); + it('should treat negative values as positive', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [-1, -3] + }], + labels: ['label0', 'label1'] + }, + options: { + legend: false, + title: false, + cutoutPercentage: 50, + rotation: Math.PI, + circumference: Math.PI * 0.5, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + 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} + ].forEach(function(expected, i) { + 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() { var chart = window.acquireChart({ type: 'doughnut', From 9a0117ad492d2c02c4be5f3488f0d8d255fe5aa9 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:46:28 -0500 Subject: [PATCH 370/685] Responsive printing docs (#5167) --- docs/general/responsive.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/general/responsive.md b/docs/general/responsive.md index bf8e8fa7a5f..f410826b84f 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -33,3 +33,15 @@ The chart can also be programmatically resized by modifying the container size: ```javascript chart.canvas.parentNode.style.height = '128px'; ``` + +## Printing Resizeable Charts + +CSS media queries allow changing styles when printing a page. The CSS applied from these media queries may cause charts to need to resize. However, the resize won't happen automatically. To support resizing charts when printing, one needs to hook the [onbeforeprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeprint) event and manually trigger resizing of each chart. + +```javascript +function beforePrintHandler () { + for (var id in Chart.instances) { + Chart.instances[id].resize() + } +} +``` From d668882971166ac11768ab659b2d5770642c33b3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:04 -0500 Subject: [PATCH 371/685] Tooltip label callback example (#5168) --- docs/configuration/tooltip.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 1be2c26bc0e..9990fc9161f 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -103,6 +103,32 @@ All functions are called with the same arguments: a [tooltip item](#tooltip-item | `footer` | `Array[tooltipItem], data` | Returns text to render as the footer of the tooltip. | `afterFooter` | `Array[tooltipItem], data` | Text to render after the footer section +### Label Callback + +The label callback can change the text that displays for a given data point. A common example to round data values; the following example rounds the data to two decimal places. + +```javascript +var chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + tooltips: { + callbacks: { + label: function(tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + + if (label) { + label += ': '; + } + label += Math.round(tooltipItem.yLabel * 100) / 100; + return label; + } + } + } + } +}); +``` + ### Label Color Callback For example, to return a red box for each item in the tooltip you could do: From f82c8adf39382ff1989664a872a0f3ec21ae1265 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:25 -0500 Subject: [PATCH 372/685] Remove copy-pasta error in polar area and doughnut chart docs (#5169) --- docs/charts/doughnut.md | 1 - docs/charts/polar.md | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index 35a7dbb56fe..cd9af9d56a0 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -55,7 +55,6 @@ The doughnut/pie chart allows a number of properties to be specified for each da | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 29952cff672..8f403149e05 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -46,7 +46,6 @@ The following options can be included in a polar area chart dataset to configure | Name | Type | Description | ---- | ---- | ----------- -| `label` | `String` | The label for the dataset which appears in the legend and tooltips. | `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors) | `borderWidth` | `Number[]` | The border width of the arcs in the dataset. From 274fca68c97d956ac4608e19988cb0c84ffe6999 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 21 Jan 2018 16:47:50 -0500 Subject: [PATCH 373/685] Update custom tooltip documentation and samples (#5166) --- docs/configuration/tooltip.md | 18 +++++++++++------- samples/tooltips/custom-line.html | 6 +++--- samples/tooltips/custom-pie.html | 6 +++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 9990fc9161f..93db40147b7 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -6,7 +6,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `enabled` | `Boolean` | `true` | Are tooltips enabled +| `enabled` | `Boolean` | `true` | Are on-canvas tooltips enabled | `custom` | `Function` | `null` | See [custom tooltip](#external-custom-tooltips) section. | `mode` | `String` | `'nearest'` | Sets which elements appear in the tooltip. [more...](../general/interactions/modes.md#interaction-modes). | `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. @@ -191,6 +191,9 @@ var myPieChart = new Chart(ctx, { data: data, options: { tooltips: { + // Disable the on-canvas tooltip + enabled: false, + custom: function(tooltipModel) { // Tooltip Element var tooltipEl = document.getElementById('chartjs-tooltip'); @@ -199,7 +202,7 @@ var myPieChart = new Chart(ctx, { if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.id = 'chartjs-tooltip'; - tooltipEl.innerHTML = "
    " + tooltipEl.innerHTML = "
    "; document.body.appendChild(tooltipEl); } @@ -238,7 +241,7 @@ var myPieChart = new Chart(ctx, { var style = 'background:' + colors.backgroundColor; style += '; border-color:' + colors.borderColor; style += '; border-width: 2px'; - var span = ''; + var span = ''; innerHtml += '' + span + body + ''; }); innerHtml += ''; @@ -252,11 +255,12 @@ var myPieChart = new Chart(ctx, { // Display, position, and set styles for font tooltipEl.style.opacity = 1; + tooltipEl.style.position = 'absolute'; tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; - tooltipEl.style.fontFamily = tooltipModel._fontFamily; - tooltipEl.style.fontSize = tooltipModel.fontSize; - tooltipEl.style.fontStyle = tooltipModel._fontStyle; + tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; + tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; + tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; } } @@ -264,7 +268,7 @@ var myPieChart = new Chart(ctx, { }); ``` -See `samples/tooltips/line-customTooltips.html` for examples on how to get started. +See [samples](http://www.chartjs.org/samples/) for examples on how to get started with custom tooltips. ## Tooltip Model The tooltip model contains parameters that can be used to render the tooltip. diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 801b12df680..011762907c4 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -102,9 +102,9 @@ tooltipEl.style.opacity = 1; tooltipEl.style.left = positionX + tooltip.caretX + 'px'; tooltipEl.style.top = positionY + tooltip.caretY + 'px'; - tooltipEl.style.fontFamily = tooltip._fontFamily; - tooltipEl.style.fontSize = tooltip.fontSize; - tooltipEl.style.fontStyle = tooltip._fontStyle; + tooltipEl.style.fontFamily = tooltip._bodyFontFamily; + tooltipEl.style.fontSize = tooltip.bodyFontSize + 'px'; + tooltipEl.style.fontStyle = tooltip._bodyFontStyle; tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px'; }; diff --git a/samples/tooltips/custom-pie.html b/samples/tooltips/custom-pie.html index 9c3cd0aa608..7bfbc56f1e9 100644 --- a/samples/tooltips/custom-pie.html +++ b/samples/tooltips/custom-pie.html @@ -98,9 +98,9 @@ tooltipEl.style.opacity = 1; tooltipEl.style.left = positionX + tooltip.caretX + 'px'; tooltipEl.style.top = positionY + tooltip.caretY + 'px'; - tooltipEl.style.fontFamily = tooltip._fontFamily; - tooltipEl.style.fontSize = tooltip.fontSize; - tooltipEl.style.fontStyle = tooltip._fontStyle; + tooltipEl.style.fontFamily = tooltip._bodyFontFamily; + tooltipEl.style.fontSize = tooltip.bodyFontSize; + tooltipEl.style.fontStyle = tooltip._bodyFontStyle; tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px'; }; From 98ef3942d982869ee867e134df95008dd612caf3 Mon Sep 17 00:00:00 2001 From: Jonathan Quach Date: Sat, 27 Jan 2018 15:47:51 +0100 Subject: [PATCH 374/685] Fix variable name error on developer api documentation for (#5173) --- docs/developers/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developers/api.md b/docs/developers/api.md index a5a0a513396..976578d572a 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -132,9 +132,9 @@ To get an item that was clicked on, `getElementAtEvent` can be used. ```javascript function clickHandler(evt) { - var item = myChart.getElementAtEvent(evt)[0]; + var firstPoint = myChart.getElementAtEvent(evt)[0]; - if (item) { + if (firstPoint) { var label = myChart.data.labels[firstPoint._index]; var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index]; } From e61392a256727bb531e80abbb2d5716fdf7d31ed Mon Sep 17 00:00:00 2001 From: Jackson Haenchen Date: Sun, 28 Jan 2018 11:59:22 -0600 Subject: [PATCH 375/685] Don't draw tick across axis/border (#5178) --- src/core/core.scale.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ffe13cbff82..f79dbee8009 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -705,10 +705,11 @@ module.exports = function(Chart) { var itemsToDraw = []; - var xTickStart = options.position === 'right' ? me.left : me.right - tl; - var xTickEnd = options.position === 'right' ? me.left + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top : me.bottom - tl; - var yTickEnd = options.position === 'bottom' ? me.top + tl : me.bottom; + var axisWidth = me.options.gridLines.lineWidth; + var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.right + tl : me.right; + var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; + var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; helpers.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) @@ -764,7 +765,7 @@ module.exports = function(Chart) { ty1 = yTickStart; ty2 = yTickEnd; y1 = chartArea.top; - y2 = chartArea.bottom; + y2 = chartArea.bottom + axisWidth; } else { var isLeft = options.position === 'left'; var labelXOffset; @@ -790,7 +791,7 @@ module.exports = function(Chart) { tx1 = xTickStart; tx2 = xTickEnd; x1 = chartArea.left; - x2 = chartArea.right; + x2 = chartArea.right + axisWidth; ty1 = ty2 = y1 = y2 = yLineValue; } @@ -906,9 +907,9 @@ module.exports = function(Chart) { context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); var x1 = me.left; - var x2 = me.right; + var x2 = me.right + axisWidth; var y1 = me.top; - var y2 = me.bottom; + var y2 = me.bottom + axisWidth; var aliasPixel = helpers.aliasPixel(context.lineWidth); if (isHorizontal) { From c2681859531977fb83fee782f8aec6b50196d644 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 1 Feb 2018 03:35:08 +0100 Subject: [PATCH 376/685] Fix Slack invitation link (#5217) Setup a new Heroku app based on rauchg/slackin, using Slack legacy token from the Chart.js (chartjs.slack@...) user and reCAPTCHA from the same Google account. --- README.md | 2 +- docs/README.md | 2 +- docs/developers/contributing.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10d2c1d8d7d..b68b39008da 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/docs/README.md b/docs/README.md index 5a0e9f35579..24ee8d49982 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Chart.js -[![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=600)](https://chart-js-automation.herokuapp.com/) +[![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) ## Installation diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index e6551d590d1..fa3511bb09a 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -12,7 +12,7 @@ New contributions to the library are welcome, but we ask that you please follow # Joining the project - Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chart-js-automation.herokuapp.com/). If you think you can help, we'd love to have you! + Active committers and contributors are invited to introduce yourself and request commit access to this project. We have a very active Slack community that you can join [here](https://chartjs-slack.herokuapp.com/). If you think you can help, we'd love to have you! # Building and Testing From 97ff45873e8e00af5e25d8adadb830fb15a272ef Mon Sep 17 00:00:00 2001 From: stockiNail Date: Fri, 2 Feb 2018 10:17:24 +0100 Subject: [PATCH 377/685] Add Charba GWT integration to extensions.md (#5225) --- docs/notes/extensions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 0998641bacc..fe2c337f156 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -56,5 +56,8 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java +### GWT (Google Web toolkit) + - Charba + ### Ember.js - ember-cli-chart From 182270ef9b1bc9fab1cefc89ce1e94a41f4d754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bourgois?= Date: Sat, 3 Feb 2018 13:28:54 +0100 Subject: [PATCH 378/685] Setup HTML and JS linters for samples (#5195) --- .eslintrc.yml | 2 + .htmllintrc | 18 + gulpfile.js | 24 +- package.json | 3 +- samples/.eslintrc.yml | 9 + samples/advanced/data-labelling.html | 225 ++++++------ samples/advanced/progress-bar.html | 169 +++++---- samples/charts/area/analyser.js | 2 +- samples/charts/area/line-boundaries.html | 5 +- samples/charts/area/line-datasets.html | 9 +- samples/charts/area/line-stacked.html | 92 ++--- samples/charts/area/radar.html | 9 +- samples/charts/bar/horizontal.html | 278 +++++++-------- samples/charts/bar/multi-axis.html | 192 +++++------ samples/charts/bar/stacked-group.html | 186 +++++----- samples/charts/bar/stacked.html | 180 +++++----- samples/charts/bar/vertical.html | 268 +++++++-------- samples/charts/bubble.html | 340 +++++++++---------- samples/charts/combo-bar-line.html | 178 +++++----- samples/charts/doughnut.html | 268 +++++++-------- samples/charts/line/basic.html | 306 ++++++++--------- samples/charts/line/interpolation-modes.html | 158 ++++----- samples/charts/line/line-styles.html | 200 +++++------ samples/charts/line/multi-axis.html | 180 +++++----- samples/charts/line/point-sizes.html | 237 +++++++------ samples/charts/line/point-styles.html | 166 ++++----- samples/charts/line/skip-points.html | 166 ++++----- samples/charts/line/stepped.html | 128 +++---- samples/charts/pie.html | 156 ++++----- samples/charts/polar-area.html | 206 +++++------ samples/charts/radar-skip-points.html | 188 +++++----- samples/charts/radar.html | 272 +++++++-------- samples/charts/scatter/basic.html | 190 +++++------ samples/charts/scatter/multi-axis.html | 30 +- samples/index.html | 4 +- samples/legend/point-style.html | 208 ++++++------ samples/legend/positioning.html | 220 ++++++------ samples/scales/filtering-labels.html | 160 +++++---- samples/scales/gridlines-display.html | 220 ++++++------ samples/scales/gridlines-style.html | 116 +++---- samples/scales/linear/min-max-suggested.html | 110 +++--- samples/scales/linear/min-max.html | 106 +++--- samples/scales/linear/step-size.html | 330 +++++++++--------- samples/scales/logarithmic/line.html | 164 ++++----- samples/scales/logarithmic/scatter.html | 134 ++++---- samples/scales/multiline-labels.html | 148 ++++---- samples/scales/non-numeric-y.html | 123 ++++--- samples/scales/time/combo.html | 70 ++-- samples/scales/time/financial.html | 14 +- samples/scales/time/line-point-data.html | 47 +-- samples/scales/time/line.html | 38 +-- samples/scales/toggle-scale-type.html | 168 ++++----- samples/scriptable/bubble.html | 6 +- samples/tooltips/border.html | 7 +- samples/tooltips/callbacks.html | 190 +++++------ samples/tooltips/custom-line.html | 44 +-- samples/tooltips/custom-pie.html | 194 +++++------ samples/tooltips/custom-points.html | 56 +-- samples/tooltips/interactions.html | 10 +- samples/tooltips/positioning.html | 10 +- 60 files changed, 3986 insertions(+), 3951 deletions(-) create mode 100644 .htmllintrc create mode 100644 samples/.eslintrc.yml diff --git a/.eslintrc.yml b/.eslintrc.yml index 8f9b4af3023..b0d9d5695a9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,3 +3,5 @@ extends: chartjs env: browser: true node: true + +plugins: ['html'] diff --git a/.htmllintrc b/.htmllintrc new file mode 100644 index 00000000000..a6b2097031b --- /dev/null +++ b/.htmllintrc @@ -0,0 +1,18 @@ +{ + "indent-style": "tabs", + "attr-quote-style": "double", + "spec-char-escape": false, + "attr-bans": [ + "align", + "background", + "bgcolor", + "border", + "frameborder", + "longdesc", + "marginwidth", + "marginheight", + "scrolling" + ], + "tag-bans": [ "b", "i" ], + "id-class-style": false +} diff --git a/gulpfile.js b/gulpfile.js index 09165c8b041..21f19645f81 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,6 @@ var concat = require('gulp-concat'); var connect = require('gulp-connect'); var eslint = require('gulp-eslint'); var file = require('gulp-file'); -var htmlv = require('gulp-html-validator'); var insert = require('gulp-insert'); var replace = require('gulp-replace'); var size = require('gulp-size'); @@ -20,6 +19,7 @@ var collapse = require('bundle-collapser/plugin'); var yargs = require('yargs'); var path = require('path'); var fs = require('fs'); +var htmllint = require('gulp-htmllint'); var package = require('./package.json'); var argv = yargs @@ -49,12 +49,13 @@ gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); gulp.task('watch', watchTask); -gulp.task('lint', lintTask); +gulp.task('lint', ['lint-html', 'lint-js']); +gulp.task('lint-html', lintHtmlTask); +gulp.task('lint-js', lintJsTask); gulp.task('docs', docsTask); -gulp.task('test', ['lint', 'validHTML', 'unittest']); +gulp.task('test', ['lint', 'unittest']); gulp.task('size', ['library-size', 'module-sizes']); gulp.task('server', serverTask); -gulp.task('validHTML', validHTMLTask); gulp.task('unittest', unittestTask); gulp.task('library-size', librarySizeTask); gulp.task('module-sizes', moduleSizesTask); @@ -153,8 +154,9 @@ function packageTask() { .pipe(gulp.dest(outDir)); } -function lintTask() { +function lintJsTask() { var files = [ + 'samples/**/*.html', 'samples/**/*.js', 'src/**/*.js', 'test/**/*.js' @@ -176,6 +178,13 @@ function lintTask() { .pipe(eslint.failAfterError()); } +function lintHtmlTask() { + return gulp.src('samples/**/*.html') + .pipe(htmllint({ + failOnError: true, + })); +} + function docsTask(done) { const script = require.resolve('gitbook-cli/bin/gitbook.js'); const cmd = process.execPath; @@ -189,11 +198,6 @@ function docsTask(done) { }); } -function validHTMLTask() { - return gulp.src('samples/*.html') - .pipe(htmlv()); -} - function startTest() { return [ {pattern: './test/fixtures/**/*.json', included: false}, diff --git a/package.json b/package.json index c7d21043b1a..03f3815ccd7 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ "coveralls": "^3.0.0", "eslint": "^4.9.0", "eslint-config-chartjs": "^0.1.0", + "eslint-plugin-html": "^4.0.2", "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", - "gulp-html-validator": "^0.0.5", + "gulp-htmllint": "^0.0.15", "gulp-insert": "~0.5.0", "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", diff --git a/samples/.eslintrc.yml b/samples/.eslintrc.yml new file mode 100644 index 00000000000..9573adbcde6 --- /dev/null +++ b/samples/.eslintrc.yml @@ -0,0 +1,9 @@ +globals: + $: true + Chart: true + Samples: true + moment: true + randomScalingFactor: true + +rules: + no-new: 0 diff --git a/samples/advanced/data-labelling.html b/samples/advanced/data-labelling.html index 17fc47c8f99..52eb07b8fd3 100644 --- a/samples/advanced/data-labelling.html +++ b/samples/advanced/data-labelling.html @@ -3,130 +3,129 @@ - Labelling Data Points - - - + Labelling Data Points + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index d460bc88e2a..b97a998ea95 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -1,96 +1,95 @@ - Animation Callbacks - - - + Animation Callbacks + + + -
    - - -
    -
    -
    - - + window.myLine.update(); + }); + - \ No newline at end of file + diff --git a/samples/charts/area/analyser.js b/samples/charts/area/analyser.js index e4ed8e90b24..36fd5671692 100644 --- a/samples/charts/area/analyser.js +++ b/samples/charts/area/analyser.js @@ -4,7 +4,7 @@ (function() { Chart.plugins.register({ - id: 'samples_filler_analyser', + id: 'samples-filler-analyser', beforeInit: function(chart, options) { this.element = document.getElementById(options.target); diff --git a/samples/charts/area/line-boundaries.html b/samples/charts/area/line-boundaries.html index edadc781c54..7ac6883bb12 100644 --- a/samples/charts/area/line-boundaries.html +++ b/samples/charts/area/line-boundaries.html @@ -94,15 +94,16 @@ }); }); - + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); Chart.helpers.each(Chart.instances, function(chart) { - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); }); } + // eslint-disable-next-line no-unused-vars function randomize() { var seed = utils.rand(); Chart.helpers.each(Chart.instances, function(chart) { diff --git a/samples/charts/area/line-datasets.html b/samples/charts/area/line-datasets.html index 7bc54e3138a..127726e7fb7 100644 --- a/samples/charts/area/line-datasets.html +++ b/samples/charts/area/line-datasets.html @@ -38,7 +38,7 @@ return utils.numbers(inputs); } - function generateLabels(config) { + function generateLabels() { return utils.months({count: inputs.count}); } @@ -122,7 +122,7 @@ filler: { propagate: false }, - samples_filler_analyser: { + 'samples-filler-analyser': { target: 'chart-analyser' } } @@ -134,18 +134,21 @@ options: options }); + // eslint-disable-next-line no-unused-vars function togglePropagate(btn) { var value = btn.classList.toggle('btn-on'); chart.options.plugins.filler.propagate = value; chart.update(); } + // eslint-disable-next-line no-unused-vars function toggleSmooth(btn) { var value = btn.classList.toggle('btn-on'); - chart.options.elements.line.tension = value? 0.4 : 0.000001; + chart.options.elements.line.tension = value ? 0.4 : 0.000001; chart.update(); } + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { dataset.data = generateData(); diff --git a/samples/charts/area/line-stacked.html b/samples/charts/area/line-stacked.html index 26ee6b18f8f..11a143b0b63 100644 --- a/samples/charts/area/line-stacked.html +++ b/samples/charts/area/line-stacked.html @@ -7,9 +7,9 @@ @@ -26,70 +26,70 @@ - - + Horizontal Bar Chart + + + -
    - -
    - - - - - - +
    + +
    + + + + + + diff --git a/samples/charts/bar/multi-axis.html b/samples/charts/bar/multi-axis.html index 28755a701a5..c206866a410 100644 --- a/samples/charts/bar/multi-axis.html +++ b/samples/charts/bar/multi-axis.html @@ -2,107 +2,107 @@ - Bar Chart Multi Axis - - - + Bar Chart Multi Axis + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked-group.html b/samples/charts/bar/stacked-group.html index 624992cfb4a..e3b734b2d17 100644 --- a/samples/charts/bar/stacked-group.html +++ b/samples/charts/bar/stacked-group.html @@ -2,104 +2,104 @@ - Stacked Bar Chart with Groups - - - + Stacked Bar Chart with Groups + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/stacked.html b/samples/charts/bar/stacked.html index 46d1c4051f3..ca9f0cafb71 100644 --- a/samples/charts/bar/stacked.html +++ b/samples/charts/bar/stacked.html @@ -2,101 +2,101 @@ - Stacked Bar Chart - - - + Stacked Bar Chart + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + barChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myBar.update(); + }); + diff --git a/samples/charts/bar/vertical.html b/samples/charts/bar/vertical.html index 906b4624c39..e9348b274fd 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -2,143 +2,143 @@ - Bar Chart - - - + Bar Chart + + + -
    - -
    - - - - - - +
    + +
    + + + + + + diff --git a/samples/charts/bubble.html b/samples/charts/bubble.html index 0df09477f5c..092c1fb464b 100644 --- a/samples/charts/bubble.html +++ b/samples/charts/bubble.html @@ -2,190 +2,190 @@ - Bubble Chart - - - + Bubble Chart + + + -
    - -
    - - - - - - + window.myChart.update(); + }); + diff --git a/samples/charts/combo-bar-line.html b/samples/charts/combo-bar-line.html index 0906029c41d..c99894e2212 100644 --- a/samples/charts/combo-bar-line.html +++ b/samples/charts/combo-bar-line.html @@ -2,100 +2,100 @@ - Combo Bar-Line Chart - - - + Combo Bar-Line Chart + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + chartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + window.myMixedChart.update(); + }); + diff --git a/samples/charts/doughnut.html b/samples/charts/doughnut.html index b288fe1293f..446dcee4e2e 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -2,143 +2,143 @@ - Doughnut Chart - - - + Doughnut Chart + + + -
    - -
    - - - - - - +
    + +
    + + + + + + diff --git a/samples/charts/line/basic.html b/samples/charts/line/basic.html index db01ccf743d..8028bee9c51 100644 --- a/samples/charts/line/basic.html +++ b/samples/charts/line/basic.html @@ -2,162 +2,162 @@ - Line Chart - - - + Line Chart + + + -
    - -
    -
    -
    - - - - - - +
    + +
    +
    +
    + + + + + + diff --git a/samples/charts/line/interpolation-modes.html b/samples/charts/line/interpolation-modes.html index 374da49c083..b11dd681972 100644 --- a/samples/charts/line/interpolation-modes.html +++ b/samples/charts/line/interpolation-modes.html @@ -2,102 +2,102 @@ - Line Chart - Cubic interpolation mode - - - + Line Chart - Cubic interpolation mode + + + -
    - -
    -
    -
    - - + diff --git a/samples/charts/line/line-styles.html b/samples/charts/line/line-styles.html index ed268e1fdb4..3ba9defcdaf 100644 --- a/samples/charts/line/line-styles.html +++ b/samples/charts/line/line-styles.html @@ -2,110 +2,110 @@ - Line Styles - - - + Line Styles + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/multi-axis.html b/samples/charts/line/multi-axis.html index 2ede74446ae..d3569e85525 100644 --- a/samples/charts/line/multi-axis.html +++ b/samples/charts/line/multi-axis.html @@ -2,103 +2,103 @@ - Line Chart Multiple Axes - - - + Line Chart Multiple Axes + + + -
    - -
    - - + window.myLine.update(); + }); + diff --git a/samples/charts/line/point-sizes.html b/samples/charts/line/point-sizes.html index 823c6d3e41c..53d3db46cbb 100644 --- a/samples/charts/line/point-sizes.html +++ b/samples/charts/line/point-sizes.html @@ -2,129 +2,128 @@ - Different Point Sizes - - - + Different Point Sizes + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/point-styles.html b/samples/charts/line/point-styles.html index 1985d394d23..2ef46a9cf35 100644 --- a/samples/charts/line/point-styles.html +++ b/samples/charts/line/point-styles.html @@ -2,95 +2,95 @@ - Line Chart - - - + Line Chart + + + -
    -
    - + var ctx = canvas.getContext('2d'); + var config = createConfig(pointStyle); + new Chart(ctx, config); + }); + }; + diff --git a/samples/charts/line/skip-points.html b/samples/charts/line/skip-points.html index 00aa81fe045..aedacce7244 100644 --- a/samples/charts/line/skip-points.html +++ b/samples/charts/line/skip-points.html @@ -2,94 +2,94 @@ - Line Chart - - - + Line Chart + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/charts/line/stepped.html b/samples/charts/line/stepped.html index ccce87c2dcf..4ad9708ebd1 100644 --- a/samples/charts/line/stepped.html +++ b/samples/charts/line/stepped.html @@ -2,9 +2,9 @@ - Stepped Line Chart - - + Stepped Line Chart + + -
    - -
    - - - - + document.getElementById('removeDataset').addEventListener('click', function() { + config.data.datasets.splice(0, 1); + window.myPie.update(); + }); + diff --git a/samples/charts/polar-area.html b/samples/charts/polar-area.html index 3981651de74..1c40a645181 100644 --- a/samples/charts/polar-area.html +++ b/samples/charts/polar-area.html @@ -2,116 +2,116 @@ - Polar Area Chart - - - + Polar Area Chart + + + -
    - -
    - - - - + var colorNames = Object.keys(window.chartColors); + document.getElementById('addData').addEventListener('click', function() { + if (config.data.datasets.length > 0) { + config.data.labels.push('data #' + config.data.labels.length); + config.data.datasets.forEach(function(dataset) { + var colorName = colorNames[config.data.labels.length % colorNames.length]; + dataset.backgroundColor.push(window.chartColors[colorName]); + dataset.data.push(randomScalingFactor()); + }); + window.myPolarArea.update(); + } + }); + document.getElementById('removeData').addEventListener('click', function() { + config.data.labels.pop(); // remove the label first + config.data.datasets.forEach(function(dataset) { + dataset.backgroundColor.pop(); + dataset.data.pop(); + }); + window.myPolarArea.update(); + }); + diff --git a/samples/charts/radar-skip-points.html b/samples/charts/radar-skip-points.html index ab29b3a0e34..ab042e2dcd6 100644 --- a/samples/charts/radar-skip-points.html +++ b/samples/charts/radar-skip-points.html @@ -2,108 +2,108 @@ - Radar Chart - - - + Radar Chart + + + -
    - -
    - - + window.myRadar.update(); + }); + diff --git a/samples/charts/radar.html b/samples/charts/radar.html index fbbb9d972bc..7eda09a7fb4 100644 --- a/samples/charts/radar.html +++ b/samples/charts/radar.html @@ -2,145 +2,145 @@ - Radar Chart - - - + Radar Chart + + + -
    - -
    - - - - - - +
    + +
    + + + + + + diff --git a/samples/charts/scatter/basic.html b/samples/charts/scatter/basic.html index c5e36e9d99c..6ac227c3b95 100644 --- a/samples/charts/scatter/basic.html +++ b/samples/charts/scatter/basic.html @@ -2,106 +2,106 @@ - Scatter Chart - - - + Scatter Chart + + + -
    - -
    - - + document.getElementById('randomizeData').addEventListener('click', function() { + scatterChartData.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return { + x: randomScalingFactor(), + y: randomScalingFactor() + }; + }); + }); + window.myScatter.update(); + }); + diff --git a/samples/charts/scatter/multi-axis.html b/samples/charts/scatter/multi-axis.html index cbf3bf73a8a..6c20b98f0de 100644 --- a/samples/charts/scatter/multi-axis.html +++ b/samples/charts/scatter/multi-axis.html @@ -23,9 +23,9 @@ var color = Chart.helpers.color; var scatterChartData = { datasets: [{ - label: "My First dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-1", + label: 'My First dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-1', borderColor: window.chartColors.red, backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), data: [{ @@ -51,9 +51,9 @@ y: randomScalingFactor(), }] }, { - label: "My Second dataset", - xAxisID: "x-axis-1", - yAxisID: "y-axis-2", + label: 'My Second dataset', + xAxisID: 'x-axis-1', + yAxisID: 'y-axis-2', borderColor: window.chartColors.blue, backgroundColor: color(window.chartColors.blue).alpha(0.2).rgbString(), data: [{ @@ -82,7 +82,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myScatter = Chart.Scatter(ctx, { data: scatterChartData, options: { @@ -95,22 +95,22 @@ }, scales: { xAxes: [{ - position: "bottom", + position: 'bottom', gridLines: { - zeroLineColor: "rgba(0,0,0,1)" + zeroLineColor: 'rgba(0,0,0,1)' } }], yAxes: [{ - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "left", - id: "y-axis-1", + position: 'left', + id: 'y-axis-1', }, { - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance display: true, - position: "right", + position: 'right', reverse: true, - id: "y-axis-2", + id: 'y-axis-2', // grid line settings gridLines: { diff --git a/samples/index.html b/samples/index.html index d855662f093..b9e13c364c4 100644 --- a/samples/index.html +++ b/samples/index.html @@ -47,8 +47,8 @@ var category = createCategory(item); var children = category.getElementsByClassName('items')[0]; - (item.items || []).forEach(function(item) { - children.appendChild(createEntry(item)); + (item.items || []).forEach(function(item2) { + children.appendChild(createEntry(item2)); }); categories.appendChild(category); diff --git a/samples/legend/point-style.html b/samples/legend/point-style.html index 727c7a6d85c..b7acea4bd84 100644 --- a/samples/legend/point-style.html +++ b/samples/legend/point-style.html @@ -2,115 +2,115 @@ - Legend Point Style - - - + Legend Point Style + + + -
    -
    - -
    -
    - -
    -
    - + window.onload = function() { + [{ + id: 'chart-legend-normal', + config: createConfig('red') + }, { + id: 'chart-legend-pointstyle', + config: createPointStyleConfig('blue') + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + new Chart(ctx, details.config); + }); + }; + diff --git a/samples/legend/positioning.html b/samples/legend/positioning.html index 97bc70fa047..5bd2259cfb1 100644 --- a/samples/legend/positioning.html +++ b/samples/legend/positioning.html @@ -2,120 +2,120 @@ - Legend Positions - - - + Legend Positions + + + -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - + window.onload = function() { + [{ + id: 'chart-legend-top', + legendPosition: 'top', + color: 'red' + }, { + id: 'chart-legend-right', + legendPosition: 'right', + color: 'blue' + }, { + id: 'chart-legend-bottom', + legendPosition: 'bottom', + color: 'green' + }, { + id: 'chart-legend-left', + legendPosition: 'left', + color: 'yellow' + }].forEach(function(details) { + var ctx = document.getElementById(details.id).getContext('2d'); + var config = createConfig(details.legendPosition, details.color); + new Chart(ctx, config); + }); + }; + diff --git a/samples/scales/filtering-labels.html b/samples/scales/filtering-labels.html index 4af89025eb4..4b4b51724e4 100644 --- a/samples/scales/filtering-labels.html +++ b/samples/scales/filtering-labels.html @@ -2,92 +2,90 @@ - Chart with xAxis Filtering - - - + Chart with xAxis Filtering + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index c21469329b8..8e30bdcdbf4 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,123 +2,123 @@ - Grid Lines Display Settings - - - + Grid Lines Display Settings + + + -
    - + var ctx = canvas.getContext('2d'); + var config = createConfig(details.gridLines, details.title); + new Chart(ctx, config); + }); + }; + diff --git a/samples/scales/gridlines-style.html b/samples/scales/gridlines-style.html index f945cb266d4..d2c00ecdda2 100644 --- a/samples/scales/gridlines-style.html +++ b/samples/scales/gridlines-style.html @@ -2,68 +2,68 @@ - Grid Lines Style Settings - - - + Grid Lines Style Settings + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max-suggested.html b/samples/scales/linear/min-max-suggested.html index 18059548a7f..10d546ac297 100644 --- a/samples/scales/linear/min-max-suggested.html +++ b/samples/scales/linear/min-max-suggested.html @@ -2,66 +2,66 @@ - Suggested Min/Max Settings - - - + Suggested Min/Max Settings + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/min-max.html b/samples/scales/linear/min-max.html index 868bc7b1db5..feafbd7a62c 100644 --- a/samples/scales/linear/min-max.html +++ b/samples/scales/linear/min-max.html @@ -2,63 +2,63 @@ - Min/Max Settings - - - + Min/Max Settings + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html index ced0b6c3ca3..fc0af2ee3e0 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -2,174 +2,174 @@ - Line Chart - - - + Line Chart + + + -
    - -
    -
    -
    - - - - - - +
    + +
    +
    +
    + + + + + + diff --git a/samples/scales/logarithmic/line.html b/samples/scales/logarithmic/line.html index 2c961abd2e1..72fd9b07dba 100644 --- a/samples/scales/logarithmic/line.html +++ b/samples/scales/logarithmic/line.html @@ -2,96 +2,96 @@ - Logarithmic Line Chart - - - + Logarithmic Line Chart + + + -
    - -
    - - + window.myLine.update(); + }); + diff --git a/samples/scales/logarithmic/scatter.html b/samples/scales/logarithmic/scatter.html index a4bd577c273..5f266a5cda1 100644 --- a/samples/scales/logarithmic/scatter.html +++ b/samples/scales/logarithmic/scatter.html @@ -2,29 +2,29 @@ - Scatter Chart - - - + Scatter Chart + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myScatter = Chart.Scatter(ctx, { + data: scatterChartData, + options: { + title: { + display: true, + text: 'Chart.js Scatter Chart - Logarithmic X-Axis' + }, + scales: { + xAxes: [{ + type: 'logarithmic', + position: 'bottom', + ticks: { + userCallback: function(tick) { + var remain = tick / (Math.pow(10, Math.floor(Chart.helpers.log10(tick)))); + if (remain === 1 || remain === 2 || remain === 5) { + return tick.toString() + 'Hz'; + } + return ''; + }, + }, + scaleLabel: { + labelString: 'Frequency', + display: true, + } + }], + yAxes: [{ + type: 'linear', + ticks: { + userCallback: function(tick) { + return tick.toString() + 'dB'; + } + }, + scaleLabel: { + labelString: 'Voltage', + display: true + } + }] + } + } + }); + }; + diff --git a/samples/scales/multiline-labels.html b/samples/scales/multiline-labels.html index b7bb041e973..0f8af2a7df1 100644 --- a/samples/scales/multiline-labels.html +++ b/samples/scales/multiline-labels.html @@ -2,85 +2,85 @@ - Line Chart - - - + Line Chart + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/non-numeric-y.html b/samples/scales/non-numeric-y.html index 07e319b7090..b3bfc5eec92 100644 --- a/samples/scales/non-numeric-y.html +++ b/samples/scales/non-numeric-y.html @@ -2,72 +2,71 @@ - Line Chart - - - + Line Chart + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/scales/time/combo.html b/samples/scales/time/combo.html index ddcc5b326bf..d435e74c69c 100644 --- a/samples/scales/time/combo.html +++ b/samples/scales/time/combo.html @@ -7,11 +7,11 @@ @@ -38,12 +38,12 @@ type: 'bar', data: { labels: [ - newDateString(0), - newDateString(1), - newDateString(2), - newDateString(3), - newDateString(4), - newDateString(5), + newDateString(0), + newDateString(1), + newDateString(2), + newDateString(3), + newDateString(4), + newDateString(5), newDateString(6) ], datasets: [{ @@ -52,12 +52,12 @@ backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -66,12 +66,12 @@ backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], }, { @@ -81,23 +81,23 @@ borderColor: window.chartColors.green, fill: false, data: [ - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), - randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), + randomScalingFactor(), randomScalingFactor() ], - }, ] + }] }, options: { - title: { - text:"Chart.js Combo Time Scale" - }, + title: { + text: 'Chart.js Combo Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, time: { format: timeFormat, @@ -109,7 +109,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 3d91dfa56b7..792eef55306 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -18,7 +18,7 @@
    -
    +


    Chart Type: @@ -33,10 +33,8 @@ } function randomBar(date, lastClose) { - var open = randomNumber(lastClose * .95, lastClose * 1.05); - var close = randomNumber(open * .95, open * 1.05); - var high = randomNumber(Math.max(open, close), Math.max(open, close) * 1.1); - var low = randomNumber(Math.min(open, close) * .9, Math.min(open, close)); + var open = randomNumber(lastClose * 0.95, lastClose * 1.05); + var close = randomNumber(open * 0.95, open * 1.05); return { t: date.valueOf(), y: close @@ -55,7 +53,7 @@ } } - var ctx = document.getElementById("chart1").getContext("2d"); + var ctx = document.getElementById('chart1').getContext('2d'); ctx.canvas.width = 1000; ctx.canvas.height = 300; var cfg = { @@ -63,7 +61,7 @@ data: { labels: labels, datasets: [{ - label: "CHRT - Chart.js Corporation", + label: 'CHRT - Chart.js Corporation', data: data, type: 'line', pointRadius: 0, @@ -94,7 +92,7 @@ document.getElementById('update').addEventListener('click', function() { var type = document.getElementById('type').value; - chart.config.data.datasets[0].type = type; + chart.config.data.datasets[0].type = type; chart.update(); }); diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index 3483c454535..e18be91f3d8 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -7,11 +7,11 @@ @@ -38,7 +38,7 @@ type: 'line', data: { datasets: [{ - label: "Dataset with string point data", + label: 'Dataset with string point data', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -56,7 +56,7 @@ y: randomScalingFactor() }], }, { - label: "Dataset with date object point data", + label: 'Dataset with date object point data', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -77,24 +77,24 @@ }, options: { responsive: true, - title:{ - display:true, - text:"Chart.js Time Point Data" + title: { + display: true, + text: 'Chart.js Time Point Data' }, scales: { xAxes: [{ - type: "time", + type: 'time', display: true, scaleLabel: { display: true, labelString: 'Date' }, - ticks: { - major: { - fontStyle: "bold", - fontColor: "#FF0000" - } - } + ticks: { + major: { + fontStyle: 'bold', + fontColor: '#FF0000' + } + } }], yAxes: [{ display: true, @@ -108,7 +108,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -122,10 +122,15 @@ window.myLine.update(); }); + // TODO : fix issue with addData + // See https://github.com/chartjs/Chart.js/issues/5197 + // The Add Data button for this sample has no effect. + // An error is logged in the console. document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var numTicks = myLine.scales['x-axis-0'].ticksAsTimestamps.length; - var lastTime = numTicks ? moment(myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var numTicks = window.myLine.scales['x-axis-0'].ticksAsTimestamps.length; + var lastTime = numTicks ? moment(window.myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); + var newTime = lastTime .clone() .add(1, 'day') @@ -143,7 +148,7 @@ }); document.getElementById('removeData').addEventListener('click', function() { - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/time/line.html b/samples/scales/time/line.html index 0cca931e009..70a6978af18 100644 --- a/samples/scales/time/line.html +++ b/samples/scales/time/line.html @@ -7,11 +7,11 @@ @@ -37,10 +37,6 @@ return moment().add(days, 'd').format(timeFormat); } - function newTimestamp(days) { - return moment().add(days, 'd').unix(); - } - var color = Chart.helpers.color; var config = { type: 'line', @@ -55,7 +51,7 @@ newDate(6) ], datasets: [{ - label: "My First dataset", + label: 'My First dataset', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), borderColor: window.chartColors.red, fill: false, @@ -69,7 +65,7 @@ randomScalingFactor() ], }, { - label: "My Second dataset", + label: 'My Second dataset', backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(), borderColor: window.chartColors.blue, fill: false, @@ -83,7 +79,7 @@ randomScalingFactor() ], }, { - label: "Dataset with point data", + label: 'Dataset with point data', backgroundColor: color(window.chartColors.green).alpha(0.5).rgbString(), borderColor: window.chartColors.green, fill: false, @@ -103,12 +99,12 @@ }] }, options: { - title:{ - text: "Chart.js Time Scale" - }, + title: { + text: 'Chart.js Time Scale' + }, scales: { xAxes: [{ - type: "time", + type: 'time', time: { format: timeFormat, // round: 'day' @@ -118,7 +114,7 @@ display: true, labelString: 'Date' } - }, ], + }], yAxes: [{ scaleLabel: { display: true, @@ -130,7 +126,7 @@ }; window.onload = function() { - var ctx = document.getElementById("canvas").getContext("2d"); + var ctx = document.getElementById('canvas').getContext('2d'); window.myLine = new Chart(ctx, config); }; @@ -152,7 +148,7 @@ var colorNames = Object.keys(window.chartColors); document.getElementById('addDataset').addEventListener('click', function() { var colorName = colorNames[config.data.datasets.length % colorNames.length]; - var newColor = window.chartColors[colorName] + var newColor = window.chartColors[colorName]; var newDataset = { label: 'Dataset ' + config.data.datasets.length, borderColor: newColor, @@ -173,7 +169,7 @@ config.data.labels.push(newDate(config.data.labels.length)); for (var index = 0; index < config.data.datasets.length; ++index) { - if (typeof config.data.datasets[index].data[0] === "object") { + if (typeof config.data.datasets[index].data[0] === 'object') { config.data.datasets[index].data.push({ x: newDate(config.data.datasets[index].data.length), y: randomScalingFactor(), @@ -195,7 +191,7 @@ document.getElementById('removeData').addEventListener('click', function() { config.data.labels.splice(-1, 1); // remove the label first - config.data.datasets.forEach(function(dataset, datasetIndex) { + config.data.datasets.forEach(function(dataset) { dataset.data.pop(); }); diff --git a/samples/scales/toggle-scale-type.html b/samples/scales/toggle-scale-type.html index b46687e823a..92c943288e3 100644 --- a/samples/scales/toggle-scale-type.html +++ b/samples/scales/toggle-scale-type.html @@ -2,98 +2,98 @@ - Toggle Scale Type - - - + Toggle Scale Type + + + -
    - -
    - - + window.myLine.update(); + }); + diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html index feaf1ecb84c..a8ce1f4743c 100644 --- a/samples/scriptable/bubble.html +++ b/samples/scriptable/bubble.html @@ -24,7 +24,6 @@ var MIN_XY = -150; var MAX_XY = 100; - var presets = window.chartColors; var utils = Samples.utils; utils.srand(110); @@ -106,13 +105,15 @@ options: options }); + // eslint-disable-next-line no-unused-vars function randomize() { chart.data.datasets.forEach(function(dataset) { - dataset.data = generateData() + dataset.data = generateData(); }); chart.update(); } + // eslint-disable-next-line no-unused-vars function addDataset() { chart.data.datasets.push({ data: generateData() @@ -120,6 +121,7 @@ chart.update(); } + // eslint-disable-next-line no-unused-vars function removeDataset() { chart.data.datasets.shift(); chart.update(); diff --git a/samples/tooltips/border.html b/samples/tooltips/border.html index 25c649e548e..0742be0c5f5 100644 --- a/samples/tooltips/border.html +++ b/samples/tooltips/border.html @@ -34,9 +34,9 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "Dataset", + label: 'Dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], @@ -45,7 +45,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Sample tooltip with border' }, @@ -78,7 +78,6 @@ var ctx = canvas.getContext('2d'); var config = createConfig(); new Chart(ctx, config); - console.log(config); }; diff --git a/samples/tooltips/callbacks.html b/samples/tooltips/callbacks.html index 590edd1a236..0aa336bd5f3 100644 --- a/samples/tooltips/callbacks.html +++ b/samples/tooltips/callbacks.html @@ -2,106 +2,106 @@ - Tooltip Hooks - - - + Tooltip Hooks + + + -
    - -
    - + window.onload = function() { + var ctx = document.getElementById('canvas').getContext('2d'); + window.myLine = new Chart(ctx, config); + }; + diff --git a/samples/tooltips/custom-line.html b/samples/tooltips/custom-line.html index 011762907c4..96cfac6fd3c 100644 --- a/samples/tooltips/custom-line.html +++ b/samples/tooltips/custom-line.html @@ -35,7 +35,7 @@
    - +
    - + Pie Chart with Custom Tooltips + + - + @@ -43,103 +43,103 @@
    diff --git a/samples/tooltips/custom-points.html b/samples/tooltips/custom-points.html index 1bb64da4fa5..f779f62f9c7 100644 --- a/samples/tooltips/custom-points.html +++ b/samples/tooltips/custom-points.html @@ -41,13 +41,13 @@
    diff --git a/samples/tooltips/positioning.html b/samples/tooltips/positioning.html index 696584b0677..f98cd638ced 100644 --- a/samples/tooltips/positioning.html +++ b/samples/tooltips/positioning.html @@ -34,15 +34,15 @@ return { type: 'line', data: { - labels: ["January", "February", "March", "April", "May", "June", "July"], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ - label: "My First dataset", + label: 'My First dataset', borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, data: [10, 30, 46, 2, 8, 50, 0], fill: false, }, { - label: "My Second dataset", + label: 'My Second dataset', borderColor: window.chartColors.blue, backgroundColor: window.chartColors.blue, data: [7, 49, 46, 13, 25, 30, 22], @@ -51,7 +51,7 @@ }, options: { responsive: true, - title:{ + title: { display: true, text: 'Tooltip Position: ' + position }, @@ -78,7 +78,7 @@ var ctx = canvas.getContext('2d'); var config = createConfig(position); new Chart(ctx, config); - }) + }); }; From bba29e591604fed8c1e3bc4cf911273d4c5e0f42 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sun, 4 Feb 2018 03:27:50 -0800 Subject: [PATCH 379/685] Remove trailing spaces from docs (#5227) --- docs/axes/README.md | 8 ++++---- docs/axes/cartesian/time.md | 2 +- docs/charts/bar.md | 12 ++++++------ docs/charts/mixed.md | 6 +++--- docs/charts/radar.md | 10 +++++----- docs/configuration/legend.md | 2 +- docs/configuration/tooltip.md | 4 ++-- docs/developers/charts.md | 4 ++-- docs/general/colors.md | 2 +- docs/general/interactions/README.md | 2 +- docs/general/interactions/events.md | 2 +- docs/general/interactions/modes.md | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/axes/README.md b/docs/axes/README.md index b7de691eae9..f65cd82d29a 100644 --- a/docs/axes/README.md +++ b/docs/axes/README.md @@ -26,18 +26,18 @@ There are a number of config callbacks that can be used to change parameters in | Name | Arguments | Description | ---- | --------- | ----------- | `beforeUpdate` | `axis` | Callback called before the update process starts. -| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. +| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. | `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. | `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. | `afterDataLimits` | `axis` | Callback that runs after data limits are determined. | `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. | `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. | `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. -| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. +| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. | `beforeCalculateTickRotation` | `axis` | Callback that runs before tick rotation is determined. | `afterCalculateTickRotation` | `axis` | Callback that runs after tick rotation is determined. -| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. -| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. +| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. +| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. | `afterUpdate` | `axis` | Callback that runs at the end of the update process. ## Updating Axis Defaults diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index 9d15b5e2674..cefe9c24806 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -147,6 +147,6 @@ The `ticks.source` property controls the ticks generation * `'labels'`: generates ticks from user given `data.labels` values ONLY ### Parser -If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. +If this property is defined as a string, it is interpreted as a custom format to be used by moment to parse the date. If this is a function, it must return a moment.js object given the appropriate data value. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 64515ed20d8..84704ba941b 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -6,12 +6,12 @@ A bar chart provides a way of showing data values represented as vertical bars. "type": "bar", "data": { "labels": [ - "January", - "February", - "March", - "April", - "May", - "June", + "January", + "February", + "March", + "April", + "May", + "June", "July" ], "datasets": [{ diff --git a/docs/charts/mixed.md b/docs/charts/mixed.md index e7f43a2193b..9a51eae860f 100644 --- a/docs/charts/mixed.md +++ b/docs/charts/mixed.md @@ -41,9 +41,9 @@ At this point we have a chart rendering how we'd like. It's important to note th "type": "bar", "data": { "labels": [ - "January", - "February", - "March", + "January", + "February", + "March", "April" ], "datasets": [{ diff --git a/docs/charts/radar.md b/docs/charts/radar.md index ac908315cf9..b8a41c8384c 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -8,9 +8,9 @@ They are often useful for comparing the points of two or more different data set "type": "radar", "data": { "labels": [ - "Eating", - "Drinking", - "Sleeping", + "Eating", + "Drinking", + "Sleeping", "Designing", "Coding", "Cycling", @@ -94,7 +94,7 @@ The style of point. Options are: * 'circle' * 'cross' * 'crossRot' -* 'dash'. +* 'dash'. * 'line' * 'rect' * 'rectRounded' @@ -127,7 +127,7 @@ It is common to want to apply a configuration setting to all created radar chart ## Data Structure -The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. +The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. ```javascript data: [20, 10] diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 52bee8687c0..6867b762d6f 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -10,7 +10,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | `display` | `Boolean` | `true` | is the legend shown | `position` | `String` | `'top'` | Position of the legend. [more...](#position) | `fullWidth` | `Boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. -| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item +| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item | `onHover` | `Function` | | A callback that is called when a 'mousemove' event is registered on top of a label item | `reverse` | `Boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `Object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 93db40147b7..b591dc1d13e 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -63,9 +63,9 @@ Example: Chart.Tooltip.positioners.custom = function(elements, eventPosition) { /** @type {Chart.Tooltip} */ var tooltip = this; - + /* ... */ - + return { x: 0, y: 0 diff --git a/docs/developers/charts.md b/docs/developers/charts.md index f4c9a97469a..e1267423bf0 100644 --- a/docs/developers/charts.md +++ b/docs/developers/charts.md @@ -75,7 +75,7 @@ The built in controller types are: For example, to derive a new chart type that extends from a bubble chart, you would do the following. ```javascript -// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. +// Sets the default config for 'derivedBubble' to be the same as the bubble defaults. // We look for the defaults by doing Chart.defaults[chartType] // It looks like a bug exists when the defaults don't exist Chart.defaults.derivedBubble = Chart.defaults.bubble; @@ -102,7 +102,7 @@ var custom = Chart.controllers.bubble.extend({ // Stores the controller so that the chart initialization routine can look it up with // Chart.controllers[type] -Chart.controllers.derivedBubble = custom; +Chart.controllers.derivedBubble = custom; // Now we can create and use our new chart type new Chart(ctx, { diff --git a/docs/general/colors.md b/docs/general/colors.md index d2d270081e6..64ab84c66cf 100644 --- a/docs/general/colors.md +++ b/docs/general/colors.md @@ -6,7 +6,7 @@ You can also pass a [CanvasGradient](https://developer.mozilla.org/en-US/docs/We ## Patterns and Gradients -An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. +An alternative option is to pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string colour. For example, if you wanted to fill a dataset with a pattern from an image you could do the following. diff --git a/docs/general/interactions/README.md b/docs/general/interactions/README.md index 585e6558204..532cab6f080 100644 --- a/docs/general/interactions/README.md +++ b/docs/general/interactions/README.md @@ -1,6 +1,6 @@ # Interactions -The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). +The hover configuration is passed into the `options.hover` namespace. The global hover configuration is at `Chart.defaults.global.hover`. To configure which events trigger chart interactions, see [events](./events.md#events). | Name | Type | Default | Description | ---- | ---- | ------- | ----------- diff --git a/docs/general/interactions/events.md b/docs/general/interactions/events.md index 8e21f75024b..d5d05138cf7 100644 --- a/docs/general/interactions/events.md +++ b/docs/general/interactions/events.md @@ -7,7 +7,7 @@ The following properties define how the chart interacts with events. | `onHover` | `Function` | `null` | Called when any of the events fire. Called in the context of the chart and passed the event and an array of active elements (bars, points, etc). | `onClick` | `Function` | `null` | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed the event and an array of active elements -## Event Option +## Event Option For example, to have the chart only respond to click events, you could do ```javascript var chart = new Chart(ctx, { diff --git a/docs/general/interactions/modes.md b/docs/general/interactions/modes.md index a46f047e70b..3e9f7982f9e 100644 --- a/docs/general/interactions/modes.md +++ b/docs/general/interactions/modes.md @@ -41,7 +41,7 @@ Finds the first item that intersects the point and returns it. Behaves like 'nea 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, in the x direction, is used to determine the 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, in the x direction, is used to determine the index. ```javascript var chart = new Chart(ctx, { From 584d1c646c84f3d49c865ab69e774cc6541027a4 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 9 Feb 2018 17:20:06 -0500 Subject: [PATCH 380/685] Fix label vertical alignment on vertical scales (#5248) --- src/core/core.scale.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f79dbee8009..1996c6c894d 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -857,11 +857,15 @@ module.exports = function(Chart) { var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = 0; i < label.length; ++i) { + var lineCount = label.length; + var lineHeight = tickFont.size * 1.5; + var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; + + for (var i = 0; i < lineCount; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); // apply same lineSpacing as calculated @ L#320 - y += (tickFont.size * 1.5); + y += lineHeight; } } else { context.fillText(label, 0, 0); From d6ce5c0772bda8e4331df1d8e867fde7e6ee0bbe Mon Sep 17 00:00:00 2001 From: Laura Cressman Date: Wed, 14 Feb 2018 21:22:19 -0500 Subject: [PATCH 381/685] Support multiple font colors for radial chart labels (#5240) * Support multiple font colors in array * Address linting error --- docs/axes/radial/linear.md | 2 +- src/scales/scale.radialLinear.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index adebe77cc61..14009be6a5c 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -104,7 +104,7 @@ The following options are used to configure the point labels that are shown on t | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. -| `fontColor` | `Color` | `'#666'` | Font color for point labels. +| `fontColor` | `Color/Color[]` | `'#666'` | Font color for point labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels | `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 9f59f17887a..e36b4e13539 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -237,7 +237,6 @@ module.exports = function(Chart) { function drawPointLabels(scale) { var ctx = scale.ctx; - var valueOrDefault = helpers.valueOrDefault; var opts = scale.options; var angleLineOpts = opts.angleLines; var pointLabelOpts = opts.pointLabels; @@ -267,7 +266,7 @@ module.exports = function(Chart) { var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); // Keep this in loop since we may support array properties here - var pointLabelFontColor = valueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, globalDefaults.defaultFontColor); ctx.font = plFont.font; ctx.fillStyle = pointLabelFontColor; From be6660c63d10b54f4972170c4f3364b61bb9b9b3 Mon Sep 17 00:00:00 2001 From: Winter Zhong Date: Fri, 16 Feb 2018 08:25:49 +0800 Subject: [PATCH 382/685] Improve title of generated documentation (#5256) --- book.json | 1 + 1 file changed, 1 insertion(+) diff --git a/book.json b/book.json index 487fd095c03..8b0f73970df 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,6 @@ { "root": "./docs", + "title": "Chart.js documentation", "author": "chartjs", "gitbook": "3.2.2", "plugins": ["-lunr", "-search", "search-plus", "anchorjs", "chartjs", "ga"], From c90cf2ebcdce88d90086c32397a9d8355c985695 Mon Sep 17 00:00:00 2001 From: Wilson Lin Date: Tue, 20 Feb 2018 04:29:10 -0800 Subject: [PATCH 383/685] Make both README.md and installation.md clearer (#5274) Address the ambiguity of "Selecting the Correct Build" section --- README.md | 18 +++++++++++++++--- docs/getting-started/installation.md | 6 +++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b68b39008da..f914944e101 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,23 @@ To install via bower: bower install chart.js --save ``` -#### Selecting the Correct Build +### Selecting the Correct Build -Chart.js provides two different builds that are available for your use. The `Chart.js` and `Chart.min.js` files include Chart.js and the accompanying color parsing library. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. -The `Chart.bundle.js` and `Chart.bundle.min.js` builds include Moment.js in a single file. This version should be used if you require time axes and want a single file to include, select this version. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +#### Stand-Alone Build +Files: +* `dist/Chart.js` +* `dist/Chart.min.js` + +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. + +#### Bundled Build +Files: +* `dist/Chart.bundle.js` +* `dist/Chart.bundle.min.js` + +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. ## Documentation diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 613d7aa7a6c..ccc02ddc4b9 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -40,18 +40,18 @@ If you download or clone the repository, you must [build](../developers/contribu # Selecting the Correct Build -Chart.js provides two different builds that are available for your use. +Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. ## Stand-Alone Build Files: * `dist/Chart.js` * `dist/Chart.min.js` -This version only includes Chart.js. If this version is used and you require the use of the time axis, [Moment.js](http://momentjs.com/) will need to be included before Chart.js. +The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](http://momentjs.com/) before Chart.js for the functionality of the time axis. ## Bundled Build Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled version includes Moment.js built into the same file. This version should be used if you wish to use time axes and want a single file to include. Do not use this build if your application already includes Moment.js. If you do, Moment.js will be included twice, increasing the page load time and potentially introducing version issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. From c2a5b1237635875b20c46d47577355ced733db14 Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 21 Feb 2018 00:44:01 +0100 Subject: [PATCH 384/685] Bugfix: Improve polyfill function of log10 to return whole powers of 10 (#5275) * Bugfix: Improve polyfill function of log10 to return whole powers of 10 as integer values, as it caused endless loop in IE11 in the tick creation loop. * Compare floating-point numbers directly instead of using unnecessary division. --- src/core/core.helpers.js | 8 +++++++- test/specs/core.helpers.tests.js | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bdce895cf70..347fd958e83 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -159,7 +159,13 @@ module.exports = function(Chart) { return Math.log10(x); } : function(x) { - return Math.log(x) / Math.LN10; + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; }; helpers.toRadians = function(degrees) { return degrees * (Math.PI / 180); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 44446d38ab8..eb96ef8c745 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -194,8 +194,12 @@ describe('Core helper tests', function() { it('should do a log10 operation', function() { expect(helpers.log10(0)).toBe(-Infinity); - expect(helpers.log10(1)).toBe(0); - expect(helpers.log10(1000)).toBeCloseTo(3, 1e-9); + + // Check all allowed powers of 10, which should return integer values + var maxPowerOf10 = Math.floor(helpers.log10(Number.MAX_VALUE)); + for (var i = 0; i < maxPowerOf10; i += 1) { + expect(helpers.log10(Math.pow(10, i))).toBe(i); + } }); it('should correctly determine if two numbers are essentially equal', function() { From ac088a04abf671a217aa03a65699524486ceab06 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 1 Mar 2018 22:20:53 +0100 Subject: [PATCH 385/685] Bump version to 2.7.2 (#5307) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03f3815ccd7..a2e6c494af1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.1", + "version": "2.7.2", "license": "MIT", "main": "src/chart.js", "repository": { From b7025fedbec8fbb2d77c8c52a3c566270d5ca4d9 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 3 Mar 2018 09:13:41 +0800 Subject: [PATCH 386/685] Add a link to chartjs-plugin-streaming to extensions.md (#5309) --- docs/notes/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index fe2c337f156..ae141d96e92 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -18,6 +18,7 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. + - chartjs-plugin-streaming - Enables to create live streaming charts. - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. From d0b26dbddd6d7257eb84ab6f5c4d4e402c2f6310 Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Sat, 10 Mar 2018 16:56:27 +0100 Subject: [PATCH 387/685] Show how to use circumference and rotation options (#5326) --- samples/charts/doughnut.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/samples/charts/doughnut.html b/samples/charts/doughnut.html index 446dcee4e2e..8466970a403 100644 --- a/samples/charts/doughnut.html +++ b/samples/charts/doughnut.html @@ -23,6 +23,7 @@ + From 7f4b1d9715f3da507893cf145009c729fad3606b Mon Sep 17 00:00:00 2001 From: Sean Sobey Date: Sat, 10 Mar 2018 17:57:27 +0200 Subject: [PATCH 388/685] Add undefined guard for window.devicePixelRatio (#5324) --- src/core/core.helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 347fd958e83..d49451d1846 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -502,7 +502,7 @@ module.exports = function(Chart) { document.defaultView.getComputedStyle(el, null).getPropertyValue(property); }; helpers.retinaScale = function(chart, forceRatio) { - var pixelRatio = chart.currentDevicePixelRatio = forceRatio || window.devicePixelRatio || 1; + var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1; if (pixelRatio === 1) { return; } From 25f26346d5fe5806d3ed9f507ed137da02badcc8 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Tue, 13 Mar 2018 18:52:19 -0400 Subject: [PATCH 389/685] Time Point Data sample works correctly (#5328) --- samples/scales/time/line-point-data.html | 27 +++++++----------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/samples/scales/time/line-point-data.html b/samples/scales/time/line-point-data.html index e18be91f3d8..604a49b4f1a 100644 --- a/samples/scales/time/line-point-data.html +++ b/samples/scales/time/line-point-data.html @@ -121,27 +121,16 @@ window.myLine.update(); }); - - // TODO : fix issue with addData - // See https://github.com/chartjs/Chart.js/issues/5197 - // The Add Data button for this sample has no effect. - // An error is logged in the console. document.getElementById('addData').addEventListener('click', function() { if (config.data.datasets.length > 0) { - var numTicks = window.myLine.scales['x-axis-0'].ticksAsTimestamps.length; - var lastTime = numTicks ? moment(window.myLine.scales['x-axis-0'].ticksAsTimestamps[numTicks - 1]) : moment(); - - var newTime = lastTime - .clone() - .add(1, 'day') - .format('MM/DD/YYYY HH:mm'); - - for (var index = 0; index < config.data.datasets.length; ++index) { - config.data.datasets[index].data.push({ - x: newTime, - y: randomScalingFactor() - }); - } + config.data.datasets[0].data.push({ + x: newDateString(config.data.datasets[0].data.length + 2), + y: randomScalingFactor() + }); + config.data.datasets[1].data.push({ + x: newDate(config.data.datasets[1].data.length + 2), + y: randomScalingFactor() + }); window.myLine.update(); } From a191921b1553d0af05dbb6ee621f171aeabf9baa Mon Sep 17 00:00:00 2001 From: Juan Eugenio Abadie Date: Tue, 20 Mar 2018 11:37:56 -0300 Subject: [PATCH 390/685] Fix typo in the legend documentation (#5348) --- docs/configuration/legend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 6867b762d6f..d2a3a88b3b6 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -1,6 +1,6 @@ # Legend Configuration -The chart legend displays data about the datasets that area appearing on the chart. +The chart legend displays data about the datasets that are appearing on the chart. ## Configuration options The legend configuration is passed into the `options.legend` namespace. The global options for the chart legend is defined in `Chart.defaults.global.legend`. From 9fbac8893865fc6f7efdab7f49d480e6a730c669 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 1 Apr 2018 12:56:45 -0400 Subject: [PATCH 391/685] Add `ticks.precision` option to linear scale. (#4841) If defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. --- docs/axes/cartesian/linear.md | 1 + docs/axes/radial/linear.md | 1 + src/scales/scale.linearbase.js | 13 ++++++- test/specs/scale.linear.tests.js | 60 ++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md index 1d0292074a8..3f68b79ff1d 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -12,6 +12,7 @@ The following options are provided by the linear scale. They are all located in | `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) | `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) | `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `precision` | `Number` | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) | `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) | `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 14009be6a5c..594901db335 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -27,6 +27,7 @@ The following options are provided by the linear scale. They are all located in | `min` | `Number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](#axis-range-settings) | `max` | `Number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](#axis-range-settings) | `maxTicksLimit` | `Number` | `11` | Maximum number of ticks and gridlines to show. +| `precision` | `Number` | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `Number` | | User defined fixed step size for the scale. [more...](#step-size) | `suggestedMax` | `Number` | | Adjustment used when calculating the maximum data value. [more...](#axis-range-settings) | `suggestedMin` | `Number` | | Adjustment used when calculating the minimum data value. [more...](#axis-range-settings) diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 3e4b5c083f3..5ac01b92a4f 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -14,12 +14,22 @@ function generateTicks(generationOptions, dataRange) { // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks // for details. + var factor; + var precision; 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); + + precision = generationOptions.precision; + if (precision !== undefined) { + // If the user specified a precision, round to that number of decimal places + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } } var niceMin = Math.floor(dataRange.min / spacing) * spacing; var niceMax = Math.ceil(dataRange.max / spacing) * spacing; @@ -41,7 +51,7 @@ function generateTicks(generationOptions, dataRange) { numSpaces = Math.ceil(numSpaces); } - var precision = 1; + precision = 1; if (spacing < 1) { precision = Math.pow(10, spacing.toString().length - 2); niceMin = Math.round(niceMin * precision) / precision; @@ -154,6 +164,7 @@ module.exports = function(Chart) { maxTicks: maxTicks, min: tickOpts.min, max: tickOpts.max, + precision: tickOpts.precision, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 60dd07698c0..b42675d8a8d 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -548,6 +548,66 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.ticks).toEqual(['11', '9', '7', '5', '3', '1']); }); + describe('precision', function() { + it('Should create integer steps if precision is 0', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [0, 1, 2, 1, 0, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + precision: 0 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(0); + expect(chart.scales.yScale0.max).toBe(2); + expect(chart.scales.yScale0.ticks).toEqual(['2', '1', '0']); + }); + + it('Should round the step size to the given number of decimal places', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [0, 0.001, 0.002, 0.003, 0, 0.001] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear', + ticks: { + precision: 2 + } + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(0); + expect(chart.scales.yScale0.max).toBe(0.01); + expect(chart.scales.yScale0.ticks).toEqual(['0.01', '0']); + }); + }); + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({ From d2846864527898225bb8061cf24793101ef98dbf Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 2 Apr 2018 10:55:52 +0200 Subject: [PATCH 392/685] Make `Chart.Animation/animations/Tooltip` importable (#5382) --- .htmllintrc | 1 + src/chart.js | 5 +- src/core/core.animation.js | 201 +--- src/core/core.animations.js | 129 +++ src/core/core.controller.js | 13 +- src/core/core.tooltip.js | 1502 +++++++++++++------------- test/specs/global.defaults.tests.js | 1 - test/specs/global.namespace.tests.js | 44 + 8 files changed, 975 insertions(+), 921 deletions(-) create mode 100644 src/core/core.animations.js create mode 100644 test/specs/global.namespace.tests.js diff --git a/.htmllintrc b/.htmllintrc index a6b2097031b..1ab933490de 100644 --- a/.htmllintrc +++ b/.htmllintrc @@ -1,5 +1,6 @@ { "indent-style": "tabs", + "line-end-style": false, "attr-quote-style": "double", "spec-char-escape": false, "attr-bans": [ diff --git a/src/chart.js b/src/chart.js index a958e343ff8..04d7df6c99a 100644 --- a/src/chart.js +++ b/src/chart.js @@ -8,6 +8,8 @@ Chart.helpers = require('./helpers/index'); // @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests! require('./core/core.helpers')(Chart); +Chart.Animation = require('./core/core.animation'); +Chart.animationService = require('./core/core.animations'); Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); @@ -16,13 +18,12 @@ Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); Chart.plugins = require('./core/core.plugins'); Chart.Ticks = require('./core/core.ticks'); +Chart.Tooltip = require('./core/core.tooltip'); -require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.scale')(Chart); -require('./core/core.tooltip')(Chart); require('./scales/scale.linearbase')(Chart); require('./scales/scale.category')(Chart); diff --git a/src/core/core.animation.js b/src/core/core.animation.js index af746588e60..8b2f4dd2ade 100644 --- a/src/core/core.animation.js +++ b/src/core/core.animation.js @@ -1,172 +1,43 @@ -/* global window: false */ 'use strict'; -var defaults = require('./core.defaults'); var Element = require('./core.element'); -var helpers = require('../helpers/index'); -defaults._set('global', { - animation: { - duration: 1000, - easing: 'easeOutQuart', - onProgress: helpers.noop, - onComplete: helpers.noop - } -}); - -module.exports = function(Chart) { - - Chart.Animation = Element.extend({ - chart: null, // the animation associated chart instance - currentStep: 0, // the current animation step - numSteps: 60, // default number of steps - easing: '', // the easing to use for this animation - render: null, // render function used by the animation service - - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null, // user specified callback to fire when the animation finishes - }); - - Chart.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - request: null, - - /** - * @param {Chart} chart - The chart to animate. - * @param {Chart.Animation} animation - The animation that we will animate. - * @param {Number} duration - The animation duration in ms. - * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions - */ - addAnimation: function(chart, animation, duration, lazy) { - var animations = this.animations; - var i, ilen; - - animation.chart = chart; - - if (!lazy) { - chart.animating = true; - } - - for (i = 0, ilen = animations.length; i < ilen; ++i) { - if (animations[i].chart === chart) { - animations[i] = animation; - return; - } - } - - animations.push(animation); - - // If there are no animations queued, manually kickstart a digest, for lack of a better word - if (animations.length === 1) { - this.requestAnimationFrame(); - } - }, - - cancelAnimation: function(chart) { - var index = helpers.findIndex(this.animations, function(animation) { - return animation.chart === chart; - }); - - if (index !== -1) { - this.animations.splice(index, 1); - chart.animating = false; - } - }, - - requestAnimationFrame: function() { - var me = this; - if (me.request === null) { - // Skip animation frame requests until the active one is executed. - // This can happen when processing mouse events, e.g. 'mousemove' - // and 'mouseout' events will trigger multiple renders. - me.request = helpers.requestAnimFrame.call(window, function() { - me.request = null; - me.startDigest(); - }); - } - }, - - /** - * @private - */ - startDigest: function() { - var me = this; - var startTime = Date.now(); - var framesToDrop = 0; +var exports = module.exports = Element.extend({ + chart: null, // the animation associated chart instance + currentStep: 0, // the current animation step + numSteps: 60, // default number of steps + easing: '', // the easing to use for this animation + render: null, // render function used by the animation service - if (me.dropFrames > 1) { - framesToDrop = Math.floor(me.dropFrames); - me.dropFrames = me.dropFrames % 1; - } - - me.advance(1 + framesToDrop); - - var endTime = Date.now(); - - me.dropFrames += (endTime - startTime) / me.frameDuration; - - // Do we have more stuff to animate? - if (me.animations.length > 0) { - me.requestAnimationFrame(); - } - }, - - /** - * @private - */ - advance: function(count) { - var animations = this.animations; - var animation, chart; - var i = 0; - - while (i < animations.length) { - animation = animations[i]; - chart = animation.chart; - - animation.currentStep = (animation.currentStep || 0) + count; - animation.currentStep = Math.min(animation.currentStep, animation.numSteps); - - helpers.callback(animation.render, [chart, animation], chart); - helpers.callback(animation.onAnimationProgress, [animation], chart); - - if (animation.currentStep >= animation.numSteps) { - helpers.callback(animation.onAnimationComplete, [animation], chart); - chart.animating = false; - animations.splice(i, 1); - } else { - ++i; - } - } - } - }; - - /** - * Provided for backward compatibility, use Chart.Animation instead - * @prop Chart.Animation#animationObject - * @deprecated since version 2.6.0 - * @todo remove at version 3 - */ - Object.defineProperty(Chart.Animation.prototype, 'animationObject', { - get: function() { - return this; - } - }); + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes +}); - /** - * Provided for backward compatibility, use Chart.Animation#chart instead - * @prop Chart.Animation#chartInstance - * @deprecated since version 2.6.0 - * @todo remove at version 3 - */ - Object.defineProperty(Chart.Animation.prototype, 'chartInstance', { - get: function() { - return this.chart; - }, - set: function(value) { - this.chart = value; - } - }); +// DEPRECATIONS + +/** + * Provided for backward compatibility, use Chart.Animation instead + * @prop Chart.Animation#animationObject + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports.prototype, 'animationObject', { + get: function() { + return this; + } +}); -}; +/** + * Provided for backward compatibility, use Chart.Animation#chart instead + * @prop Chart.Animation#chartInstance + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ +Object.defineProperty(exports.prototype, 'chartInstance', { + get: function() { + return this.chart; + }, + set: function(value) { + this.chart = value; + } +}); diff --git a/src/core/core.animations.js b/src/core/core.animations.js new file mode 100644 index 00000000000..6853cb8736d --- /dev/null +++ b/src/core/core.animations.js @@ -0,0 +1,129 @@ +/* global window: false */ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + animation: { + duration: 1000, + easing: 'easeOutQuart', + onProgress: helpers.noop, + onComplete: helpers.noop + } +}); + +module.exports = { + frameDuration: 17, + animations: [], + dropFrames: 0, + request: null, + + /** + * @param {Chart} chart - The chart to animate. + * @param {Chart.Animation} animation - The animation that we will animate. + * @param {Number} duration - The animation duration in ms. + * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + */ + addAnimation: function(chart, animation, duration, lazy) { + var animations = this.animations; + var i, ilen; + + animation.chart = chart; + + if (!lazy) { + chart.animating = true; + } + + for (i = 0, ilen = animations.length; i < ilen; ++i) { + if (animations[i].chart === chart) { + animations[i] = animation; + return; + } + } + + animations.push(animation); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (animations.length === 1) { + this.requestAnimationFrame(); + } + }, + + cancelAnimation: function(chart) { + var index = helpers.findIndex(this.animations, function(animation) { + return animation.chart === chart; + }); + + if (index !== -1) { + this.animations.splice(index, 1); + chart.animating = false; + } + }, + + requestAnimationFrame: function() { + var me = this; + if (me.request === null) { + // Skip animation frame requests until the active one is executed. + // This can happen when processing mouse events, e.g. 'mousemove' + // and 'mouseout' events will trigger multiple renders. + me.request = helpers.requestAnimFrame.call(window, function() { + me.request = null; + me.startDigest(); + }); + } + }, + + /** + * @private + */ + startDigest: function() { + var me = this; + var startTime = Date.now(); + var framesToDrop = 0; + + if (me.dropFrames > 1) { + framesToDrop = Math.floor(me.dropFrames); + me.dropFrames = me.dropFrames % 1; + } + + me.advance(1 + framesToDrop); + + var endTime = Date.now(); + + me.dropFrames += (endTime - startTime) / me.frameDuration; + + // Do we have more stuff to animate? + if (me.animations.length > 0) { + me.requestAnimationFrame(); + } + }, + + /** + * @private + */ + advance: function(count) { + var animations = this.animations; + var animation, chart; + var i = 0; + + while (i < animations.length) { + animation = animations[i]; + chart = animation.chart; + + animation.currentStep = (animation.currentStep || 0) + count; + animation.currentStep = Math.min(animation.currentStep, animation.numSteps); + + helpers.callback(animation.render, [chart, animation], chart); + helpers.callback(animation.onAnimationProgress, [animation], chart); + + if (animation.currentStep >= animation.numSteps) { + helpers.callback(animation.onAnimationComplete, [animation], chart); + chart.animating = false; + animations.splice(i, 1); + } else { + ++i; + } + } + } +}; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e29a5b0769c..8f7d04fc64d 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -1,11 +1,14 @@ 'use strict'; +var Animation = require('./core.animation'); +var animations = require('./core.animations'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); var plugins = require('./core.plugins'); +var Tooltip = require('./core.tooltip'); module.exports = function(Chart) { @@ -164,7 +167,7 @@ module.exports = function(Chart) { stop: function() { // Stops any current animation loop occurring - Chart.animationService.cancelAnimation(this); + animations.cancelAnimation(this); return this; }, @@ -519,7 +522,7 @@ module.exports = function(Chart) { }; if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { - var animation = new Chart.Animation({ + var animation = new Animation({ numSteps: (duration || animationOptions.duration) / 16.66, // 60 fps easing: config.easing || animationOptions.easing, @@ -535,12 +538,12 @@ module.exports = function(Chart) { onAnimationComplete: onComplete }); - Chart.animationService.addAnimation(me, animation, duration, lazy); + animations.addAnimation(me, animation, duration, lazy); } else { me.draw(); // See https://github.com/chartjs/Chart.js/issues/3781 - onComplete(new Chart.Animation({numSteps: 0, chart: me})); + onComplete(new Animation({numSteps: 0, chart: me})); } return me; @@ -775,7 +778,7 @@ module.exports = function(Chart) { initToolTip: function() { var me = this; - me.tooltip = new Chart.Tooltip({ + me.tooltip = new Tooltip({ _chart: me, _chartInstance: me, // deprecated, backward compatibility _data: me.data, diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 9b09d760443..3f9490f8546 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -96,853 +96,859 @@ defaults._set('global', { } }); -module.exports = function(Chart) { - +var positioners = { /** - * 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(); - } + * Average mode places the tooltip at the average position of the elements shown + * @function Chart.Tooltip.positioners.average + * @param elements {ChartElement[]} the elements being displayed in the tooltip + * @returns {Point} tooltip position + */ + average: function(elements) { + if (!elements.length) { + return false; + } - // Helper to push or concat based on if the 2nd parameter is an array or not - function pushOrConcat(base, toPush) { - if (toPush) { - if (helpers.isArray(toPush)) { - // base = base.concat(toPush); - Array.prototype.push.apply(base, toPush); - } else { - base.push(toPush); + var i, len; + var x = 0; + var y = 0; + var count = 0; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var pos = el.tooltipPosition(); + x += pos.x; + y += pos.y; + ++count; } } - return base; - } - - // Private helper to create a tooltip item model - // @param element : the chart element (point, arc, bar) to create the tooltip item for - // @return : new tooltip item - function createTooltipItem(element) { - var xScale = element._xScale; - var yScale = element._yScale || element._scale; // handle radar || polarArea charts - var index = element._index; - var datasetIndex = element._datasetIndex; - return { - xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', - yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', - index: index, - datasetIndex: datasetIndex, - x: element._model.x, - y: element._model.y + x: Math.round(x / count), + y: Math.round(y / count) }; - } + }, /** - * Helper to get the reset model for the tooltip - * @param tooltipOpts {Object} the tooltip options + * Gets the tooltip position nearest of the item nearest to the event position + * @function Chart.Tooltip.positioners.nearest + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {Point} the tooltip position */ - function getBaseModel(tooltipOpts) { - var globalDefaults = defaults.global; - var valueOrDefault = helpers.valueOrDefault; - - return { - // Positioning - xPadding: tooltipOpts.xPadding, - yPadding: tooltipOpts.yPadding, - xAlign: tooltipOpts.xAlign, - yAlign: tooltipOpts.yAlign, + nearest: function(elements, eventPosition) { + var x = eventPosition.x; + var y = eventPosition.y; + var minDistance = Number.POSITIVE_INFINITY; + var i, len, nearestElement; + + for (i = 0, len = elements.length; i < len; ++i) { + var el = elements[i]; + if (el && el.hasValue()) { + var center = el.getCenterPoint(); + var d = helpers.distanceBetweenPoints(eventPosition, center); + + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } - // Body - bodyFontColor: tooltipOpts.bodyFontColor, - _bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), - _bodyFontStyle: valueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), - _bodyAlign: tooltipOpts.bodyAlign, - bodyFontSize: valueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), - bodySpacing: tooltipOpts.bodySpacing, - - // Title - titleFontColor: tooltipOpts.titleFontColor, - _titleFontFamily: valueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), - _titleFontStyle: valueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), - titleFontSize: valueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), - _titleAlign: tooltipOpts.titleAlign, - titleSpacing: tooltipOpts.titleSpacing, - titleMarginBottom: tooltipOpts.titleMarginBottom, + if (nearestElement) { + var tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } - // Footer - footerFontColor: tooltipOpts.footerFontColor, - _footerFontFamily: valueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), - _footerFontStyle: valueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), - footerFontSize: valueOrDefault(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, - borderColor: tooltipOpts.borderColor, - borderWidth: tooltipOpts.borderWidth + return { + x: x, + y: y }; } +}; - /** - * 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; - var bodyFontSize = model.bodyFontSize; - var 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); +/** + * 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(); +} + +// Helper to push or concat based on if the 2nd parameter is an array or not +function pushOrConcat(base, toPush) { + if (toPush) { + if (helpers.isArray(toPush)) { + // base = base.concat(toPush); + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } - // Body width - ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); - helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + return base; +} + +// Private helper to create a tooltip item model +// @param element : the chart element (point, arc, bar) to create the tooltip item for +// @return : new tooltip item +function createTooltipItem(element) { + var xScale = element._xScale; + var yScale = element._yScale || element._scale; // handle radar || polarArea charts + var index = element._index; + var datasetIndex = element._datasetIndex; + + return { + xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '', + yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '', + index: index, + datasetIndex: datasetIndex, + x: element._model.x, + y: element._model.y + }; +} + +/** + * Helper to get the reset model for the tooltip + * @param tooltipOpts {Object} the tooltip options + */ +function getBaseModel(tooltipOpts) { + var globalDefaults = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + + return { + // Positioning + xPadding: tooltipOpts.xPadding, + yPadding: tooltipOpts.yPadding, + xAlign: tooltipOpts.xAlign, + yAlign: tooltipOpts.yAlign, + + // Body + bodyFontColor: tooltipOpts.bodyFontColor, + _bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily), + _bodyFontStyle: valueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle), + _bodyAlign: tooltipOpts.bodyAlign, + bodyFontSize: valueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize), + bodySpacing: tooltipOpts.bodySpacing, + + // Title + titleFontColor: tooltipOpts.titleFontColor, + _titleFontFamily: valueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily), + _titleFontStyle: valueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle), + titleFontSize: valueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize), + _titleAlign: tooltipOpts.titleAlign, + titleSpacing: tooltipOpts.titleSpacing, + titleMarginBottom: tooltipOpts.titleMarginBottom, + + // Footer + footerFontColor: tooltipOpts.footerFontColor, + _footerFontFamily: valueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily), + _footerFontStyle: valueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle), + footerFontSize: valueOrDefault(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, + borderColor: tooltipOpts.borderColor, + borderWidth: tooltipOpts.borderWidth + }; +} + +/** + * 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; + var bodyFontSize = model.bodyFontSize; + var 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); + }; - // 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); - }); + ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); + helpers.each(model.title, maxLineWidth); - // Reset back to 0 - widthPadding = 0; + // Body width + ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); + helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); - // Footer width - ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); - helpers.each(model.footer, 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); + }); - // Add padding - width += 2 * model.xPadding; + // Reset back to 0 + widthPadding = 0; - return { - width: width, - height: height - }; - } + // Footer width + ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); + helpers.each(model.footer, maxLineWidth); - /** - * 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._chart.chartArea; - var xAlign = 'center'; - var yAlign = 'center'; - - if (model.y < size.height) { - yAlign = 'top'; - } else if (model.y > (chart.height - size.height)) { - yAlign = 'bottom'; - } + // Add padding + width += 2 * model.xPadding; - 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; + 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._chart.chartArea; + var xAlign = 'center'; + var yAlign = 'center'; + + if (model.y < size.height) { + yAlign = 'top'; + } else if (model.y > (chart.height - size.height)) { + yAlign = 'bottom'; + } - 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)); - }; - } + 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; - olf = function(x) { - return x + size.width + model.caretSize + model.caretPadding > chart.width; + if (yAlign === 'center') { + lf = function(x) { + return x <= midX; }; - orf = function(x) { - return x - size.width - model.caretSize - model.caretPadding < 0; + rf = function(x) { + return x > midX; }; - yf = function(y) { - return y <= midY ? 'top' : 'bottom'; + } else { + lf = function(x) { + return x <= (size.width / 2); }; + rf = function(x) { + return x >= (chart.width - (size.width / 2)); + }; + } - if (lf(model.x)) { - xAlign = 'left'; + olf = function(x) { + return x + size.width + model.caretSize + model.caretPadding > chart.width; + }; + orf = function(x) { + return x - size.width - model.caretSize - model.caretPadding < 0; + }; + yf = function(y) { + return y <= midY ? 'top' : 'bottom'; + }; - // 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'; + if (lf(model.x)) { + xAlign = 'left'; - // Is tooltip too wide and goes outside left edge of canvas? - if (orf(model.x)) { - xAlign = 'center'; - yAlign = yf(model.y); - } + // 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'; - var opts = tooltip._options; - return { - xAlign: opts.xAlign ? opts.xAlign : xAlign, - yAlign: opts.yAlign ? opts.yAlign : yAlign - }; + // Is tooltip too wide and goes outside left edge of canvas? + if (orf(model.x)) { + xAlign = 'center'; + yAlign = yf(model.y); + } } - /** - * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment - */ - function getBackgroundPoint(vm, size, alignment, chart) { - // Background Position - var x = vm.x; - var y = vm.y; - - var caretSize = vm.caretSize; - var caretPadding = vm.caretPadding; - var cornerRadius = vm.cornerRadius; - var xAlign = alignment.xAlign; - var yAlign = alignment.yAlign; - var paddingAndSize = caretSize + caretPadding; - var radiusAndPadding = cornerRadius + caretPadding; - - if (xAlign === 'right') { - x -= size.width; - } else if (xAlign === 'center') { - x -= (size.width / 2); - if (x + size.width > chart.width) { - x = chart.width - size.width; - } - if (x < 0) { - x = 0; - } + var opts = tooltip._options; + return { + xAlign: opts.xAlign ? opts.xAlign : xAlign, + yAlign: opts.yAlign ? opts.yAlign : yAlign + }; +} + +/** + * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + */ +function getBackgroundPoint(vm, size, alignment, chart) { + // Background Position + var x = vm.x; + var y = vm.y; + + var caretSize = vm.caretSize; + var caretPadding = vm.caretPadding; + var cornerRadius = vm.cornerRadius; + var xAlign = alignment.xAlign; + var yAlign = alignment.yAlign; + var paddingAndSize = caretSize + caretPadding; + var radiusAndPadding = cornerRadius + caretPadding; + + if (xAlign === 'right') { + x -= size.width; + } else if (xAlign === 'center') { + x -= (size.width / 2); + if (x + size.width > chart.width) { + x = chart.width - size.width; } - - if (yAlign === 'top') { - y += paddingAndSize; - } else if (yAlign === 'bottom') { - y -= size.height + paddingAndSize; - } else { - y -= (size.height / 2); + if (x < 0) { + x = 0; } + } - if (yAlign === 'center') { - if (xAlign === 'left') { - x += paddingAndSize; - } else if (xAlign === 'right') { - x -= paddingAndSize; - } - } else if (xAlign === 'left') { - x -= radiusAndPadding; + 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 += radiusAndPadding; + x -= paddingAndSize; } - - return { - x: x, - y: y - }; + } else if (xAlign === 'left') { + x -= radiusAndPadding; + } else if (xAlign === 'right') { + x += radiusAndPadding; } - Chart.Tooltip = Element.extend({ - initialize: function() { - this._model = getBaseModel(this._options); - this._lastActive = []; - }, - - // Get the title - // Args are: (tooltipItem, data) - getTitle: function() { - var me = this; - var opts = me._options; - var callbacks = opts.callbacks; - - var beforeTitle = callbacks.beforeTitle.apply(me, arguments); - var title = callbacks.title.apply(me, arguments); - var afterTitle = callbacks.afterTitle.apply(me, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeTitle); - lines = pushOrConcat(lines, title); - lines = pushOrConcat(lines, afterTitle); - - return lines; - }, - - // Args are: (tooltipItem, data) - getBeforeBody: function() { - var lines = this._options.callbacks.beforeBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Args are: (tooltipItem, data) - getBody: function(tooltipItems, data) { - var me = this; - var callbacks = me._options.callbacks; - var bodyItems = []; + return { + x: x, + y: y + }; +} + +var exports = module.exports = Element.extend({ + initialize: function() { + this._model = getBaseModel(this._options); + this._lastActive = []; + }, + + // Get the title + // Args are: (tooltipItem, data) + getTitle: function() { + var me = this; + var opts = me._options; + var callbacks = opts.callbacks; + + var beforeTitle = callbacks.beforeTitle.apply(me, arguments); + var title = callbacks.title.apply(me, arguments); + var afterTitle = callbacks.afterTitle.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, beforeTitle); + lines = pushOrConcat(lines, title); + lines = pushOrConcat(lines, afterTitle); + + return lines; + }, + + // Args are: (tooltipItem, data) + getBeforeBody: function() { + var lines = this._options.callbacks.beforeBody.apply(this, arguments); + return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + }, + + // Args are: (tooltipItem, data) + getBody: function(tooltipItems, data) { + var me = this; + var callbacks = me._options.callbacks; + var bodyItems = []; + + helpers.each(tooltipItems, function(tooltipItem) { + var bodyItem = { + before: [], + lines: [], + after: [] + }; + pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data)); - helpers.each(tooltipItems, function(tooltipItem) { - var bodyItem = { - before: [], - lines: [], - after: [] - }; - pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data)); - pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); - pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data)); + bodyItems.push(bodyItem); + }); - bodyItems.push(bodyItem); - }); + return bodyItems; + }, + + // Args are: (tooltipItem, data) + getAfterBody: function() { + var lines = this._options.callbacks.afterBody.apply(this, arguments); + return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + }, + + // Get the footer and beforeFooter and afterFooter lines + // Args are: (tooltipItem, data) + getFooter: function() { + var me = this; + var callbacks = me._options.callbacks; + + var beforeFooter = callbacks.beforeFooter.apply(me, arguments); + var footer = callbacks.footer.apply(me, arguments); + var afterFooter = callbacks.afterFooter.apply(me, arguments); + + var lines = []; + lines = pushOrConcat(lines, beforeFooter); + lines = pushOrConcat(lines, footer); + lines = pushOrConcat(lines, afterFooter); + + return lines; + }, + + update: function(changed) { + var me = this; + var opts = me._options; + + // 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; + + // 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 tooltipPosition = { + x: existingModel.caretX, + y: existingModel.caretY + }; - return bodyItems; - }, - - // Args are: (tooltipItem, data) - getAfterBody: function() { - var lines = this._options.callbacks.afterBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; - }, - - // Get the footer and beforeFooter and afterFooter lines - // Args are: (tooltipItem, data) - getFooter: function() { - var me = this; - var callbacks = me._options.callbacks; - - var beforeFooter = callbacks.beforeFooter.apply(me, arguments); - var footer = callbacks.footer.apply(me, arguments); - var afterFooter = callbacks.afterFooter.apply(me, arguments); - - var lines = []; - lines = pushOrConcat(lines, beforeFooter); - lines = pushOrConcat(lines, footer); - lines = pushOrConcat(lines, afterFooter); - - return lines; - }, - - update: function(changed) { - var me = this; - var opts = me._options; - - // 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; - - // 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 tooltipPosition = { - x: existingModel.caretX, - y: existingModel.caretY - }; + var i, len; - var i, len; + if (active.length) { + model.opacity = 1; - if (active.length) { - model.opacity = 1; + var labelColors = []; + var labelTextColors = []; + tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition); - var labelColors = []; - var labelTextColors = []; - tooltipPosition = Chart.Tooltip.positioners[opts.position].call(me, active, me._eventPosition); + var tooltipItems = []; + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(active[i])); + } - var tooltipItems = []; - for (i = 0, len = active.length; i < len; ++i) { - 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 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) { + return opts.itemSort(a, b, data); + }); + } - // If the user provided a sorting function, use it to modify the tooltip items - if (opts.itemSort) { - tooltipItems = tooltipItems.sort(function(a, b) { - return opts.itemSort(a, b, data); - }); - } + // Determine colors for boxes + helpers.each(tooltipItems, function(tooltipItem) { + labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); + labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); + }); - // Determine colors for boxes - helpers.each(tooltipItems, function(tooltipItem) { - labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart)); - labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); - }); + // Build the Text Lines + 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); + + // Initial positioning and colors + model.x = Math.round(tooltipPosition.x); + model.y = Math.round(tooltipPosition.y); + model.caretPadding = opts.caretPadding; + model.labelColors = labelColors; + model.labelTextColors = labelTextColors; + + // data points + model.dataPoints = tooltipItems; + + // 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, me._chart); + } else { + model.opacity = 0; + } - // Build the Text Lines - 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); - - // Initial positioning and colors - model.x = Math.round(tooltipPosition.x); - model.y = Math.round(tooltipPosition.y); - model.caretPadding = opts.caretPadding; - model.labelColors = labelColors; - model.labelTextColors = labelTextColors; - - // data points - model.dataPoints = tooltipItems; - - // 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, me._chart); - } else { - 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; - model.xAlign = alignment.xAlign; - model.yAlign = alignment.yAlign; - model.x = backgroundPoint.x; - model.y = backgroundPoint.y; - model.width = tooltipSize.width; - model.height = tooltipSize.height; + // Point where the caret on the tooltip points to + model.caretX = tooltipPosition.x; + model.caretY = tooltipPosition.y; - // Point where the caret on the tooltip points to - model.caretX = tooltipPosition.x; - model.caretY = tooltipPosition.y; + me._model = model; - me._model = model; + if (changed && opts.custom) { + opts.custom.call(me, model); + } - if (changed && opts.custom) { - opts.custom.call(me, model); - } + return me; + }, - return me; - }, - drawCaret: function(tooltipPoint, size) { - var ctx = this._chart.ctx; - var vm = this._view; - var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); - - ctx.lineTo(caretPosition.x1, caretPosition.y1); - ctx.lineTo(caretPosition.x2, caretPosition.y2); - ctx.lineTo(caretPosition.x3, caretPosition.y3); - }, - getCaretPosition: function(tooltipPoint, size, vm) { - var x1, x2, x3, y1, y2, y3; - var caretSize = vm.caretSize; - var cornerRadius = vm.cornerRadius; - var xAlign = vm.xAlign; - var yAlign = vm.yAlign; - var ptX = tooltipPoint.x; - var ptY = tooltipPoint.y; - var width = size.width; - var height = size.height; - - if (yAlign === 'center') { - y2 = ptY + (height / 2); - - if (xAlign === 'left') { - x1 = ptX; - x2 = x1 - caretSize; - x3 = x1; - - y1 = y2 + caretSize; - y3 = y2 - caretSize; - } else { - x1 = ptX + width; - x2 = x1 + caretSize; - x3 = x1; - - y1 = y2 - caretSize; - y3 = y2 + caretSize; - } - } else { - if (xAlign === 'left') { - x2 = ptX + cornerRadius + (caretSize); - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } else if (xAlign === 'right') { - x2 = ptX + width - cornerRadius - caretSize; - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } else { - x2 = vm.caretX; - x1 = x2 - caretSize; - x3 = x2 + caretSize; - } - if (yAlign === 'top') { - y1 = ptY; - y2 = y1 - caretSize; - y3 = y1; - } else { - y1 = ptY + height; - y2 = y1 + caretSize; - y3 = y1; - // invert drawing order - var tmp = x3; - x3 = x1; - x1 = tmp; - } - } - return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; - }, - drawTitle: function(pt, vm, ctx, opacity) { - var title = vm.title; + drawCaret: function(tooltipPoint, size) { + var ctx = this._chart.ctx; + var vm = this._view; + var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); - if (title.length) { - ctx.textAlign = vm._titleAlign; - ctx.textBaseline = 'top'; + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + }, + getCaretPosition: function(tooltipPoint, size, vm) { + var x1, x2, x3, y1, y2, y3; + var caretSize = vm.caretSize; + var cornerRadius = vm.cornerRadius; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var ptX = tooltipPoint.x; + var ptY = tooltipPoint.y; + var width = size.width; + var height = size.height; - var titleFontSize = vm.titleFontSize; - var titleSpacing = vm.titleSpacing; + if (yAlign === 'center') { + y2 = ptY + (height / 2); - ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); - ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + x3 = x1; - var i, len; - for (i = 0, len = title.length; i < len; ++i) { - ctx.fillText(title[i], pt.x, pt.y); - pt.y += titleFontSize + titleSpacing; // Line Height and spacing + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + x3 = x1; - if (i + 1 === title.length) { - pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing - } - } + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + } else { + if (xAlign === 'left') { + x2 = ptX + cornerRadius + (caretSize); + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else if (xAlign === 'right') { + x2 = ptX + width - cornerRadius - caretSize; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + x2 = vm.caretX; + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + y3 = y1; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + y3 = y1; + // invert drawing order + var tmp = x3; + x3 = x1; + x1 = tmp; } - }, - drawBody: function(pt, vm, ctx, opacity) { - var bodyFontSize = vm.bodyFontSize; - var bodySpacing = vm.bodySpacing; - var body = vm.body; + } + return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; + }, + + drawTitle: function(pt, vm, ctx, opacity) { + var title = vm.title; - ctx.textAlign = vm._bodyAlign; + if (title.length) { + ctx.textAlign = vm._titleAlign; ctx.textBaseline = 'top'; - ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - // Before Body - var xLinePadding = 0; - var fillLineOfText = function(line) { - ctx.fillText(line, pt.x + xLinePadding, pt.y); - pt.y += bodyFontSize + bodySpacing; - }; + var titleFontSize = vm.titleFontSize; + var titleSpacing = vm.titleSpacing; - // Before body lines - ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); - helpers.each(vm.beforeBody, fillLineOfText); - - var drawColorBoxes = vm.displayColors; - xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; - - // Draw body lines now - helpers.each(body, function(bodyItem, i) { - var textColor = mergeOpacity(vm.labelTextColors[i], opacity); - ctx.fillStyle = textColor; - helpers.each(bodyItem.before, fillLineOfText); - - helpers.each(bodyItem.lines, function(line) { - // Draw Legend-like boxes if needed - if (drawColorBoxes) { - // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); - ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); - - // Border - ctx.lineWidth = 1; - ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); - ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); - - // Inner square - ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); - ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); - ctx.fillStyle = textColor; - } + ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); + ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); - fillLineOfText(line); - }); + var i, len; + for (i = 0, len = title.length; i < len; ++i) { + ctx.fillText(title[i], pt.x, pt.y); + pt.y += titleFontSize + titleSpacing; // Line Height and spacing - helpers.each(bodyItem.after, fillLineOfText); - }); + if (i + 1 === title.length) { + pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing + } + } + } + }, + + drawBody: function(pt, vm, ctx, opacity) { + var bodyFontSize = vm.bodyFontSize; + var bodySpacing = vm.bodySpacing; + var body = vm.body; + + ctx.textAlign = vm._bodyAlign; + ctx.textBaseline = 'top'; + ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + + // Before Body + var xLinePadding = 0; + var fillLineOfText = function(line) { + ctx.fillText(line, pt.x + xLinePadding, pt.y); + pt.y += bodyFontSize + bodySpacing; + }; - // Reset back to 0 for after body - xLinePadding = 0; + // Before body lines + ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); + helpers.each(vm.beforeBody, fillLineOfText); + + var drawColorBoxes = vm.displayColors; + xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; + + // Draw body lines now + helpers.each(body, function(bodyItem, i) { + var textColor = mergeOpacity(vm.labelTextColors[i], opacity); + ctx.fillStyle = textColor; + helpers.each(bodyItem.before, fillLineOfText); + + helpers.each(bodyItem.lines, function(line) { + // Draw Legend-like boxes if needed + if (drawColorBoxes) { + // Fill a white rect so that colours merge nicely if the opacity is < 1 + ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); + ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + + // Border + ctx.lineWidth = 1; + ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); + ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + + // Inner square + ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); + ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + ctx.fillStyle = textColor; + } - // After body lines - helpers.each(vm.afterBody, fillLineOfText); - pt.y -= bodySpacing; // Remove last body spacing - }, - drawFooter: function(pt, vm, ctx, opacity) { - var footer = vm.footer; + fillLineOfText(line); + }); - if (footer.length) { - pt.y += vm.footerMarginTop; + helpers.each(bodyItem.after, fillLineOfText); + }); - ctx.textAlign = vm._footerAlign; - ctx.textBaseline = 'top'; + // Reset back to 0 for after body + xLinePadding = 0; - ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); - ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + // After body lines + helpers.each(vm.afterBody, fillLineOfText); + pt.y -= bodySpacing; // Remove last body spacing + }, - helpers.each(footer, function(line) { - ctx.fillText(line, pt.x, pt.y); - pt.y += vm.footerFontSize + vm.footerSpacing; - }); - } - }, - drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { - ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); - ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity); - ctx.lineWidth = vm.borderWidth; - var xAlign = vm.xAlign; - var yAlign = vm.yAlign; - var x = pt.x; - var y = pt.y; - var width = tooltipSize.width; - var height = tooltipSize.height; - var radius = vm.cornerRadius; - - ctx.beginPath(); - ctx.moveTo(x + radius, y); - if (yAlign === 'top') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - if (yAlign === 'center' && xAlign === 'right') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - if (yAlign === 'bottom') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - if (yAlign === 'center' && xAlign === 'left') { - this.drawCaret(pt, tooltipSize); - } - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); + drawFooter: function(pt, vm, ctx, opacity) { + var footer = vm.footer; - ctx.fill(); + if (footer.length) { + pt.y += vm.footerMarginTop; - if (vm.borderWidth > 0) { - ctx.stroke(); - } - }, - draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; + ctx.textAlign = vm._footerAlign; + ctx.textBaseline = 'top'; - if (vm.opacity === 0) { - return; - } + ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); + ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); - var tooltipSize = { - width: vm.width, - height: vm.height - }; - var pt = { - x: vm.x, - y: vm.y - }; + helpers.each(footer, function(line) { + ctx.fillText(line, pt.x, pt.y); + pt.y += vm.footerFontSize + vm.footerSpacing; + }); + } + }, + + drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { + ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); + ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity); + ctx.lineWidth = vm.borderWidth; + var xAlign = vm.xAlign; + var yAlign = vm.yAlign; + var x = pt.x; + var y = pt.y; + var width = tooltipSize.width; + var height = tooltipSize.height; + var radius = vm.cornerRadius; + + ctx.beginPath(); + ctx.moveTo(x + radius, y); + if (yAlign === 'top') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, tooltipSize); + } + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); - // IE11/Edge does not like very small opacities, so snap to 0 - var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; + ctx.fill(); - // Truthy/falsey value for empty tooltip - var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; + if (vm.borderWidth > 0) { + ctx.stroke(); + } + }, - if (this._options.enabled && hasTooltipContent) { - // Draw Background - this.drawBackground(pt, vm, ctx, tooltipSize, opacity); + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; - // Draw Title, Body, and Footer - pt.x += vm.xPadding; - pt.y += vm.yPadding; + if (vm.opacity === 0) { + return; + } - // Titles - this.drawTitle(pt, vm, ctx, opacity); + var tooltipSize = { + width: vm.width, + height: vm.height + }; + var pt = { + x: vm.x, + y: vm.y + }; - // Body - this.drawBody(pt, vm, ctx, opacity); + // IE11/Edge does not like very small opacities, so snap to 0 + var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; - // Footer - this.drawFooter(pt, vm, ctx, opacity); - } - }, - - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @returns {Boolean} true if the tooltip changed - */ - handleEvent: function(e) { - var me = this; - var options = me._options; - var changed = false; - - me._lastActive = me._lastActive || []; - - // Find Active Elements for tooltips - if (e.type === 'mouseout') { - me._active = []; - } else { - me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); - } + // Truthy/falsey value for empty tooltip + var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; - // Remember Last Actives - changed = !helpers.arrayEquals(me._active, me._lastActive); + if (this._options.enabled && hasTooltipContent) { + // Draw Background + this.drawBackground(pt, vm, ctx, tooltipSize, opacity); - // Only handle target event on tooltip change - if (changed) { - me._lastActive = me._active; + // Draw Title, Body, and Footer + pt.x += vm.xPadding; + pt.y += vm.yPadding; - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; + // Titles + this.drawTitle(pt, vm, ctx, opacity); - me.update(true); - me.pivot(); - } - } + // Body + this.drawBody(pt, vm, ctx, opacity); - return changed; + // Footer + this.drawFooter(pt, vm, ctx, opacity); } - }); + }, /** - * @namespace Chart.Tooltip.positioners + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @returns {Boolean} true if the tooltip changed */ - Chart.Tooltip.positioners = { - /** - * Average mode places the tooltip at the average position of the elements shown - * @function Chart.Tooltip.positioners.average - * @param elements {ChartElement[]} the elements being displayed in the tooltip - * @returns {Point} tooltip position - */ - average: function(elements) { - if (!elements.length) { - return false; - } + handleEvent: function(e) { + var me = this; + var options = me._options; + var changed = false; - var i, len; - var x = 0; - var y = 0; - var count = 0; - - for (i = 0, len = elements.length; i < len; ++i) { - var el = elements[i]; - if (el && el.hasValue()) { - var pos = el.tooltipPosition(); - x += pos.x; - y += pos.y; - ++count; - } - } + me._lastActive = me._lastActive || []; - return { - x: Math.round(x / count), - y: Math.round(y / count) - }; - }, - - /** - * Gets the tooltip position nearest of the item nearest to the event position - * @function Chart.Tooltip.positioners.nearest - * @param elements {Chart.Element[]} the tooltip elements - * @param eventPosition {Point} the position of the event in canvas coordinates - * @returns {Point} the tooltip position - */ - nearest: function(elements, eventPosition) { - var x = eventPosition.x; - var y = eventPosition.y; - var minDistance = Number.POSITIVE_INFINITY; - var i, len, nearestElement; - - for (i = 0, len = elements.length; i < len; ++i) { - var el = elements[i]; - if (el && el.hasValue()) { - var center = el.getCenterPoint(); - var d = helpers.distanceBetweenPoints(eventPosition, center); - - if (d < minDistance) { - minDistance = d; - nearestElement = el; - } - } - } + // Find Active Elements for tooltips + if (e.type === 'mouseout') { + me._active = []; + } else { + me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); + } - if (nearestElement) { - var tp = nearestElement.tooltipPosition(); - x = tp.x; - y = tp.y; - } + // Remember Last Actives + changed = !helpers.arrayEquals(me._active, me._lastActive); - return { - x: x, - y: y - }; + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; + + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; + + me.update(true); + me.pivot(); + } } - }; -}; + + return changed; + } +}); + +/** + * @namespace Chart.Tooltip.positioners + */ +exports.positioners = positioners; + diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index 0e859901434..a01284c1a0b 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -1,4 +1,3 @@ -// Test the bubble chart default config describe('Default Configs', function() { describe('Bubble Chart', function() { it('should return correct tooltip strings', function() { diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js new file mode 100644 index 00000000000..4126df5268e --- /dev/null +++ b/test/specs/global.namespace.tests.js @@ -0,0 +1,44 @@ +describe('Chart namespace', function() { + describe('Chart', function() { + it('should a function (constructor)', function() { + expect(Chart instanceof Function).toBeTruthy(); + }); + it('should define "core" properties', function() { + expect(Chart instanceof Function).toBeTruthy(); + expect(Chart.Animation instanceof Object).toBeTruthy(); + expect(Chart.animationService instanceof Object).toBeTruthy(); + expect(Chart.defaults instanceof Object).toBeTruthy(); + expect(Chart.Element instanceof Object).toBeTruthy(); + expect(Chart.Interaction instanceof Object).toBeTruthy(); + expect(Chart.layouts instanceof Object).toBeTruthy(); + expect(Chart.plugins instanceof Object).toBeTruthy(); + expect(Chart.platform instanceof Object).toBeTruthy(); + expect(Chart.Ticks instanceof Object).toBeTruthy(); + expect(Chart.Tooltip instanceof Object).toBeTruthy(); + expect(Chart.Tooltip.positioners instanceof Object).toBeTruthy(); + }); + }); + + describe('Chart.elements', function() { + it('should be an object', function() { + expect(Chart.elements instanceof Object).toBeTruthy(); + }); + it('should contains "elements" classes', function() { + expect(Chart.elements.Arc instanceof Function).toBeTruthy(); + expect(Chart.elements.Line instanceof Function).toBeTruthy(); + expect(Chart.elements.Point instanceof Function).toBeTruthy(); + expect(Chart.elements.Rectangle instanceof Function).toBeTruthy(); + }); + }); + + describe('Chart.helpers', function() { + it('should be an object', function() { + expect(Chart.helpers instanceof Object).toBeTruthy(); + }); + it('should contains "helpers" namespaces', function() { + expect(Chart.helpers.easing instanceof Object).toBeTruthy(); + expect(Chart.helpers.canvas instanceof Object).toBeTruthy(); + expect(Chart.helpers.options instanceof Object).toBeTruthy(); + }); + }); +}); From bee8e3cd9bbe943c531a24c808c36870da51a75f Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 2 Apr 2018 23:43:28 +0200 Subject: [PATCH 393/685] Make `Chart.Scale/scaleService` importable (#5383) --- src/chart.js | 6 +- src/core/core.controller.js | 5 +- src/core/core.helpers.js | 7 +- src/core/core.scale.js | 1506 +++++++++++++------------- src/core/core.scaleService.js | 71 +- src/scales/scale.category.js | 10 +- src/scales/scale.linear.js | 3 +- src/scales/scale.linearbase.js | 8 +- src/scales/scale.logarithmic.js | 8 +- src/scales/scale.radialLinear.js | 3 +- src/scales/scale.time.js | 14 +- test/specs/global.namespace.tests.js | 2 + 12 files changed, 825 insertions(+), 818 deletions(-) diff --git a/src/chart.js b/src/chart.js index 04d7df6c99a..1f6982ce47e 100644 --- a/src/chart.js +++ b/src/chart.js @@ -17,13 +17,13 @@ Chart.Interaction = require('./core/core.interaction'); Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); Chart.plugins = require('./core/core.plugins'); +Chart.Scale = require('./core/core.scale'); +Chart.scaleService = require('./core/core.scaleService'); Chart.Ticks = require('./core/core.ticks'); Chart.Tooltip = require('./core/core.tooltip'); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); -require('./core/core.scaleService')(Chart); -require('./core/core.scale')(Chart); require('./scales/scale.linearbase')(Chart); require('./scales/scale.category')(Chart); @@ -50,7 +50,7 @@ require('./charts/Chart.PolarArea')(Chart); require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); -// Loading built-it plugins +// Loading built-in plugins var plugins = require('./plugins'); for (var k in plugins) { if (plugins.hasOwnProperty(k)) { diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 8f7d04fc64d..d27967d3941 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -8,6 +8,7 @@ var Interaction = require('./core.interaction'); var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); var plugins = require('./core.plugins'); +var scaleService = require('../core/core.scaleService'); var Tooltip = require('./core.tooltip'); module.exports = function(Chart) { @@ -278,7 +279,7 @@ module.exports = function(Chart) { scale.ctx = me.ctx; scale.chart = me; } else { - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + var scaleClass = scaleService.getScaleConstructor(scaleType); if (!scaleClass) { return; } @@ -310,7 +311,7 @@ module.exports = function(Chart) { me.scales = scales; - Chart.scaleService.addScalesToLayout(this); + scaleService.addScalesToLayout(this); }, buildOrUpdateControllers: function() { diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index d49451d1846..6323e9ebffc 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -5,8 +5,9 @@ var color = require('chartjs-color'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); -module.exports = function(Chart) { +module.exports = function() { // -- Basic js utility methods @@ -21,7 +22,7 @@ module.exports = function(Chart) { target[key] = helpers.scaleMerge(tval, sval); } else if (key === 'scale') { // used in polar area & radar charts since there is only one scale - target[key] = helpers.merge(tval, [Chart.scaleService.getScaleDefaults(sval.type), sval]); + target[key] = helpers.merge(tval, [scaleService.getScaleDefaults(sval.type), sval]); } else { helpers._merger(key, target, source, options); } @@ -51,7 +52,7 @@ module.exports = function(Chart) { if (!target[key][i].type || (scale.type && scale.type !== target[key][i].type)) { // new/untyped scale or type changed: let's apply the new defaults // then merge source scale to correctly overwrite the defaults. - helpers.merge(target[key][i], [Chart.scaleService.getScaleDefaults(type), scale]); + helpers.merge(target[key][i], [scaleService.getScaleDefaults(type), scale]); } else { // scales type are the same helpers.merge(target[key][i], scale); diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 1996c6c894d..5ab6b8c090f 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -89,848 +89,846 @@ function getLineValue(scale, index, offsetGridLines) { return lineValue; } -module.exports = function(Chart) { +function computeTextSize(context, tick, font) { + return helpers.isArray(tick) ? + helpers.longestText(context, font, tick) : + context.measureText(tick).width; +} - function computeTextSize(context, tick, font) { - return helpers.isArray(tick) ? - helpers.longestText(context, font, tick) : - context.measureText(tick).width; - } +function parseFontOptions(options) { + var valueOrDefault = helpers.valueOrDefault; + var globalDefaults = defaults.global; + var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var style = valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); + var family = valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); + + return { + size: size, + style: style, + family: family, + font: helpers.fontString(size, style, family) + }; +} - function parseFontOptions(options) { - var valueOrDefault = helpers.valueOrDefault; - var globalDefaults = defaults.global; - var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); - var style = valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); - var family = valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); +function parseLineHeight(options) { + return helpers.options.toLineHeight( + helpers.valueOrDefault(options.lineHeight, 1.2), + helpers.valueOrDefault(options.fontSize, defaults.global.defaultFontSize)); +} +module.exports = Element.extend({ + /** + * Get the padding needed for the scale + * @method getPadding + * @private + * @returns {Padding} the necessary padding + */ + getPadding: function() { + var me = this; return { - size: size, - style: style, - family: family, - font: helpers.fontString(size, style, family) + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 }; - } + }, - function parseLineHeight(options) { - return helpers.options.toLineHeight( - helpers.valueOrDefault(options.lineHeight, 1.2), - helpers.valueOrDefault(options.fontSize, defaults.global.defaultFontSize)); - } + /** + * Returns the scale tick objects ({label, major}) + * @since 2.7 + */ + getTicks: function() { + return this._ticks; + }, - Chart.Scale = Element.extend({ - /** - * Get the padding needed for the scale - * @method getPadding - * @private - * @returns {Padding} the necessary padding - */ - getPadding: function() { - var me = this; - return { - left: me.paddingLeft || 0, - top: me.paddingTop || 0, - right: me.paddingRight || 0, - bottom: me.paddingBottom || 0 + // These methods are ordered by lifecyle. Utilities then follow. + // Any function defined here is inherited by all scale types. + // Any function can be extended by the scale type + + mergeTicksOptions: function() { + var ticks = this.options.ticks; + if (ticks.minor === false) { + ticks.minor = { + display: false }; - }, - - /** - * Returns the scale tick objects ({label, major}) - * @since 2.7 - */ - getTicks: function() { - return this._ticks; - }, - - // These methods are ordered by lifecyle. Utilities then follow. - // Any function defined here is inherited by all scale types. - // Any function can be extended by the scale type - - mergeTicksOptions: function() { - var ticks = this.options.ticks; - if (ticks.minor === false) { - ticks.minor = { - display: false - }; - } - if (ticks.major === false) { - ticks.major = { - display: false - }; - } - for (var key in ticks) { - if (key !== 'major' && key !== 'minor') { - if (typeof ticks.minor[key] === 'undefined') { - ticks.minor[key] = ticks[key]; - } - if (typeof ticks.major[key] === 'undefined') { - ticks.major[key] = ticks[key]; - } + } + if (ticks.major === false) { + ticks.major = { + display: false + }; + } + for (var key in ticks) { + if (key !== 'major' && key !== 'minor') { + if (typeof ticks.minor[key] === 'undefined') { + ticks.minor[key] = ticks[key]; } - } - }, - beforeUpdate: function() { - helpers.callback(this.options.beforeUpdate, [this]); - }, - update: function(maxWidth, maxHeight, margins) { - var me = this; - var i, ilen, labels, label, ticks, tick; - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = helpers.extend({ - left: 0, - right: 0, - top: 0, - bottom: 0 - }, margins); - me.longestTextCache = me.longestTextCache || {}; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - - // Data min/max - me.beforeDataLimits(); - me.determineDataLimits(); - me.afterDataLimits(); - - // Ticks - `this.ticks` is now DEPRECATED! - // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member - // and must not be accessed directly from outside this class. `this.ticks` being - // around for long time and not marked as private, we can't change its structure - // without unexpected breaking changes. If you need to access the scale ticks, - // use scale.getTicks() instead. - - me.beforeBuildTicks(); - - // New implementations should return an array of objects but for BACKWARD COMPAT, - // we still support no return (`this.ticks` internally set by calling this method). - ticks = me.buildTicks() || []; - - me.afterBuildTicks(); - - me.beforeTickToLabelConversion(); - - // New implementations should return the formatted tick labels but for BACKWARD - // COMPAT, we still support no return (`this.ticks` internally changed by calling - // this method and supposed to contain only string values). - labels = me.convertTicksToLabels(ticks) || me.ticks; - - me.afterTickToLabelConversion(); - - me.ticks = labels; // BACKWARD COMPATIBILITY - - // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change! - - // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) - for (i = 0, ilen = labels.length; i < ilen; ++i) { - label = labels[i]; - tick = ticks[i]; - if (!tick) { - ticks.push(tick = { - label: label, - major: false - }); - } else { - tick.label = label; + if (typeof ticks.major[key] === 'undefined') { + ticks.major[key] = ticks[key]; } } + } + }, + beforeUpdate: function() { + helpers.callback(this.options.beforeUpdate, [this]); + }, - me._ticks = ticks; + update: function(maxWidth, maxHeight, margins) { + var me = this; + var i, ilen, labels, label, ticks, tick; - // Tick Rotation - me.beforeCalculateTickRotation(); - me.calculateTickRotation(); - me.afterCalculateTickRotation(); - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); - return me.minSize; + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = helpers.extend({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + me.longestTextCache = me.longestTextCache || {}; - }, - afterUpdate: function() { - helpers.callback(this.options.afterUpdate, [this]); - }, + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); - // + // Data min/max + me.beforeDataLimits(); + me.determineDataLimits(); + me.afterDataLimits(); - beforeSetDimensions: function() { - helpers.callback(this.options.beforeSetDimensions, [this]); - }, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + // Ticks - `this.ticks` is now DEPRECATED! + // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member + // and must not be accessed directly from outside this class. `this.ticks` being + // around for long time and not marked as private, we can't change its structure + // without unexpected breaking changes. If you need to access the scale ticks, + // use scale.getTicks() instead. - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; + me.beforeBuildTicks(); + + // New implementations should return an array of objects but for BACKWARD COMPAT, + // we still support no return (`this.ticks` internally set by calling this method). + ticks = me.buildTicks() || []; + + me.afterBuildTicks(); + + me.beforeTickToLabelConversion(); + + // New implementations should return the formatted tick labels but for BACKWARD + // COMPAT, we still support no return (`this.ticks` internally changed by calling + // this method and supposed to contain only string values). + labels = me.convertTicksToLabels(ticks) || me.ticks; + + me.afterTickToLabelConversion(); + + me.ticks = labels; // BACKWARD COMPATIBILITY + + // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change! + + // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`) + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + tick = ticks[i]; + if (!tick) { + ticks.push(tick = { + label: label, + major: false + }); + } else { + tick.label = label; } + } - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; - }, - afterSetDimensions: function() { - helpers.callback(this.options.afterSetDimensions, [this]); - }, - - // Data limits - beforeDataLimits: function() { - helpers.callback(this.options.beforeDataLimits, [this]); - }, - determineDataLimits: helpers.noop, - afterDataLimits: function() { - helpers.callback(this.options.afterDataLimits, [this]); - }, + me._ticks = ticks; + // Tick Rotation + me.beforeCalculateTickRotation(); + me.calculateTickRotation(); + me.afterCalculateTickRotation(); + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // - beforeBuildTicks: function() { - helpers.callback(this.options.beforeBuildTicks, [this]); - }, - buildTicks: helpers.noop, - afterBuildTicks: function() { - helpers.callback(this.options.afterBuildTicks, [this]); - }, - - beforeTickToLabelConversion: function() { - helpers.callback(this.options.beforeTickToLabelConversion, [this]); - }, - convertTicksToLabels: function() { - var me = this; - // Convert ticks to strings - var tickOpts = me.options.ticks; - me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this); - }, - afterTickToLabelConversion: function() { - helpers.callback(this.options.afterTickToLabelConversion, [this]); - }, + me.afterUpdate(); - // + return me.minSize; - beforeCalculateTickRotation: function() { - helpers.callback(this.options.beforeCalculateTickRotation, [this]); - }, - calculateTickRotation: function() { - var me = this; - var context = me.ctx; - var tickOpts = me.options.ticks; - var labels = labelsFromTicks(me._ticks); - - // Get the width of each grid by calculating the difference - // between x offsets between 0 and 1. - var tickFont = parseFontOptions(tickOpts); - context.font = tickFont.font; - - var labelRotation = tickOpts.minRotation || 0; - - if (labels.length && me.options.display && me.isHorizontal()) { - var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation, sinRotation; - - // Allow 3 pixels x2 padding either side for label readability - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; - - // Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { - var angleRadians = helpers.toRadians(labelRotation); - cosRotation = Math.cos(angleRadians); - sinRotation = Math.sin(angleRadians); - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - labelRotation--; - break; - } + }, + afterUpdate: function() { + helpers.callback(this.options.afterUpdate, [this]); + }, + + // + + beforeSetDimensions: function() { + helpers.callback(this.options.beforeSetDimensions, [this]); + }, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } + + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + }, + afterSetDimensions: function() { + helpers.callback(this.options.afterSetDimensions, [this]); + }, + + // Data limits + beforeDataLimits: function() { + helpers.callback(this.options.beforeDataLimits, [this]); + }, + determineDataLimits: helpers.noop, + afterDataLimits: function() { + helpers.callback(this.options.afterDataLimits, [this]); + }, + + // + beforeBuildTicks: function() { + helpers.callback(this.options.beforeBuildTicks, [this]); + }, + buildTicks: helpers.noop, + afterBuildTicks: function() { + helpers.callback(this.options.afterBuildTicks, [this]); + }, + + beforeTickToLabelConversion: function() { + helpers.callback(this.options.beforeTickToLabelConversion, [this]); + }, + convertTicksToLabels: function() { + var me = this; + // Convert ticks to strings + var tickOpts = me.options.ticks; + me.ticks = me.ticks.map(tickOpts.userCallback || tickOpts.callback, this); + }, + afterTickToLabelConversion: function() { + helpers.callback(this.options.afterTickToLabelConversion, [this]); + }, - labelRotation++; - labelWidth = cosRotation * originalLabelWidth; + // + + beforeCalculateTickRotation: function() { + helpers.callback(this.options.beforeCalculateTickRotation, [this]); + }, + calculateTickRotation: function() { + var me = this; + var context = me.ctx; + var tickOpts = me.options.ticks; + var labels = labelsFromTicks(me._ticks); + + // Get the width of each grid by calculating the difference + // between x offsets between 0 and 1. + var tickFont = parseFontOptions(tickOpts); + context.font = tickFont.font; + + var labelRotation = tickOpts.minRotation || 0; + + if (labels.length && me.options.display && me.isHorizontal()) { + var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache); + var labelWidth = originalLabelWidth; + var cosRotation, sinRotation; + + // Allow 3 pixels x2 padding either side for label readability + var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; + + // Max label rotation can be set or default to 90 - also act as a loop counter + while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); + + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; } + + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; } + } - me.labelRotation = labelRotation; - }, - afterCalculateTickRotation: function() { - helpers.callback(this.options.afterCalculateTickRotation, [this]); - }, + me.labelRotation = labelRotation; + }, + afterCalculateTickRotation: function() { + helpers.callback(this.options.afterCalculateTickRotation, [this]); + }, - // + // - beforeFit: function() { - helpers.callback(this.options.beforeFit, [this]); - }, - fit: function() { - var me = this; - // Reset - var minSize = me.minSize = { - width: 0, - height: 0 - }; + beforeFit: function() { + helpers.callback(this.options.beforeFit, [this]); + }, + fit: function() { + var me = this; + // Reset + var minSize = me.minSize = { + width: 0, + height: 0 + }; - var labels = labelsFromTicks(me._ticks); + var labels = labelsFromTicks(me._ticks); - var opts = me.options; - var tickOpts = opts.ticks; - var scaleLabelOpts = opts.scaleLabel; - var gridLineOpts = opts.gridLines; - var display = opts.display; - var isHorizontal = me.isHorizontal(); + var opts = me.options; + var tickOpts = opts.ticks; + var scaleLabelOpts = opts.scaleLabel; + var gridLineOpts = opts.gridLines; + var display = opts.display; + var isHorizontal = me.isHorizontal(); - var tickFont = parseFontOptions(tickOpts); - var tickMarkLength = opts.gridLines.tickMarkLength; + var tickFont = parseFontOptions(tickOpts); + var tickMarkLength = opts.gridLines.tickMarkLength; - // Width - if (isHorizontal) { - // subtract the margins to line up with the chartArea if we are a full width scale - minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; - } else { - minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; - } + // Width + if (isHorizontal) { + // subtract the margins to line up with the chartArea if we are a full width scale + minSize.width = me.isFullWidth() ? me.maxWidth - me.margins.left - me.margins.right : me.maxWidth; + } else { + minSize.width = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } + + // height + if (isHorizontal) { + minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + } else { + minSize.height = me.maxHeight; // fill all the height + } + + // Are we showing a title for the scale? + if (scaleLabelOpts.display && display) { + var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); + var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); + var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; - // height if (isHorizontal) { - minSize.height = display && gridLineOpts.drawTicks ? tickMarkLength : 0; + minSize.height += deltaHeight; } else { - minSize.height = me.maxHeight; // fill all the height + minSize.width += deltaHeight; } + } - // Are we showing a title for the scale? - if (scaleLabelOpts.display && display) { - var scaleLabelLineHeight = parseLineHeight(scaleLabelOpts); - var scaleLabelPadding = helpers.options.toPadding(scaleLabelOpts.padding); - var deltaHeight = scaleLabelLineHeight + scaleLabelPadding.height; + // Don't bother fitting the ticks if we are not showing them + if (tickOpts.display && display) { + var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache); + var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); + var lineSpace = tickFont.size * 0.5; + var tickPadding = me.options.ticks.padding; - if (isHorizontal) { - minSize.height += deltaHeight; + if (isHorizontal) { + // A horizontal axis is more constrained by the height. + me.longestLabelWidth = largestTextWidth; + + var angleRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + + // TODO - improve this calculation + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.size * tallestLabelHeightInLines) + + (lineSpace * (tallestLabelHeightInLines - 1)) + + lineSpace; // padding + + minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); + + me.ctx.font = tickFont.font; + var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); + var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); + + // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned + // which means that the right padding is dominated by the font height + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom' ? (cosRotation * firstLabelWidth) + 3 : (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom' ? (cosRotation * lineSpace) + 3 : (cosRotation * lastLabelWidth) + 3; } else { - minSize.width += deltaHeight; + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; } - } - - // Don't bother fitting the ticks if we are not showing them - if (tickOpts.display && display) { - var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache); - var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); - var lineSpace = tickFont.size * 0.5; - var tickPadding = me.options.ticks.padding; - - if (isHorizontal) { - // A horizontal axis is more constrained by the height. - me.longestLabelWidth = largestTextWidth; - - var angleRadians = helpers.toRadians(me.labelRotation); - var cosRotation = Math.cos(angleRadians); - var sinRotation = Math.sin(angleRadians); - - // TODO - improve this calculation - var labelHeight = (sinRotation * largestTextWidth) - + (tickFont.size * tallestLabelHeightInLines) - + (lineSpace * (tallestLabelHeightInLines - 1)) - + lineSpace; // padding - - minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding); - - me.ctx.font = tickFont.font; - var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font); - var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font); - - // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned - // which means that the right padding is dominated by the font height - if (me.labelRotation !== 0) { - me.paddingLeft = opts.position === 'bottom' ? (cosRotation * firstLabelWidth) + 3 : (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges - me.paddingRight = opts.position === 'bottom' ? (cosRotation * lineSpace) + 3 : (cosRotation * lastLabelWidth) + 3; - } else { - me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = lastLabelWidth / 2 + 3; - } + } else { + // A vertical axis is more constrained by the width. Labels are the + // dominant factor here, so get that length first and account for padding + if (tickOpts.mirror) { + largestTextWidth = 0; } else { - // A vertical axis is more constrained by the width. Labels are the - // dominant factor here, so get that length first and account for padding - if (tickOpts.mirror) { - largestTextWidth = 0; - } else { - // use lineSpace for consistency with horizontal axis - // tickPadding is not implemented for horizontal - largestTextWidth += tickPadding + lineSpace; - } + // use lineSpace for consistency with horizontal axis + // tickPadding is not implemented for horizontal + largestTextWidth += tickPadding + lineSpace; + } - minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); + minSize.width = Math.min(me.maxWidth, minSize.width + largestTextWidth); - me.paddingTop = tickFont.size / 2; - me.paddingBottom = tickFont.size / 2; - } + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; } + } - me.handleMargins(); - - me.width = minSize.width; - me.height = minSize.height; - }, - - /** - * Handle margins and padding interactions - * @private - */ - handleMargins: function() { - var me = this; - if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); - } - }, - - afterFit: function() { - helpers.callback(this.options.afterFit, [this]); - }, - - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, - isFullWidth: function() { - return (this.options.fullWidth); - }, - - // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function(rawValue) { - // Null and undefined values first - if (helpers.isNullOrUndef(rawValue)) { - return NaN; - } - // 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 - if (rawValue) { - if (this.isHorizontal()) { - if (rawValue.x !== undefined) { - return this.getRightValue(rawValue.x); - } - } else if (rawValue.y !== undefined) { - return this.getRightValue(rawValue.y); + me.handleMargins(); + + me.width = minSize.width; + me.height = minSize.height; + }, + + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; + if (me.margins) { + me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); + me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); + me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); + me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); + } + }, + + afterFit: function() { + helpers.callback(this.options.afterFit, [this]); + }, + + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, + isFullWidth: function() { + return (this.options.fullWidth); + }, + + // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not + getRightValue: function(rawValue) { + // Null and undefined values first + if (helpers.isNullOrUndef(rawValue)) { + return NaN; + } + // 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 + if (rawValue) { + if (this.isHorizontal()) { + if (rawValue.x !== undefined) { + return this.getRightValue(rawValue.x); } + } else if (rawValue.y !== undefined) { + return this.getRightValue(rawValue.y); } + } - // Value is good, return it - return rawValue; - }, - - /** - * Used to get the value to display in the tooltip for the data at the given index - * @param index - * @param datasetIndex - */ - getLabelForIndex: helpers.noop, - - /** - * Returns the location of the given data point. Value can either be an index or a numerical value - * The coordinate (0, 0) is at the upper-left corner of the canvas - * @param value - * @param index - * @param datasetIndex - */ - getPixelForValue: helpers.noop, - - /** - * Used to get the data value from a given pixel. This is the inverse of getPixelForValue - * The coordinate (0, 0) is at the upper-left corner of the canvas - * @param pixel - */ - getValueForPixel: helpers.noop, - - /** - * Returns the location of the tick at the given index - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getPixelForTick: function(index) { - var me = this; - var offset = me.options.offset; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var pixel = (tickWidth * index) + me.paddingLeft; - - if (offset) { - pixel += tickWidth / 2; - } + // Value is good, return it + return rawValue; + }, - var finalVal = me.left + Math.round(pixel); - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - return me.top + (index * (innerHeight / (me._ticks.length - 1))); - }, - - /** - * Utility for getting the pixel location of a percentage of scale - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getPixelForDecimal: function(decimal) { - var me = this; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - - var finalVal = me.left + Math.round(valueOffset); - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - return me.top + (decimal * me.height); - }, - - /** - * Returns the pixel for the minimum chart value - * The coordinate (0, 0) is at the upper-left corner of the canvas - */ - getBasePixel: function() { - return this.getPixelForValue(this.getBaseValue()); - }, - - getBaseValue: function() { - var me = this; - var min = me.min; - var max = me.max; - - return me.beginAtZero ? 0 : - min < 0 && max < 0 ? max : - min > 0 && max > 0 ? min : - 0; - }, - - /** - * Returns a subset of ticks to be plotted to avoid overlapping labels. - * @private - */ - _autoSkip: function(ticks) { - var skipRatio; - var me = this; - var isHorizontal = me.isHorizontal(); - var optionTicks = me.options.ticks.minor; - var tickCount = ticks.length; - var labelRotationRadians = helpers.toRadians(me.labelRotation); - var cosRotation = Math.cos(labelRotationRadians); - var longestRotatedLabel = me.longestLabelWidth * cosRotation; - var result = []; - var i, tick, shouldSkip; - - // figure out the maximum number of gridlines to show - var maxTicks; - if (optionTicks.maxTicksLimit) { - maxTicks = optionTicks.maxTicksLimit; + /** + * Used to get the value to display in the tooltip for the data at the given index + * @param index + * @param datasetIndex + */ + getLabelForIndex: helpers.noop, + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param value + * @param index + * @param datasetIndex + */ + getPixelForValue: helpers.noop, + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param pixel + */ + getValueForPixel: helpers.noop, + + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForTick: function(index) { + var me = this; + var offset = me.options.offset; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); + var pixel = (tickWidth * index) + me.paddingLeft; + + if (offset) { + pixel += tickWidth / 2; } - if (isHorizontal) { - skipRatio = false; + var finalVal = me.left + Math.round(pixel); + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + var innerHeight = me.height - (me.paddingTop + me.paddingBottom); + return me.top + (index * (innerHeight / (me._ticks.length - 1))); + }, - if ((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount > (me.width - (me.paddingLeft + me.paddingRight))) { - skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount) / (me.width - (me.paddingLeft + me.paddingRight))); - } + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getPixelForDecimal: function(decimal) { + var me = this; + if (me.isHorizontal()) { + var innerWidth = me.width - (me.paddingLeft + me.paddingRight); + var valueOffset = (innerWidth * decimal) + me.paddingLeft; + + var finalVal = me.left + Math.round(valueOffset); + finalVal += me.isFullWidth() ? me.margins.left : 0; + return finalVal; + } + return me.top + (decimal * me.height); + }, - // if they defined a max number of optionTicks, - // increase skipRatio until that number is met - if (maxTicks && tickCount > maxTicks) { - skipRatio = Math.max(skipRatio, Math.floor(tickCount / maxTicks)); - } - } + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + */ + getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, - for (i = 0; i < tickCount; i++) { - tick = ticks[i]; + getBaseValue: function() { + var me = this; + var min = me.min; + var max = me.max; - // Since we always show the last tick,we need may need to hide the last shown one before - shouldSkip = (skipRatio > 1 && i % skipRatio > 0) || (i % skipRatio === 0 && i + skipRatio >= tickCount); - if (shouldSkip && i !== tickCount - 1) { - // leave tick in place but make sure it's not displayed (#4635) - delete tick.label; - } - result.push(tick); + return me.beginAtZero ? 0 : + min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + }, + + /** + * Returns a subset of ticks to be plotted to avoid overlapping labels. + * @private + */ + _autoSkip: function(ticks) { + var skipRatio; + var me = this; + var isHorizontal = me.isHorizontal(); + var optionTicks = me.options.ticks.minor; + var tickCount = ticks.length; + var labelRotationRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(labelRotationRadians); + var longestRotatedLabel = me.longestLabelWidth * cosRotation; + var result = []; + var i, tick, shouldSkip; + + // figure out the maximum number of gridlines to show + var maxTicks; + if (optionTicks.maxTicksLimit) { + maxTicks = optionTicks.maxTicksLimit; + } + + if (isHorizontal) { + skipRatio = false; + + if ((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount > (me.width - (me.paddingLeft + me.paddingRight))) { + skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount) / (me.width - (me.paddingLeft + me.paddingRight))); } - return result; - }, - - // Actually draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on - draw: function(chartArea) { - var me = this; - var options = me.options; - if (!options.display) { - return; + + // if they defined a max number of optionTicks, + // increase skipRatio until that number is met + if (maxTicks && tickCount > maxTicks) { + skipRatio = Math.max(skipRatio, Math.floor(tickCount / maxTicks)); } + } - var context = me.ctx; - var globalDefaults = defaults.global; - var optionTicks = options.ticks.minor; - var optionMajorTicks = options.ticks.major || optionTicks; - var gridLines = options.gridLines; - var scaleLabel = options.scaleLabel; - - var isRotated = me.labelRotation !== 0; - var isHorizontal = me.isHorizontal(); - - var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); - var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFont = parseFontOptions(optionTicks); - var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); - var majorTickFont = parseFontOptions(optionMajorTicks); - - var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; - - var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFont = parseFontOptions(scaleLabel); - var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); - var labelRotationRadians = helpers.toRadians(me.labelRotation); - - var itemsToDraw = []; - - var axisWidth = me.options.gridLines.lineWidth; - var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; - var xTickEnd = options.position === 'right' ? me.right + tl : me.right; - var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; - var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; - - helpers.each(ticks, function(tick, index) { - // autoskipper skipped this tick (#4635) - if (helpers.isNullOrUndef(tick.label)) { - return; - } + for (i = 0; i < tickCount; i++) { + tick = ticks[i]; - var label = tick.label; - var lineWidth, lineColor, borderDash, borderDashOffset; - if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { - // Draw the first index specially - lineWidth = gridLines.zeroLineWidth; - lineColor = gridLines.zeroLineColor; - borderDash = gridLines.zeroLineBorderDash; - borderDashOffset = gridLines.zeroLineBorderDashOffset; - } else { - lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, index); - lineColor = helpers.valueAtIndexOrDefault(gridLines.color, index); - borderDash = helpers.valueOrDefault(gridLines.borderDash, globalDefaults.borderDash); - borderDashOffset = helpers.valueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); - } + // Since we always show the last tick,we need may need to hide the last shown one before + shouldSkip = (skipRatio > 1 && i % skipRatio > 0) || (i % skipRatio === 0 && i + skipRatio >= tickCount); + if (shouldSkip && i !== tickCount - 1) { + // leave tick in place but make sure it's not displayed (#4635) + delete tick.label; + } + result.push(tick); + } + return result; + }, - // Common properties - var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; - var textAlign = 'middle'; - var textBaseline = 'middle'; - var tickPadding = optionTicks.padding; - - if (isHorizontal) { - var labelYOffset = tl + tickPadding; - - if (options.position === 'bottom') { - // bottom - textBaseline = !isRotated ? 'top' : 'middle'; - textAlign = !isRotated ? 'center' : 'right'; - labelY = me.top + labelYOffset; - } else { - // top - textBaseline = !isRotated ? 'bottom' : 'middle'; - textAlign = !isRotated ? 'center' : 'left'; - labelY = me.bottom - labelYOffset; - } + // Actually draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + var me = this; + var options = me.options; + if (!options.display) { + return; + } - var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (xLineValue < me.left) { - lineColor = 'rgba(0,0,0,0)'; - } - xLineValue += helpers.aliasPixel(lineWidth); + var context = me.ctx; + var globalDefaults = defaults.global; + var optionTicks = options.ticks.minor; + var optionMajorTicks = options.ticks.major || optionTicks; + var gridLines = options.gridLines; + var scaleLabel = options.scaleLabel; + + var isRotated = me.labelRotation !== 0; + var isHorizontal = me.isHorizontal(); + + var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); + var tickFontColor = helpers.valueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); + var tickFont = parseFontOptions(optionTicks); + var majorTickFontColor = helpers.valueOrDefault(optionMajorTicks.fontColor, globalDefaults.defaultFontColor); + var majorTickFont = parseFontOptions(optionMajorTicks); + + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; + + var scaleLabelFontColor = helpers.valueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); + var scaleLabelFont = parseFontOptions(scaleLabel); + var scaleLabelPadding = helpers.options.toPadding(scaleLabel.padding); + var labelRotationRadians = helpers.toRadians(me.labelRotation); + + var itemsToDraw = []; + + var axisWidth = me.options.gridLines.lineWidth; + var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.right + tl : me.right; + var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; + var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; + + helpers.each(ticks, function(tick, index) { + // autoskipper skipped this tick (#4635) + if (helpers.isNullOrUndef(tick.label)) { + return; + } - labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) + var label = tick.label; + var lineWidth, lineColor, borderDash, borderDashOffset; + if (index === me.zeroLineIndex && options.offset === gridLines.offsetGridLines) { + // Draw the first index specially + lineWidth = gridLines.zeroLineWidth; + lineColor = gridLines.zeroLineColor; + borderDash = gridLines.zeroLineBorderDash; + borderDashOffset = gridLines.zeroLineBorderDashOffset; + } else { + lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, index); + lineColor = helpers.valueAtIndexOrDefault(gridLines.color, index); + borderDash = helpers.valueOrDefault(gridLines.borderDash, globalDefaults.borderDash); + borderDashOffset = helpers.valueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); + } + + // Common properties + var tx1, ty1, tx2, ty2, x1, y1, x2, y2, labelX, labelY; + var textAlign = 'middle'; + var textBaseline = 'middle'; + var tickPadding = optionTicks.padding; + + if (isHorizontal) { + var labelYOffset = tl + tickPadding; - tx1 = tx2 = x1 = x2 = xLineValue; - ty1 = yTickStart; - ty2 = yTickEnd; - y1 = chartArea.top; - y2 = chartArea.bottom + axisWidth; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated ? 'top' : 'middle'; + textAlign = !isRotated ? 'center' : 'right'; + labelY = me.top + labelYOffset; } else { - var isLeft = options.position === 'left'; - var labelXOffset; - - if (optionTicks.mirror) { - textAlign = isLeft ? 'left' : 'right'; - labelXOffset = tickPadding; - } else { - textAlign = isLeft ? 'right' : 'left'; - labelXOffset = tl + tickPadding; - } + // top + textBaseline = !isRotated ? 'bottom' : 'middle'; + textAlign = !isRotated ? 'center' : 'left'; + labelY = me.bottom - labelYOffset; + } - labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; + var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + if (xLineValue < me.left) { + lineColor = 'rgba(0,0,0,0)'; + } + xLineValue += helpers.aliasPixel(lineWidth); - var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (yLineValue < me.top) { - lineColor = 'rgba(0,0,0,0)'; - } - yLineValue += helpers.aliasPixel(lineWidth); + labelX = me.getPixelForTick(index) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = me.getPixelForTick(index) + optionTicks.labelOffset; + tx1 = tx2 = x1 = x2 = xLineValue; + ty1 = yTickStart; + ty2 = yTickEnd; + y1 = chartArea.top; + y2 = chartArea.bottom + axisWidth; + } else { + var isLeft = options.position === 'left'; + var labelXOffset; - tx1 = xTickStart; - tx2 = xTickEnd; - x1 = chartArea.left; - x2 = chartArea.right + axisWidth; - ty1 = ty2 = y1 = y2 = yLineValue; + if (optionTicks.mirror) { + textAlign = isLeft ? 'left' : 'right'; + labelXOffset = tickPadding; + } else { + textAlign = isLeft ? 'right' : 'left'; + labelXOffset = tl + tickPadding; } - itemsToDraw.push({ - tx1: tx1, - ty1: ty1, - tx2: tx2, - ty2: ty2, - x1: x1, - y1: y1, - x2: x2, - y2: y2, - labelX: labelX, - labelY: labelY, - glWidth: lineWidth, - glColor: lineColor, - glBorderDash: borderDash, - glBorderDashOffset: borderDashOffset, - rotation: -1 * labelRotationRadians, - label: label, - major: tick.major, - textBaseline: textBaseline, - textAlign: textAlign - }); - }); + labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; - // Draw all of the tick labels, tick marks, and grid lines at the correct places - helpers.each(itemsToDraw, function(itemToDraw) { - if (gridLines.display) { - context.save(); - context.lineWidth = itemToDraw.glWidth; - context.strokeStyle = itemToDraw.glColor; - if (context.setLineDash) { - context.setLineDash(itemToDraw.glBorderDash); - context.lineDashOffset = itemToDraw.glBorderDashOffset; - } + var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + if (yLineValue < me.top) { + lineColor = 'rgba(0,0,0,0)'; + } + yLineValue += helpers.aliasPixel(lineWidth); - context.beginPath(); + labelY = me.getPixelForTick(index) + optionTicks.labelOffset; - if (gridLines.drawTicks) { - context.moveTo(itemToDraw.tx1, itemToDraw.ty1); - context.lineTo(itemToDraw.tx2, itemToDraw.ty2); - } + tx1 = xTickStart; + tx2 = xTickEnd; + x1 = chartArea.left; + x2 = chartArea.right + axisWidth; + ty1 = ty2 = y1 = y2 = yLineValue; + } - if (gridLines.drawOnChartArea) { - context.moveTo(itemToDraw.x1, itemToDraw.y1); - context.lineTo(itemToDraw.x2, itemToDraw.y2); - } + itemsToDraw.push({ + tx1: tx1, + ty1: ty1, + tx2: tx2, + ty2: ty2, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + labelX: labelX, + labelY: labelY, + glWidth: lineWidth, + glColor: lineColor, + glBorderDash: borderDash, + glBorderDashOffset: borderDashOffset, + rotation: -1 * labelRotationRadians, + label: label, + major: tick.major, + textBaseline: textBaseline, + textAlign: textAlign + }); + }); - context.stroke(); - context.restore(); + // Draw all of the tick labels, tick marks, and grid lines at the correct places + helpers.each(itemsToDraw, function(itemToDraw) { + if (gridLines.display) { + context.save(); + context.lineWidth = itemToDraw.glWidth; + context.strokeStyle = itemToDraw.glColor; + if (context.setLineDash) { + context.setLineDash(itemToDraw.glBorderDash); + context.lineDashOffset = itemToDraw.glBorderDashOffset; } - if (optionTicks.display) { - // Make sure we draw text in the correct color and font - context.save(); - context.translate(itemToDraw.labelX, itemToDraw.labelY); - context.rotate(itemToDraw.rotation); - context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; - context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; - context.textBaseline = itemToDraw.textBaseline; - context.textAlign = itemToDraw.textAlign; - - var label = itemToDraw.label; - if (helpers.isArray(label)) { - var lineCount = label.length; - var lineHeight = tickFont.size * 1.5; - var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; - - for (var i = 0; i < lineCount; ++i) { - // We just make sure the multiline element is a string here.. - context.fillText('' + label[i], 0, y); - // apply same lineSpacing as calculated @ L#320 - y += lineHeight; - } - } else { - context.fillText(label, 0, 0); - } - context.restore(); + context.beginPath(); + + if (gridLines.drawTicks) { + context.moveTo(itemToDraw.tx1, itemToDraw.ty1); + context.lineTo(itemToDraw.tx2, itemToDraw.ty2); } - }); - if (scaleLabel.display) { - // Draw the scale label - var scaleLabelX; - var scaleLabelY; - var rotation = 0; - var halfLineHeight = parseLineHeight(scaleLabel) / 2; - - if (isHorizontal) { - scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' - ? me.bottom - halfLineHeight - scaleLabelPadding.bottom - : me.top + halfLineHeight + scaleLabelPadding.top; - } else { - var isLeft = options.position === 'left'; - scaleLabelX = isLeft - ? me.left + halfLineHeight + scaleLabelPadding.top - : me.right - halfLineHeight - scaleLabelPadding.top; - scaleLabelY = me.top + ((me.bottom - me.top) / 2); - rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; + if (gridLines.drawOnChartArea) { + context.moveTo(itemToDraw.x1, itemToDraw.y1); + context.lineTo(itemToDraw.x2, itemToDraw.y2); } - context.save(); - context.translate(scaleLabelX, scaleLabelY); - context.rotate(rotation); - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont.font; - context.fillText(scaleLabel.labelString, 0, 0); + context.stroke(); context.restore(); } - if (gridLines.drawBorder) { - // Draw the line at the edge of the axis - context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); - context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); - var x1 = me.left; - var x2 = me.right + axisWidth; - var y1 = me.top; - var y2 = me.bottom + axisWidth; - - var aliasPixel = helpers.aliasPixel(context.lineWidth); - if (isHorizontal) { - y1 = y2 = options.position === 'top' ? me.bottom : me.top; - y1 += aliasPixel; - y2 += aliasPixel; + if (optionTicks.display) { + // Make sure we draw text in the correct color and font + context.save(); + context.translate(itemToDraw.labelX, itemToDraw.labelY); + context.rotate(itemToDraw.rotation); + context.font = itemToDraw.major ? majorTickFont.font : tickFont.font; + context.fillStyle = itemToDraw.major ? majorTickFontColor : tickFontColor; + context.textBaseline = itemToDraw.textBaseline; + context.textAlign = itemToDraw.textAlign; + + var label = itemToDraw.label; + if (helpers.isArray(label)) { + var lineCount = label.length; + var lineHeight = tickFont.size * 1.5; + var y = me.isHorizontal() ? 0 : -lineHeight * (lineCount - 1) / 2; + + for (var i = 0; i < lineCount; ++i) { + // We just make sure the multiline element is a string here.. + context.fillText('' + label[i], 0, y); + // apply same lineSpacing as calculated @ L#320 + y += lineHeight; + } } else { - x1 = x2 = options.position === 'left' ? me.right : me.left; - x1 += aliasPixel; - x2 += aliasPixel; + context.fillText(label, 0, 0); } + context.restore(); + } + }); - context.beginPath(); - context.moveTo(x1, y1); - context.lineTo(x2, y2); - context.stroke(); + if (scaleLabel.display) { + // Draw the scale label + var scaleLabelX; + var scaleLabelY; + var rotation = 0; + var halfLineHeight = parseLineHeight(scaleLabel) / 2; + + if (isHorizontal) { + scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width + scaleLabelY = options.position === 'bottom' + ? me.bottom - halfLineHeight - scaleLabelPadding.bottom + : me.top + halfLineHeight + scaleLabelPadding.top; + } else { + var isLeft = options.position === 'left'; + scaleLabelX = isLeft + ? me.left + halfLineHeight + scaleLabelPadding.top + : me.right - halfLineHeight - scaleLabelPadding.top; + scaleLabelY = me.top + ((me.bottom - me.top) / 2); + rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } + + context.save(); + context.translate(scaleLabelX, scaleLabelY); + context.rotate(rotation); + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = scaleLabelFontColor; // render in correct colour + context.font = scaleLabelFont.font; + context.fillText(scaleLabel.labelString, 0, 0); + context.restore(); } - }); -}; + + if (gridLines.drawBorder) { + // Draw the line at the edge of the axis + context.lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, 0); + context.strokeStyle = helpers.valueAtIndexOrDefault(gridLines.color, 0); + var x1 = me.left; + var x2 = me.right + axisWidth; + var y1 = me.top; + var y2 = me.bottom + axisWidth; + + var aliasPixel = helpers.aliasPixel(context.lineWidth); + if (isHorizontal) { + y1 = y2 = options.position === 'top' ? me.bottom : me.top; + y1 += aliasPixel; + y2 += aliasPixel; + } else { + x1 = x2 = options.position === 'left' ? me.right : me.left; + x1 += aliasPixel; + x2 += aliasPixel; + } + + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.stroke(); + } + } +}); diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index f2ea01d329a..fe945382003 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -4,43 +4,40 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var layouts = require('./core.layouts'); -module.exports = function(Chart) { +module.exports = { + // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then + // use the new chart options to grab the correct scale + constructors: {}, + // Use a registration function so that we can move to an ES6 map when we no longer need to support + // old browsers - Chart.scaleService = { - // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then - // use the new chart options to grab the correct scale - constructors: {}, - // Use a registration function so that we can move to an ES6 map when we no longer need to support - // old browsers - - // Scale config defaults - defaults: {}, - registerScaleType: function(type, scaleConstructor, scaleDefaults) { - this.constructors[type] = scaleConstructor; - this.defaults[type] = helpers.clone(scaleDefaults); - }, - getScaleConstructor: function(type) { - return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; - }, - getScaleDefaults: function(type) { - // Return the scale defaults merged with the global settings so that we always use the latest ones - return this.defaults.hasOwnProperty(type) ? helpers.merge({}, [defaults.scale, this.defaults[type]]) : {}; - }, - updateScaleDefaults: function(type, additions) { - var me = this; - if (me.defaults.hasOwnProperty(type)) { - me.defaults[type] = helpers.extend(me.defaults[type], additions); - } - }, - addScalesToLayout: function(chart) { - // Adds each scale to the chart.boxes array to be sized accordingly - helpers.each(chart.scales, function(scale) { - // Set ILayoutItem parameters for backwards compatibility - scale.fullWidth = scale.options.fullWidth; - scale.position = scale.options.position; - scale.weight = scale.options.weight; - layouts.addBox(chart, scale); - }); + // Scale config defaults + defaults: {}, + registerScaleType: function(type, scaleConstructor, scaleDefaults) { + this.constructors[type] = scaleConstructor; + this.defaults[type] = helpers.clone(scaleDefaults); + }, + getScaleConstructor: function(type) { + return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; + }, + getScaleDefaults: function(type) { + // Return the scale defaults merged with the global settings so that we always use the latest ones + return this.defaults.hasOwnProperty(type) ? helpers.merge({}, [defaults.scale, this.defaults[type]]) : {}; + }, + updateScaleDefaults: function(type, additions) { + var me = this; + if (me.defaults.hasOwnProperty(type)) { + me.defaults[type] = helpers.extend(me.defaults[type], additions); } - }; + }, + addScalesToLayout: function(chart) { + // Adds each scale to the chart.boxes array to be sized accordingly + helpers.each(chart.scales, function(scale) { + // Set ILayoutItem parameters for backwards compatibility + scale.fullWidth = scale.options.fullWidth; + scale.position = scale.options.position; + scale.weight = scale.options.weight; + layouts.addBox(chart, scale); + }); + } }; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 5910ce88a51..dd8b01783bf 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -1,13 +1,16 @@ 'use strict'; -module.exports = function(Chart) { +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); + +module.exports = function() { // Default config for a category scale var defaultConfig = { position: 'bottom' }; - var DatasetScale = Chart.Scale.extend({ + var DatasetScale = Scale.extend({ /** * Internal function to get the correct labels. If data.xLabels or data.yLabels are defined, use those * else fall back to data.labels @@ -128,6 +131,5 @@ module.exports = function(Chart) { } }); - Chart.scaleService.registerScaleType('category', DatasetScale, defaultConfig); - + scaleService.registerScaleType('category', DatasetScale, defaultConfig); }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index aa723004fb7..a980615c5d1 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -2,6 +2,7 @@ var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); module.exports = function(Chart) { @@ -186,6 +187,6 @@ module.exports = function(Chart) { return this.getPixelForValue(this.ticksAsNumbers[index]); } }); - Chart.scaleService.registerScaleType('linear', LinearScale, defaultConfig); + scaleService.registerScaleType('linear', LinearScale, defaultConfig); }; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 5ac01b92a4f..ce53da7b98d 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,6 +1,7 @@ 'use strict'; var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); /** * Generate a set of linear ticks @@ -66,17 +67,16 @@ function generateTicks(generationOptions, dataRange) { return ticks; } - module.exports = function(Chart) { var noop = helpers.noop; - Chart.LinearScaleBase = Chart.Scale.extend({ + Chart.LinearScaleBase = Scale.extend({ getRightValue: function(value) { if (typeof value === 'string') { return +value; } - return Chart.Scale.prototype.getRightValue.call(this, value); + return Scale.prototype.getRightValue.call(this, value); }, handleTickRangeOptions: function() { @@ -191,7 +191,7 @@ module.exports = function(Chart) { me.ticksAsNumbers = me.ticks.slice(); me.zeroLineIndex = me.ticks.indexOf(0); - Chart.Scale.prototype.convertTicksToLabels.call(me); + Scale.prototype.convertTicksToLabels.call(me); } }); }; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 74a210e4473..0365568e772 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -1,6 +1,8 @@ 'use strict'; var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); /** @@ -66,7 +68,7 @@ module.exports = function(Chart) { } }; - var LogarithmicScale = Chart.Scale.extend({ + var LogarithmicScale = Scale.extend({ determineDataLimits: function() { var me = this; var opts = me.options; @@ -241,7 +243,7 @@ module.exports = function(Chart) { convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); - Chart.Scale.prototype.convertTicksToLabels.call(this); + Scale.prototype.convertTicksToLabels.call(this); }, // Get the correct tooltip label getLabelForIndex: function(index, datasetIndex) { @@ -342,6 +344,6 @@ module.exports = function(Chart) { return value; } }); - Chart.scaleService.registerScaleType('logarithmic', LogarithmicScale, defaultConfig); + scaleService.registerScaleType('logarithmic', LogarithmicScale, defaultConfig); }; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index e36b4e13539..b580e1da75b 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -2,6 +2,7 @@ var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var scaleService = require('../core/core.scaleService'); var Ticks = require('../core/core.ticks'); module.exports = function(Chart) { @@ -524,6 +525,6 @@ module.exports = function(Chart) { } } }); - Chart.scaleService.registerScaleType('radialLinear', LinearRadialScale, defaultConfig); + scaleService.registerScaleType('radialLinear', LinearRadialScale, defaultConfig); }; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 4892eea9996..dfd708b2345 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -6,6 +6,8 @@ moment = typeof moment === 'function' ? moment : window.moment; var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); +var Scale = require('../core/core.scale'); +var scaleService = require('../core/core.scaleService'); // Integer constants are from the ES6 spec. var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; @@ -424,7 +426,7 @@ function determineLabelFormat(data, timeOpts) { return 'MMM D, YYYY'; } -module.exports = function(Chart) { +module.exports = function() { var defaultConfig = { position: 'bottom', @@ -488,7 +490,7 @@ module.exports = function(Chart) { } }; - var TimeScale = Chart.Scale.extend({ + var TimeScale = Scale.extend({ initialize: function() { if (!moment) { throw new Error('Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com'); @@ -496,7 +498,7 @@ module.exports = function(Chart) { this.mergeTicksOptions(); - Chart.Scale.prototype.initialize.call(this); + Scale.prototype.initialize.call(this); }, update: function() { @@ -508,7 +510,7 @@ module.exports = function(Chart) { console.warn('options.time.format is deprecated and replaced by options.time.parser.'); } - return Chart.Scale.prototype.update.apply(me, arguments); + return Scale.prototype.update.apply(me, arguments); }, /** @@ -518,7 +520,7 @@ module.exports = function(Chart) { if (rawValue && rawValue.t !== undefined) { rawValue = rawValue.t; } - return Chart.Scale.prototype.getRightValue.call(this, rawValue); + return Scale.prototype.getRightValue.call(this, rawValue); }, determineDataLimits: function() { @@ -779,5 +781,5 @@ module.exports = function(Chart) { } }); - Chart.scaleService.registerScaleType('time', TimeScale, defaultConfig); + scaleService.registerScaleType('time', TimeScale, defaultConfig); }; diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js index 4126df5268e..49942fc9af3 100644 --- a/test/specs/global.namespace.tests.js +++ b/test/specs/global.namespace.tests.js @@ -13,6 +13,8 @@ describe('Chart namespace', function() { expect(Chart.layouts instanceof Object).toBeTruthy(); expect(Chart.plugins instanceof Object).toBeTruthy(); expect(Chart.platform instanceof Object).toBeTruthy(); + expect(Chart.Scale instanceof Object).toBeTruthy(); + expect(Chart.scaleService instanceof Object).toBeTruthy(); expect(Chart.Ticks instanceof Object).toBeTruthy(); expect(Chart.Tooltip instanceof Object).toBeTruthy(); expect(Chart.Tooltip.positioners instanceof Object).toBeTruthy(); From 7c3e934062c2679d6e06905b342f6432dd7d87f6 Mon Sep 17 00:00:00 2001 From: serhii-yakymuk Date: Tue, 3 Apr 2018 10:23:16 +0300 Subject: [PATCH 394/685] Fix line clipping at the chart area borders (#5321) --- src/controllers/controller.line.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 9d9206d5cb2..7aacf2d23e2 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -283,15 +283,23 @@ module.exports = function(Chart) { var points = meta.data || []; var area = chart.chartArea; var ilen = points.length; + var halfBorderWidth; var i = 0; - helpers.canvas.clipArea(chart.ctx, area); - if (lineEnabled(me.getDataset(), chart.options)) { + halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; + + helpers.canvas.clipArea(chart.ctx, { + left: area.left, + right: area.right, + top: area.top - halfBorderWidth, + bottom: area.bottom + halfBorderWidth + }); + meta.dataset.draw(); - } - helpers.canvas.unclipArea(chart.ctx); + helpers.canvas.unclipArea(chart.ctx); + } // Draw the points for (; i < ilen; ++i) { From a468ca17b0268a4224087f186c0e00b3f12499d7 Mon Sep 17 00:00:00 2001 From: veggiesaurus Date: Fri, 6 Apr 2018 09:29:33 +0200 Subject: [PATCH 395/685] Skip point outside the clipping area (#5363) --- src/elements/element.point.js | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/elements/element.point.js b/src/elements/element.point.js index eab5b31d453..fa7c42ec641 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -71,36 +71,18 @@ module.exports = Element.extend({ var radius = vm.radius; var x = vm.x; var y = vm.y; - var color = helpers.color; var errMargin = 1.01; // 1.01 is margin for Accumulated error. (Especially Edge, IE.) - var ratio = 0; if (vm.skip) { return; } - ctx.strokeStyle = vm.borderColor || defaultColor; - ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); - ctx.fillStyle = vm.backgroundColor || defaultColor; - - // Cliping for Points. - // going out from inner charArea? - if ((chartArea !== undefined) && ((model.x < chartArea.left) || (chartArea.right * errMargin < model.x) || (model.y < chartArea.top) || (chartArea.bottom * errMargin < model.y))) { - // Point fade out - if (model.x < chartArea.left) { - ratio = (x - model.x) / (chartArea.left - model.x); - } else if (chartArea.right * errMargin < model.x) { - ratio = (model.x - x) / (model.x - chartArea.right); - } else if (model.y < chartArea.top) { - ratio = (y - model.y) / (chartArea.top - model.y); - } else if (chartArea.bottom * errMargin < model.y) { - ratio = (model.y - y) / (model.y - chartArea.bottom); - } - ratio = Math.round(ratio * 100) / 100; - ctx.strokeStyle = color(ctx.strokeStyle).alpha(ratio).rgbString(); - ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); + // Clipping for Points. + if (chartArea === undefined || (model.x >= chartArea.left && chartArea.right * errMargin >= model.x && model.y >= chartArea.top && chartArea.bottom * errMargin >= model.y)) { + ctx.strokeStyle = vm.borderColor || defaultColor; + ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); + ctx.fillStyle = vm.backgroundColor || defaultColor; + helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } - - helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); } }); From 85169c56037a4212274b0a58d6242a832e3f6127 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Wed, 11 Apr 2018 18:16:32 -0400 Subject: [PATCH 396/685] Proper tick position for right positioned axis (#5401) * Proper tick position for right positioned axis * Test for tick mark drawing --- src/core/core.scale.js | 4 +- test/fixtures/core.scale/tick-drawing.json | 79 +++++++++++++++++++++ test/fixtures/core.scale/tick-drawing.png | Bin 0 -> 958 bytes 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/core.scale/tick-drawing.json create mode 100644 test/fixtures/core.scale/tick-drawing.png diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 5ab6b8c090f..78ede6985f6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -705,8 +705,8 @@ module.exports = Element.extend({ var itemsToDraw = []; var axisWidth = me.options.gridLines.lineWidth; - var xTickStart = options.position === 'right' ? me.right : me.right - axisWidth - tl; - var xTickEnd = options.position === 'right' ? me.right + tl : me.right; + var xTickStart = options.position === 'right' ? me.left : me.right - axisWidth - tl; + var xTickEnd = options.position === 'right' ? me.left + tl : me.right; var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; diff --git a/test/fixtures/core.scale/tick-drawing.json b/test/fixtures/core.scale/tick-drawing.json new file mode 100644 index 00000000000..7c9d6da7abe --- /dev/null +++ b/test/fixtures/core.scale/tick-drawing.json @@ -0,0 +1,79 @@ +{ + "config": { + "type": "horizontalBar", + "data": { + "labels": ["January", "February", "March", "April", "May", "June", "July"], + "datasets": [] + }, + "options": { + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "category", + "position": "top", + "id": "x-axis-1", + "ticks": { + "display": false + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }, { + "type": "category", + "position": "bottom", + "id": "x-axis-2", + "ticks": { + "display": false + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }], + "yAxes": [{ + "position": "left", + "id": "y-axis-1", + "type": "linear", + "ticks": { + "display": false, + "min": -100, + "max": 100 + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }, { + "type": "linear", + "id": "y-axis-2", + "position": "right", + "ticks": { + "display": false, + "min": 0, + "max": 50 + }, + "gridLines":{ + "drawOnChartArea": false, + "drawBorder": false, + "color": "rgba(0, 0, 0, 1)", + "zeroLineColor": "rgba(0, 0, 0, 1)" + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/tick-drawing.png b/test/fixtures/core.scale/tick-drawing.png new file mode 100644 index 0000000000000000000000000000000000000000..fb80cd0123293cde8b79ec2b63b9f9c25c589573 GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is?-TRmMILn`LHy|yv;hyxFU z-!I8|apS_qg~`sx-R@mHZi3*p zF(+usN?VyhL>@9kcpY!+>X8 zHPH^8^IVFLVZ;53_2hUF6pQ;m#{CD`eLjw)gzz5Z#ZPKbz5KWp2hu{2re)C zfgq%W#<*b$EM332QNW Date: Sun, 22 Apr 2018 19:32:42 +0100 Subject: [PATCH 397/685] Fix responsive in IE11 with padding as percentage (#4620) When the chart is responsive to the parent container, the calculations for padding assumes that the figure is in pixels so that 20% is taken to be 20 (pixels), which results in the chart exceeding the parent container. This appears to be an IE11 only issue. --- src/core/core.helpers.js | 24 ++++++++++++++++++------ test/specs/core.helpers.tests.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 6323e9ebffc..64e4180df4e 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -473,15 +473,25 @@ module.exports = function() { helpers.getConstraintHeight = function(domNode) { return getConstraintDimension(domNode, 'max-height', 'clientHeight'); }; + /** + * @private + */ + helpers._calculatePadding = function(container, padding, parentDimension) { + padding = helpers.getStyle(container, padding); + + return padding.indexOf('%') > -1 ? parentDimension / parseInt(padding, 10) : parseInt(padding, 10); + }; helpers.getMaximumWidth = function(domNode) { var container = domNode.parentNode; if (!container) { return domNode.clientWidth; } - var paddingLeft = parseInt(helpers.getStyle(container, 'padding-left'), 10); - var paddingRight = parseInt(helpers.getStyle(container, 'padding-right'), 10); - var w = container.clientWidth - paddingLeft - paddingRight; + var clientWidth = container.clientWidth; + var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth); + var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth); + + var w = clientWidth - paddingLeft - paddingRight; var cw = helpers.getConstraintWidth(domNode); return isNaN(cw) ? w : Math.min(w, cw); }; @@ -491,9 +501,11 @@ module.exports = function() { return domNode.clientHeight; } - var paddingTop = parseInt(helpers.getStyle(container, 'padding-top'), 10); - var paddingBottom = parseInt(helpers.getStyle(container, 'padding-bottom'), 10); - var h = container.clientHeight - paddingTop - paddingBottom; + var clientHeight = container.clientHeight; + var paddingTop = helpers._calculatePadding(container, 'padding-top', clientHeight); + var paddingBottom = helpers._calculatePadding(container, 'padding-bottom', clientHeight); + + var h = clientHeight - paddingTop - paddingBottom; var ch = helpers.getConstraintHeight(domNode); return isNaN(ch) ? h : Math.min(h, ch); }; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index eb96ef8c745..3c471b50510 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -746,6 +746,36 @@ describe('Core helper tests', function() { expect(canvas.style.width).toBe('400px'); }); + it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() { + + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '300px'; + div.style.height = '300px'; + document.body.appendChild(div); + + // Inner DIV to have 10% padding of parent + var innerDiv = document.createElement('div'); + + div.appendChild(innerDiv); + + var canvas = document.createElement('canvas'); + innerDiv.appendChild(canvas); + + // No padding + expect(helpers.getMaximumWidth(canvas)).toBe(300); + + // test with percentage + innerDiv.style.padding = '10%'; + expect(helpers.getMaximumWidth(canvas)).toBe(240); + + // test with pixels + innerDiv.style.padding = '10px'; + expect(helpers.getMaximumWidth(canvas)).toBe(280); + + document.body.removeChild(div); + }); + describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && obj.hasOwnProperty('values') && obj.values.hasOwnProperty('rgb'); From 14399ffe372ebabcb66bab20827b45594e7fc204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Soko=C5=82owski?= Date: Wed, 9 May 2018 09:15:04 +0200 Subject: [PATCH 398/685] Update gulpfile.js to use in strict mode (#5478) --- gulpfile.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 21f19645f81..24c01665ae4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,7 +20,7 @@ var yargs = require('yargs'); var path = require('path'); var fs = require('fs'); var htmllint = require('gulp-htmllint'); -var package = require('./package.json'); +var pkg = require('./package.json'); var argv = yargs .option('force-output', {default: false}) @@ -69,11 +69,11 @@ gulp.task('default', ['build', 'watch']); */ function bowerTask() { var json = JSON.stringify({ - name: package.name, - description: package.description, - homepage: package.homepage, - license: package.license, - version: package.version, + name: pkg.name, + description: pkg.description, + homepage: pkg.homepage, + license: pkg.license, + version: pkg.version, main: outDir + "Chart.js", ignore: [ '.github', @@ -112,11 +112,11 @@ function buildTask() { .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(gulp.dest(outDir)) .pipe(streamify(uglify())) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(streamify(concat('Chart.bundle.min.js'))) .pipe(gulp.dest(outDir)); @@ -127,11 +127,11 @@ function buildTask() { .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(gulp.dest(outDir)) .pipe(streamify(uglify())) .pipe(insert.prepend(header)) - .pipe(streamify(replace('{{ version }}', package.version))) + .pipe(streamify(replace('{{ version }}', pkg.version))) .pipe(streamify(concat('Chart.min.js'))) .pipe(gulp.dest(outDir)); From 1072ed9238ac48b6be5a7db8546103ec50a92c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Poulhi=C3=A8s?= Date: Tue, 22 May 2018 22:23:24 +0200 Subject: [PATCH 399/685] Fix typo in README.md (#5504) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f914944e101..31a14e5257c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatibility issues. ## Documentation From 25b7f41bac120fd4fc4ed72851b501381f61982e Mon Sep 17 00:00:00 2001 From: jcopperfield <33193571+jcopperfield@users.noreply.github.com> Date: Wed, 23 May 2018 03:11:28 +0200 Subject: [PATCH 400/685] Bug: Avoid updating Chart when `responsive: true` and Chart is hidden. (#5172) * Bug: Avoid updating Chart when `responsive: true` and Chart is hidden. * Prevent `drawing` when width/height is invalid. --- src/core/core.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index d27967d3941..3445cb51801 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -181,7 +181,7 @@ module.exports = function(Chart) { // 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. - // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collased + // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed var newWidth = Math.max(0, Math.floor(helpers.getMaximumWidth(canvas))); var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas))); @@ -561,6 +561,10 @@ module.exports = function(Chart) { me.transition(easingValue); + if (me.width <= 0 || me.height <= 0) { + return; + } + if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { return; } From ca9d3175c5fa9da69434cf7ed40b299fd221b667 Mon Sep 17 00:00:00 2001 From: Antoine Aumjaud Date: Sat, 26 May 2018 09:55:44 +0200 Subject: [PATCH 401/685] Fix time documentation (#5507) --- docs/axes/cartesian/time.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index cefe9c24806..fcda85d6173 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -6,7 +6,7 @@ The time scale is used to display times and dates. When building its ticks, it w ### Input Data -The x-axis data points may additionally be specified via the `t` attribute when using the time scale. +The x-axis data points may additionally be specified via the `t` or `x` attribute when using the time scale. data: [{ x: new Date(), @@ -64,6 +64,7 @@ var chart = new Chart(ctx, { options: { scales: { xAxes: [{ + type: 'time', time: { unit: 'month' } From 8a7278052fc1182a50014edabc20540946fe1ba4 Mon Sep 17 00:00:00 2001 From: Guiomar Valderrama Date: Mon, 4 Jun 2018 14:14:04 +0200 Subject: [PATCH 402/685] clarify moment.js included in bundle cannot be used outside of chartjs (#5528) --- README.md | 2 +- docs/getting-started/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31a14e5257c..d7e966ca784 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatibility issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatibility issues. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. ## Documentation diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index ccc02ddc4b9..00b19b059e4 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -54,4 +54,4 @@ Files: * `dist/Chart.bundle.js` * `dist/Chart.bundle.min.js` -The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. +The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatability issues. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. From 8198d760bbfb918f461833121eb33fcb49a5741c Mon Sep 17 00:00:00 2001 From: Matt Haff Date: Tue, 5 Jun 2018 03:14:37 -0400 Subject: [PATCH 403/685] Handle '\n' as new line in tooltips (#5521) --- src/core/core.tooltip.js | 45 +++++++--- test/specs/core.tooltip.tests.js | 148 +++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 13 deletions(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 3f9490f8546..c529812cc61 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -190,6 +190,20 @@ function pushOrConcat(base, toPush) { return base; } +/** + * Returns array of strings split by newline + * @param {String} value - The value to split by newline. + * @returns {Array} value if newline present - Returned from String split() method + * @function + */ +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} + + // Private helper to create a tooltip item model // @param element : the chart element (point, arc, bar) to create the tooltip item for // @return : new tooltip item @@ -404,7 +418,7 @@ function determineAlignment(tooltip, size) { } /** - * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + * Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment */ function getBackgroundPoint(vm, size, alignment, chart) { // Background Position @@ -457,6 +471,13 @@ function getBackgroundPoint(vm, size, alignment, chart) { }; } +/** + * Helper to build before and after body lines + */ +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} + var exports = module.exports = Element.extend({ initialize: function() { this._model = getBaseModel(this._options); @@ -475,17 +496,16 @@ var exports = module.exports = Element.extend({ var afterTitle = callbacks.afterTitle.apply(me, arguments); var lines = []; - lines = pushOrConcat(lines, beforeTitle); - lines = pushOrConcat(lines, title); - lines = pushOrConcat(lines, afterTitle); + lines = pushOrConcat(lines, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); return lines; }, // Args are: (tooltipItem, data) getBeforeBody: function() { - var lines = this._options.callbacks.beforeBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + return getBeforeAfterBodyLines(this._options.callbacks.beforeBody.apply(this, arguments)); }, // Args are: (tooltipItem, data) @@ -500,9 +520,9 @@ var exports = module.exports = Element.extend({ lines: [], after: [] }; - pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.before, splitNewlines(callbacks.beforeLabel.call(me, tooltipItem, data))); pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data)); - pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data)); + pushOrConcat(bodyItem.after, splitNewlines(callbacks.afterLabel.call(me, tooltipItem, data))); bodyItems.push(bodyItem); }); @@ -512,8 +532,7 @@ var exports = module.exports = Element.extend({ // Args are: (tooltipItem, data) getAfterBody: function() { - var lines = this._options.callbacks.afterBody.apply(this, arguments); - return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : []; + return getBeforeAfterBodyLines(this._options.callbacks.afterBody.apply(this, arguments)); }, // Get the footer and beforeFooter and afterFooter lines @@ -527,9 +546,9 @@ var exports = module.exports = Element.extend({ var afterFooter = callbacks.afterFooter.apply(me, arguments); var lines = []; - lines = pushOrConcat(lines, beforeFooter); - lines = pushOrConcat(lines, footer); - lines = pushOrConcat(lines, afterFooter); + lines = pushOrConcat(lines, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(afterFooter)); return lines; }, diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index 8878d9d9dbc..9d858a49e10 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -949,4 +949,152 @@ describe('Core.Tooltip', function() { } } }); + + it('Should split newlines into separate lines in user callbacks', function() { + var chart = 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', + callbacks: { + beforeTitle: function() { + return 'beforeTitle\nnewline'; + }, + title: function() { + return 'title\nnewline'; + }, + afterTitle: function() { + return 'afterTitle\nnewline'; + }, + beforeBody: function() { + return 'beforeBody\nnewline'; + }, + beforeLabel: function() { + return 'beforeLabel\nnewline'; + }, + label: function() { + return 'label'; + }, + afterLabel: function() { + return 'afterLabel\nnewline'; + }, + afterBody: function() { + return 'afterBody\nnewline'; + }, + beforeFooter: function() { + return 'beforeFooter\nnewline'; + }, + footer: function() { + return 'footer\nnewline'; + }, + afterFooter: function() { + return 'afterFooter\nnewline'; + }, + labelTextColor: function() { + return 'labelTextColor'; + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + var node = 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 + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var globalDefaults = Chart.defaults.global; + + expect(tooltip._view).toEqual(jasmine.objectContaining({ + // Positioning + xPadding: 6, + yPadding: 6, + xAlign: 'center', + yAlign: 'top', + + // 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', + + // Text + title: ['beforeTitle', 'newline', 'title', 'newline', 'afterTitle', 'newline'], + beforeBody: ['beforeBody', 'newline'], + body: [{ + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }, { + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }], + afterBody: ['afterBody', 'newline'], + footer: ['beforeFooter', 'newline', 'footer', 'newline', 'afterFooter', 'newline'], + caretPadding: 2, + labelTextColors: ['labelTextColor', 'labelTextColor'], + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + })); + }); }); From f5140d243bf508b87a2a4563e31c5cc2e4b394e7 Mon Sep 17 00:00:00 2001 From: Ben McCann Date: Sat, 16 Jun 2018 09:54:47 -0700 Subject: [PATCH 404/685] Direct questions and support to StackOverflow (#5571) --- .github/ISSUE_TEMPLATE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7d57bbd0c0c..e6ead94ecb3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ From 88308c600cbbd38458bc94549ca6139698508c58 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 26 Jun 2018 17:58:32 +0200 Subject: [PATCH 409/685] Enhance the rounded rectangle implementation (#5597) Use `arcTo` instead of `quadraticCurveTo` (both methods have the same compatibility level) because it generates better results when the final rect is a circle but also when it's actually a rectangle and not a square. This change is needed by the datalabels plugin where the user can configure the `borderRadius` and thus generate circle from a rounded rectangle. --- src/helpers/helpers.canvas.js | 34 ++++++++++++++++++------------ test/jasmine.context.js | 1 + test/specs/element.point.tests.js | 2 +- test/specs/helpers.canvas.tests.js | 8 +++---- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 13b110268b9..4bfae9c48fd 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -27,18 +27,20 @@ var exports = module.exports = { */ roundedRect: function(ctx, x, y, width, height, radius) { if (radius) { - var rx = Math.min(radius, width / 2); - var ry = Math.min(radius, height / 2); - - ctx.moveTo(x + rx, y); - ctx.lineTo(x + width - rx, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + ry); - ctx.lineTo(x + width, y + height - ry); - ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); - ctx.lineTo(x + rx, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - ry); - ctx.lineTo(x, y + ry); - ctx.quadraticCurveTo(x, y, x + rx, y); + // NOTE(SB) `epsilon` helps to prevent minor artifacts appearing + // on Chrome when `r` is exactly half the height or the width. + var epsilon = 0.0000001; + var r = Math.min(radius, (height / 2) - epsilon, (width / 2) - epsilon); + + ctx.moveTo(x + r, y); + ctx.lineTo(x + width - r, y); + ctx.arcTo(x + width, y, x + width, y + r, r); + ctx.lineTo(x + width, y + height - r); + ctx.arcTo(x + width, y + height, x + width - r, y + height, r); + ctx.lineTo(x + r, y + height); + ctx.arcTo(x, y + height, x, y + height - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); } else { ctx.rect(x, y, width, height); } @@ -89,7 +91,13 @@ var exports = module.exports = { var topY = y - offset; var sideSize = Math.SQRT2 * radius; ctx.beginPath(); - this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius / 2); + + // NOTE(SB) the rounded rect implementation changed to use `arcTo` + // instead of `quadraticCurveTo` since it generates better results + // when rect is almost a circle. 0.425 (instead of 0.5) produces + // results visually closer to the previous impl. + this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425); + ctx.closePath(); ctx.fill(); break; diff --git a/test/jasmine.context.js b/test/jasmine.context.js index 8f4171fee4d..3497c721918 100644 --- a/test/jasmine.context.js +++ b/test/jasmine.context.js @@ -75,6 +75,7 @@ Context.prototype._initMethods = function() { var me = this; var methods = { arc: function() {}, + arcTo: function() {}, beginPath: function() {}, bezierCurveTo: function() {}, clearRect: function() {}, diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index f09b912d3c0..0321112fb12 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -222,7 +222,7 @@ describe('Point element tests', function() { 15 - offset, Math.SQRT2 * 2, Math.SQRT2 * 2, - 2 / 2 + 2 * 0.425 ); expect(mockContext.getCalls()).toContain( jasmine.objectContaining({ diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js index 81326529b49..0c20628d456 100644 --- a/test/specs/helpers.canvas.tests.js +++ b/test/specs/helpers.canvas.tests.js @@ -30,13 +30,13 @@ describe('Chart.helpers.canvas', function() { expect(context.getCalls()).toEqual([ {name: 'moveTo', args: [15, 20]}, {name: 'lineTo', args: [35, 20]}, - {name: 'quadraticCurveTo', args: [40, 20, 40, 25]}, + {name: 'arcTo', args: [40, 20, 40, 25, 5]}, {name: 'lineTo', args: [40, 55]}, - {name: 'quadraticCurveTo', args: [40, 60, 35, 60]}, + {name: 'arcTo', args: [40, 60, 35, 60, 5]}, {name: 'lineTo', args: [15, 60]}, - {name: 'quadraticCurveTo', args: [10, 60, 10, 55]}, + {name: 'arcTo', args: [10, 60, 10, 55, 5]}, {name: 'lineTo', args: [10, 25]}, - {name: 'quadraticCurveTo', args: [10, 20, 15, 20]} + {name: 'arcTo', args: [10, 20, 15, 20, 5]} ]); }); it('should optimize path if radius is 0', function() { From 1cd0469bdcfad587f027cea9f13a5be31bacc666 Mon Sep 17 00:00:00 2001 From: Alec Fenichel Date: Sat, 7 Jul 2018 11:51:02 -0400 Subject: [PATCH 410/685] Add 15 minutes time steps (#5613) --- src/scales/scale.time.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index dfd708b2345..2496a487c3e 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -22,12 +22,12 @@ var INTERVALS = { second: { common: true, size: 1000, - steps: [1, 2, 5, 10, 30] + steps: [1, 2, 5, 10, 15, 30] }, minute: { common: true, size: 60000, - steps: [1, 2, 5, 10, 30] + steps: [1, 2, 5, 10, 15, 30] }, hour: { common: true, From 387a23df737fcf8d0faf8762b757b2a428c5a906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9da=20Housni=20Alaoui?= Date: Sat, 7 Jul 2018 17:52:29 +0200 Subject: [PATCH 411/685] Add support for Shadow DOM (#5585) https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/host --- src/core/core.helpers.js | 16 +++++++++++++--- test/specs/core.helpers.tests.js | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 64e4180df4e..844fa1fd51d 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -450,7 +450,7 @@ module.exports = function() { // @see http://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser function getConstraintDimension(domNode, maxStyle, percentageProperty) { var view = document.defaultView; - var parentNode = domNode.parentNode; + var parentNode = helpers._getParentNode(domNode); var constrainedNode = view.getComputedStyle(domNode)[maxStyle]; var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle]; var hasCNode = isConstrainedValue(constrainedNode); @@ -481,8 +481,18 @@ module.exports = function() { return padding.indexOf('%') > -1 ? parentDimension / parseInt(padding, 10) : parseInt(padding, 10); }; + /** + * @private + */ + helpers._getParentNode = function(domNode) { + var parent = domNode.parentNode; + if (parent && parent.host) { + parent = parent.host; + } + return parent; + }; helpers.getMaximumWidth = function(domNode) { - var container = domNode.parentNode; + var container = helpers._getParentNode(domNode); if (!container) { return domNode.clientWidth; } @@ -496,7 +506,7 @@ module.exports = function() { return isNaN(cw) ? w : Math.min(w, cw); }; helpers.getMaximumHeight = function(domNode) { - var container = domNode.parentNode; + var container = helpers._getParentNode(domNode); if (!container) { return domNode.clientHeight; } diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 3c471b50510..30baaf5acc1 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -568,6 +568,31 @@ describe('Core helper tests', function() { document.body.removeChild(div); }); + it ('should get the maximum width and height for a node in a ShadowRoot', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + if (!div.attachShadow) { + // Shadow DOM is not natively supported + return; + } + + var shadow = div.attachShadow({mode: 'closed'}); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + shadow.appendChild(innerDiv); + + expect(helpers.getMaximumWidth(innerDiv)).toBe(200); + expect(helpers.getMaximumHeight(innerDiv)).toBe(300); + + document.body.removeChild(div); + }); + it ('should get the maximum width of a node that has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); From 0ddd0ee16b98bad17756a6b98b2460a04fac9056 Mon Sep 17 00:00:00 2001 From: Joel Hamilton Date: Sat, 7 Jul 2018 11:54:05 -0400 Subject: [PATCH 412/685] Enable arbitrary rotation of datapoints (#5319) --- docs/charts/bubble.md | 2 + docs/charts/line.md | 1 + docs/charts/radar.md | 1 + docs/configuration/elements.md | 1 + src/controllers/controller.bubble.js | 5 +- src/controllers/controller.line.js | 14 ++ src/controllers/controller.radar.js | 1 + src/elements/element.point.js | 3 +- src/helpers/helpers.canvas.js | 72 ++++++----- test/specs/element.point.tests.js | 187 ++++++++++++++++++++++----- 10 files changed, 218 insertions(+), 69 deletions(-) diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md index 4cb2ee6994f..e9f8a7216b1 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -51,6 +51,7 @@ The bubble chart allows a number of properties to be specified for each dataset. | [`hitRadius`](#interactions) | `Number` | Yes | Yes | `1` | [`label`](#labeling) | `String` | - | - | `undefined` | [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` +| [`rotation`](#styling) | `Number` | Yes | Yes | `0` | [`radius`](#styling) | `Number` | Yes | Yes | `3` ### Labeling @@ -67,6 +68,7 @@ The style of each bubble can be controlled with the following properties: | `borderColor` | bubble border color | `borderWidth` | bubble border width (in pixels) | `pointStyle` | bubble [shape style](../configuration/elements#point-styles) +| `rotation` | bubble rotation (in degrees) | `radius` | bubble radius (in pixels) All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. diff --git a/docs/charts/line.md b/docs/charts/line.md index 90471e462dd..db0245b1e0c 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -63,6 +63,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. | `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. | `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](../configuration/elements#point-styles) +| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees. | `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. | `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. | `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. diff --git a/docs/charts/radar.md b/docs/charts/radar.md index b8a41c8384c..947e15a3102 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -82,6 +82,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointBorderColor` | `Color/Color[]` | The border color for points. | `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels. | `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees. | `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointstyle) | `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events. | `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered. diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md index 5375a7e9f1f..148b5f39a54 100644 --- a/docs/configuration/elements.md +++ b/docs/configuration/elements.md @@ -19,6 +19,7 @@ Global point options: `Chart.defaults.global.elements.point` | -----| ---- | --------| ----------- | `radius` | `Number` | `3` | Point radius. | [`pointStyle`](#point-styles) | `String` | `circle` | Point style. +| `rotation` | `Number` | `0` | Point rotation (in degrees). | `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point fill color. | `borderWidth` | `Number` | `1` | Point stroke width. | `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point stroke color. diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index b808b366b1f..f14e512300f 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -87,6 +87,7 @@ module.exports = function(Chart) { borderWidth: options.borderWidth, hitRadius: options.hitRadius, pointStyle: options.pointStyle, + rotation: options.rotation, radius: reset ? 0 : options.radius, skip: custom.skip || isNaN(x) || isNaN(y), x: x, @@ -146,7 +147,8 @@ module.exports = function(Chart) { 'hoverBorderWidth', 'hoverRadius', 'hitRadius', - 'pointStyle' + 'pointStyle', + 'rotation' ]; for (i = 0, ilen = keys.length; i < ilen; ++i) { @@ -165,7 +167,6 @@ module.exports = function(Chart) { dataset.radius, options.radius ], context, index); - return values; } }); diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 2d1856e01e1..4a18bdadb3c 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -148,6 +148,19 @@ module.exports = function(Chart) { return borderWidth; }, + getPointRotation: function(point, index) { + var pointRotation = this.chart.options.elements.point.rotation; + var dataset = this.getDataset(); + var custom = point.custom || {}; + + if (!isNaN(custom.rotation)) { + pointRotation = custom.rotation; + } else if (!isNaN(dataset.pointRotation) || helpers.isArray(dataset.pointRotation)) { + pointRotation = helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointRotation); + } + return pointRotation; + }, + updateElement: function(point, index, reset) { var me = this; var meta = me.getMeta(); @@ -185,6 +198,7 @@ module.exports = function(Chart) { // Appearance radius: custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointOptions.radius), pointStyle: custom.pointStyle || helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointOptions.pointStyle), + rotation: me.getPointRotation(point, index), backgroundColor: me.getPointBackgroundColor(point, index), borderColor: me.getPointBorderColor(point, index), borderWidth: me.getPointBorderWidth(point, index), diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 10d71cdacb5..89717157a37 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -106,6 +106,7 @@ module.exports = function(Chart) { borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth), pointStyle: custom.pointStyle ? custom.pointStyle : helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle), + rotation: custom.rotation ? custom.rotation : helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointElementOptions.rotation), // Tooltip hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius) diff --git a/src/elements/element.point.js b/src/elements/element.point.js index fa7c42ec641..2bcdc88f0f8 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -68,6 +68,7 @@ module.exports = Element.extend({ var model = this._model; var ctx = this._chart.ctx; var pointStyle = vm.pointStyle; + var rotation = vm.rotation; var radius = vm.radius; var x = vm.x; var y = vm.y; @@ -82,7 +83,7 @@ module.exports = Element.extend({ ctx.strokeStyle = vm.borderColor || defaultColor; ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; - helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y); + helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); } } }); diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 4bfae9c48fd..26f7d37212f 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -46,8 +46,9 @@ var exports = module.exports = { } }, - drawPoint: function(ctx, style, radius, x, y) { + drawPoint: function(ctx, style, radius, x, y, rotation) { var type, edgeLength, xOffset, yOffset, height, size; + rotation = rotation || 0; if (style && typeof style === 'object') { type = style.toString(); @@ -61,11 +62,15 @@ var exports = module.exports = { return; } + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation * Math.PI / 180); + switch (style) { // Default includes circle default: ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.closePath(); ctx.fill(); break; @@ -73,22 +78,22 @@ var exports = module.exports = { ctx.beginPath(); edgeLength = 3 * radius / Math.sqrt(3); height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(x - edgeLength / 2, y + height / 3); - ctx.lineTo(x + edgeLength / 2, y + height / 3); - ctx.lineTo(x, y - 2 * height / 3); + ctx.moveTo(-edgeLength / 2, height / 3); + ctx.lineTo(edgeLength / 2, height / 3); + ctx.lineTo(0, -2 * height / 3); ctx.closePath(); ctx.fill(); break; case 'rect': size = 1 / Math.SQRT2 * radius; ctx.beginPath(); - ctx.fillRect(x - size, y - size, 2 * size, 2 * size); - ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); + ctx.fillRect(-size, -size, 2 * size, 2 * size); + ctx.strokeRect(-size, -size, 2 * size, 2 * size); break; case 'rectRounded': var offset = radius / Math.SQRT2; - var leftX = x - offset; - var topY = y - offset; + var leftX = -offset; + var topY = -offset; var sideSize = Math.SQRT2 * radius; ctx.beginPath(); @@ -104,60 +109,61 @@ var exports = module.exports = { case 'rectRot': size = 1 / Math.SQRT2 * radius; ctx.beginPath(); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y - size); + ctx.moveTo(-size, 0); + ctx.lineTo(0, size); + ctx.lineTo(size, 0); + ctx.lineTo(0, -size); ctx.closePath(); ctx.fill(); break; case 'cross': ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(0, radius); + ctx.lineTo(0, -radius); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); ctx.closePath(); break; case 'crossRot': ctx.beginPath(); xOffset = Math.cos(Math.PI / 4) * radius; yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); + ctx.moveTo(-xOffset, -yOffset); + ctx.lineTo(xOffset, yOffset); + ctx.moveTo(-xOffset, yOffset); + ctx.lineTo(xOffset, -yOffset); ctx.closePath(); break; case 'star': ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(0, radius); + ctx.lineTo(0, -radius); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); xOffset = Math.cos(Math.PI / 4) * radius; yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); + ctx.moveTo(-xOffset, -yOffset); + ctx.lineTo(xOffset, yOffset); + ctx.moveTo(-xOffset, yOffset); + ctx.lineTo(xOffset, -yOffset); ctx.closePath(); break; case 'line': ctx.beginPath(); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(-radius, 0); + ctx.lineTo(radius, 0); ctx.closePath(); break; case 'dash': ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(0, 0); + ctx.lineTo(radius, 0); ctx.closePath(); break; } ctx.stroke(); + ctx.restore(); }, clipArea: function(ctx, area) { diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 0321112fb12..b2f803416b5 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -108,6 +108,7 @@ describe('Point element tests', function() { point._view = { radius: 2, pointStyle: 'circle', + rotation: 25, hitRadius: 3, borderColor: 'rgba(1, 2, 3, 1)', borderWidth: 6, @@ -128,12 +129,21 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [10, 15, 2, 0, 2 * Math.PI] + args: [0, 0, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -143,6 +153,9 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -158,18 +171,27 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10 - 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3] + args: [0 - 3 * 2 / Math.sqrt(3) / 2, 0 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3] }, { name: 'lineTo', - args: [10 + 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], + args: [0 + 3 * 2 / Math.sqrt(3) / 2, 0 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], }, { name: 'lineTo', - args: [10, 15 - 2 * 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], + args: [0, 0 - 2 * 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], }, { name: 'closePath', args: [], @@ -179,6 +201,9 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -194,18 +219,30 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'fillRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] + args: [0 - 1 / Math.SQRT2 * 2, 0 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] }, { name: 'strokeRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] + args: [0 - 1 / Math.SQRT2 * 2, 0 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); @@ -218,8 +255,8 @@ describe('Point element tests', function() { expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( mockContext, - 10 - offset, - 15 - offset, + 0 - offset, + 0 - offset, Math.SQRT2 * 2, Math.SQRT2 * 2, 2 * 0.425 @@ -245,21 +282,30 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10 - 1 / Math.SQRT2 * 2, 15] + args: [0 - 1 / Math.SQRT2 * 2, 0] }, { name: 'lineTo', - args: [10, 15 + 1 / Math.SQRT2 * 2] + args: [0, 0 + 1 / Math.SQRT2 * 2] }, { name: 'lineTo', - args: [10 + 1 / Math.SQRT2 * 2, 15], + args: [0 + 1 / Math.SQRT2 * 2, 0], }, { name: 'lineTo', - args: [10, 15 - 1 / Math.SQRT2 * 2], + args: [0, 0 - 1 / Math.SQRT2 * 2], }, { name: 'closePath', args: [] @@ -269,6 +315,9 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -284,27 +333,39 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10, 17] + args: [0, 2] }, { name: 'lineTo', - args: [10, 13], + args: [0, -2], }, { name: 'moveTo', - args: [8, 15], + args: [-2, 0], }, { name: 'lineTo', - args: [12, 15], + args: [2, 0], }, { name: 'closePath', args: [], }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -320,27 +381,39 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] + args: [0 - Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2] }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [0 + Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [0 - Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], + args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], }, { name: 'closePath', args: [], }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -356,39 +429,51 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10, 17] + args: [0, 2] }, { name: 'lineTo', - args: [10, 13], + args: [0, -2], }, { name: 'moveTo', - args: [8, 15], + args: [-2, 0], }, { name: 'lineTo', - args: [12, 15], + args: [2, 0], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] + args: [0 - Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2] }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [0 + Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [0 - Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], + args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], }, { name: 'closePath', args: [], }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -404,21 +489,33 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [8, 15] + args: [-2, 0] }, { name: 'lineTo', - args: [12, 15], + args: [2, 0], }, { name: 'closePath', args: [], }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); mockContext.resetCalls(); @@ -434,21 +531,33 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [10, 15] + args: [0, 0] }, { name: 'lineTo', - args: [12, 15], + args: [2, 0], }, { name: 'closePath', args: [], }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); }); @@ -483,12 +592,21 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0,0,0,0.1)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [10, 15] + }, { + name: 'rotate', + args: [0] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [10, 15, 2, 0, 2 * Math.PI] + args: [0, 0, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -498,6 +616,9 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] + }, { + name: 'restore', + args: [] }]); }); From 858d86eebbc7744cb183a3f2427f16b588559fae Mon Sep 17 00:00:00 2001 From: Bart Deslagmulder Date: Mon, 9 Jul 2018 20:27:43 +0200 Subject: [PATCH 413/685] Add label for first dataset in progress-bar example (#5625) While the second dataset already has a label ("My Second dataset") the first dataset showed "undefined" as a label. Added a label to the first dataset object. --- samples/advanced/progress-bar.html | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index b97a998ea95..b9c72446e59 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -28,6 +28,7 @@ data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ + label: 'My First dataset', fill: false, borderColor: window.chartColors.red, backgroundColor: window.chartColors.red, From 48fefd92b6dc61345021c6508b23698830ff392f Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Thu, 12 Jul 2018 06:59:16 +0800 Subject: [PATCH 414/685] Fix the example of Linear Radial Axis (#5633) --- docs/axes/radial/linear.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 594901db335..e19a33d75fd 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -76,14 +76,12 @@ This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5 ```javascript let options = { - scales: { - yAxes: [{ - ticks: { - max: 5, - min: 0, - stepSize: 0.5 - } - }] + scale: { + ticks: { + max: 5, + min: 0, + stepSize: 0.5 + } } }; ``` From 9c3b0d2e299d2fbd0b3482869f9be827db17a1e0 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 14 Jul 2018 15:51:50 +0800 Subject: [PATCH 415/685] Add a link to chartjs-plugin-style to extensions.md (#5638) --- docs/notes/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index ae141d96e92..2dc246c5377 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -19,6 +19,7 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. - chartjs-plugin-streaming - Enables to create live streaming charts. + - chartjs-plugin-style - Provides more styling options. - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. From 246b9a1a40b72e57ba256d679ca6e44f1cd82411 Mon Sep 17 00:00:00 2001 From: Niel Mistry Date: Sat, 14 Jul 2018 03:52:49 -0400 Subject: [PATCH 416/685] Add circular option documentation for grid lines (#5637) --- docs/axes/styling.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/axes/styling.md b/docs/axes/styling.md index f60afd9bb68..0318c9781f8 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -9,6 +9,7 @@ The grid line configuration is nested under the scale configuration in the `grid | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | If false, do not display grid lines for this axis. +| `circular` | `Boolean` | `false` | If true, gridlines are circular (on radar chart only) | `color` | `Color/Color[]` | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. | `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) From 119a86f3995e373865ce33a1687cb834ecef6eb3 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 14 Jul 2018 15:57:16 +0800 Subject: [PATCH 417/685] Update the descriptions of barThickness, offsetGridLines and offset (#5600) --- docs/axes/cartesian/README.md | 2 +- docs/axes/styling.md | 2 +- docs/charts/bar.md | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 8518da9f250..1bf35b79f16 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -15,7 +15,7 @@ All of the included cartesian axes support a number of common options. | -----| ---- | --------| ----------- | `type` | `String` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. | `position` | `String` | | Position of the axis in the chart. Possible values are: `'top'`, `'left'`, `'bottom'`, `'right'` -| `offset` | `Boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` in the bar chart by default. +| `offset` | `Boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a category scale in a bar chart by default. | `id` | `String` | | The ID is used to link datasets and scale axes together. [more...](#axis-id) | `gridLines` | `Object` | | Grid line configuration. [more...](../styling.md#grid-line-configuration) | `scaleLabel` | `Object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 0318c9781f8..12a170f3e54 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -22,7 +22,7 @@ The grid line configuration is nested under the scale configuration in the `grid | `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). | `zeroLineBorderDash` | `Number[]` | `[]` | Length and spacing of dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `zeroLineBorderDashOffset` | `Number` | `0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -| `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` in the bar chart by default. +| `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` for a category scale in a bar chart by default. ## Tick Configuration 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. diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 84704ba941b..e4d146b2c67 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -95,12 +95,19 @@ The bar chart defines the following configuration options. These options are mer | ---- | ---- | ------- | ----------- | `barPercentage` | `Number` | `0.9` | Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) | `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) -| `barThickness` | `Number` | | Manually set width of each bar in pixels. If not set, the base sample widths are calculated automatically so that they take the full available widths without overlap. Then, the bars are sized using `barPercentage` and `categoryPercentage`. +| `barThickness` | `Number/String` | | Manually set width of each bar in pixels. If set to `'flex'`, it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. [more...](#barthickness) | `maxBarThickness` | `Number` | | Set this to ensure that bars are not sized thicker than this. | `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) +### barThickness +If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. + +If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. + +If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. + ### offsetGridLines -If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a bar chart while false for other charts by default. +If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. This setting applies to the axis configuration. If axes are added to the chart, this setting will need to be set for each new axis. From 0963c8f76c997643aa3aa1101c34ea31cb58eab2 Mon Sep 17 00:00:00 2001 From: Jung Oh Date: Sat, 14 Jul 2018 01:06:02 -0700 Subject: [PATCH 418/685] Fix positioning in the custom tooltip example (#5454) --- docs/configuration/tooltip.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index b591dc1d13e..566bc5696bc 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -256,12 +256,13 @@ var myPieChart = new Chart(ctx, { // Display, position, and set styles for font tooltipEl.style.opacity = 1; tooltipEl.style.position = 'absolute'; - tooltipEl.style.left = position.left + tooltipModel.caretX + 'px'; - tooltipEl.style.top = position.top + tooltipModel.caretY + 'px'; + tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily; tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px'; tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle; tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px'; + tooltipEl.style.pointerEvents = 'none'; } } } From 493eaa842444b4e5b1f6d192fadf2bb1aaa02eba Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sun, 29 Jul 2018 16:31:28 +0700 Subject: [PATCH 419/685] Refactor helpers.canvas.drawPoint() (#5623) Bring ctx.beginPath() before switch and replace ctx.fillRect()/ctx.strokeRect() with ctx.rect()/ctx.fill() to make it consistent with the other styles. It is also preferable that helpers.canvas.roundedRect() include ctx.closePath() at the end because CanvasRenderingContext2D.rect() closes the subpath. Get rid of ctx.closePath() for cross, crossRot, star, line and dash because these have no closed path shape, and it should be avoided that ctx.closePath() makes a round-trip path. --- src/helpers/helpers.canvas.js | 29 +++++-------------------- test/specs/element.point.tests.js | 26 +++++++++++----------- test/specs/global.deprecations.tests.js | 1 - test/specs/helpers.canvas.tests.js | 4 +++- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 26f7d37212f..60fb6e1a299 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -41,6 +41,8 @@ var exports = module.exports = { ctx.arcTo(x, y + height, x, y + height - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.moveTo(x, y); } else { ctx.rect(x, y, width, height); } @@ -65,77 +67,61 @@ var exports = module.exports = { ctx.save(); ctx.translate(x, y); ctx.rotate(rotation * Math.PI / 180); + ctx.beginPath(); switch (style) { // Default includes circle default: - ctx.beginPath(); ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.closePath(); - ctx.fill(); break; case 'triangle': - ctx.beginPath(); edgeLength = 3 * radius / Math.sqrt(3); height = edgeLength * Math.sqrt(3) / 2; ctx.moveTo(-edgeLength / 2, height / 3); ctx.lineTo(edgeLength / 2, height / 3); ctx.lineTo(0, -2 * height / 3); ctx.closePath(); - ctx.fill(); break; case 'rect': size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.fillRect(-size, -size, 2 * size, 2 * size); - ctx.strokeRect(-size, -size, 2 * size, 2 * size); + ctx.rect(-size, -size, 2 * size, 2 * size); break; case 'rectRounded': var offset = radius / Math.SQRT2; var leftX = -offset; var topY = -offset; var sideSize = Math.SQRT2 * radius; - ctx.beginPath(); // NOTE(SB) the rounded rect implementation changed to use `arcTo` // instead of `quadraticCurveTo` since it generates better results // when rect is almost a circle. 0.425 (instead of 0.5) produces // results visually closer to the previous impl. this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425); - - ctx.closePath(); - ctx.fill(); break; case 'rectRot': size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); ctx.moveTo(-size, 0); ctx.lineTo(0, size); ctx.lineTo(size, 0); ctx.lineTo(0, -size); ctx.closePath(); - ctx.fill(); break; case 'cross': - ctx.beginPath(); ctx.moveTo(0, radius); ctx.lineTo(0, -radius); ctx.moveTo(-radius, 0); ctx.lineTo(radius, 0); - ctx.closePath(); break; case 'crossRot': - ctx.beginPath(); xOffset = Math.cos(Math.PI / 4) * radius; yOffset = Math.sin(Math.PI / 4) * radius; ctx.moveTo(-xOffset, -yOffset); ctx.lineTo(xOffset, yOffset); ctx.moveTo(-xOffset, yOffset); ctx.lineTo(xOffset, -yOffset); - ctx.closePath(); break; case 'star': - ctx.beginPath(); ctx.moveTo(0, radius); ctx.lineTo(0, -radius); ctx.moveTo(-radius, 0); @@ -146,22 +132,18 @@ var exports = module.exports = { ctx.lineTo(xOffset, yOffset); ctx.moveTo(-xOffset, yOffset); ctx.lineTo(xOffset, -yOffset); - ctx.closePath(); break; case 'line': - ctx.beginPath(); ctx.moveTo(-radius, 0); ctx.lineTo(radius, 0); - ctx.closePath(); break; case 'dash': - ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(radius, 0); - ctx.closePath(); break; } + ctx.fill(); ctx.stroke(); ctx.restore(); }, @@ -224,5 +206,4 @@ helpers.clear = exports.clear; helpers.drawRoundedRectangle = function(ctx) { ctx.beginPath(); exports.roundedRect.apply(exports, arguments); - ctx.closePath(); }; diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index b2f803416b5..f1da35a50d4 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -232,11 +232,11 @@ describe('Point element tests', function() { name: 'beginPath', args: [] }, { - name: 'fillRect', + name: 'rect', args: [0 - 1 / Math.SQRT2 * 2, 0 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] }, { - name: 'strokeRect', - args: [0 - 1 / Math.SQRT2 * 2, 0 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] + name: 'fill', + args: [] }, { name: 'stroke', args: [] @@ -358,8 +358,8 @@ describe('Point element tests', function() { name: 'lineTo', args: [2, 0], }, { - name: 'closePath', - args: [], + name: 'fill', + args: [] }, { name: 'stroke', args: [] @@ -406,8 +406,8 @@ describe('Point element tests', function() { name: 'lineTo', args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], }, { - name: 'closePath', - args: [], + name: 'fill', + args: [] }, { name: 'stroke', args: [] @@ -466,8 +466,8 @@ describe('Point element tests', function() { name: 'lineTo', args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], }, { - name: 'closePath', - args: [], + name: 'fill', + args: [] }, { name: 'stroke', args: [] @@ -508,8 +508,8 @@ describe('Point element tests', function() { name: 'lineTo', args: [2, 0], }, { - name: 'closePath', - args: [], + name: 'fill', + args: [] }, { name: 'stroke', args: [] @@ -550,8 +550,8 @@ describe('Point element tests', function() { name: 'lineTo', args: [2, 0], }, { - name: 'closePath', - args: [], + name: 'fill', + args: [] }, { name: 'stroke', args: [] diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 535b9af3307..11d25d1d9b6 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -113,7 +113,6 @@ describe('Deprecations', function() { var calls = ctx.getCalls(); expect(calls[0]).toEqual({name: 'beginPath', args: []}); - expect(calls[calls.length - 1]).toEqual({name: 'closePath', args: []}); expect(Chart.helpers.canvas.roundedRect).toHaveBeenCalledWith(ctx, 10, 20, 30, 40, 5); }); }); diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js index 0c20628d456..1a342c1cb3b 100644 --- a/test/specs/helpers.canvas.tests.js +++ b/test/specs/helpers.canvas.tests.js @@ -36,7 +36,9 @@ describe('Chart.helpers.canvas', function() { {name: 'lineTo', args: [15, 60]}, {name: 'arcTo', args: [10, 60, 10, 55, 5]}, {name: 'lineTo', args: [10, 25]}, - {name: 'arcTo', args: [10, 20, 15, 20, 5]} + {name: 'arcTo', args: [10, 20, 15, 20, 5]}, + {name: 'closePath', args: []}, + {name: 'moveTo', args: [10, 20]} ]); }); it('should optimize path if radius is 0', function() { From 352268616b439017c1ed76eec512de99105db4a3 Mon Sep 17 00:00:00 2001 From: Colin <34158322+teroman@users.noreply.github.com> Date: Sun, 29 Jul 2018 17:09:16 +0100 Subject: [PATCH 420/685] Fix min and max option checks in linear scales (#5209) When 0, the min and max options was considered not being set, instead we should check for null or undefined. --- src/scales/scale.linearbase.js | 2 +- test/specs/scale.linear.tests.js | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index ce53da7b98d..c78dc64f298 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -36,7 +36,7 @@ function generateTicks(generationOptions, dataRange) { var niceMax = Math.ceil(dataRange.max / spacing) * spacing; // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + if (!helpers.isNullOrUndef(generationOptions.min) && !helpers.isNullOrUndef(generationOptions.max) && generationOptions.stepSize) { // If very close to our whole number, use it. if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { niceMin = generationOptions.min; diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index b42675d8a8d..ab046078391 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -965,4 +965,60 @@ describe('Linear Scale', function() { expect(chart.scales['x-axis-0'].min).toEqual(20); expect(chart.scales['x-axis-0'].max).toEqual(21); }); + + it('min settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [2500, 2000, 1500] + }] + }; + + var chart = window.acquireChart({ + type: 'horizontalBar', + data: barData, + options: { + scales: { + xAxes: [{ + ticks: { + min: 0, + max: 3000 + } + }] + } + } + }); + + expect(chart.scales['x-axis-0'].min).toEqual(0); + }); + + it('max settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [-2500, -2000, -1500] + }] + }; + + var chart = window.acquireChart({ + type: 'horizontalBar', + data: barData, + options: { + scales: { + xAxes: [{ + ticks: { + min: -3000, + max: 0 + } + }] + } + } + }); + + expect(chart.scales['x-axis-0'].max).toEqual(0); + }); }); From 9a295816b3ed406ff34d078746d8afd94d0b13a2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Lokhorst Date: Sun, 29 Jul 2018 22:16:10 +0200 Subject: [PATCH 421/685] Replace ES6 by Webpack in the integration docs (#5555) --- docs/getting-started/integration.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md index 173448f58b4..95ccbc46d0a 100644 --- a/docs/getting-started/integration.md +++ b/docs/getting-started/integration.md @@ -2,13 +2,6 @@ Chart.js can be integrated with plain JavaScript or with different module loaders. The examples below show how to load Chart.js in different systems. -## ES6 Modules - -```javascript -import Chart from 'chart.js'; -var myChart = new Chart(ctx, {...}); -``` - ## Script Tag ```html @@ -18,6 +11,13 @@ var myChart = new Chart(ctx, {...}); ``` +## Webpack + +```javascript +import Chart from 'chart.js'; +var myChart = new Chart(ctx, {...}); +``` + ## Common JS ```javascript From 91608398b683afa627c9ab65a65d3e9b50cdb794 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 9 Jul 2018 21:14:25 +0200 Subject: [PATCH 422/685] Add "point style" image tests Replace the old style canvas "mock" context checks by image based unit tests which are easier to maintain and allow more flexibility in the drawing logic since we are not testing the context calls but the final painted result. --- .../controller.bubble/point-style.json | 129 +++++ .../controller.bubble/point-style.png | Bin 0 -> 6566 bytes .../fixtures/controller.line/point-style.json | 98 ++++ test/fixtures/controller.line/point-style.png | Bin 0 -> 6273 bytes .../controller.radar/point-style.json | 95 ++++ .../fixtures/controller.radar/point-style.png | Bin 0 -> 6986 bytes .../element.point/point-style-circle.json | 67 +++ .../element.point/point-style-circle.png | Bin 0 -> 11926 bytes .../element.point/point-style-cross-rot.json | 67 +++ .../element.point/point-style-cross-rot.png | Bin 0 -> 5455 bytes .../element.point/point-style-cross.json | 67 +++ .../element.point/point-style-cross.png | Bin 0 -> 3944 bytes .../element.point/point-style-dash.json | 67 +++ .../element.point/point-style-dash.png | Bin 0 -> 3375 bytes .../element.point/point-style-line.json | 67 +++ .../element.point/point-style-line.png | Bin 0 -> 3391 bytes .../element.point/point-style-rect-rot.json | 67 +++ .../element.point/point-style-rect-rot.png | Bin 0 -> 6520 bytes .../point-style-rect-rounded.json | 67 +++ .../point-style-rect-rounded.png | Bin 0 -> 8262 bytes .../element.point/point-style-rect.json | 67 +++ .../element.point/point-style-rect.png | Bin 0 -> 4857 bytes .../element.point/point-style-star.json | 67 +++ .../element.point/point-style-star.png | Bin 0 -> 6115 bytes .../element.point/point-style-triangle.json | 67 +++ .../element.point/point-style-triangle.png | Bin 0 -> 9173 bytes test/specs/controller.bubble.tests.js | 2 + test/specs/controller.line.tests.js | 2 + test/specs/controller.radar.tests.js | 2 + test/specs/element.point.tests.js | 472 +----------------- 30 files changed, 1000 insertions(+), 470 deletions(-) create mode 100644 test/fixtures/controller.bubble/point-style.json create mode 100644 test/fixtures/controller.bubble/point-style.png create mode 100644 test/fixtures/controller.line/point-style.json create mode 100644 test/fixtures/controller.line/point-style.png create mode 100644 test/fixtures/controller.radar/point-style.json create mode 100644 test/fixtures/controller.radar/point-style.png create mode 100644 test/fixtures/element.point/point-style-circle.json create mode 100644 test/fixtures/element.point/point-style-circle.png create mode 100644 test/fixtures/element.point/point-style-cross-rot.json create mode 100644 test/fixtures/element.point/point-style-cross-rot.png create mode 100644 test/fixtures/element.point/point-style-cross.json create mode 100644 test/fixtures/element.point/point-style-cross.png create mode 100644 test/fixtures/element.point/point-style-dash.json create mode 100644 test/fixtures/element.point/point-style-dash.png create mode 100644 test/fixtures/element.point/point-style-line.json create mode 100644 test/fixtures/element.point/point-style-line.png create mode 100644 test/fixtures/element.point/point-style-rect-rot.json create mode 100644 test/fixtures/element.point/point-style-rect-rot.png create mode 100644 test/fixtures/element.point/point-style-rect-rounded.json create mode 100644 test/fixtures/element.point/point-style-rect-rounded.png create mode 100644 test/fixtures/element.point/point-style-rect.json create mode 100644 test/fixtures/element.point/point-style-rect.png create mode 100644 test/fixtures/element.point/point-style-star.json create mode 100644 test/fixtures/element.point/point-style-star.png create mode 100644 test/fixtures/element.point/point-style-triangle.json create mode 100644 test/fixtures/element.point/point-style-triangle.png diff --git a/test/fixtures/controller.bubble/point-style.json b/test/fixtures/controller.bubble/point-style.json new file mode 100644 index 00000000000..a645075168b --- /dev/null +++ b/test/fixtures/controller.bubble/point-style.json @@ -0,0 +1,129 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3}, + {"x": 1, "y": 3}, + {"x": 2, "y": 3}, + {"x": 3, "y": 3}, + {"x": 4, "y": 3}, + {"x": 5, "y": 3}, + {"x": 6, "y": 3}, + {"x": 7, "y": 3}, + {"x": 8, "y": 3}, + {"x": 9, "y": 3} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 2}, + {"x": 1, "y": 2}, + {"x": 2, "y": 2}, + {"x": 3, "y": 2}, + {"x": 4, "y": 2}, + {"x": 5, "y": 2}, + {"x": 6, "y": 2}, + {"x": 7, "y": 2}, + {"x": 8, "y": 2}, + {"x": 9, "y": 2} + ], + "backgroundColor": "transparent", + "borderColor": "0000ff", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 1}, + {"x": 1, "y": 1}, + {"x": 2, "y": 1}, + {"x": 3, "y": 1}, + {"x": 4, "y": 1}, + {"x": 5, "y": 1}, + {"x": 6, "y": 1}, + {"x": 7, "y": 1}, + {"x": 8, "y": 1}, + {"x": 9, "y": 1} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{ + "display": false, + "ticks": { + "min": 0, + "max": 4 + } + }] + }, + "elements": { + "line": { + "borderColor": "transparent", + "borderWidth": 0, + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bubble/point-style.png b/test/fixtures/controller.bubble/point-style.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d3b8168c07d1e70eda4c04dc50325d25c0b39c GIT binary patch literal 6566 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/point-style.json b/test/fixtures/controller.radar/point-style.json new file mode 100644 index 00000000000..913c1cb87cd --- /dev/null +++ b/test/fixtures/controller.radar/point-style.json @@ -0,0 +1,95 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [{ + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scale": { + "display": false, + "ticks": { + "min": 0, + "max": 3 + } + }, + "elements": { + "line": { + "fill": false + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 512, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png new file mode 100644 index 0000000000000000000000000000000000000000..562cb620b054de95898cba7bbc3908b479bfe2da GIT binary patch literal 6986 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 P!tmG;N89qlzSsT(xdDrf literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-circle.json b/test/fixtures/element.point/point-style-circle.json new file mode 100644 index 00000000000..714d421a049 --- /dev/null +++ b/test/fixtures/element.point/point-style-circle.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "circle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-circle.png b/test/fixtures/element.point/point-style-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b9bf531b237df3f2078a114c527f806694d28a GIT binary patch literal 11926 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-cross-rot.json b/test/fixtures/element.point/point-style-cross-rot.json new file mode 100644 index 00000000000..e81b8b73113 --- /dev/null +++ b/test/fixtures/element.point/point-style-cross-rot.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "crossRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross-rot.png b/test/fixtures/element.point/point-style-cross-rot.png new file mode 100644 index 0000000000000000000000000000000000000000..3f6d1df091c73fd4644e0d1baafc8e7c00c2a471 GIT binary patch literal 5455 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-cross.json b/test/fixtures/element.point/point-style-cross.json new file mode 100644 index 00000000000..5eb99441fce --- /dev/null +++ b/test/fixtures/element.point/point-style-cross.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "cross" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross.png b/test/fixtures/element.point/point-style-cross.png new file mode 100644 index 0000000000000000000000000000000000000000..ecf3cda77e88e4f2a2909775f647f59391b571de GIT binary patch literal 3944 zcmeHKdr(tX8b9}Xu>lr|C`DY*D^yUd1X0>jSh;TPBFl4u5Ia0A2nZrE?O;I&H*KYM z3X+wThmVHcYSBajmh!5W+}44_R?$%-^3Vj!f*XUjyiI`Yx%bkY-JQb2LqIX*zvMSE{Lz5My1KycSTKPEW!icen)GYZ)s82#RsInVyZ!0T&i6!wC`&tOW@uJ&NCmBEXh} zFWlL2M8AYf8%Rn}w07{OYYnrO0}jw@FwU0cTwquASVJ!)2++2O`KkRY2{PN>4)$O? zqm(PVg`E{fTs{-v%k0htj~BvNhl7>g_x@~6??VFNbF?dxcnv{r2>)>;jM)bx0~lA> zN*fMY5_%wwi4+F|jH_oM!<&3|%g=!EvzK3l(_LAtHm?1&5~$m#StU38mf!CgqZ$_RJV9lAjT2-kf`=f|-d3P#JN`$Sa6GL!@NBtdOM$Z> zh9)Nc3kV`~I1MxeYv7C^Tv1UpKVCbvEq#9Gc242@)-?4sDoLO*b_f60x^mzv{nUY` zwLcKx{e&V+D)Bo8BuS`wT47N0`|S%(nR8CYfwaHCsr~d+I*Q`0LPUDOk#&{nfdi4P z6L5}4e$pUpL!+fbF6KAS?dlUGjCEqfz8n-!-2i-*pE^{>5g;wj9sXCH`*CoIjWJm% zSHV+nXVlRiBz;i>V`53TE$oA>*c0y zsz&@Lex$;ndoH6ep?U5Hebb;iX;v`vTG7SXgHvPLJj+BBFRHodozao0Gvw-ho9!ny zKhY`VxdLsZ+)yR6qI9bs?!GUPDqI_4|_dIu5xTgDyINFKn10 z&csqKXiqeA@YF}{aN-=4M|iF6hiu8xdKOy5W{9)+HlVp{^V0_ZH})pgU~?k_Das#3 z^)BlEm_imhP!b~?xWIFpiGwfN<3%fE8io-XEd=FG7R)XX%tt+DRm9cy+J`o7YfzlUjLd|+{R%?7e&l@M8>%4?xa|0UT3 zGci2Enl+v#!A?<55*uqNj7Z53F6A0V1=e-%i9?9~Rthbs9=1=y>3zC`> zJD&9Kft;}A8;kP`|7HLsVdDb(qxeYHSS>YwUVgtp4)y2n=8jWtIEdI)ILckn97Ot- zUp}GN^5%INhe_m`T%dZW^XpV2xkj_W>FLYj2Y2lEnu4+9IXCg`oQd`iEnb6Fs%6C! zhZS3k=f35dqbbGX$?p%^l9%6Zu<&kG$;`IouAPXmGztuidI06U^@z|YYwMJaEhW+= zTTxA0$BZhbXiY6gSq|LJ^GZywGKmTEgvz%8Dlx5mj=*0~VI2m`RFwT6LtI)QeQALd z(2>`$3fcq}bYac^Hh?ORL1IK7xVq17qE6Odq0{ICUnh%^hCpvNGXzQ1!UD&Z1Al_S z3=X4vx5a$T_$awNXnI^1)LVsi=rrXJE>wUQ4D+MOrEJPM+!e60-y`8kk>kv>#ve8j zpTp@TJHWa>~LCEV7+m*jhdH0`vLB{p~ literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-dash.json b/test/fixtures/element.point/point-style-dash.json new file mode 100644 index 00000000000..16f059eef0d --- /dev/null +++ b/test/fixtures/element.point/point-style-dash.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "dash" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-dash.png b/test/fixtures/element.point/point-style-dash.png new file mode 100644 index 0000000000000000000000000000000000000000..9c381d83d54687cef9b6cfbbe44f692e47775042 GIT binary patch literal 3375 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is?dzInPhhE&A8y>YQu)>WYG z;w+s8gAbEf1=_AhGDO}`<7Nrm;2?Iw#gL=oCVNTftGlZMuh&<+yJuaxO11ux#(zNt zhXw{lra~t7J4e{mz-$&Th6)xI4gm!Rg#ZTz7QMSCz#IpL3g%H!1&7ga7)=kO`C_yr zpr|DHyMN;O{8g_`ullv})eq%q53jP@+pk}>{rtZ-^FxpKum5K}vHnBNnpNlXdj9Qu z^+S8oGgs&NK=rT1|8y>2^?17Y9r>ECf8O1C*e9R&cOS!!GHdaFzka^m%<$vyA%4kk zm!FIO7iN%e=dHYG@qD)%L(Q%ikInww{QOXq>A~|}#&!9+iCyMa9nZN4Q+1{OU ztNQLgJ6rp{^W5=ORsWmfe+HE9sXqAkVEFmZz%p(2|6|+@+1raBuIFy}{Q!t}{QDfv zdZ0E3h-3cOZf1H=x8t7?sKgRb*r4>_Kf{_8JDowTSQayJ0fyQ77oH1)TD6U$jEox= zJ>vr_yb)E`9h?~BD!_6< zPM^Ve!UrY;=SkuX;*&l!F=z`sU}X%eS!cC?b5Z$Xwb?b*|8IxtXD5H&_D4X$p@D&s zsg!B^u3lXq5R-*NhxLFUSiI4Rk#&JViMB9U5$gfoQBhzBjfTT$dKk?Ypdw+kd?2$# z)cf*!_SS3D<=6hb9JSs4mBhTB^X%2u2 zzkhSym;d$hYtq`UUzywgZ@Bh$I{Uu*=PzIHKC^S#vR!`*-m9`EXtmEMUzh)XDZ?K* z<9mh8G4X%enLhm6u~p9K!`Ek$TnFO$CI2q$kJD%7I&hv}^~aOP*T2s!_Vq2@y?Z~C z!PMq*4p8atFrk4-K%^}Y%sDWnfyqGitt_xk5Ks_MS-`;3nkWM17^p5_@bRo}2iF%Y zoEi*{i4v_~jt`I%eCeJdxLR>!;&KR(aD#AynYa#2*)nR)XrPUzw2_f-4ByARzZSLL zPU`vBsO{ekAIqAb_*%5~>+882?bnq4eg!F-64!ox#Oc33=JmJR!u#yMTz>sE>h)XZ z_WJGD-kxQ?zyI@>($sUNrdQv-`(FVm<3t*s_c}3F*!i<2oDX9Bv1K+>!TV(*4SV-n z#0!FJrNyiw4T~Qe+ko1#0yl&M7{1-9aRoPR`@{km`ufg)0F`GB3H}a@H;O-tf|XfB zIWSsS*&PNqZ5nMEfo7i#2P=D!!N~gH#Sb2TunFH-L>g?(M=cs`LDyK%z&pW`aplZ+ Ri+~*?22WQ%mvv4FO#tm-dMN+^ literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-rect-rot.json b/test/fixtures/element.point/point-style-rect-rot.json new file mode 100644 index 00000000000..85875655e1c --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rot.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rectRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rot.png b/test/fixtures/element.point/point-style-rect-rot.png new file mode 100644 index 0000000000000000000000000000000000000000..09c0adac3d3e32580845000e7188e63fb60b7c95 GIT binary patch literal 6520 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-rect-rounded.json b/test/fixtures/element.point/point-style-rect-rounded.json new file mode 100644 index 00000000000..6d65b46d820 --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rounded.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rectRounded" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rounded.png b/test/fixtures/element.point/point-style-rect-rounded.png new file mode 100644 index 0000000000000000000000000000000000000000..a58e9e62361a96fb187cc342c58d89c954faaaa4 GIT binary patch literal 8262 zcmeHN_gfQNx1J;<1PBn7UZY4+EK#Z=B#H&3swf?TDAJ2akx&wef=bhafPxT@iXc*? z3j`u478DSSbO<0)BSET^_fGxX<@P9+=GDtIb~Tde@rVv@|~=zx#zzlZg*vhN_)C?KJ$yIgC#+@+*_2YGX1qOiv!nQ@ z0(I(L6g3a(g|NNm0^ihnmT+p5DN_d6q?{W~CfJ3*`R-wFQK8Vg4mRReQjq)IXCkJ7 z*|hr|i$Q(F>gC84xt6#~&-1wRxgnKx9Tn9(nU6?NK{YA~xI#1_Bqe#6#d% zV95~t7Q9906O;l}{M+Ps`hPF=cy$C>`S~Ic(Q;P7BFQ# z(Yvr=84BQ_)(sX^!;+y0l0e3lm=_yrA@r=jtXBbz!IJ;@nIyih79keebDI7OAz9#tvR56?lZ&8?Xx!Sjfg8g|p+u3# zXc!%9#X7TY8P$~hzJt2Vsxy1%4b>an^BzdD|H|P7^P`x1hacyRaLIrG7D)c>>dD{< zI{-f))P(C>_M3ef*nA0!0(>$(z0Rx$MyeJ+?ZKUFT%tSo?aSteb$pl)CPAoss#!dF zRXflJP`af_uW5;vjGiSdps7*{TMr=lPx$%y9&S$mtl!BXeR9x);JeK_z<0}1QQqDC z@G$k&r+{VKomzx@&3P(>`eaANIq()C8~Sqjx}PKAvrOIUnnLAW`kDsKVc+Pn1(<=I zDUEhyQPtnH{eduG2G@56?Q<`|PwVtr04)dweSGdBkP4l~_zwc3_ZX!wrn#d%ZdXO< zfaSNu=Zu};!<>h2D}*KRr{qSpHP|)761Mphq7Ax<&mmM=pzB3mC^XQq-ohEq!@*H~ z&5O$HmsRn%JtU2*XY`+?g%PiFbjXd>o5J|1MYa_V>*m^#-(O~gEU}k7dcOIwtqEJ8 z9p7+INht^tROQ2F-np0%I2sJAbts&{=eJoyxZ17KK7QVS`wGf73o2-ujK3hxjHjQY zpFGnTxz}IZUxln_o@~_MGU1D<0&tk)lrY$K&mwX6w)nl-9j7aohZKAGbJk;$eLoeo(~=p z2l#LNN?-QjJI4}x0->I1A18=O(eM{!b0ox!D+-jXrp=<#=!z$tO$V*xruw4%XwCRN zszp~H-q^xu1T3Sfgn{{d?=A@Wy*eqL94O$>OWqV8f~47htDrYTYJ}k)r3oKz^jd1o zJJD}T3vrsRyg^@{e_(E@4#Tf~&<4I%6ki;K(nRy-(2ULWL?olSUcroSn+YsXvhA%g z#WOJ=@h~3>h<4vylMkWRJ_0}Yb_+?4!AYfWM(@&(@nxP`ifweRg z{Ig5yZGzt8Tj}YfQkQpHVZv(IPD4^4(p#G;UNz7Qx|_grHy6x()LG20Bg`JH!hnNh zN3ZgS8Ey#Da-h#&@+=8s&^yzxmh>M7Ltyq>(lqA6PFB;|640uf^x>1kHr_M@$=vK& z^eH^K4ZbZASjabEO47vGFZm+REL+B$j{1&d_8E@p(GJ%1p_k>IQxT+@F5*RAx_knY z!tVRmPqgfx2^jy@|9kt+#bsHQ{Ch7)OXb-JzR2{KEi)k(0V%GW4G(ef$I0(C$Xv*g zY^TMo?3<<$WzS?`c;b)xle{e6FK~2WYC5mTJ6d6M-fvB@mzubcDRTgI%f48Tc0Oam z_saK^H{x%xdC*ASDsc20+(6`zQ0N?BZLEI72`bk*u5X9}V5>%*Ry+VYM7D_#PpYRs z(5jVK()U*;uQbVyk;LD5O1315gnp|`pSED8WgiRWr{+>ec}SeBj<%JpEGUA!Fk~)g zYsmr+1jB0A!4lsE`i}klyYUTH-xp)`=;@3L;BM;9&r8;0Ldo-T_!NGiJzN^N#mOxT z<%O#edPn?Gl~B=<;o2KLuOHI8`F_3+kcQz!j;-+OgPJ|2cniBMMfCh7Xh|T$9eA(o zf#V6hD0I}EFW7G!W4_I*C*hJF3g}W}8S#7yCEKjspt&|R21q=rGQ2a71ul$~YVh<8 z!MlT0F)MzY+0l1^<*LPV7*EanJ*`9)OoTGFfFITliz0qsZH{_=2ZC9#h?ieSBf%?2 zsEfgPyB*kwMT6%-P~S2BMAysuth$_6Sm4Ugv#*DF1qgMPT5+CGrJi);@WIUV@i5%j z;vY_|^oOq3>#BmJfr*n{=IaLH$sz($z(LLXBF8o^tH82Vb-J7f7xmVKLf{9t6M(VS3kStEE+@b;`(Ks+ z?*mH^g9aQRl$lyriP!htUzW*{h0*a2+B^!Dwu@Z3oed=f5^i&{)S)46mqT~4qv#ju zPuFmD{DXe?mz^)@AV@AkJ$1Ycegq95GA)^+LXr3wuUQeb0v%<-3qCpMU`S#F1Uxr=y$`Cx_@*3XaycMpJ zu^~XBUrP)(JSi+;Vlyg4#*T3snOFL<>R5*u zw^QiSV82Qb2kF%OzGW!gvW8ZFchXZB(x*Sh^rn_{&&-2mFpFlresCprqT6Ba<7F>=!U_-V_Fsi| z7F8TRsp~+`g`Ra^-a?r};eRMOeH}5~IcaqBPddF(mlDe7uS!vp z+ziOn<>@t&w3w(d818B}_0$&$3 zcs!7w@A=4KoDj&k?V3ewe6kZgT$0T&Q@9ZkdOZDLBpf@|D-9sT(|-ky8j{K&!a!NB zcMb_Ev?WIYYFe20^x}b09N$tnlZG~L^R^_XEgTrg|6xc{1yoniOv88@%#y1LQs;q> zDS~Q6Qskv;{S96WFH)+e<1^yrT-RmK2?^whz5N)K$vN|7SJe*BN93=1Kv@(9^_M0= zlrtvV$Gm+qLeX|^PjEmWlfYCJjas_ABwoHE7;u~88PQ;e9MeIY`&Jz8n?M0UPraXk z2RF`$x{TIC8#zBUbrf6@B=m7k(@(_bP_OOTq6k@q*Ps4PoFfQ6B^-lAun=@4qGP)T27&Y!i-lxFu;g)6@hK=o=jERy-h7jV;Og`m#(Y+Im z6Mn&PhRX~#}ccYrBm7Wy+;bc{jL+b`A+*SmN0$y#6_+_;MKnJ$ERf=yz9r~h%O z5)V5r!-qbC4En)p)99dvAH{aIQ7_|aNsi|Mk439@@$?k5Z_C+a2qyK(8m}i5+lfwv zHuIOC?`RLJ`&}^}+GWkmtA27T5_(9eUjM*H5S9KOwSh$P5#7Z*Qre+Nc$Y?nqw6c8 zZRqkb_N?l&DiJb!`hcM3zl)E+hjvmzVMQowZ>X$uS98FVZtaJ#eZ6L=#AtR*_H~(m z`|{V&nLhfan!ys<^jc5&)KzGP4efp?hN6_7;`ujKBthXqZ|0Z-ZlAiuGVe+ZdClDM zG@aZ;S4Nk6+U5SkW{)A?D3j(%46hfSsS=s5c6Kzlu|{rM00nOI_QcyW$^NTrpJ%-s zh3fs%lflW^pQEqVk!2EoJ}i6{O;BRqfh7}h{=5D6`|DEn)oszikCdf1c32G)N5=iT z3(8t#yc(<~r3;OeYY_E2%slr2nqOwur+3scDC2T@7N64^{$?WAhbGSQb%qyotLgh| zbH54^*C=yE4lxOwMeg(}T5i5zf8+c@BK-^h6FaVO{VB6s+W*M82W6;+gcU}3DFi6W zEgKk#COy@fnp6lWXeBPLk{%BxBr89cpZpes&3-yFAz@%ty(B>JGpH4<*}Yaq8t#m6 zsdGL3=-rXdLgTmpNquW!OX#Q&*>LV}_7mq%H6OW-;^jEmJ!X1?mjx+TdTNwT91;k1 zoWHR4bG#5Pm-|iZ2Xi}8)c2fbbek}sfuCJ(*{L*WfFZ$rK7yO-46lCJUGsHLB+PF6 zsGmu8ZSW0j_0?xSW3PeM8@5FVa5Zx)G{6=^%cAWlsuz1eBoEA$pA?pC(msocMC-=- z>>GavWefS zi$91bWj32z6yWC4s!`z){7ZA`?Y=&NucB@S#k{l*dzH2i{-^u*@aQK0AY!h#jCd-Pyi11WE9&wQ{f-^$`l0M$Ll@H< zBvP}rab&h=w8PULA8?>J+QB2G^5|G+sMCBBd5V9q6QszdhB;7%gWG4~c2$;$SO3>M zrHIh^bCbU++R1JzwOe#=g@j8PBy+0Lh9~9+Z`PR$7m$lM_|)+$uHa~OTV@R&*fE9V^BvaEV0FjZ&c`$C2wpdIM38=swr=7f{~So?#R^?=iRVV^^G6F)I!#9x&Jx-W_)kuZYo!WrebOhT z=MT;Su{T8yuh$~xVKT>&R97e6Ox$#6TvFqmF%g*3l-&~+HZg33>Al$gE8J?W5^veG8J7Z|=b4Lv^}!2pN`eL+_mFXdluWaPeI;@9y6pxw$jX zuEe#_qKww*6p%g{Z99WSsDf?KqGth=sJ$^kt@i%;U|nnat+VoJ1djn38LD^KmWzW6eC_dgnkBK)c*Tg7P+ zuIsTFoH(rIOgY0jz*sUy`gz&C_@~|pg0r9YwYe;aiT{U`tKZC|ZZt;MtzELe4K`+R zH;XMmck$$Fd_()_wTFI>(&j}{Xw{BoEshB7YPra@H&z`dFd+7m5hS@=3%nPrmPL36 zL?tp*usU44_U<{V@0XXe9XZ+|6Mea%VFhJUC}8&kR}&u1luE@r2pd^+<~KIAP_`_R zMt;gz@1srjm&w&Eo3Q8Q+ql&Np*tx9BRf9=NhWQPJpTnHV?FXIepZDhzDrF`$k6V$ z!_w_-{_yh@*7f z<|VIM>mINHs{Ya7xuHhtCipgb`zNQP>zRrt2ZICrZY{-gk9lh7MxYucmxXN`Hk6vs zf!2cGMKT*|j&CCXQUWECfAtQIpbf#FV;CE1u$#eG=0<6>*bN(4V4qd$W$@zohMGIb lRh)zP4#8_#``3RNu>M;B`>*8rWbgtoK4yNj;0WQ`{{U`D&pZGC literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-rect.json b/test/fixtures/element.point/point-style-rect.json new file mode 100644 index 00000000000..5e2be596366 --- /dev/null +++ b/test/fixtures/element.point/point-style-rect.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "rect" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect.png b/test/fixtures/element.point/point-style-rect.png new file mode 100644 index 0000000000000000000000000000000000000000..493c9a692729feab5ccf8665ad766ffcc1d45898 GIT binary patch literal 4857 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 Js@y^O=|7H?N(le} literal 0 HcmV?d00001 diff --git a/test/fixtures/element.point/point-style-triangle.json b/test/fixtures/element.point/point-style-triangle.json new file mode 100644 index 00000000000..e7657f1af25 --- /dev/null +++ b/test/fixtures/element.point/point-style-triangle.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "elements": { + "point": { + "pointStyle": "triangle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "xAxes": [{"display": false}], + "yAxes": [{"display": false}] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-triangle.png b/test/fixtures/element.point/point-style-triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..d2cc5c6f1e8d55281ef142dc347db3dfd91b7b14 GIT binary patch literal 9173 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

    GI>)?*qwyPYj!$Gn8x`h-c`5r zu<$>6`fJ|1S512xqHAe!NKCd!ceI!s?`YWG%Vm2%Kmbi~`O`Znj@@Pq*%bKZ`3dgue=u zL#1i3@U`1|-6`*U8z=rqW1lNRWxjdCS~q6k3jlJZX1pk8^o7qHJ5Rk-KXoy6SBBfj zL^hxtR0etYmHBymdyPx(FXP~MDk0~ZRtkOq%a$v*-dyfWra_gJ%f1A1Y1yJDy{E5XnVdF{>;|7a znz`O9m4LGLZLswIv2_DCogV$78i`&Ue3mX{%OxjT6SmGhIFb<9@5+~Jqnus%4L1{= z5h79pO^3s1-^pSgitNBb#&1;0N8T?v5Jq%9sqzS)edxkzcZ}VfI&^&g)~41)9$HXQ zK5A?Z+PLvf0J_+}>Jn~hn=xbX`Qz8A_Y*2Z-YXZ{v|gHT86T%Q2y`g-5@&eJ8mtXf z9Y_-26V?*ZjnKZI=BhQ}fwL(9{9I3QivO~X)Y`FR#LpYPyN0-YSpjvonU53cGn^B0 z9kF^Ik2ogZxTaY#zkc8LvDPIysiLg-l@bDC?k3+-c6^IqKDOWYHLTV0whm|OD~Hg* z-w~mxSh)e?G?ph%8O5F0ZSS+;tG1TzCvMToqe7}O zU3NKAj6M0r&N6*i*Z1xHODL&5;G5qBZI}+t?odZ7SNy7*iiMv?WW*J4*DdenueDon zSN`qe!Fd9X4G1&;1zN3V(|dw}qxID^zk5lM)s20cyt-~dqxZN4$>xUJIwRkQ1}9sm zWr|upgc%oW2nB-0`%99zoneygQ`EOC+>mWn)S0}{9ae!|q5h^;tGB$Z$DQCUlyIWr z88K$YJNOQsO}n!{%<6!$+~N{bD77|QSI(+oHedbT*#yv8jq&t_FP{MLI9CYgWAC3Q z8Y6_yCa_04SWjIMr8nQoE;Q>$y*=1*pIWj~{6b{oz1b1P&FK}hO4nxL(+Po4;mF?t zAgK<%BE+S`0q`Kq9EsI)_Z>L*dU>_)j}x8aQAg{`8rskz$x0PALH)@a8&j_fFUL@m`mD$&I56P{O_A(8H3j{LqF z>lnFww(`Qva|gnlyzbcWHvauU77LBKL}Ra?*UAP+8Xowt7hPJ{`H@ z*-ksv@y!INr8OA>rP@%3Zne~tF-&q1LQMGRGs|!0{QuZCt!9x4h|S3b3X5aGyu9j@>F#RK@9 zRE{yG0PzwF!OrrTHkIT6`aJGA?jk(oM91vk6j!WrcKN3LY({oNfB3~oWh%gT_I%kf z=g^#qhGbZZ#8SfyAct-gF=Enp!FKfG$m0DHGO#3I=rNO&K~QQZ@Cy%n8kKPz1^IG- zUx=uXRs28xLd2ei+5qBI(a&W4XIXYgD;32GhHg+9;)$RJAgBRa_ILgRQ;)$* literal 0 HcmV?d00001 diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 4799883ddfb..2597b9ae641 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.bubble', function() { + describe('auto', jasmine.specsFromFixtures('controller.bubble')); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'bubble', diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index b03162ea9d4..ed8f4ec5c80 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.line', function() { + describe('auto', jasmine.specsFromFixtures('controller.line')); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'line', diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 8e3b8d49130..2bc0a2e0d24 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -1,4 +1,6 @@ describe('Chart.controllers.radar', function() { + describe('auto', jasmine.specsFromFixtures('controller.radar')); + it('Should be constructed', function() { var chart = window.acquireChart({ type: 'radar', diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index f1da35a50d4..8d1227003b9 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -1,6 +1,6 @@ -// Test the point element +describe('Chart.elements.Point', function() { + describe('auto', jasmine.specsFromFixtures('element.point')); -describe('Point element tests', function() { it ('Should be constructed', function() { var point = new Chart.elements.Point({ _datasetIndex: 2, @@ -94,474 +94,6 @@ describe('Point element tests', function() { expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); - it ('should draw correctly', function() { - var mockContext = window.createMockContext(); - var point = new Chart.elements.Point({ - _datasetIndex: 2, - _index: 1, - _chart: { - ctx: mockContext, - } - }); - - // Attach a view object as if we were the controller - point._view = { - radius: 2, - pointStyle: 'circle', - rotation: 25, - hitRadius: 3, - borderColor: 'rgba(1, 2, 3, 1)', - borderWidth: 6, - backgroundColor: 'rgba(0, 255, 0)', - x: 10, - y: 15, - ctx: mockContext - }; - - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'arc', - args: [0, 0, 2, 0, 2 * Math.PI] - }, { - name: 'closePath', - args: [], - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'triangle'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0 - 3 * 2 / Math.sqrt(3) / 2, 0 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3] - }, { - name: 'lineTo', - args: [0 + 3 * 2 / Math.sqrt(3) / 2, 0 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], - }, { - name: 'lineTo', - args: [0, 0 - 2 * 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], - }, { - name: 'closePath', - args: [], - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'rect'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'rect', - args: [0 - 1 / Math.SQRT2 * 2, 0 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); - var drawRoundedRectangle = Chart.helpers.canvas.roundedRect; - var offset = point._view.radius / Math.SQRT2; - Chart.helpers.canvas.roundedRect = drawRoundedRectangleSpy; - mockContext.resetCalls(); - point._view.pointStyle = 'rectRounded'; - point.draw(); - - expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( - mockContext, - 0 - offset, - 0 - offset, - Math.SQRT2 * 2, - Math.SQRT2 * 2, - 2 * 0.425 - ); - expect(mockContext.getCalls()).toContain( - jasmine.objectContaining({ - name: 'fill', - args: [], - }) - ); - - Chart.helpers.canvas.roundedRect = drawRoundedRectangle; - mockContext.resetCalls(); - point._view.pointStyle = 'rectRot'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0 - 1 / Math.SQRT2 * 2, 0] - }, { - name: 'lineTo', - args: [0, 0 + 1 / Math.SQRT2 * 2] - }, { - name: 'lineTo', - args: [0 + 1 / Math.SQRT2 * 2, 0], - }, { - name: 'lineTo', - args: [0, 0 - 1 / Math.SQRT2 * 2], - }, { - name: 'closePath', - args: [] - }, { - name: 'fill', - args: [], - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'cross'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 2] - }, { - name: 'lineTo', - args: [0, -2], - }, { - name: 'moveTo', - args: [-2, 0], - }, { - name: 'lineTo', - args: [2, 0], - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'crossRot'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0 - Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2] - }, { - name: 'lineTo', - args: [0 + Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'moveTo', - args: [0 - Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'lineTo', - args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'star'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 2] - }, { - name: 'lineTo', - args: [0, -2], - }, { - name: 'moveTo', - args: [-2, 0], - }, { - name: 'lineTo', - args: [2, 0], - }, { - name: 'moveTo', - args: [0 - Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2] - }, { - name: 'lineTo', - args: [0 + Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'moveTo', - args: [0 - Math.cos(Math.PI / 4) * 2, 0 + Math.sin(Math.PI / 4) * 2], - }, { - name: 'lineTo', - args: [0 + Math.cos(Math.PI / 4) * 2, 0 - Math.sin(Math.PI / 4) * 2], - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'line'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [-2, 0] - }, { - name: 'lineTo', - args: [2, 0], - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - mockContext.resetCalls(); - point._view.pointStyle = 'dash'; - point.draw(); - - expect(mockContext.getCalls()).toEqual([{ - name: 'setStrokeStyle', - args: ['rgba(1, 2, 3, 1)'] - }, { - name: 'setLineWidth', - args: [6] - }, { - name: 'setFillStyle', - args: ['rgba(0, 255, 0)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [25 * Math.PI / 180] - }, { - name: 'beginPath', - args: [] - }, { - name: 'moveTo', - args: [0, 0] - }, { - name: 'lineTo', - args: [2, 0], - }, { - name: 'fill', - args: [] - }, { - name: 'stroke', - args: [] - }, { - name: 'restore', - args: [] - }]); - - }); - it ('should draw correctly with default settings if necessary', function() { var mockContext = window.createMockContext(); var point = new Chart.elements.Point({ From 301017373353443755665acf8f4494384c6c9725 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 29 Jul 2018 13:23:31 +0200 Subject: [PATCH 423/685] Disable hardware acceleration for unit tests Explicitly disable hardware acceleration to make image diff more stable when ran on Travis and dev machine. --- karma.conf.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index c41b23e0e03..5bb73ef03a0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,9 +3,27 @@ module.exports = function(karma) { var args = karma.args || {}; var config = { - browsers: ['Firefox'], frameworks: ['browserify', 'jasmine'], reporters: ['progress', 'kjhtml'], + browsers: ['chrome', 'firefox'], + + // Explicitly disable hardware acceleration to make image + // diff more stable when ran on Travis and dev machine. + // https://github.com/chartjs/Chart.js/pull/5629 + customLaunchers: { + chrome: { + base: 'Chrome', + flags: [ + '--disable-accelerated-2d-canvas' + ] + }, + firefox: { + base: 'Firefox', + prefs: { + 'layers.acceleration.disabled': true + } + } + }, preprocessors: { './test/jasmine.index.js': ['browserify'], @@ -24,15 +42,7 @@ module.exports = function(karma) { // https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647 if (process.env.TRAVIS) { - config.browsers.push('chrome_travis_ci'); - config.customLaunchers = { - chrome_travis_ci: { - base: 'Chrome', - flags: ['--no-sandbox'] - } - }; - } else { - config.browsers.push('Chrome'); + config.customLaunchers.chrome.flags.push('--no-sandbox'); } if (args.coverage) { From a9c4e377a7f529a82fc1dbcef32ab23d40c9b158 Mon Sep 17 00:00:00 2001 From: Tom Pullen Date: Wed, 8 Aug 2018 17:44:53 +0100 Subject: [PATCH 424/685] Add color to financial time series sample (#5661) --- samples/scales/time/financial.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 792eef55306..b261b898661 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -56,12 +56,16 @@ var ctx = document.getElementById('chart1').getContext('2d'); ctx.canvas.width = 1000; ctx.canvas.height = 300; + + var color = Chart.helpers.color; var cfg = { type: 'bar', data: { labels: labels, datasets: [{ label: 'CHRT - Chart.js Corporation', + backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), + borderColor: window.chartColors.red, data: data, type: 'line', pointRadius: 0, From ab41173d9b2abb9c543c82c360abb51c3efcd778 Mon Sep 17 00:00:00 2001 From: Tom Pullen Date: Wed, 8 Aug 2018 17:52:56 +0100 Subject: [PATCH 425/685] Fix adding and removing datasets in bar samples (#5663) Account for zero indexing of arrays when creating a name for an added dataset and remove the last dataset in the array when removing a dataset rather than removing the first. --- samples/charts/bar/horizontal.html | 4 ++-- samples/charts/bar/vertical.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/charts/bar/horizontal.html b/samples/charts/bar/horizontal.html index 6450e014d92..e174ad2ca35 100644 --- a/samples/charts/bar/horizontal.html +++ b/samples/charts/bar/horizontal.html @@ -102,7 +102,7 @@ var colorName = colorNames[horizontalBarChartData.datasets.length % colorNames.length]; var dsColor = window.chartColors[colorName]; var newDataset = { - label: 'Dataset ' + horizontalBarChartData.datasets.length, + label: 'Dataset ' + (horizontalBarChartData.datasets.length + 1), backgroundColor: color(dsColor).alpha(0.5).rgbString(), borderColor: dsColor, data: [] @@ -130,7 +130,7 @@ }); document.getElementById('removeDataset').addEventListener('click', function() { - horizontalBarChartData.datasets.splice(0, 1); + horizontalBarChartData.datasets.pop(); window.myHorizontalBar.update(); }); diff --git a/samples/charts/bar/vertical.html b/samples/charts/bar/vertical.html index e9348b274fd..5127d4937c2 100644 --- a/samples/charts/bar/vertical.html +++ b/samples/charts/bar/vertical.html @@ -95,7 +95,7 @@ var colorName = colorNames[barChartData.datasets.length % colorNames.length]; var dsColor = window.chartColors[colorName]; var newDataset = { - label: 'Dataset ' + barChartData.datasets.length, + label: 'Dataset ' + (barChartData.datasets.length + 1), backgroundColor: color(dsColor).alpha(0.5).rgbString(), borderColor: dsColor, borderWidth: 1, @@ -125,7 +125,7 @@ }); document.getElementById('removeDataset').addEventListener('click', function() { - barChartData.datasets.splice(0, 1); + barChartData.datasets.pop(); window.myBar.update(); }); From 3830216420d7f85ed4a037bfb3bc7be1d7e70512 Mon Sep 17 00:00:00 2001 From: Colin <34158322+teroman@users.noreply.github.com> Date: Fri, 10 Aug 2018 08:32:35 +0100 Subject: [PATCH 426/685] Event handling to use target instead currentTarget (#5575) If you attach event handlers to a container rather than directly to the canvas then the currentTarget is the container, event.target is the canvas that triggers the event. It's useful to do this if you have many charts or are creating them dynamically. --- src/core/core.helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 844fa1fd51d..5cd1d8f6e91 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -383,7 +383,7 @@ module.exports = function() { helpers.getRelativePosition = function(evt, chart) { var mouseX, mouseY; var e = evt.originalEvent || evt; - var canvas = evt.currentTarget || evt.srcElement; + var canvas = evt.target || evt.srcElement; var boundingRect = canvas.getBoundingClientRect(); var touches = e.touches; From 1aa54b074bda2f07a315b26d738bd90f617bd278 Mon Sep 17 00:00:00 2001 From: Wei-Wei Wu Date: Sun, 9 Sep 2018 11:42:18 -0700 Subject: [PATCH 427/685] Add gulp watch task for docs (#5724) gulp docs --watch --- docs/developers/contributing.md | 1 + gulpfile.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index fa3511bb09a..551ac71ca97 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -37,6 +37,7 @@ The following commands are now available from the repository root: > gulp lint // perform code linting (ESLint) > gulp test // perform code linting and run unit tests > gulp docs // build the documentation in ./dist/docs +> gulp docs --watch // starts the gitbook live reloaded server ``` More information can be found in [gulpfile.js](https://github.com/chartjs/Chart.js/blob/master/gulpfile.js). diff --git a/gulpfile.js b/gulpfile.js index 24c01665ae4..f6795fa7f56 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -190,7 +190,7 @@ function docsTask(done) { const cmd = process.execPath; exec([cmd, script, 'install', './'].join(' ')).then(() => { - return exec([cmd, script, 'build', './', './dist/docs'].join(' ')); + return exec([cmd, script, argv.watch ? 'serve' : 'build', './', './dist/docs'].join(' ')); }).catch((err) => { console.error(err.stdout); }).then(() => { From bbd589d5ab1606b970d04cb28496e338a7a4c35f Mon Sep 17 00:00:00 2001 From: Wei-Wei Wu Date: Tue, 11 Sep 2018 00:12:28 -0700 Subject: [PATCH 428/685] Add "Accessibility" documentation page (#5719) --- docs/SUMMARY.md | 1 + docs/general/accessibility.md | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 docs/general/accessibility.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8591dad86ae..15265bd2b5a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ * [Integration](getting-started/integration.md) * [Usage](getting-started/usage.md) * [General](general/README.md) + * [Accessibility](general/accessibility.md) * [Responsive](general/responsive.md) * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) diff --git a/docs/general/accessibility.md b/docs/general/accessibility.md new file mode 100644 index 00000000000..a59df93475a --- /dev/null +++ b/docs/general/accessibility.md @@ -0,0 +1,39 @@ +# Accessible Charts + +Chart.js charts are rendered on user provided `canvas` elements. Thus, it is up to the user to create the `canvas` element in a way that is accessible. The `canvas` element has support in all browsers and will render on screen but the `canvas` content will not be accessible to screen readers. + +With `canvas`, the accessibility has to be added with `ARIA` attributes on the `canvas` element or added using internal fallback content placed within the opening and closing canvas tags. + +This [website](http://pauljadam.com/demos/canvas.html) has a more detailed explanation of `canvas` accessibility as well as in depth examples. + +## Examples + +These are some examples of **accessible** `canvas` elements. + +By setting the `role` and `aria-label`, this `canvas` now has an accessible name. + +```html + +``` + +This `canvas` element has a text alternative via fallback content. + +```html + +

    Hello Fallback World

    + +``` + +These are some bad examples of **inaccessible** `canvas` elements. + +This `canvas` element does not have an accessible name or role. + +```html + +``` + +This `canvas` element has inaccessible fallback content. + +```html +Your browser does not support the canvas element. +``` From 7a65546629d8561d63eccb3b07ae44bce54d7595 Mon Sep 17 00:00:00 2001 From: Carl Osterwisch Date: Sun, 16 Sep 2018 05:33:48 -0400 Subject: [PATCH 429/685] Fix scale when data is all small numbers (#5723) * Add test for correct handling of small numbers * Calculate tick precision for arbitrarily small numbers * Use scientific notation for very small tick numbers * Calculate significant digits for exponential tick values --- src/core/core.ticks.js | 12 +++++++++--- src/scales/scale.linearbase.js | 2 +- test/specs/scale.linear.tests.js | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 3898842a49a..f63e525983a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -46,9 +46,15 @@ module.exports = { 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); + var maxTick = Math.max(Math.abs(ticks[0]), Math.abs(ticks[ticks.length - 1])); + if (maxTick < 1e-4) { // all ticks are small numbers; use scientific notation + var logTick = helpers.log10(Math.abs(tickValue)); + tickString = tickValue.toExponential(Math.floor(logTick) - Math.floor(logDelta)); + } else { + 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 } diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index c78dc64f298..7408eb9f989 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -54,7 +54,7 @@ function generateTicks(generationOptions, dataRange) { precision = 1; if (spacing < 1) { - precision = Math.pow(10, spacing.toString().length - 2); + precision = Math.pow(10, 1 - Math.floor(helpers.log10(spacing))); niceMin = Math.round(niceMin * precision) / precision; niceMax = Math.round(niceMax * precision) / precision; } diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index ab046078391..380feaac09d 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -212,6 +212,31 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0.max).toBe(90); }); + it('Should correctly determine the max & min data values for small numbers', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale0', + data: [-1e-8, 3e-8, -4e-8, 6e-8] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } + }); + + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min * 1e8).toBeCloseTo(-4); + expect(chart.scales.yScale0.max * 1e8).toBeCloseTo(6); + }); + it('Should correctly determine the max & min for scatter data', function() { var chart = window.acquireChart({ type: 'line', From 2f9c663d011618e264c2db7a5bccd25c08838e34 Mon Sep 17 00:00:00 2001 From: Maxim Atanasov <32562426+maximAtanasov@users.noreply.github.com> Date: Fri, 21 Sep 2018 21:21:34 +0200 Subject: [PATCH 430/685] Added Wicked-Charts to the Popular Extensions Page (#5734) Wicked-Charts is a Java wrapper around Chart.js and allows users to create charts in Java using the Wicket framework. The latest version of Wicked-Charts (3.1.0) supports Chartjs and Wicket 8. --- docs/notes/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 2dc246c5377..97f70e8fadd 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -57,6 +57,7 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java + - Wicked-Charts ### GWT (Google Web toolkit) - Charba From 9293c30d4f5727c4f64967d709f37f44d43b716f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Mart=C3=ADnez=20Serrano?= Date: Fri, 21 Sep 2018 21:22:31 +0200 Subject: [PATCH 431/685] Add scatter link in charts documentation (#5736) --- docs/charts/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/charts/README.md b/docs/charts/README.md index 26677b04d76..dda7e343811 100644 --- a/docs/charts/README.md +++ b/docs/charts/README.md @@ -4,9 +4,10 @@ Chart.js comes with built-in chart types: * [line](./line.md) * [bar](./bar.md) * [radar](./radar.md) -* [polar area](./polar.md) * [doughnut and pie](./doughnut.md) +* [polar area](./polar.md) * [bubble](./bubble.md) +* [scatter](./scatter.md) [Area charts](area.md) can be built from a line or radar chart using the dataset `fill` option. From 1ba06a26fddde17285072f3a446679e9388b382f Mon Sep 17 00:00:00 2001 From: Daniel Correa Date: Tue, 9 Oct 2018 11:20:09 -0500 Subject: [PATCH 432/685] Add aspectRatio property to responsive doc (#5756) --- docs/general/responsive.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/general/responsive.md b/docs/general/responsive.md index f410826b84f..fd92aa494f0 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -16,6 +16,7 @@ Chart.js provides a [few options](#configuration-options) to enable responsivene | `responsive` | `Boolean` | `true` | Resizes the chart canvas when its container does ([important note...](#important-note)). | `responsiveAnimationDuration` | `Number` | `0` | Duration in milliseconds it takes to animate to new size after a resize event. | `maintainAspectRatio` | `Boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. +| `aspectRatio` | `Number` | `2` | Canvas aspect ratio (i.e. `width / height`, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. | `onResize` | `Function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. ## Important Note From d7eab1b9945a68a0cc0abf7f6ab4fd4c28a5b7d5 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 15 Oct 2018 18:48:00 +0200 Subject: [PATCH 433/685] Bump version to 2.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2e6c494af1..30e365f7467 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.2", + "version": "2.7.3", "license": "MIT", "main": "src/chart.js", "repository": { From dfbb57468f8cb4f670d99f26050fb499c7068294 Mon Sep 17 00:00:00 2001 From: Niladri Dutta Date: Wed, 17 Oct 2018 02:42:51 +0530 Subject: [PATCH 434/685] Add 'parser' instead of deprecated 'format' in samples (#5769) --- samples/scales/time/line.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/scales/time/line.html b/samples/scales/time/line.html index 70a6978af18..77c81b7aa1b 100644 --- a/samples/scales/time/line.html +++ b/samples/scales/time/line.html @@ -106,7 +106,7 @@ xAxes: [{ type: 'time', time: { - format: timeFormat, + parser: timeFormat, // round: 'day' tooltipFormat: 'll HH:mm' }, From f815dd51968a0bec9305e1cf14d738d6f23c93aa Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Thu, 18 Oct 2018 15:28:49 -0400 Subject: [PATCH 435/685] Ensure that when the time axis accesses `data.labels` it actually exists (#5750) --- src/scales/scale.time.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 2496a487c3e..405d8ff8c80 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -534,10 +534,11 @@ module.exports = function() { var datasets = []; var labels = []; var i, j, ilen, jlen, data, timestamp; + var dataLabels = chart.data.labels || []; // Convert labels to timestamps - for (i = 0, ilen = chart.data.labels.length; i < ilen; ++i) { - labels.push(parse(chart.data.labels[i], me)); + for (i = 0, ilen = dataLabels.length; i < ilen; ++i) { + labels.push(parse(dataLabels[i], me)); } // Convert data to timestamps From 5816770e459284847be10d1d0c00e5a4ee2b8434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Dub=C3=A9?= Date: Thu, 18 Oct 2018 16:28:56 -0400 Subject: [PATCH 436/685] Introduce the 'minBarLength' bar option (#5741) --- docs/charts/bar.md | 1 + src/controllers/controller.bar.js | 11 +++++++ test/specs/scale.linear.tests.js | 48 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/docs/charts/bar.md b/docs/charts/bar.md index e4d146b2c67..eaaad0b58ce 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -97,6 +97,7 @@ The bar chart defines the following configuration options. These options are mer | `categoryPercentage` | `Number` | `0.8` | Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) | `barThickness` | `Number/String` | | Manually set width of each bar in pixels. If set to `'flex'`, it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. [more...](#barthickness) | `maxBarThickness` | `Number` | | Set this to ensure that bars are not sized thicker than this. +| `minBarLength` | `Number` | | Set this to ensure that bars have a minimum length in pixels. | `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) ### barThickness diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index cadae3fdb6c..74e6d2289f0 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -382,8 +382,10 @@ module.exports = function(Chart) { var chart = me.chart; var meta = me.getMeta(); var scale = me.getValueScale(); + var isHorizontal = scale.isHorizontal(); var datasets = chart.data.datasets; var value = scale.getRightValue(datasets[datasetIndex].data[index]); + var minBarLength = scale.options.minBarLength; var stacked = scale.options.stacked; var stack = meta.stack; var start = 0; @@ -410,6 +412,15 @@ module.exports = function(Chart) { head = scale.getPixelForValue(start + value); size = (head - base) / 2; + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = minBarLength; + if (value >= 0 && !isHorizontal || value < 0 && isHorizontal) { + head = base - minBarLength; + } else { + head = base + minBarLength; + } + } + return { size: size, base: base, diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 380feaac09d..959a76db01e 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -1046,4 +1046,52 @@ describe('Linear Scale', function() { expect(chart.scales['x-axis-0'].max).toEqual(0); }); + + it('minBarLength settings should be used on Y axis on bar chart', function() { + var minBarLength = 4; + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + scales: { + yAxes: [{ + minBarLength: minBarLength + }] + } + } + }); + + var data = chart.getDatasetMeta(0).data; + + expect(data[0]._model.base - minBarLength).toEqual(data[0]._model.y); + expect(data[1]._model.base + minBarLength).toEqual(data[1]._model.y); + }); + + it('minBarLength settings should be used on X axis on horizontalBar chart', function() { + var minBarLength = 4; + var chart = window.acquireChart({ + type: 'horizontalBar', + data: { + datasets: [{ + data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + scales: { + xAxes: [{ + minBarLength: minBarLength + }] + } + } + }); + + var data = chart.getDatasetMeta(0).data; + + expect(data[0]._model.base + minBarLength).toEqual(data[0]._model.x); + expect(data[1]._model.base - minBarLength).toEqual(data[1]._model.x); + }); }); From 2dbf1cd1af69f494f971c3603063e4265fb10e88 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 20 Oct 2018 11:38:48 +0200 Subject: [PATCH 437/685] Add support for *.js test fixture config (#5777) JSON doesn't support functions which are needed to create scriptable options, so implement a very basic method to load a JavaScript file exporting the config in `module.exports`. Also rename test sources (remove the `jasmine.` prefix), cleanup `karma.conf.js` and add an example .js fixture config (bubble radius option). --- gulpfile.js | 20 +---- karma.conf.js | 13 ++- test/{jasmine.context.js => context.js} | 0 test/fixture.js | 85 ++++++++++++++++++ .../controller.bubble/radius-scriptable.js | 45 ++++++++++ .../controller.bubble/radius-scriptable.png | Bin 0 -> 3989 bytes test/{jasmine.index.js => index.js} | 11 ++- test/{jasmine.matchers.js => matchers.js} | 2 +- test/specs/controller.bar.tests.js | 2 +- test/specs/controller.bubble.tests.js | 2 +- test/specs/controller.line.tests.js | 2 +- test/specs/controller.polarArea.tests.js | 2 +- test/specs/controller.radar.tests.js | 2 +- test/specs/core.scale.tests.js | 2 +- test/specs/element.point.tests.js | 2 +- test/specs/plugin.filler.tests.js | 2 +- test/{jasmine.utils.js => utils.js} | 57 +----------- 17 files changed, 162 insertions(+), 87 deletions(-) rename test/{jasmine.context.js => context.js} (100%) create mode 100644 test/fixture.js create mode 100644 test/fixtures/controller.bubble/radius-scriptable.js create mode 100644 test/fixtures/controller.bubble/radius-scriptable.png rename test/{jasmine.index.js => index.js} (88%) rename test/{jasmine.matchers.js => matchers.js} (99%) rename test/{jasmine.utils.js => utils.js} (72%) diff --git a/gulpfile.js b/gulpfile.js index f6795fa7f56..daba620f0f5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -198,27 +198,15 @@ function docsTask(done) { }); } -function startTest() { - return [ - {pattern: './test/fixtures/**/*.json', included: false}, - {pattern: './test/fixtures/**/*.png', included: false}, - './node_modules/moment/min/moment.min.js', - './test/jasmine.index.js', - './src/**/*.js', - ].concat( - argv.inputs ? - argv.inputs.split(';') : - ['./test/specs/**/*.js'] - ); -} - function unittestTask(done) { new karma.Server({ configFile: path.join(__dirname, 'karma.conf.js'), singleRun: !argv.watch, - files: startTest(), args: { - coverage: !!argv.coverage + coverage: !!argv.coverage, + inputs: argv.inputs + ? argv.inputs.split(';') + : ['./test/specs/**/*.js'] } }, // https://github.com/karma-runner/gulp-karma/issues/18 diff --git a/karma.conf.js b/karma.conf.js index 5bb73ef03a0..4b3a301f178 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -25,9 +25,18 @@ module.exports = function(karma) { } }, + files: [ + {pattern: 'test/fixtures/**/*.js', included: false}, + {pattern: 'test/fixtures/**/*.json', included: false}, + {pattern: 'test/fixtures/**/*.png', included: false}, + 'node_modules/moment/min/moment.min.js', + 'test/index.js', + 'src/**/*.js' + ].concat(args.inputs), + preprocessors: { - './test/jasmine.index.js': ['browserify'], - './src/**/*.js': ['browserify'] + 'test/index.js': ['browserify'], + 'src/**/*.js': ['browserify'] }, browserify: { diff --git a/test/jasmine.context.js b/test/context.js similarity index 100% rename from test/jasmine.context.js rename to test/context.js diff --git a/test/fixture.js b/test/fixture.js new file mode 100644 index 00000000000..5652c13e944 --- /dev/null +++ b/test/fixture.js @@ -0,0 +1,85 @@ +/* global __karma__ */ + +'use strict'; + +var utils = require('./utils'); + +function readFile(url, callback) { + var request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState === 4) { + return callback(request.responseText); + } + }; + + request.open('GET', url, false); + request.send(null); +} + +function loadConfig(url, callback) { + var regex = /\.(json|js)$/i; + var matches = url.match(regex); + var type = matches ? matches[1] : 'json'; + var cfg = null; + + readFile(url, function(content) { + switch (type) { + case 'js': + // eslint-disable-next-line + cfg = new Function('"use strict"; var module = {};' + content + '; return module.exports;')(); + break; + case 'json': + cfg = JSON.parse(content); + break; + default: + } + + callback(cfg); + }); +} + +function specFromFixture(description, inputs) { + var input = inputs.js || inputs.json; + it(input, function(done) { + loadConfig(input, function(json) { + var chart = utils.acquireChart(json.config, json.options); + if (!inputs.png) { + fail('Missing PNG comparison file for ' + inputs.json); + done(); + } + + utils.readImageData(inputs.png, function(expected) { + expect(chart).toEqualImageData(expected, json); + utils.releaseChart(chart); + done(); + }); + }); + }); +} + +function specsFromFixtures(path) { + var regex = new RegExp('(^/base/test/fixtures/' + path + '.+)\\.(png|json|js)'); + var inputs = {}; + + Object.keys(__karma__.files || {}).forEach(function(file) { + var matches = file.match(regex); + var name = matches && matches[1]; + var type = matches && matches[2]; + + if (name && type) { + inputs[name] = inputs[name] || {}; + inputs[name][type] = file; + } + }); + + return function() { + Object.keys(inputs).forEach(function(key) { + specFromFixture(key, inputs[key]); + }); + }; +} + +module.exports = { + specs: specsFromFixtures +}; + diff --git a/test/fixtures/controller.bubble/radius-scriptable.js b/test/fixtures/controller.bubble/radius-scriptable.js new file mode 100644 index 00000000000..593316b3c73 --- /dev/null +++ b/test/fixtures/controller.bubble/radius-scriptable.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [ + {x: 0, y: 0}, + {x: 1, y: 0}, + {x: 2, y: 0}, + {x: 3, y: 0}, + {x: 4, y: 0}, + {x: 5, y: 0} + ], + radius: function(ctx) { + return ctx.dataset.data[ctx.dataIndex].x * 4; + } + }] + }, + options: { + legend: false, + title: false, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + }, + elements: { + point: { + backgroundColor: '#444' + } + }, + layout: { + padding: { + left: 24, + right: 24 + } + } + } + }, + options: { + canvas: { + height: 128, + width: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/radius-scriptable.png b/test/fixtures/controller.bubble/radius-scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..546466f7192830b3be443775f1dadba6d202e158 GIT binary patch literal 3989 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 O8!$1nG^o*YOZpGX%prFG literal 0 HcmV?d00001 diff --git a/test/jasmine.index.js b/test/index.js similarity index 88% rename from test/jasmine.index.js rename to test/index.js index c1706130f2d..b3fa6cf3b60 100644 --- a/test/jasmine.index.js +++ b/test/index.js @@ -1,6 +1,9 @@ -var Context = require('./jasmine.context'); -var matchers = require('./jasmine.matchers'); -var utils = require('./jasmine.utils'); +'use strict'; + +var fixture = require('./fixture'); +var Context = require('./context'); +var matchers = require('./matchers'); +var utils = require('./utils'); (function() { @@ -42,7 +45,7 @@ var utils = require('./jasmine.utils'); 'position: absolute' + '}'); - jasmine.specsFromFixtures = utils.specsFromFixtures; + jasmine.fixture = fixture; jasmine.triggerMouseEvent = utils.triggerMouseEvent; beforeEach(function() { diff --git a/test/jasmine.matchers.js b/test/matchers.js similarity index 99% rename from test/jasmine.matchers.js rename to test/matchers.js index 88f5de8fe7e..92a6f970427 100644 --- a/test/jasmine.matchers.js +++ b/test/matchers.js @@ -1,7 +1,7 @@ 'use strict'; var pixelmatch = require('pixelmatch'); -var utils = require('./jasmine.utils'); +var utils = require('./utils'); function toPercent(value) { return Math.round(value * 10000) / 100; diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index 7957b6ab5db..0e843e1435f 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1,5 +1,5 @@ describe('Chart.controllers.bar', function() { - describe('auto', jasmine.specsFromFixtures('controller.bar')); + describe('auto', jasmine.fixture.specs('controller.bar')); it('should be constructed', function() { var chart = window.acquireChart({ diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 2597b9ae641..a5f5b89a5de 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -1,5 +1,5 @@ describe('Chart.controllers.bubble', function() { - describe('auto', jasmine.specsFromFixtures('controller.bubble')); + describe('auto', jasmine.fixture.specs('controller.bubble')); it('should be constructed', function() { var chart = window.acquireChart({ diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index ed8f4ec5c80..25a9ed6b0f5 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -1,5 +1,5 @@ describe('Chart.controllers.line', function() { - describe('auto', jasmine.specsFromFixtures('controller.line')); + describe('auto', jasmine.fixture.specs('controller.line')); it('should be constructed', function() { var chart = window.acquireChart({ diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index 11e0be0b7d3..b706f8376a6 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -1,4 +1,4 @@ -describe('auto', jasmine.specsFromFixtures('controller.polarArea')); +describe('auto', jasmine.fixture.specs('controller.polarArea')); describe('Chart.controllers.polarArea', function() { it('should be constructed', function() { diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 2bc0a2e0d24..7e85e9e7eac 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -1,5 +1,5 @@ describe('Chart.controllers.radar', function() { - describe('auto', jasmine.specsFromFixtures('controller.radar')); + describe('auto', jasmine.fixture.specs('controller.radar')); it('Should be constructed', function() { var chart = window.acquireChart({ diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index dc2da042aca..2981c35c9b4 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -1,5 +1,5 @@ describe('Core.scale', function() { - describe('auto', jasmine.specsFromFixtures('core.scale')); + describe('auto', jasmine.fixture.specs('core.scale')); it('should provide default scale label options', function() { expect(Chart.defaults.scale.scaleLabel).toEqual({ diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 8d1227003b9..0f3a03e319d 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -1,5 +1,5 @@ describe('Chart.elements.Point', function() { - describe('auto', jasmine.specsFromFixtures('element.point')); + describe('auto', jasmine.fixture.specs('element.point')); it ('Should be constructed', function() { var point = new Chart.elements.Point({ diff --git a/test/specs/plugin.filler.tests.js b/test/specs/plugin.filler.tests.js index 117f680360e..94979375832 100644 --- a/test/specs/plugin.filler.tests.js +++ b/test/specs/plugin.filler.tests.js @@ -7,7 +7,7 @@ describe('Plugin.filler', function() { }); } - describe('auto', jasmine.specsFromFixtures('plugin.filler')); + describe('auto', jasmine.fixture.specs('plugin.filler')); describe('dataset.fill', function() { it('should support boundaries', function() { diff --git a/test/jasmine.utils.js b/test/utils.js similarity index 72% rename from test/jasmine.utils.js rename to test/utils.js index 2c2006a2c07..80430d4d5bc 100644 --- a/test/jasmine.utils.js +++ b/test/utils.js @@ -1,18 +1,3 @@ -/* global __karma__ */ - -function loadJSON(url, callback) { - var request = new XMLHttpRequest(); - request.onreadystatechange = function() { - if (request.readyState === 4) { - return callback(JSON.parse(request.responseText)); - } - }; - - request.overrideMimeType('application/json'); - request.open('GET', url, true); - request.send(null); -} - function createCanvas(w, h) { var canvas = document.createElement('canvas'); canvas.width = w; @@ -112,46 +97,6 @@ function injectCSS(css) { head.appendChild(style); } -function specFromFixture(description, inputs) { - it(inputs.json, function(done) { - loadJSON(inputs.json, function(json) { - var chart = acquireChart(json.config, json.options); - if (!inputs.png) { - fail('Missing PNG comparison file for ' + inputs.json); - done(); - } - - readImageData(inputs.png, function(expected) { - expect(chart).toEqualImageData(expected, json); - releaseChart(chart); - done(); - }); - }); - }); -} - -function specsFromFixtures(path) { - var regex = new RegExp('(^/base/test/fixtures/' + path + '.+)\\.(png|json)'); - var inputs = {}; - - Object.keys(__karma__.files || {}).forEach(function(file) { - var matches = file.match(regex); - var name = matches && matches[1]; - var type = matches && matches[2]; - - if (name && type) { - inputs[name] = inputs[name] || {}; - inputs[name][type] = file; - } - }); - - return function() { - Object.keys(inputs).forEach(function(key) { - specFromFixture(key, inputs[key]); - }); - }; -} - function waitForResize(chart, callback) { var override = chart.resize; chart.resize = function() { @@ -180,7 +125,7 @@ module.exports = { createCanvas: createCanvas, acquireChart: acquireChart, releaseChart: releaseChart, - specsFromFixtures: specsFromFixtures, + readImageData: readImageData, triggerMouseEvent: triggerMouseEvent, waitForResize: waitForResize }; From 81b4b87666e2bdb6d7cc58d678002358adb72aba Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 21 Oct 2018 12:56:57 -0700 Subject: [PATCH 438/685] Radar code cleanup (#5624) --- src/controllers/controller.radar.js | 49 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 89717157a37..dc2b86c617b 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -29,11 +29,12 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var line = meta.dataset; - var points = meta.data; + var points = meta.data || []; var custom = line.custom || {}; var dataset = me.getDataset(); var lineElementOptions = me.chart.options.elements.line; var scale = me.chart.scale; + var i, ilen; // Compatibility: If the properties are defined with only the old name, use those values if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { @@ -65,12 +66,17 @@ module.exports = function(Chart) { meta.dataset.pivot(); // Update Points - helpers.each(points, function(point, index) { - me.updateElement(point, index, reset); - }, me); + for (i = 0, ilen = points.length; i < ilen; i++) { + me.updateElement(points[i], i, reset); + } // Update bezier control points me.updateBezierControlPoints(); + + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; i++) { + points[i].pivot(); + } }, updateElement: function(point, index, reset) { var me = this; @@ -116,28 +122,31 @@ module.exports = function(Chart) { point._model.skip = custom.skip ? custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); }, updateBezierControlPoints: function() { - var chartArea = this.chart.chartArea; - var meta = this.getMeta(); + var me = this; + var meta = me.getMeta(); + var area = me.chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; - helpers.each(meta.data, function(point, index) { - var model = point._model; - var controlPoints = helpers.splineCurve( - helpers.previousItem(meta.data, index, true)._model, + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + for (i = 0, ilen = points.length; i < ilen; i++) { + model = points[i]._model; + controlPoints = helpers.splineCurve( + helpers.previousItem(points, i, true)._model, model, - helpers.nextItem(meta.data, index, true)._model, + helpers.nextItem(points, i, true)._model, model.tension ); // Prevent the bezier going outside of the bounds of the graph - model.controlPointPreviousX = Math.max(Math.min(controlPoints.previous.x, chartArea.right), chartArea.left); - model.controlPointPreviousY = Math.max(Math.min(controlPoints.previous.y, chartArea.bottom), chartArea.top); - - model.controlPointNextX = Math.max(Math.min(controlPoints.next.x, chartArea.right), chartArea.left); - model.controlPointNextY = Math.max(Math.min(controlPoints.next.y, chartArea.bottom), chartArea.top); - - // Now pivot the point for animation - point.pivot(); - }); + model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); + model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); + model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); + model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); + } }, setHoverStyle: function(point) { From b3b5c7de4f8780be009619d1732a1915c81f0ea9 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 22 Oct 2018 03:52:05 -0400 Subject: [PATCH 439/685] Ensure that when we check typeof x == 'number' we also check instanceof Number (#5752) --- src/core/core.element.js | 2 +- src/core/core.scale.js | 2 +- src/helpers/helpers.core.js | 9 +++++++++ src/plugins/plugin.filler.js | 2 +- test/specs/helpers.core.tests.js | 18 ++++++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/core/core.element.js b/src/core/core.element.js index 2ca60692b97..665f20c4302 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -42,7 +42,7 @@ function interpolate(start, view, model, ease) { continue; } } - } else if (type === 'number' && isFinite(origin) && isFinite(target)) { + } else if (helpers.isFinite(origin) && helpers.isFinite(target)) { view[key] = origin + (target - origin) * ease; continue; } diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 78ede6985f6..363815e63b6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -519,7 +519,7 @@ module.exports = Element.extend({ return NaN; } // isNaN(object) returns true, so make sure NaN is checking for a number; Discard Infinite values - if (typeof rawValue === 'number' && !isFinite(rawValue)) { + if ((typeof rawValue === 'number' || rawValue instanceof Number) && !isFinite(rawValue)) { return NaN; } // If it is in fact an object, dive in one more level diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 2a4b0098ccf..c22dff651b9 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -51,6 +51,15 @@ var helpers = { return value !== null && Object.prototype.toString.call(value) === '[object Object]'; }, + /** + * Returns true if `value` is a finite number, else returns false + * @param {*} value - The value to test. + * @returns {Boolean} + */ + isFinite: function(value) { + return (typeof value === 'number' || value instanceof Number) && isFinite(value); + }, + /** * Returns `value` if defined, else returns `defaultValue`. * @param {*} value - The value to return if defined. diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index eb8dad4c3b0..e89bb0d8785 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -128,7 +128,7 @@ function computeBoundary(source) { return target; } - if (typeof target === 'number' && isFinite(target)) { + if (helpers.isFinite(target)) { horizontal = scale.isHorizontal(); return { x: horizontal ? target : null, diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js index 80b0640b2cc..e3993de635a 100644 --- a/test/specs/helpers.core.tests.js +++ b/test/specs/helpers.core.tests.js @@ -56,6 +56,24 @@ describe('Chart.helpers.core', function() { }); }); + describe('isFinite', function() { + it('should return true if value is a finite number', function() { + expect(helpers.isFinite(0)).toBeTruthy(); + // eslint-disable-next-line no-new-wrappers + expect(helpers.isFinite(new Number(10))).toBeTruthy(); + }); + + it('should return false if the value is infinite', function() { + expect(helpers.isFinite(Number.POSITIVE_INFINITY)).toBeFalsy(); + expect(helpers.isFinite(Number.NEGATIVE_INFINITY)).toBeFalsy(); + }); + + it('should return false if the value is not a number', function() { + expect(helpers.isFinite('a')).toBeFalsy(); + expect(helpers.isFinite({})).toBeFalsy(); + }); + }); + describe('isNullOrUndef', function() { it('should return true if value is null/undefined', function() { expect(helpers.isNullOrUndef(null)).toBeTruthy(); From cb14217c88c0e307d2ed129449d7d5d04b7d90ea Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 27 Oct 2018 23:55:11 +0800 Subject: [PATCH 440/685] Add error margin for detecting if a point or line is in the chartArea (#5790) --- src/core/core.scale.js | 6 ++++-- src/elements/element.point.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 363815e63b6..c267cb4adbf 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -710,6 +710,8 @@ module.exports = Element.extend({ var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; var yTickEnd = options.position === 'bottom' ? me.top + axisWidth + tl : me.bottom + axisWidth; + var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. + helpers.each(ticks, function(tick, index) { // autoskipper skipped this tick (#4635) if (helpers.isNullOrUndef(tick.label)) { @@ -753,7 +755,7 @@ module.exports = Element.extend({ } var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (xLineValue < me.left) { + if (xLineValue < me.left - epsilon) { lineColor = 'rgba(0,0,0,0)'; } xLineValue += helpers.aliasPixel(lineWidth); @@ -780,7 +782,7 @@ module.exports = Element.extend({ labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); - if (yLineValue < me.top) { + if (yLineValue < me.top - epsilon) { lineColor = 'rgba(0,0,0,0)'; } yLineValue += helpers.aliasPixel(lineWidth); diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 2bcdc88f0f8..56eb5796617 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -72,14 +72,14 @@ module.exports = Element.extend({ var radius = vm.radius; var x = vm.x; var y = vm.y; - var errMargin = 1.01; // 1.01 is margin for Accumulated error. (Especially Edge, IE.) + var epsilon = 0.0000001; // 0.0000001 is margin in pixels for Accumulated error. if (vm.skip) { return; } // Clipping for Points. - if (chartArea === undefined || (model.x >= chartArea.left && chartArea.right * errMargin >= model.x && model.y >= chartArea.top && chartArea.bottom * errMargin >= model.y)) { + if (chartArea === undefined || (model.x > chartArea.left - epsilon && chartArea.right + epsilon > model.x && model.y > chartArea.top - epsilon && chartArea.bottom + epsilon > model.y)) { ctx.strokeStyle = vm.borderColor || defaultColor; ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; From 820d28907bbaaddb4f6ce980f519bf2da3073053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20R=C3=B6hm?= Date: Thu, 1 Nov 2018 16:26:20 +0100 Subject: [PATCH 441/685] Remove dead and broken code from gulpfile (#5794) In commit c216c0af76, the task unittestWatch was removed. However, the watch task links to it, when executed with the test flag. As watching the unittest is possible with `gulp unittest --watch`, this code is not needed anymore and thus removed. --- gulpfile.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index daba620f0f5..49fd66ade16 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -233,9 +233,6 @@ function moduleSizesTask() { } function watchTask() { - if (util.env.test) { - return gulp.watch('./src/**', ['build', 'unittest', 'unittestWatch']); - } return gulp.watch('./src/**', ['build']); } From 6bea6aba7b8160a8c89b6fe24a2a6f175edf7a48 Mon Sep 17 00:00:00 2001 From: Jordan Ephron Date: Thu, 1 Nov 2018 11:28:12 -0400 Subject: [PATCH 442/685] Document padding option for ticks configuration (#5795) --- docs/axes/styling.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 12a170f3e54..0e45efcd62e 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -38,6 +38,7 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | `reverse` | `Boolean` | `false` | Reverses order of tick labels. | `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. | `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. +| `padding` | `Number` | `0` | Sets the offset of the tick labels from the axis ## Minor Tick Configuration The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. From f74699a591111a3478667923cfa0256e7086184f Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Fri, 2 Nov 2018 16:44:10 +0900 Subject: [PATCH 443/685] Fix offsetGridLine behavior with a single data point (#5609) --- src/core/core.scale.js | 12 ++- test/specs/core.scale.tests.js | 133 +++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index c267cb4adbf..f08c68fe918 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -76,11 +76,15 @@ function labelsFromTicks(ticks) { return labels; } -function getLineValue(scale, index, offsetGridLines) { +function getPixelForGridLine(scale, index, offsetGridLines) { var lineValue = scale.getPixelForTick(index); if (offsetGridLines) { - if (index === 0) { + if (scale.getTicks().length === 1) { + lineValue -= scale.isHorizontal() ? + Math.max(lineValue - scale.left, scale.right - lineValue) : + Math.max(lineValue - scale.top, scale.bottom - lineValue); + } else if (index === 0) { lineValue -= (scale.getPixelForTick(1) - lineValue) / 2; } else { lineValue -= (lineValue - scale.getPixelForTick(index - 1)) / 2; @@ -754,7 +758,7 @@ module.exports = Element.extend({ labelY = me.bottom - labelYOffset; } - var xLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + var xLineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); if (xLineValue < me.left - epsilon) { lineColor = 'rgba(0,0,0,0)'; } @@ -781,7 +785,7 @@ module.exports = Element.extend({ labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; - var yLineValue = getLineValue(me, index, gridLines.offsetGridLines && ticks.length > 1); + var yLineValue = getPixelForGridLine(me, index, gridLines.offsetGridLines); if (yLineValue < me.top - epsilon) { lineColor = 'rgba(0,0,0,0)'; } diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index 2981c35c9b4..573c132ae07 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -19,4 +19,137 @@ describe('Core.scale', function() { } }); }); + + var gridLineTests = [{ + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: false, + offset: false, + expected: [0.5, 128.5, 256.5, 384.5, 512.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: false, + offset: true, + expected: [51.5, 154.5, 256.5, 358.5, 461.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: true, + offset: false, + expected: [-63.5, 64.5, 192.5, 320.5, 448.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: true, + offset: true, + expected: [0, 103, 205.5, 307.5, 410] + }, { + labels: ['tick1'], + offsetGridLines: false, + offset: false, + expected: [0.5] + }, { + labels: ['tick1'], + offsetGridLines: false, + offset: true, + expected: [256.5] + }, { + labels: ['tick1'], + offsetGridLines: true, + offset: false, + expected: [-511.5] + }, { + labels: ['tick1'], + offsetGridLines: true, + offset: true, + expected: [0.5] + }]; + + gridLineTests.forEach(function(test) { + it('should get the correct pixels for ' + test.labels.length + ' gridLine(s) for the horizontal scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: test.labels + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + gridLines: { + offsetGridLines: test.offsetGridLines, + drawTicks: false + }, + ticks: { + display: false + }, + offset: test.offset + }], + yAxes: [{ + display: false + }] + }, + legend: { + display: false + } + } + }); + + var xScale = chart.scales.xScale0; + xScale.ctx = window.createMockContext(); + chart.draw(); + + expect(xScale.ctx.getCalls().filter(function(x) { + return x.name === 'moveTo' && x.args[1] === 0; + }).map(function(x) { + return x.args[0]; + })).toEqual(test.expected); + }); + }); + + gridLineTests.forEach(function(test) { + it('should get the correct pixels for ' + test.labels.length + ' gridLine(s) for the vertical scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: test.labels + }, + options: { + scales: { + xAxes: [{ + display: false + }], + yAxes: [{ + type: 'category', + id: 'yScale0', + gridLines: { + offsetGridLines: test.offsetGridLines, + drawTicks: false + }, + ticks: { + display: false + }, + offset: test.offset + }] + }, + legend: { + display: false + } + } + }); + + var yScale = chart.scales.yScale0; + yScale.ctx = window.createMockContext(); + chart.draw(); + + expect(yScale.ctx.getCalls().filter(function(x) { + return x.name === 'moveTo' && x.args[0] === 0; + }).map(function(x) { + return x.args[1]; + })).toEqual(test.expected); + }); + }); }); From f40abe924425f03a592248373603c1ef169a84bd Mon Sep 17 00:00:00 2001 From: Bart Deslagmulder Date: Fri, 2 Nov 2018 08:46:06 +0100 Subject: [PATCH 444/685] Consistent use of punctuation and quick review in docs (#5796) --- docs/README.md | 2 +- docs/axes/README.md | 8 +++--- docs/axes/cartesian/README.md | 2 +- docs/axes/cartesian/time.md | 10 +++---- docs/axes/labelling.md | 4 +-- docs/axes/radial/linear.md | 12 ++++---- docs/axes/styling.md | 12 ++++---- docs/charts/README.md | 2 +- docs/charts/area.md | 2 +- docs/charts/bar.md | 8 +++--- docs/charts/bubble.md | 22 +++++++-------- docs/charts/doughnut.md | 8 +++--- docs/charts/line.md | 24 ++++++++-------- docs/charts/polar.md | 4 +-- docs/charts/radar.md | 14 ++++----- docs/configuration/README.md | 1 - docs/configuration/animations.md | 2 +- docs/configuration/elements.md | 14 ++++----- docs/configuration/legend.md | 16 +++++------ docs/configuration/title.md | 10 +++---- docs/configuration/tooltip.md | 44 ++++++++++++++--------------- docs/developers/api.md | 2 +- docs/developers/charts.md | 2 +- docs/general/colors.md | 2 +- docs/general/interactions/README.md | 2 +- docs/general/interactions/events.md | 4 +-- docs/general/interactions/modes.md | 4 +-- 27 files changed, 117 insertions(+), 120 deletions(-) diff --git a/docs/README.md b/docs/README.md index 24ee8d49982..7cf993d3c9f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ You can download the latest version of Chart.js from the [GitHub releases](https It's easy to get started with Chart.js. All that's required is the script included in your page along with a single `` node to render the chart. -In this example, we create a bar chart for a single dataset and render that in our page. You can see all the ways to use Chart.js in the [usage documentation](./getting-started/usage.md) +In this example, we create a bar chart for a single dataset and render that in our page. You can see all the ways to use Chart.js in the [usage documentation](./getting-started/usage.md). ```html + + + +
    +
    +
    + + + +
    +
    + + + + diff --git a/samples/utils.js b/samples/utils.js index 50bb81c0c1b..71ac5e4e122 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -11,7 +11,7 @@ window.chartColors = { }; (function(global) { - var Months = [ + var MONTHS = [ 'January', 'February', 'March', @@ -106,7 +106,7 @@ window.chartColors = { var i, value; for (i = 0; i < count; ++i) { - value = Months[Math.ceil(i) % 12]; + value = MONTHS[Math.ceil(i) % 12]; values.push(value.substring(0, section)); } diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 74e6d2289f0..36f7c7e6e37 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -213,27 +213,24 @@ module.exports = function(Chart) { updateElement: function(rectangle, index, reset) { var me = this; - var chart = me.chart; var meta = me.getMeta(); var dataset = me.getDataset(); - var custom = rectangle.custom || {}; - var rectangleOptions = chart.options.elements.rectangle; + var options = me._resolveElementOptions(rectangle, index); rectangle._xScale = me.getScaleForId(meta.xAxisID); rectangle._yScale = me.getScaleForId(meta.yAxisID); rectangle._datasetIndex = me.index; rectangle._index = index; - rectangle._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderSkipped: options.borderSkipped, + borderWidth: options.borderWidth, datasetLabel: dataset.label, - label: chart.data.labels[index], - borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleOptions.borderSkipped, - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.valueAtIndexOrDefault(dataset.backgroundColor, index, rectangleOptions.backgroundColor), - borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.borderColor, index, rectangleOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.borderWidth, index, rectangleOptions.borderWidth) + label: me.chart.data.labels[index] }; - me.updateElementGeometry(rectangle, index, reset); + me._updateElementGeometry(rectangle, index, reset); rectangle.pivot(); }, @@ -241,7 +238,7 @@ module.exports = function(Chart) { /** * @private */ - updateElementGeometry: function(rectangle, index, reset) { + _updateElementGeometry: function(rectangle, index, reset) { var me = this; var model = rectangle._model; var vscale = me.getValueScale(); @@ -472,6 +469,47 @@ module.exports = function(Chart) { helpers.canvas.unclipArea(chart.ctx); }, + + /** + * @private + */ + _resolveElementOptions: function(rectangle, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; + var custom = rectangle.custom || {}; + var options = chart.options.elements.rectangle; + var resolve = helpers.options.resolve; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ], context, index); + } + + return values; + } }); Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index f14e512300f..ed1e2045c71 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -103,12 +103,14 @@ module.exports = function(Chart) { setHoverStyle: function(point) { var model = point._model; var options = point._options; + point.$previousStyle = { backgroundColor: model.backgroundColor, borderColor: model.borderColor, borderWidth: model.borderWidth, radius: model.radius }; + model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); @@ -167,6 +169,7 @@ module.exports = function(Chart) { dataset.radius, options.radius ], context, index); + return values; } }); diff --git a/test/fixtures/controller.bar/backgroundColor/indexable.js b/test/fixtures/controller.bar/backgroundColor/indexable.js new file mode 100644 index 00000000000..781f81b878f --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/indexable.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ] + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/indexable.png b/test/fixtures/controller.bar/backgroundColor/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..eecc4ac6df20de91e23ae5df2518d246c2eed8dd GIT binary patch literal 4491 zcmeH}Ye*DP7>2(&9%FV(ns!Mmx}0*&FiE?ZQqaztmTP8aRz_u#r6xgz8RfP*8(okh z7elfw+N29<=o(5lyew2Eh?$6OHxV)t+%+p>T-0?tQ~&zakBEYqKWEOI!+b9v-}~|u zB_*!)Q_fHVfS*1tCK&+HR|eP#ba`rio&_)^Ss$ZIIhr=`R(F18Bp1`s?<1c2h;wJ_ z`Ytcl7&3gKyH1$GFKpNzS<+qn(0y&OtyPPz20vCZij zc~DR|*~@a|bFe932CBu_9t4?Hb-Pv*4T0#I=6e_mkUelUSK$gGAU+D~5x4Nb3U7nH zFbG#WRq!&sOaKlZZ+X)`ms4>tH#K8BLOj$RX@7`C`oYkB@ql3=0c;;AaRYJ+Cr5qFiAVKYt^sfc-VhY|fF5Bg#Q%o@Xpi?CZ-KFdR1;8?>ud6%-ancxjNN*6MF@ zWs3+{B12&j!=>7#405mhG#1DUb}PnwZX~@H=dwcFZHqqr3eBFh3YsoO9yKU7FIAjV zbI`VMDj;&V5Rvkn`KCh%T|0IY@t_emNI9YjPvl{lErn*(lW~P`vhoscRsa^T@I_1Teqbi}~oT1k_8#48{A(V&u-qGxLAB zDg0Bshah}}b2t9P9;0K}nJ;Bh))&L^osQ97>)_p%@|^sgjsu;8gYEB!-T6&vbDEl- zb!(Y}!5^B`geMd^s@DPbr-UN~4@YVeB<|VU`pu?8q&Fvo;*YG;w$uxH2m zAev$GBuMeEDuEW>5-))1oWw?#+P)(5p@>onKW8PLO0A1egC*u$Dq1Iyf)JlsTmS11 z0)$C7Dlp5nu(XExb!Zq&-?~YXLFtGUV()zLB|iHk5lbZ4+OxcMBH!GVf-oCOkqf1F zgdAgJ333F{O+%gTT_+h)T%#03zbGr10)45ov%?x8yGIyO pa8b!Wj^9s|kL9+= 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + } + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/scriptable.png b/test/fixtures/controller.bar/backgroundColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..e78839af1d8b9a268db6eaeda414131687b80dc4 GIT binary patch literal 4553 zcmeHK`%@EF6u$Se!3~xgf(%$J1W?clg0z6PNEQnMS{?!dGv$#{M$v*&C<+G1#!)+p z3KW$B3Rr8cFF@2HPq%6j1PWC&$TN-<+6GJzc_xJ3^)Kl3H)oPxcIWJud%ivA`}W*> zG$_Ew(BK0D0Dz&NuU9Yt2;V|L>f_6AIlu1&z$nMh%QG}JdbBI*WbdOJhqpWf;>v?V zvLms2!@pM4W+rX7`}kSf?ZvUz$rqXV*)0;{eg1SqS-GMn^K|B8*~^;D`LxNYiKxu% zhXYe8ONa<Pd%IdEv9=byLQyxCt{;6oMz+~BjtE5jZu~#=G^^4ltTlvTO(ZIRt)0qf91xH zuo8f<6@@oWu$Z2u7fh`xs%g-b?isCgVF^Unnn?#-yV8mO`l0qHP)`Q!j?r#~CH(|q}WKT3fxC`M`;u~GmC z=8@Pri)ltkJ~;ZmCIM04J$JFNfcX+f99$*4!rAou>iw18A~y&tdN$Uz`yh-;|Ax{! zOFa=1byV7ELNUm;w|c-=beIMa=i?e(SxkKL@#sU$5XOvi(&}X_fz;7$nq4TFLomI4 zmNP9JT)@{Ab1M*pkVm;x+F?eNa@M_osmHYIC;v>4@OS_*LOvpjjd`Gb-Ass^5st`h zFa6`f?c@+t1}9o{kuBRQWwSl9TS(G*RiCMo>&rK z-k0R122j9#crdWfixo-H*(00De{unY*d#VOll*s+lva-@6^fd&n2X`lH5M|ZM5wM+TgYAtm1EV1 zuu@85a=F9~+^C^q4r4i2z8PF=xL5vg!;`0uJ1rm*vrjCrew%trK+~PVP4X2Gc0^vn z|13}&gC*f1+@)9@g^I-h36dp9@N)M@(hFFL$>(tQVqXP<*hv57iKl1~R~CXB+&agl zk6*n^oW$B}OECa2r&$o2vQdbsRSi8Vzt!R$mh zW6>$RPf+Bu2t<(f?2r~UTg^@JAn%~d8355{cK$~s`69uCID5P|+rUniLjf(OH1FCC zg5*V5vjgh8&tG$N09_q)dU)r2!G{&GF58c|H)?`~r?BD8-yTamd|*dEJ!~v+3TC&v zlP?z1lltx@K$^??|JI@yaLY|TLy*T8<1a>O#xyFehwXNh=tp)3J$m01j2fnTKxD&i z-{UMBK-^wtoXay~&n}c&#i7J{#ep>A%YK2nzjXxN=Bji=Y-G=99Uker)VhM}WcGi} aHP3l|6XsuizT6(*pPzStSJ?*9!G8gzNe6iV literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/backgroundColor/value.js b/test/fixtures/controller.bar/backgroundColor/value.js new file mode 100644 index 00000000000..d82d60d5dfc --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/value.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: '#00ff00' + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/value.png b/test/fixtures/controller.bar/backgroundColor/value.png new file mode 100644 index 0000000000000000000000000000000000000000..4885e1086070373c7f29779af35b114929f4e866 GIT binary patch literal 4911 zcmeI0`&ScZ8pq$6o!Chzm=I{%y47%LNTCX@<)X`FtWcl^$QDAVivgBXS%E^V?g{~9 z(iWtm3$BM2NjAw@yHEp5vg-vYL?hr0CDa83m159V1Vv)GBou^wC-@Kaw>^9E)699# z^L^j%e4fjBlP`qv(awz582|w1*qF!<06^g@0`OJvajDvr1%T^ZY~f;;`G>&j+j95$D`SJp_q?#t9Vn*qw>+NLJ#wb^)7;32nj4^{w|cuUxt3@G(c?N-A#fwb z>reY{OFu!Pde-2HTtYwg92xVo)614GZj@JIZe(S7NvBu@m22jP_j?C;Xn=q( zyPSzp6gFOe6orSTLJBsrU6Mc%m92U3j=TusE6Q5#&Todcbg8*%hm5NbTKccF(GMvd zx1}VHC;gH{rMKGKCm|joSd77$-@p$I^l5NmQ5oHqJdopB;lu~eSOxM}%2MsHPp_wC z7j)j!XRx%r1!0!uO!={GM2Pv(y%KrX5v*3@(jMXLA=u~ch}f*7JY z&yP0xLZx%j)8D$*GKGNW7WQDHokHzLGoIL5SPP-Wghb~<5j7myQ)e_lo#9mJkHKQ9 z&^Da)k4pax78)|(wU^RW-wh9DhioU1kQ~X#pCKMTIN*)p{SaNg6&eNVycIAg7$FUG*iWho+LS}H2^Pr(L`ya94aP*hmw z-=|2O-@z>vm^~qezh~UDh}@yFJk0tAT>wvFA5uMg)lT8YZ)7ZVHLQiT;auGyq~S)6 zTn>pdOQ3G#_JyAmRImAQ?tbN|-%21u^@v6#w05p%ClZOg|~kQ;ZN`LU(AD0q@#Bhy0HV^8n9T=vo1_T z8CQL)7^yV&K`sq(T?@tV5I#s-Umd}d!URfh>ho<{O4+Ge_zT8^QS_+Zg2Q$SyIwQk z@`JDzR%;xHU2dPxF$xiF* zR{PaY*EfdEZAQ>6*rXR*>5cRHKWxc=eesKPbwGXumdAU;11HJ`ic>cJEqLeTzk2D+ zkVFE(O>rqb2i^M$`^*Vl0~nqQM^VuI=C$ja9(EoYZy(RDDv2kJ$00~*UPpF#Q>{s{ zs?@WYN36`T{j-03;YeKcWG`x;WS{P3Cr@Ww+Bfo!jlCxjLD#^NI&!Wt+G)knw|6qt zY_7DPM43v(3*#5n6^(S}5Fc{s+8MUvg`KVjGjY7YNfFtO&k5Yg?&QteNFdKZ zG6tL)84;8_;cee6oXHJ%o7*JI5RGOz%sI$8YR=IRzi5oT!JJB>OPC+ zSX=zu$r)@ofi%|Ge+o#7Y1}Zx3h!Y5pe~?abJCEQ1mklIPlXpKJ{Z3w2LEA8BM?*! sFHa0Y_RJx7phK%2W%EDNGRvjz9!SOfmWDI%k1!A$6(8CBZu+PH1-Vs`O#lD@ literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderColor/indexable.js b/test/fixtures/controller.bar/borderColor/indexable.js new file mode 100644 index 00000000000..de39b134ae8 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/indexable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + borderWidth: 8 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/indexable.png b/test/fixtures/controller.bar/borderColor/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3a45cdd214a70f46635a7a0329e11cec036056 GIT binary patch literal 4654 zcmeHLeNasec0r^a$qo~M9+O}TY08zbk*b{;`1yhCkKyJ zlOzjnbYC@kgkRfsmYx1#6X8@p$lNci0=DQQQjfR(x~XHSPfDRX5~M zos*bN7F)f|G5Jhyb-d218$^-?!i30o0c@=Pr{LpjE1zmgix<0E!#HIxtk_N+P6m6-MvyB7~HB8H2KoN z6lRlsg`K)EyW=kJ%2pw`(qEO|VZ@;4rY}v|BF-TQr5%1|pGJ#9!afjv%=r*R=$X#X zyLiajb~~JZ)|X~NKXULmw*+rwMO?SjE+G42P;CYYkliHN3GFikb*x_#dW-GXQum(+aZw^)=tXgvheyl9a2$7OOG z4|C>f`Pqv!qgQ~V;x%}j&{J!Q63bW-=x@l=uEL`4+l;N`t^fw?%1#!%`~xIm_EkrvOpt`m;Q)iQYy* zTXwORoN-yir9?^W*ei$PHGjb;4q?OhxFdu?UONknbAlq+}t8*C~M?)Y}L_ zdf$_u+Dn2W)wW@asli$T2pEZw`TiIpE&VbUkcVY7V~&Kd;)D+r0@Cgch|7yeieeM| z5%n3rF9FrTUnmrU+HQPkt&FRX?WYK&&^e1_QH@>-;OcD@O2C_I@sWjYC+La@#5{1g zPHF%RC7RK~V7;xdvA2}x2|?SZ3xb?@J!Wx&2;m^tE7$OyB34G5y+0~Z|oX`laN z>@zN42F#%F^F%@HLQ?c|#_U%z=w}!e2{coymWvc8$NN$@{Fqu?IDNvLH}9fCTinxX z%q`Az%$3N{+Zf+4VKHVkO$Im|(x`_kw;o#87QTAQdfAfOJ6oVONbXGj20b#@kMZSN z%`<~Ny<|FBmZEpWvZyTKt{|0SwK)MGB`gb7Yj_iXq_(31EO}mGBkyoz^1}?MW!XVcT0pM)r8tgiVfTOqD8kA+{?7ppvsW@DeeW1Up^1YU0x(3 z&CqRlQG{mLo#Q8(-e!R`47bJR%BAD3;@My31PED 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + borderWidth: 8 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/scriptable.png b/test/fixtures/controller.bar/borderColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..c24a2beab8e8fc50d8b19977fe473c530059e70c GIT binary patch literal 4614 zcmeHKT~t$77C!fKgEt`dLad-7Tos{~j-k|6sSqv}3oH<*)iM>{}oD)Ond9FF9-9 zefH0{zkSX=AICdCaz!GW{Ypj`fMMh3iF*jki)X|CygqUseI zRONkV*LDsn*(Do&MFj-GTpx;!Lw1@{|C#GN>m{&2h%N1O)Tc(UemyM2Yn8v90GC;WmR-YC4A!xw+fA zjaWp%L{yf4yotPx^y+UC-D_YXho$!?ITW^{n4-tevmm?sX3STi$bWIvofMasTq!UZ zrxQ#rrU0JQ6Eyt_9=C)7ajjZzU@KJOE}KsaLvftmlUe=^vXjv8sD(TvqJTE_y0sa) zAozH@j59oqVSdB4K6yK!d+)J(Sza~AS)hLFcJdWxXj#?e)OA=##hBmP__WO}Byq(M$Y*w_Itf((RY zE6p^$>w`ejdA&PvmS~67o_;Z#3lcz()R{4oKl7&r1b;d$OP3{rHPVgA_u`+jpg`?D z@-pH`pDag=Ya!PfmOqiCJ?BU->+bCr&VYqYV?yZ;=w7(Vyh1p(k_MBp&z8y%{QF;f zTfAA-2p5WwMC0p-HHq;jmEL}F4CrjJMk+F)WL>~P=?NagaUIz1`96%5V6>LNFZo88 zTBD&uBY~vD`7{@tD20sZ37EcAj6h2N>{8BFd=OUK=FrTSB9O)!eUw{t?a)$t!=~}? zgaGqPpKSH#V345)7ae{Pab)z37FRA6?$Ugct!#MCkrZZ;$jyreFN>|MsVS9(WpP(4 zW%AiTYogU7#cIrL7O#W^wECKa#6HNI_S_{vYH3&l5_kS2*S6(9>JcAL)ttI0ZaMGi z&FI!c+olSbJUQtD@b&Nzn}Q@QcmD{M`r0hPutOxwRdvT6so1amS%m_XZ=l5e!ZAt} zu$hI3)giRM3AS>k?7 zoXP@{zW!Qs_Jitgr&HMKgldr8)e$42$Fs1bj;f5MIIjeRRRplJs|j*S#J;y2!a}=U z0OT$Am5}^b`cY3@qXoZVq==kJML5l59s`io2kc<6hz}S490NHY0<6S53W$&YCny|b z0fw(245Nzp6uHf`4h|o!ilITNc*~x1b72n$tEWrs3b)4XZ|a@Ntenu7G&EP%$!9v7 zswb{%6?2^(ZHAPfyu!!&WRKZ?kJ)3MG{XW;3lH!Ok7Tl*nHwXGjueny#C45zQ)}S> z_BlE*ix&1Mf!BdU?MZ^E@o`boiAvU_S#JGq8rb z`kv|q_xhSD>m;Z0)MBMv(`!)_hPBm)1?%RbQFB|-d!S~nLJ=k4Luw^oKv6Dg!kQbW zW=%_R9sPUHNZ7LpXaG^~4WF}7SUUA>;TneJ*F7I){Jo&y>*oSEzB9mm{qNv2P58QHgr>u z{N%`vEFaIqiMP=kD>f((i?Gi+|H@ ane&w1@b}ngboU4F7qKNev}$uw{(k_LjMa|- literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderColor/value.js b/test/fixtures/controller.bar/borderColor/value.js new file mode 100644 index 00000000000..4f328b59168 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/value.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#00ff00', + borderWidth: 8 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/value.png b/test/fixtures/controller.bar/borderColor/value.png new file mode 100644 index 0000000000000000000000000000000000000000..6e170412c308114a5698bbfe92f61ae61d578a26 GIT binary patch literal 5144 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 8 ? 'left' + : value > 0 ? 'right' + : value > -8 ? 'top' + : 'bottom'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#888', + borderSkipped: function(ctx) { + var index = ctx.dataIndex; + return index > 4 ? 'left' + : index > 3 ? 'right' + : index > 1 ? 'top' + : 'bottom'; + }, + borderWidth: 8 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderSkipped/scriptable.png b/test/fixtures/controller.bar/borderSkipped/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..54708387e1eab91a7a88fb121f6e7a1e12b51204 GIT binary patch literal 5681 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/indexable.js b/test/fixtures/controller.bar/borderWidth/indexable.js new file mode 100644 index 00000000000..ddd544ff201 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/indexable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: [ + 5, + 4, + 3, + 2, + 1, + 0 + ] + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/indexable.png b/test/fixtures/controller.bar/borderWidth/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..30011d3b722e474f7ae790f8a1c477f3679195b8 GIT binary patch literal 4969 zcmeHKX;4#H7QXNKkQW>@Aqvwd5CH?D0yeE6%JSMq(U#2~TtJqTc8~Nhtb(8MRvib+6CY!|>Nh$yJ+O2n`)nh-*u@1yb8)Ktx%shX-Gf0BAR=eyro zzH{D9_4W49*E7}w0O)Vq>i!J?DEx^4d_MeokXd&C0Q#D3?qB*P1P{D=l4h~{*|8nx zRs=jU{O69$z)Yi{Sb;yYCT&M}_X$J$ADpazr`H%-s2R1g?Rb3Z3t2|-ty=o&nu3om zCX*S`bj-V;ajYw9+~iGE3mJsKmjH|RRr&?9 z(vabl->XwHRTQ>~xjI!e55<~RGCpMZQApkG>wVO;R?#U{yzI-tSL<{==2}$vN;S{pNNbW)vhVN_}EhvXgYH7`&`8FLQv{H&aDl{X31=kQQS`LKiv zI0q!M5L0hG>d$0T0xtYcwAR&>!e0<_+LmqC^RlLo{$ za%g*UAh#Cx*7_h6g@0BMQ(1%sf;E4>bs`#KoG#v%mYkU4t9iLpTw4Pk5KM%?RQ0*ts< zp*llpwPb*ZePbSBFncJgq)KrIFZP1m!p#SJY{^;<1~8I61*f?nbZ!4ax1A7aVd9{p zK_Qh2mYgUpHzsX%T|X7^H|ZlCO!`Q-TUknnP@R=&BjG3qGU#|UG|FE9kTTf=Wj^i+ z{ph|y_GGj~gyy^_YcGnj_Kw8}=aERxmKYw$WB}WPqa|r%yOo{Z8JB~-K~aV)>gf9i z(o60tHsra$w7K{QyCUfT6?DDU)M60^PzdGf{0cqNt*I?G1Z#pwO%62GK|Wc_c$-W& z=F_cECB}!2=dFZD$6ZuQDSkN4)!W>>CylhZ;=jdl6H5*$$>+r+Hd7!}RNKRG2i_gn zltcaTYMBlcihQ4D2gZbcltSBVGFpa^CN$v_mbpPfU5z^05>R!z!LsLz;ktu;!Ay@WmAZl|7@m>5&Dr}`8MMZxyz%Wo28 z*Cwy8mf2Om9T`&fac-?$RmvGySJyK(Hj==uQV!iwabV4g4%Not{Xqo4tk@eaB)KeG zC`q20R0}=YAM))&rUK8&Gct^>0+G*j?1tOAj|;Bue;3yHoqv09aB%L>MtOCzHhR%e zM^SoFwrb!thIhj#>ZY_I?Wi;vv3^A$zQ4RT#{ytZIz>8!Rbzpf6ubf66s(tGik6M{~|wB7Zhf{%8q5Z_2+w)-+KuGAet*3jZ( zzejwhuNi=km-Ahe(6j|s)o#3AZg^v2J=IKm=b&(6QQZ`oJ0ENl^6z-A622;&> zzzc`^>}nYTim$?A1^O_~~{V*+hErQZv$GNvQT!Pu0i6CA1vIoQe z@<&*~9_B2mwc{!4e+0Z#s08!D_s>2>;E~vW5N(>9JAzo8l+>qA zs%yTyL?Wj(Q$dFBq4@}k-GOftvm>1P^ByV^b>Siq)yaeuD#Mq2Wg=?L+oEYmly0uu z3W6&wnR)lA+_*E@3KUre9R~=yo80124=O#?MIh^!-isJc9WN&rq~d@(_{pl1jbZ`h z#?B08Bbm9}F$y(wX3HNL(IJkYbykGn(v0;3BcPzH&jY-3^}|aNumv#ddWw%cvHrzVIn%ybAZasy@$c zT3M4gKDrq=Kht#78T^Oqvg{m4;Bd5Rw(IoA4<9}>TIiOqmbt-;UyQTqbW*ptZbAa@ ZVSYys`@d?L^aJp-ZL_y~=_c0EUjgZ(L^ literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/scriptable.js b/test/fixtures/controller.bar/borderWidth/scriptable.js new file mode 100644 index 00000000000..40128657749 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/scriptable.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return Math.abs(value); + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: function(ctx) { + return ctx.dataIndex * 2; + } + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/scriptable.png b/test/fixtures/controller.bar/borderWidth/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..fd8b193460fdd37160b3b07774a99158645f5c36 GIT binary patch literal 5219 zcmeHLX;c$g7JgL}Qh`PbB4XPhB?t)Z75Jg24OpY5gi-B>0VKKC_D6*pjS;D-cJw0>g&;0H)|w!czhHH;{0A(ksByHhu~BL4_dZr8 z^DC%JRtO3PjEtxEIMeban0n3AJyDy{B|;v*L}KUUTFt|7*O>%h=fuA0EvZNhKi9Fk zKR0Ftit*tp2OSySg-S{yydslsXTSY3PIx<0;M#Is#lTIuHRrWh{I)+L+INviq1?7$ zLe|(l^jV;#d|)#6w%0UGStZ2qjExk?*q`>GJckAm0ya=DB1l4y0D9(iM({73ezZ`c zWnQz81Q)ehhHj%M1zrTDJ;yV=pk4SKGg&$yfSI9EWYKpVdO)k4UKtPo^>6O}xEFh+ zPk@8fkDiw-Aav=f;I@e|pCOsxvBP*ATGhmHX^GQJF$lFsP%(Y4W>DErRW#uMJY6j;b5lSOQJ zV6tdQzQtk)O4a3YD_JoFCCB%W-zoWY6!q8ga2jIa0I4a8>2YoGM$s1U!GT-fFu{gh z*3pzJ1s-Skh7n9c({JX@O{MPB;DCuxD;Hidnsr2+v7x03T2(za9&W~9Vt0ogEEVQp znB4sk_f{SagGqX_J4~e)Y2Y1bn%QB_c;fFOO)M5~qQF;khK?6lLmZFWtBhG5I8JMC z2v}&wAiK2k-CBI%A`;9pNhzyja!hHQ?Zv+EEeYD6sne9c@bvDB?r=8vYe9%K^2|mV z@ZU9Z!!1G{h(( znQ6#=Xx09H`dAPO6Y~}LrOVyn$*fkI?BX$O$I^Z?dX$jIryVXe(9Ym3XZ!Z^wBA+6O;9Ryim`Bz%1z#XO*n8klae%k=}{HK>r^BbRxVCnCYs*~nZ zwA@#C-LL?Z_nuH95rRQm@VT^~g<^R0RrvfYy=MAI57@X*u*jRd{CN~j-S};F3?&1s zfvHd19JqBRd{#A8T0i7~BiZQGMhMyzJX56jV@knEhb|AV;D4!6POYIRd8PG>$YJ|l zvSY%?5)8S8Ss*B;4V$N)eFi_N*mTBKF>t(7u>!-11+xfHEaDya1Ff!_)Lx12dKw6K zBS?y1=Z0ZT>Y2xXZ*=&}9Gh7U$ zDU@pUov^_6edmfN6sqQ^%qq^cz?d9`L-NIq&noz-OR6(37jySmgQYj~&0(=RLw($L zE)oeY4UsuSF;+4#tcr^@yc}3-J>D@Jk)F(328*ZL0wTtFFJF$f41G>LE7K{P$+c$l zeqArV>jvzE?Pl;$!52cso+zv z{}Bl%JOH|czc)w!^ilLj*l`z$Do2+o4&QI)_V#y&i^uzIGHsLrd^lzmeEz%~7NJc2 z`lxqkp{ME!(Js#2M00@w+9}- zXlPRURc%n)%u5|OuO;(s)`OBO9ffcyVp5rwUO8QR(5%z{SPPdDpdmsi&&TA?1fzG? noP%9i+$7IN3ljc^o+mVPwcQJIk8Y0!@Z-E;t7HB;|G)kVQn7p* literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/value.js b/test/fixtures/controller.bar/borderWidth/value.js new file mode 100644 index 00000000000..33cd40f780e --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/value.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: 2 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: 4 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/value.png b/test/fixtures/controller.bar/borderWidth/value.png new file mode 100644 index 0000000000000000000000000000000000000000..3c81aff7f2dfb863751bc423a7c8db002e959871 GIT binary patch literal 5109 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 Date: Wed, 14 Nov 2018 11:08:40 +0100 Subject: [PATCH 450/685] Check pixel values using the pixel proximity matcher (#5833) --- test/specs/scale.logarithmic.tests.js | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 3d79e6cfc37..cb44f108d36 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -822,8 +822,8 @@ describe('Logarithmic Scale tests', function() { var start = chart.chartArea[chartStart]; var end = chart.chartArea[chartEnd]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -835,8 +835,8 @@ describe('Logarithmic Scale tests', function() { start = chart.chartArea[chartEnd]; end = chart.chartArea[chartStart]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -964,9 +964,9 @@ describe('Logarithmic Scale tests', function() { var start = chart.chartArea[axis.start]; var end = chart.chartArea[axis.end]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); - expect(scale.getPixelForValue(0, 0, 0)).toBe(start); // 0 is invalid, put it at the start. + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(start); // 0 is invalid, put it at the start. expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -978,8 +978,8 @@ describe('Logarithmic Scale tests', function() { start = chart.chartArea[axis.end]; end = chart.chartArea[axis.start]; - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -1093,9 +1093,9 @@ describe('Logarithmic Scale tests', function() { var end = chart.chartArea[axis.end]; var sign = scale.isHorizontal() ? 1 : -1; - expect(scale.getPixelForValue(0, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start + sign * fontSize); + expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start + sign * fontSize); expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); @@ -1108,9 +1108,9 @@ describe('Logarithmic Scale tests', function() { start = chart.chartArea[axis.end]; end = chart.chartArea[axis.start]; - expect(scale.getPixelForValue(0, 0, 0)).toBe(start); - expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); - expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start - sign * fontSize, 4); + expect(scale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBeCloseToPixel(start - sign * fontSize, 4); expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); From 3ea93a09f2047b727891f9e13dbd3fb7ebe44adf Mon Sep 17 00:00:00 2001 From: Jan Tagscherer <3198913+jtagscherer@users.noreply.github.com> Date: Wed, 14 Nov 2018 11:12:57 +0100 Subject: [PATCH 451/685] Add regression test for legend layout issue (#5776) --- test/specs/plugin.legend.tests.js | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 924276587f0..b6bb1a89426 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -525,6 +525,43 @@ describe('Legend block tests', function() { }); }); + it('should not draw legend items outside of the chart bounds', function() { + var chart = window.acquireChart( + { + type: 'line', + data: { + datasets: [1, 2, 3].map(function(n) { + return { + label: 'dataset' + n, + data: [] + }; + }), + labels: [] + }, + options: { + legend: { + position: 'right' + } + } + }, + { + canvas: { + width: 512, + height: 105 + } + } + ); + + // Check some basic assertions about the test setup + expect(chart.width).toBe(512); + expect(chart.legend.legendHitBoxes.length).toBe(3); + + // Check whether any legend items reach outside the established bounds + chart.legend.legendHitBoxes.forEach(function(item) { + expect(item.left + item.width).toBeLessThanOrEqual(chart.width); + }); + }); + describe('config update', function() { it ('should update the options', function() { var chart = acquireChart({ From ecf64d361dc284acedd4b6a815905614ea79034c Mon Sep 17 00:00:00 2001 From: Thomas David Baker Date: Thu, 15 Nov 2018 06:41:02 -0800 Subject: [PATCH 452/685] Correct spelling mistake. (#5831) Use a simpler phrase for this heading. --- docs/general/fonts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/fonts.md b/docs/general/fonts.md index 082456f3d95..f00c5aa5373 100644 --- a/docs/general/fonts.md +++ b/docs/general/fonts.md @@ -27,6 +27,6 @@ let chart = new Chart(ctx, { | `defaultFontSize` | `Number` | `12` | Default font size (in px) for text. Does not apply to radialLinear scale point labels. | `defaultFontStyle` | `String` | `'normal'` | Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. -## Non-Existant Fonts +## Missing Fonts If a font is specified for a chart that does exist on the system, the browser will not apply the font when it is set. If you notice odd fonts appearing in your charts, check that the font you are applying exists on your system. See [issue 3318](https://github.com/chartjs/Chart.js/issues/3318) for more details. From 75aa44eef677b790768262638df0de223fd01a56 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 18 Nov 2018 09:33:34 +0100 Subject: [PATCH 453/685] Upgrade dev dependencies to reduce vulnerabilities (#5840) --- gulpfile.js | 12 ++++---- package.json | 44 ++++++++++++++--------------- src/platforms/platform.dom.js | 2 ++ test/specs/core.controller.tests.js | 16 ++--------- test/specs/core.plugin.tests.js | 2 ++ 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index d8872df0e31..06229d6f89c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -49,19 +49,19 @@ gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); gulp.task('watch', watchTask); -gulp.task('lint', ['lint-html', 'lint-js']); gulp.task('lint-html', lintHtmlTask); gulp.task('lint-js', lintJsTask); +gulp.task('lint', gulp.parallel('lint-html', 'lint-js')); gulp.task('docs', docsTask); -gulp.task('test', ['lint', 'unittest']); -gulp.task('size', ['library-size', 'module-sizes']); gulp.task('server', serverTask); gulp.task('unittest', unittestTask); +gulp.task('test', gulp.parallel('lint', 'unittest')); gulp.task('library-size', librarySizeTask); gulp.task('module-sizes', moduleSizesTask); +gulp.task('size', gulp.parallel('library-size', 'module-sizes')); gulp.task('_open', _openTask); -gulp.task('dev', ['server', 'default']); -gulp.task('default', ['build', 'watch']); +gulp.task('default', gulp.parallel('build', 'watch')); +gulp.task('dev', gulp.parallel('server', 'default')); /** * Generates the bower.json manifest file which will be pushed along release tags. @@ -233,7 +233,7 @@ function moduleSizesTask() { } function watchTask() { - return gulp.watch('./src/**', ['build']); + return gulp.watch('./src/**', gulp.parallel('build')); } function serverTask() { diff --git a/package.json b/package.json index 30e365f7467..b8c72f99afb 100644 --- a/package.json +++ b/package.json @@ -10,42 +10,42 @@ "url": "https://github.com/chartjs/Chart.js.git" }, "devDependencies": { - "browserify": "^14.5.0", + "browserify": "^16.2.3", "browserify-istanbul": "^3.0.1", "bundle-collapser": "^1.3.0", "child-process-promise": "^2.2.1", "coveralls": "^3.0.0", - "eslint": "^4.9.0", + "eslint": "^5.9.0", "eslint-config-chartjs": "^0.1.0", - "eslint-plugin-html": "^4.0.2", + "eslint-plugin-html": "^5.0.0", "gitbook-cli": "^2.3.2", - "gulp": "3.9.x", - "gulp-concat": "~2.6.x", - "gulp-connect": "~5.0.0", - "gulp-eslint": "^4.0.0", - "gulp-file": "^0.3.0", - "gulp-htmllint": "^0.0.15", - "gulp-insert": "~0.5.0", - "gulp-replace": "^0.6.1", - "gulp-size": "~2.1.0", + "gulp": "^4.0.0", + "gulp-concat": "^2.6.0", + "gulp-connect": "^5.6.1", + "gulp-eslint": "^5.0.0", + "gulp-file": "^0.4.0", + "gulp-htmllint": "^0.0.16", + "gulp-insert": "^0.5.0", + "gulp-replace": "^1.0.0", + "gulp-size": "^3.0.0", "gulp-streamify": "^1.0.2", - "gulp-uglify": "~3.0.x", - "gulp-util": "~3.0.x", - "gulp-zip": "~4.0.0", - "jasmine": "^2.8.0", - "jasmine-core": "^2.8.0", - "karma": "^1.7.1", + "gulp-uglify": "^3.0.0", + "gulp-util": "^3.0.0", + "gulp-zip": "^4.2.0", + "jasmine": "^3.3.0", + "jasmine-core": "^3.3.0", + "karma": "^3.1.1", "karma-browserify": "^5.1.1", "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", - "karma-jasmine": "^1.1.0", - "karma-jasmine-html-reporter": "^0.2.2", + "karma-jasmine": "^2.0.0", + "karma-jasmine-html-reporter": "^1.4.0", "merge-stream": "^1.0.1", "pixelmatch": "^4.0.2", - "vinyl-source-stream": "^1.1.0", + "vinyl-source-stream": "^2.0.0", "watchify": "^3.9.0", - "yargs": "^9.0.1" + "yargs": "^12.0.2" }, "spm": { "main": "Chart.js" diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index a882d22a537..549ec1a1ca4 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -108,6 +108,7 @@ var supportsEventListenerOptions = (function() { var supports = false; try { var options = Object.defineProperty({}, 'passive', { + // eslint-disable-next-line getter-return get: function() { supports = true; } @@ -391,6 +392,7 @@ module.exports = { // we can't use save() and restore() to restore the initial state. So make sure that at // least the canvas context is reset to the default state by setting the canvas width. // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + // eslint-disable-next-line no-self-assign canvas.width = canvas.width; delete canvas[EXPANDO_KEY]; diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 1ca699bc132..cf9eb30a22b 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -949,25 +949,13 @@ describe('Chart', function() { var meta = chart.getDatasetMeta(0); var point = meta.data[1]; - var node = 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 - }); - - // Manually trigger rather than having an async test - node.dispatchEvent(evt); + jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; expect(chart.lastActive).toEqual([point]); - expect(tooltip._lastActive).toEqual([]); + expect(tooltip._lastActive).toEqual([point]); // Update and confirm tooltip is reset chart.update(); diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index 3a9e908a38d..977f607a7c9 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -323,6 +323,8 @@ describe('Chart.plugins', function() { expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[1]).toEqual({a: 42}); + + delete Chart.defaults.global.plugins.a; }); From ab06831f691d5a6520dd069b746d072dfc0c11a2 Mon Sep 17 00:00:00 2001 From: Kakhaber Date: Sun, 18 Nov 2018 12:36:21 +0400 Subject: [PATCH 454/685] Is node shadow root check improved (#5828) --- src/core/core.helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index d8175af50c5..4b0a18c1648 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -506,7 +506,7 @@ module.exports = function() { */ helpers._getParentNode = function(domNode) { var parent = domNode.parentNode; - if (parent && parent.host) { + if (parent && parent.toString() === '[object ShadowRoot]') { parent = parent.host; } return parent; From f6d9a39cb8e8661e1d6a733d4b77fb3f3238935a Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 18 Nov 2018 03:45:41 -0500 Subject: [PATCH 455/685] Fix axis line width when option is an array (#5751) When the axis lineWidth setting is set to an array, use the first item when determining the size of the axis area. --- src/core/core.scale.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index f08c68fe918..28211b7f43a 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -708,7 +708,7 @@ module.exports = Element.extend({ var itemsToDraw = []; - var axisWidth = me.options.gridLines.lineWidth; + var axisWidth = helpers.valueAtIndexOrDefault(me.options.gridLines.lineWidth, 0); var xTickStart = options.position === 'right' ? me.left : me.right - axisWidth - tl; var xTickEnd = options.position === 'right' ? me.left + tl : me.right; var yTickStart = options.position === 'bottom' ? me.top + axisWidth : me.bottom - tl - axisWidth; From bc494e0a811f735ad58d70330023959ff58c0837 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Wed, 21 Nov 2018 15:58:39 +0800 Subject: [PATCH 456/685] Use empty labels for tests so as not to be affected by the font width (#5842) --- test/specs/plugin.legend.tests.js | 75 ++++++++++++++++--------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index b6bb1a89426..7b7159dbb10 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -205,8 +205,8 @@ describe('Legend block tests', function() { expect(chart.legend.legendHitBoxes.length).toBe(3); [ - {h: 12, l: 107, t: 10, w: 93}, - {h: 12, l: 210, t: 10, w: 93}, + {h: 12, l: 106, t: 10, w: 93}, + {h: 12, l: 209, t: 10, w: 93}, {h: 12, l: 312, t: 10, w: 93} ].forEach(function(expected, i) { expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); @@ -438,9 +438,9 @@ describe('Legend block tests', function() { var chart = window.acquireChart({ type: 'bar', data: { - datasets: [1, 2, 3, 4, 5].map(function(n) { + datasets: Array.apply(null, Array(9)).map(function() { return { - label: 'dataset' + n, + label: ' ', data: [] }; }), @@ -452,15 +452,18 @@ describe('Legend block tests', function() { expect(chart.legend.top).toBeCloseToPixel(0); expect(chart.legend.width).toBeCloseToPixel(512); expect(chart.legend.height).toBeCloseToPixel(54); - expect(chart.legend.legendHitBoxes.length).toBe(5); - expect(chart.legend.legendHitBoxes.length).toBe(5); + expect(chart.legend.legendHitBoxes.length).toBe(9); [ - {h: 12, l: 56, t: 10, w: 93}, - {h: 12, l: 158, t: 10, w: 93}, - {h: 12, l: 261, t: 10, w: 93}, - {h: 12, l: 364, t: 10, w: 93}, - {h: 12, l: 210, t: 32, w: 93} + {h: 12, l: 24, t: 10, w: 49}, + {h: 12, l: 83, t: 10, w: 49}, + {h: 12, l: 142, t: 10, w: 49}, + {h: 12, l: 202, t: 10, w: 49}, + {h: 12, l: 261, t: 10, w: 49}, + {h: 12, l: 320, t: 10, w: 49}, + {h: 12, l: 380, t: 10, w: 49}, + {h: 12, l: 439, t: 10, w: 49}, + {h: 12, l: 231, t: 32, w: 49} ].forEach(function(expected, i) { expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); expect(chart.legend.legendHitBoxes[i].left).toBeCloseToPixel(expected.l); @@ -473,9 +476,9 @@ describe('Legend block tests', function() { var chart = window.acquireChart({ type: 'bar', data: { - datasets: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22].map(function(n) { + datasets: Array.apply(null, Array(22)).map(function() { return { - label: 'dataset' + n, + label: ' ', data: [] }; }), @@ -490,33 +493,33 @@ describe('Legend block tests', function() { expect(chart.legend.left).toBeCloseToPixel(0); expect(chart.legend.top).toBeCloseToPixel(6); - expect(chart.legend.width).toBeCloseToPixel(228.7); + expect(chart.legend.width).toBeCloseToPixel(128); expect(chart.legend.height).toBeCloseToPixel(478); expect(chart.legend.legendHitBoxes.length).toBe(22); [ - {h: 12, l: 10, t: 16, w: 93}, - {h: 12, l: 10, t: 38, w: 93}, - {h: 12, l: 10, t: 60, w: 93}, - {h: 12, l: 10, t: 82, w: 93}, - {h: 12, l: 10, t: 104, w: 93}, - {h: 12, l: 10, t: 126, w: 93}, - {h: 12, l: 10, t: 148, w: 93}, - {h: 12, l: 10, t: 170, w: 93}, - {h: 12, l: 10, t: 192, w: 93}, - {h: 12, l: 10, t: 214, w: 99}, - {h: 12, l: 10, t: 236, w: 99}, - {h: 12, l: 10, t: 258, w: 99}, - {h: 12, l: 10, t: 280, w: 99}, - {h: 12, l: 10, t: 302, w: 99}, - {h: 12, l: 10, t: 324, w: 99}, - {h: 12, l: 10, t: 346, w: 99}, - {h: 12, l: 10, t: 368, w: 99}, - {h: 12, l: 10, t: 390, w: 99}, - {h: 12, l: 10, t: 412, w: 99}, - {h: 12, l: 10, t: 434, w: 99}, - {h: 12, l: 10, t: 456, w: 99}, - {h: 12, l: 119, t: 16, w: 99} + {h: 12, l: 10, t: 16, w: 49}, + {h: 12, l: 10, t: 38, w: 49}, + {h: 12, l: 10, t: 60, w: 49}, + {h: 12, l: 10, t: 82, w: 49}, + {h: 12, l: 10, t: 104, w: 49}, + {h: 12, l: 10, t: 126, w: 49}, + {h: 12, l: 10, t: 148, w: 49}, + {h: 12, l: 10, t: 170, w: 49}, + {h: 12, l: 10, t: 192, w: 49}, + {h: 12, l: 10, t: 214, w: 49}, + {h: 12, l: 10, t: 236, w: 49}, + {h: 12, l: 10, t: 258, w: 49}, + {h: 12, l: 10, t: 280, w: 49}, + {h: 12, l: 10, t: 302, w: 49}, + {h: 12, l: 10, t: 324, w: 49}, + {h: 12, l: 10, t: 346, w: 49}, + {h: 12, l: 10, t: 368, w: 49}, + {h: 12, l: 10, t: 390, w: 49}, + {h: 12, l: 10, t: 412, w: 49}, + {h: 12, l: 10, t: 434, w: 49}, + {h: 12, l: 10, t: 456, w: 49}, + {h: 12, l: 69, t: 16, w: 49} ].forEach(function(expected, i) { expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); expect(chart.legend.legendHitBoxes[i].left).toBeCloseToPixel(expected.l); From b68341d9b888f0289edc7879619bdac86958709b Mon Sep 17 00:00:00 2001 From: chtheis Date: Wed, 21 Nov 2018 09:35:49 +0100 Subject: [PATCH 457/685] Correct calculation of padding in percent (#5846) --- src/core/core.helpers.js | 2 +- test/specs/core.helpers.tests.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 4b0a18c1648..3b1eca5edb3 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -499,7 +499,7 @@ module.exports = function() { helpers._calculatePadding = function(container, padding, parentDimension) { padding = helpers.getStyle(container, padding); - return padding.indexOf('%') > -1 ? parentDimension / parseInt(padding, 10) : parseInt(padding, 10); + return padding.indexOf('%') > -1 ? parentDimension * parseInt(padding, 10) / 100 : parseInt(padding, 10); }; /** * @private diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index de6d0b41301..70f0981df0e 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -790,7 +790,7 @@ describe('Core helper tests', function() { div.style.height = '300px'; document.body.appendChild(div); - // Inner DIV to have 10% padding of parent + // Inner DIV to have 5% padding of parent var innerDiv = document.createElement('div'); div.appendChild(innerDiv); @@ -802,8 +802,8 @@ describe('Core helper tests', function() { expect(helpers.getMaximumWidth(canvas)).toBe(300); // test with percentage - innerDiv.style.padding = '10%'; - expect(helpers.getMaximumWidth(canvas)).toBe(240); + innerDiv.style.padding = '5%'; + expect(helpers.getMaximumWidth(canvas)).toBe(270); // test with pixels innerDiv.style.padding = '10px'; From 0351a88a631cc6b964af203d2ed92fe2e48f9d99 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Mon, 26 Nov 2018 15:57:31 +0800 Subject: [PATCH 458/685] Add support for gridLines/angleLines borderDash for polarArea/radar charts (#5850) --- docs/axes/radial/linear.md | 2 + docs/axes/styling.md | 4 +- src/core/core.scale.js | 17 +++-- src/scales/scale.radialLinear.js | 64 +++++++++++------- .../scale.radialLinear/border-dash.json | 33 +++++++++ .../scale.radialLinear/border-dash.png | Bin 0 -> 31221 bytes .../circular-border-dash.json | 34 ++++++++++ .../circular-border-dash.png | Bin 0 -> 39138 bytes .../indexable-gridlines.json | 40 +++++++++++ .../indexable-gridlines.png | Bin 0 -> 54355 bytes test/specs/scale.radialLinear.tests.js | 6 +- 11 files changed, 166 insertions(+), 34 deletions(-) create mode 100644 test/fixtures/scale.radialLinear/border-dash.json create mode 100644 test/fixtures/scale.radialLinear/border-dash.png create mode 100644 test/fixtures/scale.radialLinear/circular-border-dash.json create mode 100644 test/fixtures/scale.radialLinear/circular-border-dash.png create mode 100644 test/fixtures/scale.radialLinear/indexable-gridlines.json create mode 100644 test/fixtures/scale.radialLinear/indexable-gridlines.png diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index d59b62251e8..d1edd18be54 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -95,6 +95,8 @@ The following options are used to configure angled lines that radiate from the c | `display` | `Boolean` | `true` | if true, angle lines are shown. | `color` | `Color` | `rgba(0, 0, 0, 0.1)` | Color of angled lines. | `lineWidth` | `Number` | `1` | Width of angled lines. +| `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on angled lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | `Number` | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). ## Point Label Options diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 80a7a2e66cb..7184bc96faf 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -12,7 +12,7 @@ The grid line configuration is nested under the scale configuration in the `grid | `circular` | `Boolean` | `false` | If true, gridlines are circular (on radar chart only). | `color` | `Color/Color[]` | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. | `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). -| `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderDashOffset` | `Number` | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `lineWidth` | `Number/Number[]` | `1` | Stroke width of grid lines. | `drawBorder` | `Boolean` | `true` | If true, draw border at the edge between the axis and the chart area. | `drawOnChartArea` | `Boolean` | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. @@ -21,7 +21,7 @@ The grid line configuration is nested under the scale configuration in the `grid | `zeroLineWidth` | `Number` | `1` | Stroke width of the grid line for the first index (index 0). | `zeroLineColor` | Color | `'rgba(0, 0, 0, 0.25)'` | Stroke color of the grid line for the first index (index 0). | `zeroLineBorderDash` | `Number[]` | `[]` | Length and spacing of dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). -| `zeroLineBorderDashOffset` | `Number` | `0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `zeroLineBorderDashOffset` | `Number` | `0.0` | Offset for line dashes of the grid line for the first index (index 0). See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `offsetGridLines` | `Boolean` | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` for a category scale in a bar chart by default. ## Tick Configuration diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 28211b7f43a..e3a7979440c 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -728,13 +728,13 @@ module.exports = Element.extend({ // Draw the first index specially lineWidth = gridLines.zeroLineWidth; lineColor = gridLines.zeroLineColor; - borderDash = gridLines.zeroLineBorderDash; - borderDashOffset = gridLines.zeroLineBorderDashOffset; + borderDash = gridLines.zeroLineBorderDash || []; + borderDashOffset = gridLines.zeroLineBorderDashOffset || 0.0; } else { lineWidth = helpers.valueAtIndexOrDefault(gridLines.lineWidth, index); lineColor = helpers.valueAtIndexOrDefault(gridLines.color, index); - borderDash = helpers.valueOrDefault(gridLines.borderDash, globalDefaults.borderDash); - borderDashOffset = helpers.valueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); + borderDash = gridLines.borderDash || []; + borderDashOffset = gridLines.borderDashOffset || 0.0; } // Common properties @@ -825,10 +825,13 @@ module.exports = Element.extend({ // Draw all of the tick labels, tick marks, and grid lines at the correct places helpers.each(itemsToDraw, function(itemToDraw) { - if (gridLines.display) { + var glWidth = itemToDraw.glWidth; + var glColor = itemToDraw.glColor; + + if (gridLines.display && glWidth && glColor) { context.save(); - context.lineWidth = itemToDraw.glWidth; - context.strokeStyle = itemToDraw.glColor; + context.lineWidth = glWidth; + context.strokeStyle = glColor; if (context.setLineDash) { context.setLineDash(itemToDraw.glBorderDash); context.lineDashOffset = itemToDraw.glBorderDashOffset; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index b580e1da75b..90ac22f0db7 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -19,7 +19,9 @@ module.exports = function(Chart) { angleLines: { display: true, color: 'rgba(0, 0, 0, 0.1)', - lineWidth: 1 + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 }, gridLines: { @@ -240,26 +242,34 @@ module.exports = function(Chart) { var ctx = scale.ctx; var opts = scale.options; var angleLineOpts = opts.angleLines; + var gridLineOpts = opts.gridLines; var pointLabelOpts = opts.pointLabels; - - ctx.lineWidth = angleLineOpts.lineWidth; - ctx.strokeStyle = angleLineOpts.color; + var lineWidth = helpers.valueOrDefault(angleLineOpts.lineWidth, gridLineOpts.lineWidth); + var lineColor = helpers.valueOrDefault(angleLineOpts.color, gridLineOpts.color); + + ctx.save(); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = lineColor; + if (ctx.setLineDash) { + ctx.setLineDash(helpers.valueOrDefault(angleLineOpts.borderDash, gridLineOpts.borderDash) || []); + ctx.lineDashOffset = helpers.valueOrDefault(angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset) || 0.0; + } var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max); // Point Label Font var plFont = getPointLabelFontOptions(scale); + ctx.font = plFont.font; ctx.textBaseline = 'top'; for (var i = getValueCount(scale) - 1; i >= 0; i--) { - if (angleLineOpts.display) { + if (angleLineOpts.display && lineWidth && lineColor) { var outerPosition = scale.getPointPosition(i, outerDistance); ctx.beginPath(); ctx.moveTo(scale.xCenter, scale.yCenter); ctx.lineTo(outerPosition.x, outerPosition.y); ctx.stroke(); - ctx.closePath(); } if (pointLabelOpts.display) { @@ -268,7 +278,6 @@ module.exports = function(Chart) { // Keep this in loop since we may support array properties here var pointLabelFontColor = helpers.valueAtIndexOrDefault(pointLabelOpts.fontColor, i, globalDefaults.defaultFontColor); - ctx.font = plFont.font; ctx.fillStyle = pointLabelFontColor; var angleRadians = scale.getIndexAngle(i); @@ -278,39 +287,46 @@ module.exports = function(Chart) { fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size); } } + ctx.restore(); } function drawRadiusLine(scale, gridLineOpts, radius, index) { var ctx = scale.ctx; - ctx.strokeStyle = helpers.valueAtIndexOrDefault(gridLineOpts.color, index - 1); - ctx.lineWidth = helpers.valueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); + var circular = gridLineOpts.circular; + var valueCount = getValueCount(scale); + var lineColor = helpers.valueAtIndexOrDefault(gridLineOpts.color, index - 1); + var lineWidth = helpers.valueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); + var pointPosition; - if (scale.options.gridLines.circular) { + if ((!circular && !valueCount) || !lineColor || !lineWidth) { + return; + } + + ctx.save(); + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth; + if (ctx.setLineDash) { + ctx.setLineDash(gridLineOpts.borderDash || []); + ctx.lineDashOffset = gridLineOpts.borderDashOffset || 0.0; + } + + ctx.beginPath(); + if (circular) { // Draw circular arcs between the points - ctx.beginPath(); ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); } else { // Draw straight lines connecting each index - var valueCount = getValueCount(scale); - - if (valueCount === 0) { - return; - } - - ctx.beginPath(); - var pointPosition = scale.getPointPosition(0, radius); + pointPosition = scale.getPointPosition(0, radius); ctx.moveTo(pointPosition.x, pointPosition.y); for (var i = 1; i < valueCount; i++) { pointPosition = scale.getPointPosition(i, radius); ctx.lineTo(pointPosition.x, pointPosition.y); } - - ctx.closePath(); - ctx.stroke(); } + ctx.closePath(); + ctx.stroke(); + ctx.restore(); } function numberOrZero(param) { diff --git a/test/fixtures/scale.radialLinear/border-dash.json b/test/fixtures/scale.radialLinear/border-dash.json new file mode 100644 index 00000000000..5a28ffab087 --- /dev/null +++ b/test/fixtures/scale.radialLinear/border-dash.json @@ -0,0 +1,33 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scale": { + "gridLines": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/border-dash.png b/test/fixtures/scale.radialLinear/border-dash.png new file mode 100644 index 0000000000000000000000000000000000000000..eea0db5ff007aeccdbf4403974f9c10d0ce15747 GIT binary patch literal 31221 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/scale.radialLinear/circular-border-dash.json b/test/fixtures/scale.radialLinear/circular-border-dash.json new file mode 100644 index 00000000000..b69250de975 --- /dev/null +++ b/test/fixtures/scale.radialLinear/circular-border-dash.json @@ -0,0 +1,34 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scale": { + "gridLines": { + "circular": true, + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/circular-border-dash.png b/test/fixtures/scale.radialLinear/circular-border-dash.png new file mode 100644 index 0000000000000000000000000000000000000000..fedb709e9a9cae3af99c8c70d02971f08c525452 GIT binary patch literal 39138 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

    !ZqH3jW5J>(-5E9Y8wGTERg_>cC=ZY`!u& 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 literal 0 HcmV?d00001 diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 707ce0b7c52..5390ef77d5f 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -1,5 +1,7 @@ // Tests for the radial linear scale used by the polar area and radar charts describe('Test the radial linear scale', function() { + describe('auto', jasmine.fixture.specs('scale.radialLinear')); + it('Should register the constructor with the scale service', function() { var Constructor = Chart.scaleService.getScaleConstructor('radialLinear'); expect(Constructor).not.toBe(undefined); @@ -12,7 +14,9 @@ describe('Test the radial linear scale', function() { angleLines: { display: true, color: 'rgba(0, 0, 0, 0.1)', - lineWidth: 1 + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 }, animate: true, display: true, From 08447e9e19a7535b24b4c8888aaf24d013204ea1 Mon Sep 17 00:00:00 2001 From: Stef Louwers Date: Tue, 27 Nov 2018 15:14:41 +0100 Subject: [PATCH 459/685] Draw radial scale angle lines before tick labels (#5855) Moved drawing of radial lines before drawing the tick labels, such that the radial lines are not drawn on top of the tick labels and their backdrop. --- src/scales/scale.radialLinear.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 90ac22f0db7..ffd76dd43f4 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -497,6 +497,10 @@ module.exports = function(Chart) { var tickFontFamily = valueOrDefault(tickOpts.fontFamily, globalDefaults.defaultFontFamily); var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); + if (opts.angleLines.display || opts.pointLabels.display) { + drawPointLabels(me); + } + helpers.each(me.ticks, function(label, index) { // Don't draw a centre value (if it is minimum) if (index > 0 || tickOpts.reverse) { @@ -534,10 +538,6 @@ module.exports = function(Chart) { } } }); - - if (opts.angleLines.display || opts.pointLabels.display) { - drawPointLabels(me); - } } } }); From f5437fe548e5ca4d9236f3724fae692c30913c84 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 27 Nov 2018 17:26:41 +0200 Subject: [PATCH 460/685] Fix nearest interaction mode to return all items (#5857) Return all items that are at the nearest distance to the point and add unit tests for nearest + axis: 'x' and nearest + axis: 'y' --- docs/general/interactions/modes.md | 2 +- src/core/core.interaction.js | 21 +- test/specs/core.interaction.tests.js | 363 +++++++++++++++------------ 3 files changed, 208 insertions(+), 178 deletions(-) diff --git a/docs/general/interactions/modes.md b/docs/general/interactions/modes.md index 30c40616887..d6a8c261fba 100644 --- a/docs/general/interactions/modes.md +++ b/docs/general/interactions/modes.md @@ -20,7 +20,7 @@ var chart = new Chart(ctx, { ``` ## 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. +Gets the items that are at the nearest distance to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). You can use the `axis` setting to define which directions are used in distance calculation. 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. ```javascript var chart = new Chart(ctx, { diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index be85a080f76..9b99e53bb1b 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -243,26 +243,7 @@ module.exports = { var position = getRelativePosition(e, chart); options.axis = options.axis || 'xy'; var distanceMetric = getDistanceMetricForAxis(options.axis); - var nearestItems = getNearestItems(chart, position, options.intersect, distanceMetric); - - // 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); + return getNearestItems(chart, position, options.intersect, distanceMetric); }, /** diff --git a/test/specs/core.interaction.tests.js b/test/specs/core.interaction.tests.js index 49cd3bd193d..356d0de95b1 100644 --- a/test/specs/core.interaction.tests.js +++ b/test/specs/core.interaction.tests.js @@ -347,73 +347,145 @@ describe('Core.Interaction', function() { }); }); - it ('axis: xy should return the nearest item', function() { - var chart = this.chart; - var evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: 0, - y: 0 - }; + describe('axis: xy', function() { + it ('should return the nearest item', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: 0, + y: 0 + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}); + var meta = chart.getDatasetMeta(1); + expect(elements).toEqual([meta.data[0]]); + }); - // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}); - var meta = chart.getDatasetMeta(1); - expect(elements).toEqual([meta.data[0]]); + it ('should return all items at the same nearest distance', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.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 evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Both points are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); }); - it ('should return the smallest item if more than 1 are at the same distance', function() { - var chart = this.chart; - var meta0 = chart.getDatasetMeta(0); - var meta1 = chart.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 evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: pt.x, - y: pt.y - }; + describe('axis: x', function() { + it ('should return all items at current x', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // At 'Point 2', 10 + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[0]._view.y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Middle point from both series are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); - // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}); - expect(elements).toEqual([meta0.data[1]]); + it ('should return all items at nearest x-distance', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Haflway between 'Point 1' and 'Point 2', y=10 + var pt = { + x: (meta0.data[0]._view.x + meta0.data[1]._view.x) / 2, + y: meta0.data[0]._view.y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Should return all (4) points from 'Point 1' and 'Point 2' + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}); + expect(elements).toEqual([meta0.data[0], meta0.data[1], meta1.data[0], meta1.data[1]]); + }); }); - it ('should return the lowest dataset index if size and area are the same', function() { - var chart = this.chart; - // Make equal sized points at index: 1 - chart.data.datasets[0].pointRadius[1] = 10; - chart.update(); - - // Trigger an event over top of the - var meta0 = chart.getDatasetMeta(0); - var meta1 = chart.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 evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: pt.x, - y: pt.y - }; + describe('axis: y', function() { + it ('should return item with value 30', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + + // 'Point 1', y = 30 + var pt = { + x: meta0.data[0]._view.x, + y: meta0.data[2]._view.y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Middle point from both series are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}); + expect(elements).toEqual([meta0.data[2]]); + }); - // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}); - expect(elements).toEqual([meta0.data[1]]); + it ('should return all items at value 40', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // 'Point 1', y = 40 + var pt = { + x: meta0.data[0]._view.x, + y: meta0.data[1]._view.y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Should return points with value 40 + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + }); }); }); @@ -438,117 +510,94 @@ describe('Core.Interaction', function() { }); }); - it ('should return the nearest item', function() { - var chart = this.chart; - var meta = chart.getDatasetMeta(1); - var point = meta.data[1]; - - var evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: point._view.x + 15, - y: point._view.y - }; - - // Nothing intersects so find nothing - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); - expect(elements).toEqual([]); - - evt = { - type: 'click', - chart: chart, - native: true, - x: point._view.x, - y: point._view.y - }; - elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); - expect(elements).toEqual([point]); - }); - - it ('should return the nearest item even if 2 intersect', function() { - var chart = this.chart; - chart.data.datasets[0].pointRadius = [5, 30, 5]; - chart.data.datasets[0].data[1] = 39; + describe('axis=xy', function() { + it ('should return the nearest item', function() { + var chart = this.chart; + var meta = chart.getDatasetMeta(1); + var point = meta.data[1]; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point._view.x + 15, + y: point._view.y + }; + + // Nothing intersects so find nothing + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); + expect(elements).toEqual([]); + + evt = { + type: 'click', + chart: chart, + native: true, + x: point._view.x, + y: point._view.y + }; + elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); + expect(elements).toEqual([point]); + }); - chart.data.datasets[1].pointRadius = [10, 10, 10]; + it ('should return the nearest item even if 2 intersect', function() { + var chart = this.chart; + chart.data.datasets[0].pointRadius = [5, 30, 5]; + chart.data.datasets[0].data[1] = 39; - // Trigger an event over top of the - var meta0 = chart.getDatasetMeta(0); + chart.data.datasets[1].pointRadius = [10, 10, 10]; - // Halfway between 2 mid points - var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y - }; + chart.update(); - var evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: pt.x, - y: pt.y - }; + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); - expect(elements).toEqual([meta0.data[1]]); - }); + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; - it ('should return the smallest item if more than 1 are at the same distance', function() { - var chart = this.chart; - chart.data.datasets[0].pointRadius = [5, 5, 5]; - chart.data.datasets[0].data[1] = 40; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; - chart.data.datasets[1].pointRadius = [10, 10, 10]; + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1]]); + }); - // Trigger an event over top of the - var meta0 = chart.getDatasetMeta(0); + it ('should return the all items if more than 1 are at the same distance', function() { + var chart = this.chart; + chart.data.datasets[0].pointRadius = [5, 5, 5]; + chart.data.datasets[0].data[1] = 40; - // Halfway between 2 mid points - var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y - }; + chart.data.datasets[1].pointRadius = [10, 10, 10]; - var evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: pt.x, - y: pt.y - }; + chart.update(); - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); - expect(elements).toEqual([meta0.data[1]]); - }); + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); - it ('should return the item at the lowest dataset index if distance and area are the same', function() { - var chart = this.chart; - chart.data.datasets[0].pointRadius = [5, 10, 5]; - chart.data.datasets[0].data[1] = 40; + // Halfway between 2 mid points + var pt = { + x: meta0.data[1]._view.x, + y: meta0.data[1]._view.y + }; - chart.data.datasets[1].pointRadius = [10, 10, 10]; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; - // Trigger an event over top of the - var meta0 = chart.getDatasetMeta(0); - - // Halfway between 2 mid points - var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y - }; - - var evt = { - type: 'click', - chart: chart, - native: true, // needed otherwise things its a DOM event - x: pt.x, - y: pt.y - }; - - // Nearest to 0,0 (top left) will be first point of dataset 2 - var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); - expect(elements).toEqual([meta0.data[1]]); + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); }); }); }); From 241499d27f6b684a348eee3a7b1e615c031809b6 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Wed, 28 Nov 2018 08:54:03 +0800 Subject: [PATCH 461/685] Make getHoverColor() return the original value if it is CanvasGradient (#5865) --- src/core/core.helpers.js | 2 +- test/specs/core.helpers.tests.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 3b1eca5edb3..ca8a7d20c38 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -647,7 +647,7 @@ module.exports = function() { helpers.getHoverColor = function(colorValue) { /* global CanvasPattern */ - return (colorValue instanceof CanvasPattern) ? + return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ? colorValue : helpers.color(colorValue).saturate(0.5).darken(0.1).rgbString(); }; diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index 70f0981df0e..796148aaf6c 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -848,6 +848,13 @@ describe('Core helper tests', function() { }; }); + it('should return a CanvasGradient when called with a CanvasGradient', function() { + var context = document.createElement('canvas').getContext('2d'); + var gradient = context.createLinearGradient(0, 1, 2, 3); + + expect(helpers.getHoverColor(gradient) instanceof CanvasGradient).toBe(true); + }); + it('should return a modified version of color when called with a color', function() { var originalColorRGB = 'rgb(70, 191, 189)'; From 1f2fa5c90c7a270874893919af1af9ae6fd5666b Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Wed, 28 Nov 2018 14:53:41 +0800 Subject: [PATCH 462/685] Adjust the size of rectRounded/rectRot point to fit pointRadius (#5858) - Calculate the vertices of the shapes so that they are inscribed in the circle that has the radius of `pointRadius` - Remove `translate()` and `rotate()` to fix the regression introduced by #5319 - Refactor `rectRounded` for better performance --- src/helpers/helpers.canvas.js | 166 ++++++++++-------- .../controller.bubble/point-style.png | Bin 6566 -> 11225 bytes test/fixtures/controller.line/point-style.png | Bin 6273 -> 6440 bytes .../fixtures/controller.radar/point-style.png | Bin 6986 -> 7070 bytes .../element.point/point-style-rect-rot.png | Bin 6520 -> 3174 bytes .../point-style-rect-rounded.png | Bin 8262 -> 4469 bytes test/fixtures/element.point/rotation.js | 56 ++++++ test/fixtures/element.point/rotation.png | Bin 0 -> 52523 bytes test/fixtures/helpers.canvas/rounded-rect.js | 39 ++++ test/fixtures/helpers.canvas/rounded-rect.png | Bin 0 -> 13050 bytes test/specs/element.point.tests.js | 14 +- test/specs/helpers.canvas.tests.js | 55 +++++- 12 files changed, 237 insertions(+), 93 deletions(-) create mode 100644 test/fixtures/element.point/rotation.js create mode 100644 test/fixtures/element.point/rotation.png create mode 100644 test/fixtures/helpers.canvas/rounded-rect.js create mode 100644 test/fixtures/helpers.canvas/rounded-rect.png diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 60fb6e1a299..ea0c6f1cefe 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -2,6 +2,13 @@ var helpers = require('./helpers.core'); +var PI = Math.PI; +var RAD_PER_DEG = PI / 180; +var DOUBLE_PI = PI * 2; +var HALF_PI = PI / 2; +var QUARTER_PI = PI / 4; +var TWO_THIRDS_PI = PI * 2 / 3; + /** * @namespace Chart.helpers.canvas */ @@ -27,20 +34,28 @@ var exports = module.exports = { */ roundedRect: function(ctx, x, y, width, height, radius) { if (radius) { - // NOTE(SB) `epsilon` helps to prevent minor artifacts appearing - // on Chrome when `r` is exactly half the height or the width. - var epsilon = 0.0000001; - var r = Math.min(radius, (height / 2) - epsilon, (width / 2) - epsilon); - - ctx.moveTo(x + r, y); - ctx.lineTo(x + width - r, y); - ctx.arcTo(x + width, y, x + width, y + r, r); - ctx.lineTo(x + width, y + height - r); - ctx.arcTo(x + width, y + height, x + width - r, y + height, r); - ctx.lineTo(x + r, y + height); - ctx.arcTo(x, y + height, x, y + height - r, r); - ctx.lineTo(x, y + r); - ctx.arcTo(x, y, x + r, y, r); + var r = Math.min(radius, height / 2, width / 2); + var left = x + r; + var top = y + r; + var right = x + width - r; + var bottom = y + height - r; + + ctx.moveTo(x, top); + if (left < right && top < bottom) { + ctx.arc(left, top, r, -PI, -HALF_PI); + ctx.arc(right, top, r, -HALF_PI, 0); + ctx.arc(right, bottom, r, 0, HALF_PI); + ctx.arc(left, bottom, r, HALF_PI, PI); + } else if (left < right) { + ctx.moveTo(left, y); + ctx.arc(right, top, r, -HALF_PI, HALF_PI); + ctx.arc(left, top, r, HALF_PI, PI + HALF_PI); + } else if (top < bottom) { + ctx.arc(left, top, r, -PI, 0); + ctx.arc(left, bottom, r, 0, PI); + } else { + ctx.arc(left, top, r, -PI, PI); + } ctx.closePath(); ctx.moveTo(x, y); } else { @@ -49,8 +64,8 @@ var exports = module.exports = { }, drawPoint: function(ctx, style, radius, x, y, rotation) { - var type, edgeLength, xOffset, yOffset, height, size; - rotation = rotation || 0; + var type, xOffset, yOffset, size, cornerRadius; + var rad = (rotation || 0) * RAD_PER_DEG; if (style && typeof style === 'object') { type = style.toString(); @@ -64,88 +79,97 @@ var exports = module.exports = { return; } - ctx.save(); - ctx.translate(x, y); - ctx.rotate(rotation * Math.PI / 180); ctx.beginPath(); switch (style) { // Default includes circle default: - ctx.arc(0, 0, radius, 0, Math.PI * 2); + ctx.arc(x, y, radius, 0, DOUBLE_PI); ctx.closePath(); break; case 'triangle': - edgeLength = 3 * radius / Math.sqrt(3); - height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(-edgeLength / 2, height / 3); - ctx.lineTo(edgeLength / 2, height / 3); - ctx.lineTo(0, -2 * height / 3); + ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); ctx.closePath(); break; - case 'rect': - size = 1 / Math.SQRT2 * radius; - ctx.rect(-size, -size, 2 * size, 2 * size); - break; case 'rectRounded': - var offset = radius / Math.SQRT2; - var leftX = -offset; - var topY = -offset; - var sideSize = Math.SQRT2 * radius; - - // NOTE(SB) the rounded rect implementation changed to use `arcTo` - // instead of `quadraticCurveTo` since it generates better results - // when rect is almost a circle. 0.425 (instead of 0.5) produces - // results visually closer to the previous impl. - this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425); + // NOTE: the rounded rect implementation changed to use `arc` instead of + // `quadraticCurveTo` since it generates better results when rect is + // almost a circle. 0.516 (instead of 0.5) produces results with visually + // closer proportion to the previous impl and it is inscribed in the + // circle with `radius`. For more details, see the following PRs: + // https://github.com/chartjs/Chart.js/issues/5597 + // https://github.com/chartjs/Chart.js/issues/5858 + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + yOffset = Math.sin(rad + QUARTER_PI) * size; + ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + ctx.rect(x - size, y - size, 2 * size, 2 * size); + break; + } + rad += QUARTER_PI; + /* falls through */ case 'rectRot': - size = 1 / Math.SQRT2 * radius; - ctx.moveTo(-size, 0); - ctx.lineTo(0, size); - ctx.lineTo(size, 0); - ctx.lineTo(0, -size); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + yOffset, y - xOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.lineTo(x - yOffset, y + xOffset); ctx.closePath(); break; - case 'cross': - ctx.moveTo(0, radius); - ctx.lineTo(0, -radius); - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); - break; case 'crossRot': - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(-xOffset, -yOffset); - ctx.lineTo(xOffset, yOffset); - ctx.moveTo(-xOffset, yOffset); - ctx.lineTo(xOffset, -yOffset); + rad += QUARTER_PI; + /* falls through */ + case 'cross': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); break; case 'star': - ctx.moveTo(0, radius); - ctx.lineTo(0, -radius); - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(-xOffset, -yOffset); - ctx.lineTo(xOffset, yOffset); - ctx.moveTo(-xOffset, yOffset); - ctx.lineTo(xOffset, -yOffset); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + rad += QUARTER_PI; + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); break; case 'line': - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); break; case 'dash': - ctx.moveTo(0, 0); - ctx.lineTo(radius, 0); + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); break; } ctx.fill(); ctx.stroke(); - ctx.restore(); }, clipArea: function(ctx, area) { diff --git a/test/fixtures/controller.bubble/point-style.png b/test/fixtures/controller.bubble/point-style.png index f1d3b8168c07d1e70eda4c04dc50325d25c0b39c..d949141d81d4f46dbb844b9a33e63d0b2f81a3fa 100644 GIT binary patch literal 11225 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 literal 6273 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 diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png index 562cb620b054de95898cba7bbc3908b479bfe2da..3f73ff96f91a02142de3504a59531b8f954d59d2 100644 GIT binary patch literal 7070 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 P!tmG;N89qlzSsT(xdDrf diff --git a/test/fixtures/element.point/point-style-rect-rot.png b/test/fixtures/element.point/point-style-rect-rot.png index 09c0adac3d3e32580845000e7188e63fb60b7c95..a7c128855892682594576c778eeafaf043fd7f36 100644 GIT binary patch literal 3174 zcma)8X;f3!7Ctuw2?&}XXd*<#6m6|cB7>k3zy_g@!2zu}5TP1W1VluXNN$xP2niIZ zRZ&4-pHM}nDh32eYr-Ic@qvQMWTFD1kW>m$Lhd^!Z>_G?f6ZEV?S1zC_HfQU`+ms? z3Gy>DnQsCBF!Nu(ZX*CBsA7OJ0}5GAI}ZR;JO6dwo0BfS?+q4NKf#*RM^90|jj%Do zd`g`wr5-&XMza0~cRwb}eCDJX_YtYt+%|sh1|7FWWS&vFcxr}2=BV3f^^F%#o`g>8 zPOJ?+TUv1})Lo}jW$}Dqk-z`&Ei%VcdRk8x0A+iuhyfd;^nZA%blbL2CP6$V1;j_940Pld=Ss|iAp zpEn*+qL9R6$XQWE@0;bsw~9zQH?>UGVg)PVKT1`t)cb`;uY3$oe|e{-d~?5yXJMrg5 zBz-vbn+>@m3sA)S!56ewQQY7@+f;xbA%@XS{Vzmf}XBrmmLiGI|@+VFzoD6cam#O`6jb| z(;;y+N0KvjgjCA&M79mg{#hEaE3&zeLaw)R*ahm~w+k@%E{xd2Z-jWYvaoX%DXpj6 z3=g~LhK_EdzAJmM-}xYU+>-cNgbD|O&bQ-~A!nWcGgR|9DCyNuhCFrt=k+asDA`L2 zl{m3Sh}%q#fFc*CI%b5%RjP8F3_Y`{5p8n@7z*w~srX=M>t&=wgMnHUEe7oPOUUpq zFc3N-f*F(;XexDvU1b{rcvTR{9Y{8}W0QRyi>x`UW;pcU<~vKFxRDW_Mio$ObT*TL4>tMWUyBWZFeCh)DWQAx z#g}!qy@vP$pP7f`()RmI?DL1Kc8?t- zcUnBXFlxfyd-!BuE2YG##HEjce|h(_S2wfq3%+3=da)&$*}mxnthi({@NLyP$#y1Y zN51tv54bN|o5Ijd7ao3|`!ePr`Ibfd*~^9l?cb<`xZ8GC_suMP?x|-AXN>7-d7L#M zcJbXgMTuIKZ|7xY;jq@!MrW6*DeiImr9WR{g_Yms6dhndBhe|E8hi{_xPamX@=$`F0k*;8eHL+1XqB zc8_LK(7w9ZCzgD1FgIs%H7Pz#JWi7zrX;5%iX6e&Jz#kgIM)Q+%LXYE+IJTRd_n6U zHu!4|Rk9ilXSU5d^?4hsLbBMpknElvOj=!5yl7&)G{jV37Pm?_e_%Cy4y{S8Jk29} z49vvZlru4ASNx*wDXvAn_bgPa;d96?txCotsoLWQ$DrNd=!A*76w;sglEO4ext>TK$mHi#sFS3+FocR-DE^Taq=c@KNKd*8PQOj zSx$>VtDWX?Xw_z10|N}&jccLsy_UKYX7Mg2Iq2Y}uaswcfdsCXMll;OGd5_ke=t6O zK}tA$!IS|QTF0mQ@T;fPizW{O{E2Kb@n!big^#KUDy$h$BRFCuMx@5tW^509R1m*+x;S7p?KP59ivbVQMMt5}FXZ*e(@ zy-J=L>N%D(Q%Yzk;=-w6fOjpY6Ka}D3S+L8QV}l)R7dD(^|2))$s9lJ%1N^a2{J^$ zJGR#K%HC`kEy%a^MVK13Ld1qa2~Kbcj1 zHU}lD7(|J#)8yU=nDpzZMtoQj4LM;0u56F z6P5Xq*{?6cQ@IBNqpA#V%kYO z8H!ShktaaN2}AezZx=c$7g^;AXPGCy2n zFs=S*`QafQ3vBYrn7m;Hagt;0bZn>8>z639bt93XF>rjGN|T3b^GrCB5p_f1)J-hp z$2qK`R?SkwY*>oRV6lou;6j8JXwNFoQ!(-V%gax~tr}E_>S>E~$tI`;N$n^{0+$xZS~)kHth>sK}0+j^zS5+!1r&9#O(fXg;6YV16; zSlZLE@+QK2v={?R3Y<`)u((DFRRE$|QcKyonu%bQaMPWroYPjzPpL8d_CSfYl ziYR^|m;qKUOQHH`C+)Q`_jLN$X)2-2p2mti)xN~**?)bU09dXh_C8s^f%l2`9kTj= L6|}C*hkNpWYD$7~ literal 6520 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 diff --git a/test/fixtures/element.point/point-style-rect-rounded.png b/test/fixtures/element.point/point-style-rect-rounded.png index a58e9e62361a96fb187cc342c58d89c954faaaa4..8b58b44303ac578744a267231301427f461475b8 100644 GIT binary patch literal 4469 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 diff --git a/test/fixtures/element.point/rotation.js b/test/fixtures/element.point/rotation.js new file mode 100644 index 00000000000..b713f5d6bf4 --- /dev/null +++ b/test/fixtures/element.point/rotation.js @@ -0,0 +1,56 @@ +var gradient; + +var datasets = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle'].map(function(style, y) { + return { + pointStyle: style, + data: Array.apply(null, Array(17)).map(function(v, x) { + return {x: x, y: 10 - y}; + }) + }; +}); + +var angles = Array.apply(null, Array(17)).map(function(v, i) { + return -180 + i * 22.5; +}); + +module.exports = { + config: { + type: 'bubble', + data: { + datasets: datasets + }, + options: { + responsive: false, + legend: false, + title: false, + elements: { + point: { + rotation: angles, + radius: 10, + backgroundColor: function(context) { + if (!gradient) { + gradient = context.chart.ctx.createLinearGradient(0, 0, 512, 256); + gradient.addColorStop(0, '#ff0000'); + gradient.addColorStop(1, '#0000ff'); + } + return gradient; + }, + borderColor: '#cccccc' + } + }, + layout: { + padding: 20 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.point/rotation.png b/test/fixtures/element.point/rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..579c712ecf621a087cf574b48e2c14be27a28aa7 GIT binary patch literal 52523 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�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 I-TKM@0=vZ0?*IS* literal 0 HcmV?d00001 diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 0f3a03e319d..887250acd8d 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -124,21 +124,12 @@ describe('Chart.elements.Point', function() { }, { name: 'setFillStyle', args: ['rgba(0,0,0,0.1)'] - }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [10, 15] - }, { - name: 'rotate', - args: [0] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [0, 0, 2, 0, 2 * Math.PI] + args: [10, 15, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -148,9 +139,6 @@ describe('Chart.elements.Point', function() { }, { name: 'stroke', args: [] - }, { - name: 'restore', - args: [] }]); }); diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js index 1a342c1cb3b..ee42a414e6f 100644 --- a/test/specs/helpers.canvas.tests.js +++ b/test/specs/helpers.canvas.tests.js @@ -1,6 +1,8 @@ 'use strict'; describe('Chart.helpers.canvas', function() { + describe('auto', jasmine.fixture.specs('helpers.canvas')); + var helpers = Chart.helpers; describe('clear', function() { @@ -28,15 +30,50 @@ describe('Chart.helpers.canvas', function() { helpers.canvas.roundedRect(context, 10, 20, 30, 40, 5); expect(context.getCalls()).toEqual([ - {name: 'moveTo', args: [15, 20]}, - {name: 'lineTo', args: [35, 20]}, - {name: 'arcTo', args: [40, 20, 40, 25, 5]}, - {name: 'lineTo', args: [40, 55]}, - {name: 'arcTo', args: [40, 60, 35, 60, 5]}, - {name: 'lineTo', args: [15, 60]}, - {name: 'arcTo', args: [10, 60, 10, 55, 5]}, - {name: 'lineTo', args: [10, 25]}, - {name: 'arcTo', args: [10, 20, 15, 20, 5]}, + {name: 'moveTo', args: [10, 25]}, + {name: 'arc', args: [15, 25, 5, -Math.PI, -Math.PI / 2]}, + {name: 'arc', args: [35, 25, 5, -Math.PI / 2, 0]}, + {name: 'arc', args: [35, 55, 5, 0, Math.PI / 2]}, + {name: 'arc', args: [15, 55, 5, Math.PI / 2, Math.PI]}, + {name: 'closePath', args: []}, + {name: 'moveTo', args: [10, 20]} + ]); + }); + it('should optimize path if radius is exactly half of height', function() { + var context = window.createMockContext(); + + helpers.canvas.roundedRect(context, 10, 20, 40, 30, 15); + + expect(context.getCalls()).toEqual([ + {name: 'moveTo', args: [10, 35]}, + {name: 'moveTo', args: [25, 20]}, + {name: 'arc', args: [35, 35, 15, -Math.PI / 2, Math.PI / 2]}, + {name: 'arc', args: [25, 35, 15, Math.PI / 2, Math.PI * 3 / 2]}, + {name: 'closePath', args: []}, + {name: 'moveTo', args: [10, 20]} + ]); + }); + it('should optimize path if radius is exactly half of width', function() { + var context = window.createMockContext(); + + helpers.canvas.roundedRect(context, 10, 20, 30, 40, 15); + + expect(context.getCalls()).toEqual([ + {name: 'moveTo', args: [10, 35]}, + {name: 'arc', args: [25, 35, 15, -Math.PI, 0]}, + {name: 'arc', args: [25, 45, 15, 0, Math.PI]}, + {name: 'closePath', args: []}, + {name: 'moveTo', args: [10, 20]} + ]); + }); + it('should optimize path if radius is exactly half of width and height', function() { + var context = window.createMockContext(); + + helpers.canvas.roundedRect(context, 10, 20, 30, 30, 15); + + expect(context.getCalls()).toEqual([ + {name: 'moveTo', args: [10, 35]}, + {name: 'arc', args: [25, 35, 15, -Math.PI, Math.PI]}, {name: 'closePath', args: []}, {name: 'moveTo', args: [10, 20]} ]); From bbca2fc78982e09414330f5addd7be5e0de57527 Mon Sep 17 00:00:00 2001 From: jedrekdomanski Date: Wed, 28 Nov 2018 07:56:41 +0100 Subject: [PATCH 463/685] Enhance documentation for bar specific scale options (#5854) --- docs/charts/bar.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/charts/bar.md b/docs/charts/bar.md index e71c38e24ba..945539ea59a 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -125,9 +125,8 @@ The interaction with each bar can be controlled with the following properties: All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. -## Configuration Options - -The bar chart defines the following configuration options. These options are merged with the global chart configuration options, `Chart.defaults.global`, to form the options passed to the chart. +## Scale Configuration +The bar chart accepts the following configuration from the associated `scale` options: | Name | Type | Default | Description | ---- | ---- | ------- | ----------- @@ -138,22 +137,16 @@ The bar chart defines the following configuration options. These options are mer | `minBarLength` | `Number` | | Set this to ensure that bars have a minimum length in pixels. | `gridLines.offsetGridLines` | `Boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) -### barThickness -If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. - -If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. - -If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. - -### offsetGridLines -If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. - -This setting applies to the axis configuration. If axes are added to the chart, this setting will need to be set for each new axis. +### Example Usage ```javascript options = { scales: { xAxes: [{ + barPercentage: 0.5, + barThickness: 6, + maxBarThickness: 8, + minBarLength: 2, gridLines: { offsetGridLines: true } @@ -161,6 +154,15 @@ options = { } } ``` +### barThickness +If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. + +If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. + +If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. + +### offsetGridLines +If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. ## Default Options @@ -287,6 +289,6 @@ var myBarChart = new Chart(ctx, { ``` ## Config Options -The configuration options for the horizontal bar chart are the same as for the [bar chart](#configuration-options). However, any options specified on the x axis in a bar chart, are applied to the y axis in a horizontal bar chart. +The configuration options for the horizontal bar chart are the same as for the [bar chart](#scale-configuration). However, any options specified on the x axis in a bar chart, are applied to the y axis in a horizontal bar chart. The default horizontal bar configuration is specified in `Chart.defaults.horizontalBar`. From d6ac7d8a80a80866d64845c99cbfaec66b377ddd Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Wed, 28 Nov 2018 15:35:15 +0800 Subject: [PATCH 464/685] Fix cut off tick labels in radial scale (#5848) Fix the issue that the topmost tick label and the bottom of the chart area are cut off with a radial scale. --- src/scales/scale.radialLinear.js | 35 +++++++++++++----- .../fixtures/controller.radar/point-style.png | Bin 7070 -> 15089 bytes .../fill-radar-boundary-origin-spline.png | Bin 15638 -> 10472 bytes .../fill-radar-boundary-origin.png | Bin 12866 -> 9297 bytes test/specs/controller.doughnut.tests.js | 8 ++-- test/specs/controller.polarArea.tests.js | 16 ++++---- test/specs/controller.radar.tests.js | 18 ++++----- test/specs/scale.radialLinear.tests.js | 8 ++-- 8 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index ffd76dd43f4..71e156e0f09 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -79,6 +79,16 @@ module.exports = function(Chart) { }; } + function getTickFontSize(scale) { + var opts = scale.options; + var tickOpts = opts.ticks; + + if (tickOpts.display && opts.display) { + return helpers.valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); + } + return 0; + } + function measureLabelSize(ctx, fontSize, label) { if (helpers.isArray(label)) { return { @@ -145,6 +155,7 @@ module.exports = function(Chart) { */ var plFont = getPointLabelFontOptions(scale); + var paddingTop = getTickFontSize(scale) / 2; // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points @@ -194,6 +205,11 @@ module.exports = function(Chart) { } } + if (paddingTop && -paddingTop < furthestLimits.t) { + furthestLimits.t = -paddingTop; + furthestAngles.t = 0; + } + scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles); } @@ -201,9 +217,10 @@ module.exports = function(Chart) { * Helper function to fit a radial linear scale with no point labels */ function fit(scale) { - var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); - scale.drawingArea = Math.round(largestPossibleRadius); - scale.setCenterPoint(0, 0, 0, 0); + var paddingTop = getTickFontSize(scale) / 2; + var largestPossibleRadius = Math.min((scale.height - paddingTop) / 2, scale.width / 2); + scale.drawingArea = Math.floor(largestPossibleRadius); + scale.setCenterPoint(0, 0, paddingTop, 0); } function getTextAlignForAngle(angle) { @@ -341,8 +358,8 @@ module.exports = function(Chart) { // Set the unconstrained dimension before label rotation me.width = me.maxWidth; me.height = me.maxHeight; - me.xCenter = Math.round(me.width / 2); - me.yCenter = Math.round(me.height / 2); + me.xCenter = Math.floor(me.width / 2); + me.yCenter = Math.floor(me.height / 2); var minSize = helpers.min([me.height, me.width]); var tickFontSize = helpers.valueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); @@ -416,8 +433,8 @@ module.exports = function(Chart) { radiusReductionBottom = numberOrZero(radiusReductionBottom); me.drawingArea = Math.min( - Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), - Math.round(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); + Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); }, setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) { @@ -427,8 +444,8 @@ module.exports = function(Chart) { var maxTop = topMovement + me.drawingArea; var maxBottom = me.height - bottomMovement - me.drawingArea; - me.xCenter = Math.round(((maxLeft + maxRight) / 2) + me.left); - me.yCenter = Math.round(((maxTop + maxBottom) / 2) + me.top); + me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left); + me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top); }, getIndexAngle: function(index) { diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png index 3f73ff96f91a02142de3504a59531b8f954d59d2..c5437ed24affbc9078ab004a6d0db2c5f7fdf5a0 100644 GIT binary patch literal 15089 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%0DJ=3Iv)NF#Pciave~c_$lC^ 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 IUKSGmKWDH5cK`qY literal 15638 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 M(#ld*62<}l1Et}jfdBvi diff --git a/test/fixtures/plugin.filler/fill-radar-boundary-origin.png b/test/fixtures/plugin.filler/fill-radar-boundary-origin.png index 2c07f7f0b2c2bd996ed549ee512779905de26883..66c6e563a796d1c50039329dafff1e339a841f53 100644 GIT binary patch literal 9297 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 diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index b1969e2817a..2f9602bda73 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -195,10 +195,10 @@ describe('Chart.controllers.doughnut', function() { {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(510); - expect(meta.data[i]._model.y).toBeCloseToPixel(510); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(509); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(381); + expect(meta.data[i]._model.x).toBeCloseToPixel(511); + expect(meta.data[i]._model.y).toBeCloseToPixel(511); + expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(510); + expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(382); 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); diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index b706f8376a6..1023bf0789b 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -99,13 +99,13 @@ describe('Chart.controllers.polarArea', function() { expect(meta.data.length).toBe(4); [ - {o: 179, s: -0.5 * Math.PI, e: 0}, - {o: 243, s: 0, e: 0.5 * Math.PI}, + {o: 177, s: -0.5 * Math.PI, e: 0}, + {o: 240, s: 0, e: 0.5 * Math.PI}, {o: 51, 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(256); + expect(meta.data[i]._model.y).toBeCloseToPixel(259); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); expect(meta.data[i]._model.startAngle).toBe(expected.s); @@ -141,9 +141,9 @@ describe('Chart.controllers.polarArea', function() { chart.update(); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(256); + expect(meta.data[0]._model.y).toBeCloseToPixel(259); expect(meta.data[0]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(179); + expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(177); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ startAngle: -0.5 * Math.PI, endAngle: 0, @@ -183,13 +183,13 @@ describe('Chart.controllers.polarArea', function() { expect(meta.data.length).toBe(4); [ - {o: 179, s: 0, e: 0.5 * Math.PI}, - {o: 243, s: 0.5 * Math.PI, e: Math.PI}, + {o: 177, s: 0, e: 0.5 * Math.PI}, + {o: 240, s: 0.5 * Math.PI, e: Math.PI}, {o: 51, 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(256); + expect(meta.data[i]._model.y).toBeCloseToPixel(259); expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); expect(meta.data[i]._model.startAngle).toBe(expected.s); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 7e85e9e7eac..b71a2e47398 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -154,10 +154,10 @@ describe('Chart.controllers.radar', function() { meta.controller.update(); [ - {x: 256, y: 117, cppx: 246, cppy: 117, cpnx: 272, cpny: 117}, - {x: 464, y: 256, cppx: 464, cppy: 248, cpnx: 464, cpny: 262}, - {x: 256, y: 256, cppx: 276.9, cppy: 256, cpnx: 250.4, cpny: 256}, - {x: 200, y: 256, cppx: 200, cppy: 259, cpnx: 200, cpny: 245}, + {x: 256, y: 116, cppx: 246, cppy: 116, cpnx: 273, cpny: 116}, + {x: 466, y: 256, cppx: 466, cppy: 248, cpnx: 466, cpny: 262}, + {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250.4, cpny: 256}, + {x: 200, y: 256, cppx: 200, cppy: 260, cpnx: 200, cpny: 246}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -211,8 +211,8 @@ describe('Chart.controllers.radar', function() { // Since tension is now 0, we don't care about the control points [ - {x: 256, y: 117}, - {x: 464, y: 256}, + {x: 256, y: 116}, + {x: 466, y: 256}, {x: 256, y: 256}, {x: 200, y: 256}, ].forEach(function(expected, i) { @@ -270,11 +270,11 @@ describe('Chart.controllers.radar', function() { })); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(117); + expect(meta.data[0]._model.y).toBeCloseToPixel(116); expect(meta.data[0]._model.controlPointPreviousX).toBeCloseToPixel(241); - expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(117); + expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(116); expect(meta.data[0]._model.controlPointNextX).toBeCloseToPixel(281); - expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(117); + expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(116); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ radius: 2.2, backgroundColor: 'rgb(0, 1, 3)', diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js index 5390ef77d5f..da19f9e000d 100644 --- a/test/specs/scale.radialLinear.tests.js +++ b/test/specs/scale.radialLinear.tests.js @@ -349,9 +349,9 @@ describe('Test the radial linear scale', function() { } }); - expect(chart.scale.drawingArea).toBe(233); + expect(chart.scale.drawingArea).toBe(232); expect(chart.scale.xCenter).toBe(256); - expect(chart.scale.yCenter).toBe(280); + expect(chart.scale.yCenter).toBe(279); }); it('should correctly get the label for a given data index', function() { @@ -397,7 +397,7 @@ describe('Test the radial linear scale', function() { }); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(233); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(232); expect(chart.scale.getPointPositionForValue(1, 5)).toEqual({ x: 270, y: 275, @@ -406,7 +406,7 @@ describe('Test the radial linear scale', function() { chart.scale.options.ticks.reverse = true; chart.update(); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(233); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(232); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(0); }); From aa652df240a98a5283381591bed0bca555cc5b55 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 29 Nov 2018 07:56:20 +0100 Subject: [PATCH 465/685] Deprecate Chart.{Type} classes (#5868) It looks like these classes are a legacy from version 1 but we actually never promoted their usage. Instead, the regular way to create a chart is to set the type in the config, for example: `new Chart(ctx, {type: 'bar'})`. Some types are actually missing (no `Chart.HorizontalBar` or `Chart.Pie`) but it's also not scalable because it can easily conflict with other classes scoped under the `Chart` namespace. --- src/chart.js | 35 ++++++++++++++++++------- src/charts/Chart.Bar.js | 11 -------- src/charts/Chart.Bubble.js | 10 ------- src/charts/Chart.Doughnut.js | 11 -------- src/charts/Chart.Line.js | 11 -------- src/charts/Chart.PolarArea.js | 11 -------- src/charts/Chart.Radar.js | 11 -------- src/charts/Chart.Scatter.js | 8 ------ test/specs/global.deprecations.tests.js | 27 +++++++++++++++++++ 9 files changed, 53 insertions(+), 82 deletions(-) delete mode 100644 src/charts/Chart.Bar.js delete mode 100644 src/charts/Chart.Bubble.js delete mode 100644 src/charts/Chart.Doughnut.js delete mode 100644 src/charts/Chart.Line.js delete mode 100644 src/charts/Chart.PolarArea.js delete mode 100644 src/charts/Chart.Radar.js delete mode 100644 src/charts/Chart.Scatter.js diff --git a/src/chart.js b/src/chart.js index 1f6982ce47e..7b393e6b069 100644 --- a/src/chart.js +++ b/src/chart.js @@ -42,14 +42,6 @@ require('./controllers/controller.polarArea')(Chart); require('./controllers/controller.radar')(Chart); require('./controllers/controller.scatter')(Chart); -require('./charts/Chart.Bar')(Chart); -require('./charts/Chart.Bubble')(Chart); -require('./charts/Chart.Doughnut')(Chart); -require('./charts/Chart.Line')(Chart); -require('./charts/Chart.PolarArea')(Chart); -require('./charts/Chart.Radar')(Chart); -require('./charts/Chart.Scatter')(Chart); - // Loading built-in plugins var plugins = require('./plugins'); for (var k in plugins) { @@ -116,8 +108,33 @@ Chart.canvasHelpers = Chart.helpers.canvas; /** * Provided for backward compatibility, use Chart.layouts instead. * @namespace Chart.layoutService - * @deprecated since version 2.8.0 + * @deprecated since version 2.7.3 * @todo remove at version 3 * @private */ Chart.layoutService = Chart.layouts; + +/** + * Provided for backward compatibility, instead we should create a new Chart + * by setting the type in the config (`new Chart(id, {type: '{chart-type}'}`). + * @deprecated since version 2.8.0 + * @todo remove at version 3 + */ +Chart.helpers.each( + [ + 'Bar', + 'Bubble', + 'Doughnut', + 'Line', + 'PolarArea', + 'Radar', + 'Scatter' + ], + function(klass) { + Chart[klass] = function(ctx, cfg) { + return new Chart(ctx, Chart.helpers.merge(cfg || {}, { + type: klass.charAt(0).toLowerCase() + klass.slice(1) + })); + }; + } +); diff --git a/src/charts/Chart.Bar.js b/src/charts/Chart.Bar.js deleted file mode 100644 index e1ad7962f9a..00000000000 --- a/src/charts/Chart.Bar.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.Bar = function(context, config) { - config.type = 'bar'; - - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.Bubble.js b/src/charts/Chart.Bubble.js deleted file mode 100644 index 2de4a1047ec..00000000000 --- a/src/charts/Chart.Bubble.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.Bubble = function(context, config) { - config.type = 'bubble'; - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.Doughnut.js b/src/charts/Chart.Doughnut.js deleted file mode 100644 index e1e8ce54b34..00000000000 --- a/src/charts/Chart.Doughnut.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.Doughnut = function(context, config) { - config.type = 'doughnut'; - - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.Line.js b/src/charts/Chart.Line.js deleted file mode 100644 index e89662ff7c1..00000000000 --- a/src/charts/Chart.Line.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.Line = function(context, config) { - config.type = 'line'; - - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.PolarArea.js b/src/charts/Chart.PolarArea.js deleted file mode 100644 index e07e4bac5a1..00000000000 --- a/src/charts/Chart.PolarArea.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.PolarArea = function(context, config) { - config.type = 'polarArea'; - - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.Radar.js b/src/charts/Chart.Radar.js deleted file mode 100644 index d17bd5d8108..00000000000 --- a/src/charts/Chart.Radar.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - - Chart.Radar = function(context, config) { - config.type = 'radar'; - - return new Chart(context, config); - }; - -}; diff --git a/src/charts/Chart.Scatter.js b/src/charts/Chart.Scatter.js deleted file mode 100644 index 9006e571293..00000000000 --- a/src/charts/Chart.Scatter.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -module.exports = function(Chart) { - Chart.Scatter = function(context, config) { - config.type = 'scatter'; - return new Chart(context, config); - }; -}; diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 11d25d1d9b6..6992f23cf1e 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,5 +1,32 @@ describe('Deprecations', function() { describe('Version 2.8.0', function() { + [ + ['Bar', 'bar'], + ['Bubble', 'bubble'], + ['Doughnut', 'doughnut'], + ['Line', 'line'], + ['PolarArea', 'polarArea'], + ['Radar', 'radar'], + ['Scatter', 'scatter'] + ].forEach(function(descriptor) { + var klass = descriptor[0]; + var type = descriptor[1]; + + describe('Chart.' + klass, function() { + it('should be defined as a function', function() { + expect(Chart[klass]).toBeDefined(); + expect(typeof Chart[klass]).toBe('function'); + }); + it('should create a chart of type "' + type + '"', function() { + var chart = new Chart[klass]('foo', {data: {}}); + expect(chart instanceof Chart.Controller).toBeTruthy(); + expect(chart.config.type).toBe(type); + }); + }); + }); + }); + + describe('Version 2.7.3', function() { describe('Chart.layoutService', function() { it('should be defined and an alias of Chart.layouts', function() { expect(Chart.layoutService).toBeDefined(); From ecfa7b24c64d873befe625d21b64c826a304e10e Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Thu, 29 Nov 2018 20:52:56 +0800 Subject: [PATCH 466/685] Add support for CanvasPattern and CanvasGradient in tooltip (#5869) --- src/core/core.tooltip.js | 47 ++++++----- test/fixtures/core.tooltip/opacity.js | 104 +++++++++++++++++++++++++ test/fixtures/core.tooltip/opacity.png | Bin 0 -> 11800 bytes test/specs/core.tooltip.tests.js | 2 + 4 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 test/fixtures/core.tooltip/opacity.js create mode 100644 test/fixtures/core.tooltip/opacity.png diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c529812cc61..c3245d9cf1e 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -168,14 +168,6 @@ var positioners = { } }; -/** - * 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(); -} - // Helper to push or concat based on if the 2nd parameter is an array or not function pushOrConcat(base, toPush) { if (toPush) { @@ -734,7 +726,7 @@ var exports = module.exports = Element.extend({ return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; }, - drawTitle: function(pt, vm, ctx, opacity) { + drawTitle: function(pt, vm, ctx) { var title = vm.title; if (title.length) { @@ -744,7 +736,7 @@ var exports = module.exports = Element.extend({ var titleFontSize = vm.titleFontSize; var titleSpacing = vm.titleSpacing; - ctx.fillStyle = mergeOpacity(vm.titleFontColor, opacity); + ctx.fillStyle = vm.titleFontColor; ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); var i, len; @@ -759,7 +751,7 @@ var exports = module.exports = Element.extend({ } }, - drawBody: function(pt, vm, ctx, opacity) { + drawBody: function(pt, vm, ctx) { var bodyFontSize = vm.bodyFontSize; var bodySpacing = vm.bodySpacing; var body = vm.body; @@ -776,7 +768,7 @@ var exports = module.exports = Element.extend({ }; // Before body lines - ctx.fillStyle = mergeOpacity(vm.bodyFontColor, opacity); + ctx.fillStyle = vm.bodyFontColor; helpers.each(vm.beforeBody, fillLineOfText); var drawColorBoxes = vm.displayColors; @@ -784,7 +776,7 @@ var exports = module.exports = Element.extend({ // Draw body lines now helpers.each(body, function(bodyItem, i) { - var textColor = mergeOpacity(vm.labelTextColors[i], opacity); + var textColor = vm.labelTextColors[i]; ctx.fillStyle = textColor; helpers.each(bodyItem.before, fillLineOfText); @@ -792,16 +784,16 @@ var exports = module.exports = Element.extend({ // Draw Legend-like boxes if needed if (drawColorBoxes) { // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = mergeOpacity(vm.legendColorBackground, opacity); + ctx.fillStyle = vm.legendColorBackground; ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Border ctx.lineWidth = 1; - ctx.strokeStyle = mergeOpacity(vm.labelColors[i].borderColor, opacity); + ctx.strokeStyle = vm.labelColors[i].borderColor; ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); // Inner square - ctx.fillStyle = mergeOpacity(vm.labelColors[i].backgroundColor, opacity); + ctx.fillStyle = vm.labelColors[i].backgroundColor; ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); ctx.fillStyle = textColor; } @@ -820,7 +812,7 @@ var exports = module.exports = Element.extend({ pt.y -= bodySpacing; // Remove last body spacing }, - drawFooter: function(pt, vm, ctx, opacity) { + drawFooter: function(pt, vm, ctx) { var footer = vm.footer; if (footer.length) { @@ -829,7 +821,7 @@ var exports = module.exports = Element.extend({ ctx.textAlign = vm._footerAlign; ctx.textBaseline = 'top'; - ctx.fillStyle = mergeOpacity(vm.footerFontColor, opacity); + ctx.fillStyle = vm.footerFontColor; ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); helpers.each(footer, function(line) { @@ -839,9 +831,9 @@ var exports = module.exports = Element.extend({ } }, - drawBackground: function(pt, vm, ctx, tooltipSize, opacity) { - ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity); - ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity); + drawBackground: function(pt, vm, ctx, tooltipSize) { + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; ctx.lineWidth = vm.borderWidth; var xAlign = vm.xAlign; var yAlign = vm.yAlign; @@ -906,21 +898,26 @@ var exports = module.exports = Element.extend({ var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; if (this._options.enabled && hasTooltipContent) { + ctx.save(); + ctx.globalAlpha = opacity; + // Draw Background - this.drawBackground(pt, vm, ctx, tooltipSize, opacity); + this.drawBackground(pt, vm, ctx, tooltipSize); // Draw Title, Body, and Footer pt.x += vm.xPadding; pt.y += vm.yPadding; // Titles - this.drawTitle(pt, vm, ctx, opacity); + this.drawTitle(pt, vm, ctx); // Body - this.drawBody(pt, vm, ctx, opacity); + this.drawBody(pt, vm, ctx); // Footer - this.drawFooter(pt, vm, ctx, opacity); + this.drawFooter(pt, vm, ctx); + + ctx.restore(); } }, diff --git a/test/fixtures/core.tooltip/opacity.js b/test/fixtures/core.tooltip/opacity.js new file mode 100644 index 00000000000..8b872e75d73 --- /dev/null +++ b/test/fixtures/core.tooltip/opacity.js @@ -0,0 +1,104 @@ +var patternCanvas = document.createElement('canvas'); +var patternContext = patternCanvas.getContext('2d'); + +patternCanvas.width = 6; +patternCanvas.height = 6; +patternContext.fillStyle = '#ff0000'; +patternContext.fillRect(0, 0, 6, 6); +patternContext.fillStyle = '#ffff00'; +patternContext.fillRect(0, 0, 4, 4); + +var pattern = patternContext.createPattern(patternCanvas, 'repeat'); + +var gradient; + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8], + pointBorderColor: '#ff0000', + pointBackgroundColor: '#00ff00', + showLine: false + }, { + label: '', + data: [4, 4, 4, 4, 4, 5, 3, 4, 4, 4, 4], + pointBorderColor: pattern, + pointBackgroundColor: pattern, + showLine: false + }, { + label: '', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + showLine: false + }], + labels: ['', '', '', '', '', '', '', '', '', '', ''] + }, + options: { + legend: false, + title: false, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + }, + elements: { + line: { + fill: false + } + }, + tooltips: { + mode: 'nearest', + intersect: false, + callbacks: { + label: function() { + return '\u200b'; + } + } + }, + layout: { + padding: 15 + } + }, + plugins: [{ + beforeDatasetsUpdate: function(chart) { + if (!gradient) { + gradient = chart.ctx.createLinearGradient(0, 0, 512, 256); + gradient.addColorStop(0, '#ff0000'); + gradient.addColorStop(1, '#0000ff'); + } + chart.config.data.datasets[2].pointBorderColor = gradient; + chart.config.data.datasets[2].pointBackgroundColor = gradient; + + return true; + }, + afterDraw: function(chart) { + var canvas = chart.canvas; + var rect = canvas.getBoundingClientRect(); + var point, event; + + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 11; ++j) { + point = chart.getDatasetMeta(i).data[j]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y + }; + chart.handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.transition(1); + chart.tooltip._view.opacity = j / 10; + chart.tooltip.draw(); + } + } + } + }] + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.tooltip/opacity.png b/test/fixtures/core.tooltip/opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..142dcc05f60fb6f966594da191fb7c8b109c23ea GIT binary patch literal 11800 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 Date: Fri, 30 Nov 2018 04:05:36 +0800 Subject: [PATCH 467/685] Fix test failures on Windows (#5872) --- test/specs/controller.radar.tests.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index b71a2e47398..6a447a1ccdb 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -154,10 +154,10 @@ describe('Chart.controllers.radar', function() { meta.controller.update(); [ - {x: 256, y: 116, cppx: 246, cppy: 116, cpnx: 273, cpny: 116}, - {x: 466, y: 256, cppx: 466, cppy: 248, cpnx: 466, cpny: 262}, - {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250.4, cpny: 256}, - {x: 200, y: 256, cppx: 200, cppy: 260, cpnx: 200, cpny: 246}, + {x: 256, y: 117, cppx: 246, cppy: 117, cpnx: 272, cpny: 117}, + {x: 464, y: 256, cppx: 464, cppy: 248, cpnx: 464, cpny: 262}, + {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250, cpny: 256}, + {x: 200, y: 256, cppx: 200, cppy: 259, cpnx: 200, cpny: 245}, ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); @@ -211,8 +211,8 @@ describe('Chart.controllers.radar', function() { // Since tension is now 0, we don't care about the control points [ - {x: 256, y: 116}, - {x: 466, y: 256}, + {x: 256, y: 117}, + {x: 464, y: 256}, {x: 256, y: 256}, {x: 200, y: 256}, ].forEach(function(expected, i) { @@ -270,11 +270,11 @@ describe('Chart.controllers.radar', function() { })); expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(116); + expect(meta.data[0]._model.y).toBeCloseToPixel(117); expect(meta.data[0]._model.controlPointPreviousX).toBeCloseToPixel(241); - expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(116); + expect(meta.data[0]._model.controlPointPreviousY).toBeCloseToPixel(117); expect(meta.data[0]._model.controlPointNextX).toBeCloseToPixel(281); - expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(116); + expect(meta.data[0]._model.controlPointNextY).toBeCloseToPixel(117); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ radius: 2.2, backgroundColor: 'rgb(0, 1, 3)', From be8d78a900b560a1dbd465a29736063e4e8fa4ef Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 29 Nov 2018 21:06:34 +0100 Subject: [PATCH 468/685] Make Chart.controllers.* importable (#5871) `controllers.*.js` and `core.datasetController.js` are now importable (no more function export), that's why there is so many changes mainly due to one indentation level removed. Split code for `bar/horizontalBar` and `doughnut/pie` in separate files, added a global controllers import (`src/controllers/index.js`) and add tests to check that all dataset controllers are correctly registered under `chart.controllers.{type}`. --- src/chart.js | 13 +- src/controllers/controller.bar.js | 699 +++++++++----------- src/controllers/controller.bubble.js | 263 ++++---- src/controllers/controller.doughnut.js | 309 +++++---- src/controllers/controller.horizontalBar.js | 79 +++ src/controllers/controller.line.js | 564 ++++++++-------- src/controllers/controller.pie.js | 13 + src/controllers/controller.polarArea.js | 254 ++++--- src/controllers/controller.radar.js | 284 ++++---- src/controllers/controller.scatter.js | 9 +- src/controllers/index.js | 18 + src/core/core.controller.js | 6 +- src/core/core.datasetController.js | 585 ++++++++-------- test/specs/controller.bar.tests.js | 5 + test/specs/controller.bubble.tests.js | 4 + test/specs/controller.doughnut.tests.js | 5 + test/specs/controller.line.tests.js | 4 + test/specs/controller.polarArea.tests.js | 8 +- test/specs/controller.radar.tests.js | 4 + test/specs/controller.scatter.test.js | 4 + 20 files changed, 1576 insertions(+), 1554 deletions(-) create mode 100644 src/controllers/controller.horizontalBar.js create mode 100644 src/controllers/controller.pie.js create mode 100644 src/controllers/index.js diff --git a/src/chart.js b/src/chart.js index 7b393e6b069..acb57862c50 100644 --- a/src/chart.js +++ b/src/chart.js @@ -10,6 +10,8 @@ require('./core/core.helpers')(Chart); Chart.Animation = require('./core/core.animation'); Chart.animationService = require('./core/core.animations'); +Chart.controllers = require('./controllers/index'); +Chart.DatasetController = require('./core/core.datasetController'); Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); @@ -23,7 +25,6 @@ Chart.Ticks = require('./core/core.ticks'); Chart.Tooltip = require('./core/core.tooltip'); require('./core/core.controller')(Chart); -require('./core/core.datasetController')(Chart); require('./scales/scale.linearbase')(Chart); require('./scales/scale.category')(Chart); @@ -32,16 +33,6 @@ require('./scales/scale.logarithmic')(Chart); require('./scales/scale.radialLinear')(Chart); require('./scales/scale.time')(Chart); -// Controllers must be loaded after elements -// See Chart.core.datasetController.dataElementType -require('./controllers/controller.bar')(Chart); -require('./controllers/controller.bubble')(Chart); -require('./controllers/controller.doughnut')(Chart); -require('./controllers/controller.line')(Chart); -require('./controllers/controller.polarArea')(Chart); -require('./controllers/controller.radar')(Chart); -require('./controllers/controller.scatter')(Chart); - // Loading built-in plugins var plugins = require('./plugins'); for (var k in plugins) { diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 36f7c7e6e37..760fd8887ef 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -12,15 +13,9 @@ defaults._set('bar', { scales: { xAxes: [{ type: 'category', - - // Specific to Bar Controller categoryPercentage: 0.8, barPercentage: 0.9, - - // offset settings offset: true, - - // grid line settings gridLines: { offsetGridLines: true } @@ -32,69 +27,6 @@ defaults._set('bar', { } }); -defaults._set('horizontalBar', { - hover: { - mode: 'index', - axis: 'y' - }, - - scales: { - xAxes: [{ - type: 'linear', - position: 'bottom' - }], - - yAxes: [{ - position: 'left', - type: 'category', - - // Specific to Horizontal Bar Controller - categoryPercentage: 0.8, - barPercentage: 0.9, - - // offset settings - offset: true, - - // grid line settings - gridLines: { - offsetGridLines: true - } - }] - }, - - elements: { - rectangle: { - borderSkipped: 'left' - } - }, - - tooltips: { - callbacks: { - title: function(item, data) { - // Pick first xLabel for now - var title = ''; - - if (item.length > 0) { - if (item[0].yLabel) { - title = item[0].yLabel; - } else if (data.labels.length > 0 && item[0].index < data.labels.length) { - title = data.labels[item[0].index]; - } - } - - return title; - }, - - label: function(item, data) { - var datasetLabel = data.datasets[item.datasetIndex].label || ''; - return datasetLabel + ': ' + item.xLabel; - } - }, - mode: 'index', - axis: 'y' - } -}); - /** * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. * @private @@ -182,349 +114,330 @@ function computeFlexCategoryTraits(index, ruler, options) { }; } -module.exports = function(Chart) { +module.exports = DatasetController.extend({ - Chart.controllers.bar = Chart.DatasetController.extend({ + dataElementType: elements.Rectangle, - dataElementType: elements.Rectangle, + initialize: function() { + var me = this; + var meta; - initialize: function() { - var me = this; - var meta; + DatasetController.prototype.initialize.apply(me, arguments); - Chart.DatasetController.prototype.initialize.apply(me, arguments); + meta = me.getMeta(); + meta.stack = me.getDataset().stack; + meta.bar = true; + }, - meta = me.getMeta(); - meta.stack = me.getDataset().stack; - meta.bar = true; - }, + update: function(reset) { + var me = this; + var rects = me.getMeta().data; + var i, ilen; - update: function(reset) { - var me = this; - var rects = me.getMeta().data; - var i, ilen; + me._ruler = me.getRuler(); - me._ruler = me.getRuler(); + for (i = 0, ilen = rects.length; i < ilen; ++i) { + me.updateElement(rects[i], i, reset); + } + }, - for (i = 0, ilen = rects.length; i < ilen; ++i) { - me.updateElement(rects[i], i, reset); - } - }, - - updateElement: function(rectangle, index, reset) { - var me = this; - var meta = me.getMeta(); - var dataset = me.getDataset(); - var options = me._resolveElementOptions(rectangle, index); - - rectangle._xScale = me.getScaleForId(meta.xAxisID); - rectangle._yScale = me.getScaleForId(meta.yAxisID); - rectangle._datasetIndex = me.index; - rectangle._index = index; - rectangle._model = { - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderSkipped: options.borderSkipped, - borderWidth: options.borderWidth, - datasetLabel: dataset.label, - label: me.chart.data.labels[index] - }; - - me._updateElementGeometry(rectangle, index, reset); - - rectangle.pivot(); - }, - - /** - * @private - */ - _updateElementGeometry: function(rectangle, index, reset) { - var me = this; - var model = rectangle._model; - var vscale = me.getValueScale(); - var base = vscale.getBasePixel(); - var horizontal = vscale.isHorizontal(); - var ruler = me._ruler || me.getRuler(); - var vpixels = me.calculateBarValuePixels(me.index, index); - var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); - - model.horizontal = horizontal; - model.base = reset ? base : vpixels.base; - model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; - model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; - model.height = horizontal ? ipixels.size : undefined; - model.width = horizontal ? undefined : ipixels.size; - }, - - /** - * @private - */ - getValueScaleId: function() { - return this.getMeta().yAxisID; - }, - - /** - * @private - */ - getIndexScaleId: function() { - return this.getMeta().xAxisID; - }, - - /** - * @private - */ - getValueScale: function() { - return this.getScaleForId(this.getValueScaleId()); - }, - - /** - * @private - */ - getIndexScale: function() { - return this.getScaleForId(this.getIndexScaleId()); - }, - - /** - * Returns the stacks based on groups and bar visibility. - * @param {Number} [last] - The dataset index - * @returns {Array} The stack list - * @private - */ - _getStacks: function(last) { - var me = this; - var chart = me.chart; - var scale = me.getIndexScale(); - var stacked = scale.options.stacked; - var ilen = last === undefined ? chart.data.datasets.length : last + 1; - var stacks = []; - var i, meta; - - for (i = 0; i < ilen; ++i) { - meta = chart.getDatasetMeta(i); - if (meta.bar && chart.isDatasetVisible(i) && - (stacked === false || - (stacked === true && stacks.indexOf(meta.stack) === -1) || - (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { - stacks.push(meta.stack); - } - } + updateElement: function(rectangle, index, reset) { + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + var options = me._resolveElementOptions(rectangle, index); + + rectangle._xScale = me.getScaleForId(meta.xAxisID); + rectangle._yScale = me.getScaleForId(meta.yAxisID); + rectangle._datasetIndex = me.index; + rectangle._index = index; + rectangle._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderSkipped: options.borderSkipped, + borderWidth: options.borderWidth, + datasetLabel: dataset.label, + label: me.chart.data.labels[index] + }; + + me._updateElementGeometry(rectangle, index, reset); + + rectangle.pivot(); + }, + + /** + * @private + */ + _updateElementGeometry: function(rectangle, index, reset) { + var me = this; + var model = rectangle._model; + var vscale = me.getValueScale(); + var base = vscale.getBasePixel(); + var horizontal = vscale.isHorizontal(); + var ruler = me._ruler || me.getRuler(); + var vpixels = me.calculateBarValuePixels(me.index, index); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); + + model.horizontal = horizontal; + model.base = reset ? base : vpixels.base; + model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; + model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; + model.height = horizontal ? ipixels.size : undefined; + model.width = horizontal ? undefined : ipixels.size; + }, + + /** + * @private + */ + getValueScaleId: function() { + return this.getMeta().yAxisID; + }, + + /** + * @private + */ + getIndexScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + getValueScale: function() { + return this.getScaleForId(this.getValueScaleId()); + }, - return stacks; - }, - - /** - * Returns the effective number of stacks based on groups and bar visibility. - * @private - */ - getStackCount: function() { - return this._getStacks().length; - }, - - /** - * Returns the stack index for the given dataset based on groups and bar visibility. - * @param {Number} [datasetIndex] - The dataset index - * @param {String} [name] - The stack name to find - * @returns {Number} The stack index - * @private - */ - getStackIndex: function(datasetIndex, name) { - var stacks = this._getStacks(datasetIndex); - var index = (name !== undefined) - ? stacks.indexOf(name) - : -1; // indexOf returns -1 if element is not present - - return (index === -1) - ? stacks.length - 1 - : index; - }, - - /** - * @private - */ - getRuler: function() { - var me = this; - var scale = me.getIndexScale(); - var stackCount = me.getStackCount(); - var datasetIndex = me.index; - var isHorizontal = scale.isHorizontal(); - var start = isHorizontal ? scale.left : scale.top; - var end = start + (isHorizontal ? scale.width : scale.height); - var pixels = []; - var i, ilen, min; - - for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { - pixels.push(scale.getPixelForValue(null, i, datasetIndex)); + /** + * @private + */ + getIndexScale: function() { + return this.getScaleForId(this.getIndexScaleId()); + }, + + /** + * Returns the stacks based on groups and bar visibility. + * @param {Number} [last] - The dataset index + * @returns {Array} The stack list + * @private + */ + _getStacks: function(last) { + var me = this; + var chart = me.chart; + var scale = me.getIndexScale(); + var stacked = scale.options.stacked; + var ilen = last === undefined ? chart.data.datasets.length : last + 1; + var stacks = []; + var i, meta; + + for (i = 0; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + if (meta.bar && chart.isDatasetVisible(i) && + (stacked === false || + (stacked === true && stacks.indexOf(meta.stack) === -1) || + (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { + stacks.push(meta.stack); } + } + + return stacks; + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function() { + return this._getStacks().length; + }, + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {Number} [datasetIndex] - The dataset index + * @param {String} [name] - The stack name to find + * @returns {Number} The stack index + * @private + */ + getStackIndex: function(datasetIndex, name) { + var stacks = this._getStacks(datasetIndex); + var index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; + }, + + /** + * @private + */ + getRuler: function() { + var me = this; + var scale = me.getIndexScale(); + var stackCount = me.getStackCount(); + var datasetIndex = me.index; + var isHorizontal = scale.isHorizontal(); + var start = isHorizontal ? scale.left : scale.top; + var end = start + (isHorizontal ? scale.width : scale.height); + var pixels = []; + var i, ilen, min; + + for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { + pixels.push(scale.getPixelForValue(null, i, datasetIndex)); + } + + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + + return { + min: min, + pixels: pixels, + start: start, + end: end, + stackCount: stackCount, + scale: scale + }; + }, - min = helpers.isNullOrUndef(scale.options.barThickness) - ? computeMinSampleSize(scale, pixels) - : -1; - - return { - min: min, - pixels: pixels, - start: start, - end: end, - stackCount: stackCount, - scale: scale - }; - }, - - /** - * Note: pixel values are not clamped to the scale area. - * @private - */ - calculateBarValuePixels: function(datasetIndex, index) { - var me = this; - var chart = me.chart; - var meta = me.getMeta(); - var scale = me.getValueScale(); - var isHorizontal = scale.isHorizontal(); - var datasets = chart.data.datasets; - var value = scale.getRightValue(datasets[datasetIndex].data[index]); - var minBarLength = scale.options.minBarLength; - var stacked = scale.options.stacked; - var stack = meta.stack; - var start = 0; - var i, imeta, ivalue, base, head, size; - - if (stacked || (stacked === undefined && stack !== undefined)) { - for (i = 0; i < datasetIndex; ++i) { - imeta = chart.getDatasetMeta(i); - - if (imeta.bar && - imeta.stack === stack && - imeta.controller.getValueScaleId() === scale.id && - chart.isDatasetVisible(i)) { - - ivalue = scale.getRightValue(datasets[i].data[index]); - if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { - start += ivalue; - } + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + calculateBarValuePixels: function(datasetIndex, index) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var scale = me.getValueScale(); + var isHorizontal = scale.isHorizontal(); + var datasets = chart.data.datasets; + var value = scale.getRightValue(datasets[datasetIndex].data[index]); + var minBarLength = scale.options.minBarLength; + var stacked = scale.options.stacked; + var stack = meta.stack; + var start = 0; + var i, imeta, ivalue, base, head, size; + + if (stacked || (stacked === undefined && stack !== undefined)) { + for (i = 0; i < datasetIndex; ++i) { + imeta = chart.getDatasetMeta(i); + + if (imeta.bar && + imeta.stack === stack && + imeta.controller.getValueScaleId() === scale.id && + chart.isDatasetVisible(i)) { + + ivalue = scale.getRightValue(datasets[i].data[index]); + if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + start += ivalue; } } } + } - base = scale.getPixelForValue(start); - head = scale.getPixelForValue(start + value); - size = (head - base) / 2; + base = scale.getPixelForValue(start); + head = scale.getPixelForValue(start + value); + size = (head - base) / 2; - if (minBarLength !== undefined && Math.abs(size) < minBarLength) { - size = minBarLength; - if (value >= 0 && !isHorizontal || value < 0 && isHorizontal) { - head = base - minBarLength; - } else { - head = base + minBarLength; - } + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = minBarLength; + if (value >= 0 && !isHorizontal || value < 0 && isHorizontal) { + head = base - minBarLength; + } else { + head = base + minBarLength; } + } - return { - size: size, - base: base, - head: head, - center: head + size / 2 - }; - }, - - /** - * @private - */ - calculateBarIndexPixels: function(datasetIndex, index, ruler) { - var me = this; - var options = ruler.scale.options; - var range = options.barThickness === 'flex' - ? computeFlexCategoryTraits(index, ruler, options) - : computeFitCategoryTraits(index, ruler, options); - - var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); - var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); - var size = Math.min( - helpers.valueOrDefault(options.maxBarThickness, Infinity), - range.chunk * range.ratio); - - return { - base: center - size / 2, - head: center + size / 2, - center: center, - size: size - }; - }, - - draw: function() { - var me = this; - var chart = me.chart; - var scale = me.getValueScale(); - var rects = me.getMeta().data; - var dataset = me.getDataset(); - var ilen = rects.length; - var i = 0; - - helpers.canvas.clipArea(chart.ctx, chart.chartArea); - - for (; i < ilen; ++i) { - if (!isNaN(scale.getRightValue(dataset.data[i]))) { - rects[i].draw(); - } - } + return { + size: size, + base: base, + head: head, + center: head + size / 2 + }; + }, - helpers.canvas.unclipArea(chart.ctx); - }, - - /** - * @private - */ - _resolveElementOptions: function(rectangle, index) { - var me = this; - var chart = me.chart; - var datasets = chart.data.datasets; - var dataset = datasets[me.index]; - var custom = rectangle.custom || {}; - var options = chart.options.elements.rectangle; - var resolve = helpers.options.resolve; - var values = {}; - var i, ilen, key; - - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderSkipped', - 'borderWidth' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve([ - custom[key], - dataset[key], - options[key] - ], context, index); - } + /** + * @private + */ + calculateBarIndexPixels: function(datasetIndex, index, ruler) { + var me = this; + var options = ruler.scale.options; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); + + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); + + return { + base: center - size / 2, + head: center + size / 2, + center: center, + size: size + }; + }, + + draw: function() { + var me = this; + var chart = me.chart; + var scale = me.getValueScale(); + var rects = me.getMeta().data; + var dataset = me.getDataset(); + var ilen = rects.length; + var i = 0; + + helpers.canvas.clipArea(chart.ctx, chart.chartArea); - return values; + for (; i < ilen; ++i) { + if (!isNaN(scale.getRightValue(dataset.data[i]))) { + rects[i].draw(); + } } - }); - - Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ - /** - * @private - */ - getValueScaleId: function() { - return this.getMeta().xAxisID; - }, - - /** - * @private - */ - getIndexScaleId: function() { - return this.getMeta().yAxisID; + + helpers.canvas.unclipArea(chart.ctx); + }, + + /** + * @private + */ + _resolveElementOptions: function(rectangle, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; + var custom = rectangle.custom || {}; + var options = chart.options.elements.rectangle; + var resolve = helpers.options.resolve; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ], context, index); } - }); -}; + + return values; + } +}); diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index ed1e2045c71..77956c9dc7b 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -37,140 +38,136 @@ defaults._set('bubble', { } }); +module.exports = DatasetController.extend({ + /** + * @protected + */ + dataElementType: elements.Point, + + /** + * @protected + */ + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var points = meta.data; + + // Update Points + helpers.each(points, function(point, index) { + me.updateElement(point, index, reset); + }); + }, -module.exports = function(Chart) { - - Chart.controllers.bubble = Chart.DatasetController.extend({ - /** - * @protected - */ - dataElementType: elements.Point, - - /** - * @protected - */ - update: function(reset) { - var me = this; - var meta = me.getMeta(); - var points = meta.data; - - // Update Points - helpers.each(points, function(point, index) { - me.updateElement(point, index, reset); - }); - }, - - /** - * @protected - */ - updateElement: function(point, index, reset) { - var me = this; - var meta = me.getMeta(); - var custom = point.custom || {}; - var xScale = me.getScaleForId(meta.xAxisID); - var yScale = me.getScaleForId(meta.yAxisID); - var options = me._resolveElementOptions(point, index); - var data = me.getDataset().data[index]; - var dsIndex = me.index; - - var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); - var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); - - point._xScale = xScale; - point._yScale = yScale; - point._options = options; - point._datasetIndex = dsIndex; - point._index = index; - point._model = { - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - hitRadius: options.hitRadius, - pointStyle: options.pointStyle, - rotation: options.rotation, - radius: reset ? 0 : options.radius, - skip: custom.skip || isNaN(x) || isNaN(y), - x: x, - y: y, - }; - - point.pivot(); - }, - - /** - * @protected - */ - setHoverStyle: function(point) { - var model = point._model; - var options = point._options; - - point.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; - - model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); - model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); - model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); - model.radius = options.radius + options.hoverRadius; - }, - - /** - * @private - */ - _resolveElementOptions: function(point, index) { - var me = this; - var chart = me.chart; - var datasets = chart.data.datasets; - var dataset = datasets[me.index]; - var custom = point.custom || {}; - var options = chart.options.elements.point; - var resolve = helpers.options.resolve; - var data = dataset.data[index]; - var values = {}; - var i, ilen, key; - - // Scriptable options - var context = { - chart: chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - var keys = [ - 'backgroundColor', - 'borderColor', - 'borderWidth', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', - 'hoverRadius', - 'hitRadius', - 'pointStyle', - 'rotation' - ]; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - values[key] = resolve([ - custom[key], - dataset[key], - options[key] - ], context, index); - } + /** + * @protected + */ + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var xScale = me.getScaleForId(meta.xAxisID); + var yScale = me.getScaleForId(meta.yAxisID); + var options = me._resolveElementOptions(point, index); + var data = me.getDataset().data[index]; + var dsIndex = me.index; + + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); + + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = dsIndex; + point._index = index; + point._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + hitRadius: options.hitRadius, + pointStyle: options.pointStyle, + rotation: options.rotation, + radius: reset ? 0 : options.radius, + skip: custom.skip || isNaN(x) || isNaN(y), + x: x, + y: y, + }; + + point.pivot(); + }, - // Custom radius resolution - values.radius = resolve([ - custom.radius, - data ? data.r : undefined, - dataset.radius, - options.radius - ], context, index); + /** + * @protected + */ + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = helpers.valueOrDefault(options.hoverBackgroundColor, helpers.getHoverColor(options.backgroundColor)); + model.borderColor = helpers.valueOrDefault(options.hoverBorderColor, helpers.getHoverColor(options.borderColor)); + model.borderWidth = helpers.valueOrDefault(options.hoverBorderWidth, options.borderWidth); + model.radius = options.radius + options.hoverRadius; + }, - return values; + /** + * @private + */ + _resolveElementOptions: function(point, index) { + var me = this; + var chart = me.chart; + var datasets = chart.data.datasets; + var dataset = datasets[me.index]; + var custom = point.custom || {}; + var options = chart.options.elements.point; + var resolve = helpers.options.resolve; + var data = dataset.data[index]; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var keys = [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle', + 'rotation' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ], context, index); } - }); -}; + + // Custom radius resolution + values.radius = resolve([ + custom.radius, + data ? data.r : undefined, + dataset.radius, + options.radius + ], context, index); + + return values; + } +}); diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index eb759fe60b8..b24a7a00e9f 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -118,184 +119,176 @@ defaults._set('doughnut', { } }); -defaults._set('pie', helpers.clone(defaults.doughnut)); -defaults._set('pie', { - cutoutPercentage: 0 -}); - -module.exports = function(Chart) { +module.exports = DatasetController.extend({ - Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({ + dataElementType: elements.Arc, - dataElementType: elements.Arc, + linkScales: helpers.noop, - linkScales: helpers.noop, + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly + getRingIndex: function(datasetIndex) { + var ringIndex = 0; - // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly - getRingIndex: function(datasetIndex) { - var ringIndex = 0; - - for (var j = 0; j < datasetIndex; ++j) { - if (this.chart.isDatasetVisible(j)) { - ++ringIndex; - } + for (var j = 0; j < datasetIndex; ++j) { + if (this.chart.isDatasetVisible(j)) { + ++ringIndex; } + } - return ringIndex; - }, - - update: function(reset) { - var me = this; - var chart = me.chart; - var chartArea = chart.chartArea; - var opts = chart.options; - var arcOpts = opts.elements.arc; - var availableWidth = chartArea.right - chartArea.left - arcOpts.borderWidth; - var availableHeight = chartArea.bottom - chartArea.top - arcOpts.borderWidth; - var minSize = Math.min(availableWidth, availableHeight); - var offset = {x: 0, y: 0}; - var meta = me.getMeta(); - var cutoutPercentage = opts.cutoutPercentage; - var circumference = opts.circumference; - - // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc - if (circumference < Math.PI * 2.0) { - var startAngle = opts.rotation % (Math.PI * 2.0); - startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); - var endAngle = startAngle + circumference; - var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; - var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; - var contains0 = (startAngle <= 0 && endAngle >= 0) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); - var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); - var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); - var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); - var cutout = cutoutPercentage / 100.0; - var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; - var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; - var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; - minSize = Math.min(availableWidth / size.width, availableHeight / size.height); - offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; - } + return ringIndex; + }, - chart.borderWidth = me.getMaxBorderWidth(meta.data); - chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); - chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); - chart.offsetX = offset.x * chart.outerRadius; - chart.offsetY = offset.y * chart.outerRadius; + update: function(reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var arcOpts = opts.elements.arc; + var availableWidth = chartArea.right - chartArea.left - arcOpts.borderWidth; + var availableHeight = chartArea.bottom - chartArea.top - arcOpts.borderWidth; + var minSize = Math.min(availableWidth, availableHeight); + var offset = {x: 0, y: 0}; + var meta = me.getMeta(); + var cutoutPercentage = opts.cutoutPercentage; + var circumference = opts.circumference; + + // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc + if (circumference < Math.PI * 2.0) { + var startAngle = opts.rotation % (Math.PI * 2.0); + startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); + var endAngle = startAngle + circumference; + var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; + var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; + var contains0 = (startAngle <= 0 && endAngle >= 0) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); + var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); + var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); + var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); + var cutout = cutoutPercentage / 100.0; + var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; + var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; + var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; + minSize = Math.min(availableWidth / size.width, availableHeight / size.height); + offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; + } - meta.total = me.calculateTotal(); + chart.borderWidth = me.getMaxBorderWidth(meta.data); + chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + chart.offsetX = offset.x * chart.outerRadius; + chart.offsetY = offset.y * chart.outerRadius; - me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); + meta.total = me.calculateTotal(); - helpers.each(meta.data, function(arc, index) { - me.updateElement(arc, index, reset); - }); - }, + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); - updateElement: function(arc, index, reset) { - var me = this; - var chart = me.chart; - var chartArea = chart.chartArea; - var opts = chart.options; - var animationOpts = opts.animation; - var centerX = (chartArea.left + chartArea.right) / 2; - var centerY = (chartArea.top + chartArea.bottom) / 2; - var startAngle = opts.rotation; // non reset case handled later - var endAngle = opts.rotation; // non reset case handled later - var dataset = me.getDataset(); - var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)); - var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; - var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; - var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault; - - helpers.extend(arc, { - // Utility - _datasetIndex: me.index, - _index: index, - - // Desired view properties - _model: { - x: centerX + chart.offsetX, - y: centerY + chart.offsetY, - startAngle: startAngle, - endAngle: endAngle, - circumference: circumference, - outerRadius: outerRadius, - innerRadius: innerRadius, - label: valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index]) - } - }); - - var model = arc._model; - - // Resets the visual styles - var custom = arc.custom || {}; - var valueOrDefault = helpers.valueAtIndexOrDefault; - var elementOpts = this.chart.options.elements.arc; - model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); - model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); - model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); - - // Set correct angles if not resetting - if (!reset || !animationOpts.animateRotate) { - if (index === 0) { - model.startAngle = opts.rotation; - } else { - model.startAngle = me.getMeta().data[index - 1]._model.endAngle; - } + helpers.each(meta.data, function(arc, index) { + me.updateElement(arc, index, reset); + }); + }, - model.endAngle = model.startAngle + model.circumference; + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var animationOpts = opts.animation; + var centerX = (chartArea.left + chartArea.right) / 2; + var centerY = (chartArea.top + chartArea.bottom) / 2; + var startAngle = opts.rotation; // non reset case handled later + var endAngle = opts.rotation; // non reset case handled later + var dataset = me.getDataset(); + var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)); + var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; + var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; + var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault; + + helpers.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + + // Desired view properties + _model: { + x: centerX + chart.offsetX, + y: centerY + chart.offsetY, + startAngle: startAngle, + endAngle: endAngle, + circumference: circumference, + outerRadius: outerRadius, + innerRadius: innerRadius, + label: valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index]) + } + }); + + var model = arc._model; + + // Resets the visual styles + var custom = arc.custom || {}; + var valueOrDefault = helpers.valueAtIndexOrDefault; + var elementOpts = this.chart.options.elements.arc; + model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); + model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); + model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); + + // Set correct angles if not resetting + if (!reset || !animationOpts.animateRotate) { + if (index === 0) { + model.startAngle = opts.rotation; + } else { + model.startAngle = me.getMeta().data[index - 1]._model.endAngle; } - arc.pivot(); - }, + model.endAngle = model.startAngle + model.circumference; + } - calculateTotal: function() { - var dataset = this.getDataset(); - var meta = this.getMeta(); - var total = 0; - var value; + arc.pivot(); + }, - helpers.each(meta.data, function(element, index) { - value = dataset.data[index]; - if (!isNaN(value) && !element.hidden) { - total += Math.abs(value); - } - }); + calculateTotal: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var total = 0; + var value; - /* if (total === 0) { - total = NaN; - }*/ + helpers.each(meta.data, function(element, index) { + value = dataset.data[index]; + if (!isNaN(value) && !element.hidden) { + total += Math.abs(value); + } + }); - return total; - }, + /* if (total === 0) { + total = NaN; + }*/ - calculateCircumference: function(value) { - var total = this.getMeta().total; - if (total > 0 && !isNaN(value)) { - return (Math.PI * 2.0) * (Math.abs(value) / total); - } - return 0; - }, + return total; + }, - // gets the max border or hover width to properly scale pie charts - getMaxBorderWidth: function(arcs) { - var max = 0; - var index = this.index; - var length = arcs.length; - var borderWidth; - var hoverWidth; + calculateCircumference: function(value) { + var total = this.getMeta().total; + if (total > 0 && !isNaN(value)) { + return (Math.PI * 2.0) * (Math.abs(value) / total); + } + return 0; + }, - for (var i = 0; i < length; i++) { - borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0; - hoverWidth = arcs[i]._chart ? arcs[i]._chart.config.data.datasets[index].hoverBorderWidth : 0; + // gets the max border or hover width to properly scale pie charts + getMaxBorderWidth: function(arcs) { + var max = 0; + var index = this.index; + var length = arcs.length; + var borderWidth; + var hoverWidth; - max = borderWidth > max ? borderWidth : max; - max = hoverWidth > max ? hoverWidth : max; - } - return max; + for (var i = 0; i < length; i++) { + borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0; + hoverWidth = arcs[i]._chart ? arcs[i]._chart.config.data.datasets[index].hoverBorderWidth : 0; + + max = borderWidth > max ? borderWidth : max; + max = hoverWidth > max ? hoverWidth : max; } - }); -}; + return max; + } +}); diff --git a/src/controllers/controller.horizontalBar.js b/src/controllers/controller.horizontalBar.js new file mode 100644 index 00000000000..b761c344403 --- /dev/null +++ b/src/controllers/controller.horizontalBar.js @@ -0,0 +1,79 @@ + +'use strict'; + +var BarController = require('./controller.bar'); +var defaults = require('../core/core.defaults'); + +defaults._set('horizontalBar', { + hover: { + mode: 'index', + axis: 'y' + }, + + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }], + + yAxes: [{ + type: 'category', + position: 'left', + categoryPercentage: 0.8, + barPercentage: 0.9, + offset: true, + gridLines: { + offsetGridLines: true + } + }] + }, + + elements: { + rectangle: { + borderSkipped: 'left' + } + }, + + tooltips: { + callbacks: { + title: function(item, data) { + // Pick first xLabel for now + var title = ''; + + if (item.length > 0) { + if (item[0].yLabel) { + title = item[0].yLabel; + } else if (data.labels.length > 0 && item[0].index < data.labels.length) { + title = data.labels[item[0].index]; + } + } + + return title; + }, + + label: function(item, data) { + var datasetLabel = data.datasets[item.datasetIndex].label || ''; + return datasetLabel + ': ' + item.xLabel; + } + }, + mode: 'index', + axis: 'y' + } +}); + +module.exports = BarController.extend({ + /** + * @private + */ + getValueScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + getIndexScaleId: function() { + return this.getMeta().yAxisID; + } +}); + diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 4a18bdadb3c..fffb1ce55d2 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -24,321 +25,318 @@ defaults._set('line', { } }); -module.exports = function(Chart) { +function lineEnabled(dataset, options) { + return helpers.valueOrDefault(dataset.showLine, options.showLines); +} - function lineEnabled(dataset, options) { - return helpers.valueOrDefault(dataset.showLine, options.showLines); - } +module.exports = DatasetController.extend({ - Chart.controllers.line = Chart.DatasetController.extend({ + datasetElementType: elements.Line, - datasetElementType: elements.Line, + dataElementType: elements.Point, - dataElementType: elements.Point, + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var options = me.chart.options; + var lineElementOptions = options.elements.line; + var scale = me.getScaleForId(meta.yAxisID); + var i, ilen, custom; + var dataset = me.getDataset(); + var showLine = lineEnabled(dataset, options); - update: function(reset) { - var me = this; - var meta = me.getMeta(); - var line = meta.dataset; - var points = meta.data || []; - var options = me.chart.options; - var lineElementOptions = options.elements.line; - var scale = me.getScaleForId(meta.yAxisID); - var i, ilen, custom; - var dataset = me.getDataset(); - var showLine = lineEnabled(dataset, options); + // Update Line + if (showLine) { + custom = line.custom || {}; - // Update Line - if (showLine) { - custom = line.custom || {}; + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { - dataset.lineTension = dataset.tension; - } + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + // Model + line._model = { + // Appearance + // The default behavior of lines is to break at null values, according + // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 + // This option gives lines the ability to span gaps + spanGaps: dataset.spanGaps ? dataset.spanGaps : options.spanGaps, + tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, lineElementOptions.tension), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), + borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), + borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), + borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), + borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), + borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), + borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), + fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), + steppedLine: custom.steppedLine ? custom.steppedLine : helpers.valueOrDefault(dataset.steppedLine, lineElementOptions.stepped), + cubicInterpolationMode: custom.cubicInterpolationMode ? custom.cubicInterpolationMode : helpers.valueOrDefault(dataset.cubicInterpolationMode, lineElementOptions.cubicInterpolationMode), + }; - // Utility - line._scale = scale; - line._datasetIndex = me.index; - // Data - line._children = points; - // Model - line._model = { - // Appearance - // The default behavior of lines is to break at null values, according - // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 - // This option gives lines the ability to span gaps - spanGaps: dataset.spanGaps ? dataset.spanGaps : options.spanGaps, - tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, lineElementOptions.tension), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), - borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), - borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), - borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), - borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), - borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), - borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), - fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), - steppedLine: custom.steppedLine ? custom.steppedLine : helpers.valueOrDefault(dataset.steppedLine, lineElementOptions.stepped), - cubicInterpolationMode: custom.cubicInterpolationMode ? custom.cubicInterpolationMode : helpers.valueOrDefault(dataset.cubicInterpolationMode, lineElementOptions.cubicInterpolationMode), - }; - - line.pivot(); - } + line.pivot(); + } - // Update Points - for (i = 0, ilen = points.length; i < ilen; ++i) { - me.updateElement(points[i], i, reset); - } + // Update Points + for (i = 0, ilen = points.length; i < ilen; ++i) { + me.updateElement(points[i], i, reset); + } - if (showLine && line._model.tension !== 0) { - me.updateBezierControlPoints(); - } + if (showLine && line._model.tension !== 0) { + me.updateBezierControlPoints(); + } - // Now pivot the point for animation - for (i = 0, ilen = points.length; i < ilen; ++i) { - points[i].pivot(); - } - }, - - getPointBackgroundColor: function(point, index) { - var backgroundColor = this.chart.options.elements.point.backgroundColor; - var dataset = this.getDataset(); - var custom = point.custom || {}; - - if (custom.backgroundColor) { - backgroundColor = custom.backgroundColor; - } else if (dataset.pointBackgroundColor) { - backgroundColor = helpers.valueAtIndexOrDefault(dataset.pointBackgroundColor, index, backgroundColor); - } else if (dataset.backgroundColor) { - backgroundColor = dataset.backgroundColor; - } + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; ++i) { + points[i].pivot(); + } + }, - return backgroundColor; - }, + getPointBackgroundColor: function(point, index) { + var backgroundColor = this.chart.options.elements.point.backgroundColor; + var dataset = this.getDataset(); + var custom = point.custom || {}; - getPointBorderColor: function(point, index) { - var borderColor = this.chart.options.elements.point.borderColor; - var dataset = this.getDataset(); - var custom = point.custom || {}; + if (custom.backgroundColor) { + backgroundColor = custom.backgroundColor; + } else if (dataset.pointBackgroundColor) { + backgroundColor = helpers.valueAtIndexOrDefault(dataset.pointBackgroundColor, index, backgroundColor); + } else if (dataset.backgroundColor) { + backgroundColor = dataset.backgroundColor; + } - if (custom.borderColor) { - borderColor = custom.borderColor; - } else if (dataset.pointBorderColor) { - borderColor = helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, borderColor); - } else if (dataset.borderColor) { - borderColor = dataset.borderColor; - } + return backgroundColor; + }, - return borderColor; - }, + getPointBorderColor: function(point, index) { + var borderColor = this.chart.options.elements.point.borderColor; + var dataset = this.getDataset(); + var custom = point.custom || {}; - getPointBorderWidth: function(point, index) { - var borderWidth = this.chart.options.elements.point.borderWidth; - var dataset = this.getDataset(); - var custom = point.custom || {}; + if (custom.borderColor) { + borderColor = custom.borderColor; + } else if (dataset.pointBorderColor) { + borderColor = helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, borderColor); + } else if (dataset.borderColor) { + borderColor = dataset.borderColor; + } - if (!isNaN(custom.borderWidth)) { - borderWidth = custom.borderWidth; - } else if (!isNaN(dataset.pointBorderWidth) || helpers.isArray(dataset.pointBorderWidth)) { - borderWidth = helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); - } else if (!isNaN(dataset.borderWidth)) { - borderWidth = dataset.borderWidth; - } + return borderColor; + }, - return borderWidth; - }, + getPointBorderWidth: function(point, index) { + var borderWidth = this.chart.options.elements.point.borderWidth; + var dataset = this.getDataset(); + var custom = point.custom || {}; - getPointRotation: function(point, index) { - var pointRotation = this.chart.options.elements.point.rotation; - var dataset = this.getDataset(); - var custom = point.custom || {}; + if (!isNaN(custom.borderWidth)) { + borderWidth = custom.borderWidth; + } else if (!isNaN(dataset.pointBorderWidth) || helpers.isArray(dataset.pointBorderWidth)) { + borderWidth = helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); + } else if (!isNaN(dataset.borderWidth)) { + borderWidth = dataset.borderWidth; + } - if (!isNaN(custom.rotation)) { - pointRotation = custom.rotation; - } else if (!isNaN(dataset.pointRotation) || helpers.isArray(dataset.pointRotation)) { - pointRotation = helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointRotation); - } - return pointRotation; - }, - - updateElement: function(point, index, reset) { - var me = this; - var meta = me.getMeta(); - var custom = point.custom || {}; - var dataset = me.getDataset(); - var datasetIndex = me.index; - var value = dataset.data[index]; - var yScale = me.getScaleForId(meta.yAxisID); - var xScale = me.getScaleForId(meta.xAxisID); - var pointOptions = me.chart.options.elements.point; - var x, y; + return borderWidth; + }, - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { - dataset.pointRadius = dataset.radius; - } - if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { - dataset.pointHitRadius = dataset.hitRadius; - } + getPointRotation: function(point, index) { + var pointRotation = this.chart.options.elements.point.rotation; + var dataset = this.getDataset(); + var custom = point.custom || {}; + + if (!isNaN(custom.rotation)) { + pointRotation = custom.rotation; + } else if (!isNaN(dataset.pointRotation) || helpers.isArray(dataset.pointRotation)) { + pointRotation = helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointRotation); + } + return pointRotation; + }, - x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); - y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var dataset = me.getDataset(); + var datasetIndex = me.index; + var value = dataset.data[index]; + var yScale = me.getScaleForId(meta.yAxisID); + var xScale = me.getScaleForId(meta.xAxisID); + var pointOptions = me.chart.options.elements.point; + var x, y; + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { + dataset.pointRadius = dataset.radius; + } + if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { + dataset.pointHitRadius = dataset.hitRadius; + } + + x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); + y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + + // Utility + point._xScale = xScale; + point._yScale = yScale; + point._datasetIndex = datasetIndex; + point._index = index; + + // Desired view properties + point._model = { + x: x, + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointOptions.radius), + pointStyle: custom.pointStyle || helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointOptions.pointStyle), + rotation: me.getPointRotation(point, index), + backgroundColor: me.getPointBackgroundColor(point, index), + borderColor: me.getPointBorderColor(point, index), + borderWidth: me.getPointBorderWidth(point, index), + tension: meta.dataset._model ? meta.dataset._model.tension : 0, + steppedLine: meta.dataset._model ? meta.dataset._model.steppedLine : false, + // Tooltip + hitRadius: custom.hitRadius || helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointOptions.hitRadius) + }; + }, - // Utility - point._xScale = xScale; - point._yScale = yScale; - point._datasetIndex = datasetIndex; - point._index = index; - - // Desired view properties - point._model = { - x: x, - y: y, - skip: custom.skip || isNaN(x) || isNaN(y), - // Appearance - radius: custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointOptions.radius), - pointStyle: custom.pointStyle || helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointOptions.pointStyle), - rotation: me.getPointRotation(point, index), - backgroundColor: me.getPointBackgroundColor(point, index), - borderColor: me.getPointBorderColor(point, index), - borderWidth: me.getPointBorderWidth(point, index), - tension: meta.dataset._model ? meta.dataset._model.tension : 0, - steppedLine: meta.dataset._model ? meta.dataset._model.steppedLine : false, - // Tooltip - hitRadius: custom.hitRadius || helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointOptions.hitRadius) - }; - }, - - calculatePointY: function(value, index, datasetIndex) { - var me = this; - var chart = me.chart; - var meta = me.getMeta(); - var yScale = me.getScaleForId(meta.yAxisID); - var sumPos = 0; - var sumNeg = 0; - var i, ds, dsMeta; - - if (yScale.options.stacked) { - for (i = 0; i < datasetIndex; i++) { - ds = chart.data.datasets[i]; - dsMeta = chart.getDatasetMeta(i); - if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { - var stackedRightValue = Number(yScale.getRightValue(ds.data[index])); - if (stackedRightValue < 0) { - sumNeg += stackedRightValue || 0; - } else { - sumPos += stackedRightValue || 0; - } + calculatePointY: function(value, index, datasetIndex) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + var sumPos = 0; + var sumNeg = 0; + var i, ds, dsMeta; + + if (yScale.options.stacked) { + for (i = 0; i < datasetIndex; i++) { + ds = chart.data.datasets[i]; + dsMeta = chart.getDatasetMeta(i); + if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + var stackedRightValue = Number(yScale.getRightValue(ds.data[index])); + if (stackedRightValue < 0) { + sumNeg += stackedRightValue || 0; + } else { + sumPos += stackedRightValue || 0; } } - - var rightValue = Number(yScale.getRightValue(value)); - if (rightValue < 0) { - return yScale.getPixelForValue(sumNeg + rightValue); - } - return yScale.getPixelForValue(sumPos + rightValue); } - return yScale.getPixelForValue(value); - }, - - updateBezierControlPoints: function() { - var me = this; - var meta = me.getMeta(); - var area = me.chart.chartArea; - var points = (meta.data || []); - var i, ilen, point, model, controlPoints; - - // Only consider points that are drawn in case the spanGaps option is used - if (meta.dataset._model.spanGaps) { - points = points.filter(function(pt) { - return !pt._model.skip; - }); + var rightValue = Number(yScale.getRightValue(value)); + if (rightValue < 0) { + return yScale.getPixelForValue(sumNeg + rightValue); } + return yScale.getPixelForValue(sumPos + rightValue); + } - function capControlPoint(pt, min, max) { - return Math.max(Math.min(pt, max), min); - } + return yScale.getPixelForValue(value); + }, - if (meta.dataset._model.cubicInterpolationMode === 'monotone') { - helpers.splineCurveMonotone(points); - } else { - for (i = 0, ilen = points.length; i < ilen; ++i) { - point = points[i]; - model = point._model; - controlPoints = helpers.splineCurve( - helpers.previousItem(points, i)._model, - model, - helpers.nextItem(points, i)._model, - meta.dataset._model.tension - ); - model.controlPointPreviousX = controlPoints.previous.x; - model.controlPointPreviousY = controlPoints.previous.y; - model.controlPointNextX = controlPoints.next.x; - model.controlPointNextY = controlPoints.next.y; - } + updateBezierControlPoints: function() { + var me = this; + var meta = me.getMeta(); + var area = me.chart.chartArea; + var points = (meta.data || []); + var i, ilen, point, model, controlPoints; + + // Only consider points that are drawn in case the spanGaps option is used + if (meta.dataset._model.spanGaps) { + points = points.filter(function(pt) { + return !pt._model.skip; + }); + } + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + if (meta.dataset._model.cubicInterpolationMode === 'monotone') { + helpers.splineCurveMonotone(points); + } else { + for (i = 0, ilen = points.length; i < ilen; ++i) { + point = points[i]; + model = point._model; + controlPoints = helpers.splineCurve( + helpers.previousItem(points, i)._model, + model, + helpers.nextItem(points, i)._model, + meta.dataset._model.tension + ); + model.controlPointPreviousX = controlPoints.previous.x; + model.controlPointPreviousY = controlPoints.previous.y; + model.controlPointNextX = controlPoints.next.x; + model.controlPointNextY = controlPoints.next.y; } + } - if (me.chart.options.elements.line.capBezierPoints) { - for (i = 0, ilen = points.length; i < ilen; ++i) { - model = points[i]._model; - model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); - model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); - model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); - model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); - } - } - }, - - draw: function() { - var me = this; - var chart = me.chart; - var meta = me.getMeta(); - var points = meta.data || []; - var area = chart.chartArea; - var ilen = points.length; - var halfBorderWidth; - var i = 0; - - if (lineEnabled(me.getDataset(), chart.options)) { - halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; - - helpers.canvas.clipArea(chart.ctx, { - left: area.left, - right: area.right, - top: area.top - halfBorderWidth, - bottom: area.bottom + halfBorderWidth - }); - - meta.dataset.draw(); - - helpers.canvas.unclipArea(chart.ctx); + if (me.chart.options.elements.line.capBezierPoints) { + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); + model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); + model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); + model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); } + } + }, - // Draw the points - for (; i < ilen; ++i) { - points[i].draw(area); - } - }, - - setHoverStyle: function(element) { - // Point - var dataset = this.chart.data.datasets[element._datasetIndex]; - var index = element._index; - var custom = element.custom || {}; - var model = element._model; - - element.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; + draw: function() { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var points = meta.data || []; + var area = chart.chartArea; + var ilen = points.length; + var halfBorderWidth; + var i = 0; + + if (lineEnabled(me.getDataset(), chart.options)) { + halfBorderWidth = (meta.dataset._model.borderWidth || 0) / 2; + + helpers.canvas.clipArea(chart.ctx, { + left: area.left, + right: area.right, + top: area.top - halfBorderWidth, + bottom: area.bottom + halfBorderWidth + }); + + meta.dataset.draw(); + + helpers.canvas.unclipArea(chart.ctx); + } + + // Draw the points + for (; i < ilen; ++i) { + points[i].draw(area); + } + }, - model.backgroundColor = custom.hoverBackgroundColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); - model.borderColor = custom.hoverBorderColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); - model.borderWidth = custom.hoverBorderWidth || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); - model.radius = custom.hoverRadius || helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); - }, - }); -}; + setHoverStyle: function(element) { + // Point + var dataset = this.chart.data.datasets[element._datasetIndex]; + var index = element._index; + var custom = element.custom || {}; + var model = element._model; + + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = custom.hoverBackgroundColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); + model.borderColor = custom.hoverBorderColor || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); + model.borderWidth = custom.hoverBorderWidth || helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); + model.radius = custom.hoverRadius || helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); + } +}); diff --git a/src/controllers/controller.pie.js b/src/controllers/controller.pie.js new file mode 100644 index 00000000000..2f50c9d4d88 --- /dev/null +++ b/src/controllers/controller.pie.js @@ -0,0 +1,13 @@ +'use strict'; + +var DoughnutController = require('./controller.doughnut'); +var defaults = require('../core/core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('pie', helpers.clone(defaults.doughnut)); +defaults._set('pie', { + cutoutPercentage: 0 +}); + +// Pie charts are Doughnut chart with different defaults +module.exports = DoughnutController; diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index 663a9534d55..fb045e30271 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -108,148 +109,145 @@ defaults._set('polarArea', { } }); -module.exports = function(Chart) { +module.exports = DatasetController.extend({ - Chart.controllers.polarArea = Chart.DatasetController.extend({ + dataElementType: elements.Arc, - dataElementType: elements.Arc, + linkScales: helpers.noop, - linkScales: helpers.noop, + update: function(reset) { + var me = this; + var dataset = me.getDataset(); + var meta = me.getMeta(); + var start = me.chart.options.startAngle || 0; + var starts = me._starts = []; + var angles = me._angles = []; + var i, ilen, angle; - update: function(reset) { - var me = this; - var dataset = me.getDataset(); - var meta = me.getMeta(); - var start = me.chart.options.startAngle || 0; - var starts = me._starts = []; - var angles = me._angles = []; - var i, ilen, angle; + me._updateRadius(); - me._updateRadius(); + meta.count = me.countVisibleElements(); - meta.count = me.countVisibleElements(); - - for (i = 0, ilen = dataset.data.length; i < ilen; i++) { - starts[i] = start; - angle = me._computeAngle(i); - angles[i] = angle; - start += angle; - } - - helpers.each(meta.data, function(arc, index) { - me.updateElement(arc, index, reset); - }); - }, - - /** - * @private - */ - _updateRadius: function() { - var me = this; - var chart = me.chart; - var chartArea = chart.chartArea; - var opts = chart.options; - var arcOpts = opts.elements.arc; - var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); - - chart.outerRadius = Math.max((minSize - arcOpts.borderWidth / 2) / 2, 0); - chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); - chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); - - me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); - me.innerRadius = me.outerRadius - chart.radiusLength; - }, + for (i = 0, ilen = dataset.data.length; i < ilen; i++) { + starts[i] = start; + angle = me._computeAngle(i); + angles[i] = angle; + start += angle; + } - updateElement: function(arc, index, reset) { - var me = this; - var chart = me.chart; - var dataset = me.getDataset(); - var opts = chart.options; - var animationOpts = opts.animation; - var scale = chart.scale; - var labels = chart.data.labels; - - var centerX = scale.xCenter; - var centerY = scale.yCenter; - - // var negHalfPI = -0.5 * Math.PI; - var datasetStartAngle = opts.startAngle; - var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); - var startAngle = me._starts[index]; - var endAngle = startAngle + (arc.hidden ? 0 : me._angles[index]); - - var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); - - helpers.extend(arc, { - // Utility - _datasetIndex: me.index, - _index: index, - _scale: scale, - - // Desired view properties - _model: { - x: centerX, - y: centerY, - innerRadius: 0, - outerRadius: reset ? resetRadius : distance, - startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, - endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle, - label: helpers.valueAtIndexOrDefault(labels, index, labels[index]) - } - }); + helpers.each(meta.data, function(arc, index) { + me.updateElement(arc, index, reset); + }); + }, - // Apply border and fill style - var elementOpts = this.chart.options.elements.arc; - var custom = arc.custom || {}; - var valueOrDefault = helpers.valueAtIndexOrDefault; - var model = arc._model; + /** + * @private + */ + _updateRadius: function() { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var arcOpts = opts.elements.arc; + var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + + chart.outerRadius = Math.max((minSize - arcOpts.borderWidth / 2) / 2, 0); + chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); + me.innerRadius = me.outerRadius - chart.radiusLength; + }, - model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); - model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); - model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var opts = chart.options; + var animationOpts = opts.animation; + var scale = chart.scale; + var labels = chart.data.labels; + + var centerX = scale.xCenter; + var centerY = scale.yCenter; + + // var negHalfPI = -0.5 * Math.PI; + var datasetStartAngle = opts.startAngle; + var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var startAngle = me._starts[index]; + var endAngle = startAngle + (arc.hidden ? 0 : me._angles[index]); + + var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + + helpers.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius: reset ? resetRadius : distance, + startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, + endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle, + label: helpers.valueAtIndexOrDefault(labels, index, labels[index]) + } + }); - arc.pivot(); - }, + // Apply border and fill style + var elementOpts = this.chart.options.elements.arc; + var custom = arc.custom || {}; + var valueOrDefault = helpers.valueAtIndexOrDefault; + var model = arc._model; - countVisibleElements: function() { - var dataset = this.getDataset(); - var meta = this.getMeta(); - var count = 0; + model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor); + model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor); + model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth); - helpers.each(meta.data, function(element, index) { - if (!isNaN(dataset.data[index]) && !element.hidden) { - count++; - } - }); + arc.pivot(); + }, - return count; - }, + countVisibleElements: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var count = 0; - /** - * @private - */ - _computeAngle: function(index) { - var me = this; - var count = this.getMeta().count; - var dataset = me.getDataset(); - var meta = me.getMeta(); - - if (isNaN(dataset.data[index]) || meta.data[index].hidden) { - return 0; + helpers.each(meta.data, function(element, index) { + if (!isNaN(dataset.data[index]) && !element.hidden) { + count++; } + }); - // Scriptable options - var context = { - chart: me.chart, - dataIndex: index, - dataset: dataset, - datasetIndex: me.index - }; - - return helpers.options.resolve([ - me.chart.options.elements.arc.angle, - (2 * Math.PI) / count - ], context, index); + return count; + }, + + /** + * @private + */ + _computeAngle: function(index) { + var me = this; + var count = this.getMeta().count; + var dataset = me.getDataset(); + var meta = me.getMeta(); + + if (isNaN(dataset.data[index]) || meta.data[index].hidden) { + return 0; } - }); -}; + + // Scriptable options + var context = { + chart: me.chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + return helpers.options.resolve([ + me.chart.options.elements.arc.angle, + (2 * Math.PI) / count + ], context, index); + } +}); diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index dc2b86c617b..4d69928d87b 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -1,5 +1,6 @@ 'use strict'; +var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); @@ -15,158 +16,157 @@ defaults._set('radar', { } }); -module.exports = function(Chart) { +module.exports = DatasetController.extend({ - Chart.controllers.radar = Chart.DatasetController.extend({ + datasetElementType: elements.Line, - datasetElementType: elements.Line, + dataElementType: elements.Point, - dataElementType: elements.Point, + linkScales: helpers.noop, - linkScales: helpers.noop, + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var custom = line.custom || {}; + var dataset = me.getDataset(); + var lineElementOptions = me.chart.options.elements.line; + var scale = me.chart.scale; + var i, ilen; - update: function(reset) { - var me = this; - var meta = me.getMeta(); - var line = meta.dataset; - var points = meta.data || []; - var custom = line.custom || {}; - var dataset = me.getDataset(); - var lineElementOptions = me.chart.options.elements.line; - var scale = me.chart.scale; - var i, ilen; + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { - dataset.lineTension = dataset.tension; + helpers.extend(meta.dataset, { + // Utility + _datasetIndex: me.index, + _scale: scale, + // Data + _children: points, + _loop: true, + // Model + _model: { + // Appearance + tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, lineElementOptions.tension), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), + borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), + borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), + fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), + borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), + borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), + borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), + borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), } + }); - helpers.extend(meta.dataset, { - // Utility - _datasetIndex: me.index, - _scale: scale, - // Data - _children: points, - _loop: true, - // Model - _model: { - // Appearance - tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, lineElementOptions.tension), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), - borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), - borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), - fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), - borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), - borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), - borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), - borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), - } - }); - - meta.dataset.pivot(); - - // Update Points - for (i = 0, ilen = points.length; i < ilen; i++) { - me.updateElement(points[i], i, reset); - } + meta.dataset.pivot(); - // Update bezier control points - me.updateBezierControlPoints(); + // Update Points + for (i = 0, ilen = points.length; i < ilen; i++) { + me.updateElement(points[i], i, reset); + } - // Now pivot the point for animation - for (i = 0, ilen = points.length; i < ilen; i++) { - points[i].pivot(); - } - }, - updateElement: function(point, index, reset) { - var me = this; - var custom = point.custom || {}; - var dataset = me.getDataset(); - var scale = me.chart.scale; - var pointElementOptions = me.chart.options.elements.point; - var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); - - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { - dataset.pointRadius = dataset.radius; - } - if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { - dataset.pointHitRadius = dataset.hitRadius; - } + // Update bezier control points + me.updateBezierControlPoints(); - helpers.extend(point, { - // Utility - _datasetIndex: me.index, - _index: index, - _scale: scale, - - // Desired view properties - _model: { - x: reset ? scale.xCenter : pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales - y: reset ? scale.yCenter : pointPosition.y, - - // Appearance - tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, me.chart.options.elements.line.tension), - radius: custom.radius ? custom.radius : helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius), - backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.valueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor), - borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), - borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth), - pointStyle: custom.pointStyle ? custom.pointStyle : helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle), - rotation: custom.rotation ? custom.rotation : helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointElementOptions.rotation), - - // Tooltip - hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius) - } - }); - - point._model.skip = custom.skip ? custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); - }, - updateBezierControlPoints: function() { - var me = this; - var meta = me.getMeta(); - var area = me.chart.chartArea; - var points = meta.data || []; - var i, ilen, model, controlPoints; - - function capControlPoint(pt, min, max) { - return Math.max(Math.min(pt, max), min); - } + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; i++) { + points[i].pivot(); + } + }, + + updateElement: function(point, index, reset) { + var me = this; + var custom = point.custom || {}; + var dataset = me.getDataset(); + var scale = me.chart.scale; + var pointElementOptions = me.chart.options.elements.point; + var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { + dataset.pointRadius = dataset.radius; + } + if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { + dataset.pointHitRadius = dataset.hitRadius; + } - for (i = 0, ilen = points.length; i < ilen; i++) { - model = points[i]._model; - controlPoints = helpers.splineCurve( - helpers.previousItem(points, i, true)._model, - model, - helpers.nextItem(points, i, true)._model, - model.tension - ); - - // Prevent the bezier going outside of the bounds of the graph - model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); - model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); - model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); - model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); + helpers.extend(point, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + x: reset ? scale.xCenter : pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales + y: reset ? scale.yCenter : pointPosition.y, + + // Appearance + tension: custom.tension ? custom.tension : helpers.valueOrDefault(dataset.lineTension, me.chart.options.elements.line.tension), + radius: custom.radius ? custom.radius : helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.valueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor), + borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth), + pointStyle: custom.pointStyle ? custom.pointStyle : helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle), + rotation: custom.rotation ? custom.rotation : helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointElementOptions.rotation), + + // Tooltip + hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius) } - }, - - setHoverStyle: function(point) { - // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var custom = point.custom || {}; - var index = point._index; - var model = point._model; - - point.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; - - model.radius = custom.hoverRadius ? custom.hoverRadius : helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); - model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); - model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); - model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); - }, - }); -}; + }); + + point._model.skip = custom.skip ? custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); + }, + + updateBezierControlPoints: function() { + var me = this; + var meta = me.getMeta(); + var area = me.chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + for (i = 0, ilen = points.length; i < ilen; i++) { + model = points[i]._model; + controlPoints = helpers.splineCurve( + helpers.previousItem(points, i, true)._model, + model, + helpers.nextItem(points, i, true)._model, + model.tension + ); + + // Prevent the bezier going outside of the bounds of the graph + model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); + model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); + model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); + model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); + } + }, + + setHoverStyle: function(point) { + // Point + var dataset = this.chart.data.datasets[point._datasetIndex]; + var custom = point.custom || {}; + var index = point._index; + var model = point._model; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.radius = custom.hoverRadius ? custom.hoverRadius : helpers.valueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); + model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); + model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); + model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.valueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); + } +}); diff --git a/src/controllers/controller.scatter.js b/src/controllers/controller.scatter.js index b2e2cf1f7e1..319296d33ba 100644 --- a/src/controllers/controller.scatter.js +++ b/src/controllers/controller.scatter.js @@ -1,5 +1,6 @@ 'use strict'; +var LineController = require('./controller.line'); var defaults = require('../core/core.defaults'); defaults._set('scatter', { @@ -34,9 +35,5 @@ defaults._set('scatter', { } }); -module.exports = function(Chart) { - - // Scatter charts use line controllers - Chart.controllers.scatter = Chart.controllers.line; - -}; +// Scatter charts use line controllers +module.exports = LineController; diff --git a/src/controllers/index.js b/src/controllers/index.js new file mode 100644 index 00000000000..d2fa6c96e3b --- /dev/null +++ b/src/controllers/index.js @@ -0,0 +1,18 @@ +'use strict'; + +// NOTE export a map in which the key represents the controller type, not +// the class, and so must be CamelCase in order to be correctly retrieved +// by the controller in core.controller.js (`controllers[meta.type]`). + +/* eslint-disable global-require */ +module.exports = { + bar: require('./controller.bar'), + bubble: require('./controller.bubble'), + doughnut: require('./controller.doughnut'), + horizontalBar: require('./controller.horizontalBar'), + line: require('./controller.line'), + polarArea: require('./controller.polarArea'), + pie: require('./controller.pie'), + radar: require('./controller.radar'), + scatter: require('./controller.scatter') +}; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 48365fb224d..117a6d576ea 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -2,6 +2,7 @@ var Animation = require('./core.animation'); var animations = require('./core.animations'); +var controllers = require('../controllers/index'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); @@ -20,9 +21,6 @@ module.exports = function(Chart) { // Destroy method on the chart will remove the instance of the chart from this reference. Chart.instances = {}; - // Controllers available for dataset visualization eg. bar, line, slice, etc. - Chart.controllers = {}; - /** * Initializes the given config with global and chart default values. */ @@ -337,7 +335,7 @@ module.exports = function(Chart) { meta.controller.updateIndex(datasetIndex); meta.controller.linkScales(); } else { - var ControllerClass = Chart.controllers[meta.type]; + var ControllerClass = controllers[meta.type]; if (ControllerClass === undefined) { throw new Error('"' + meta.type + '" is not a chart type.'); } diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 4928f98913e..8d2de1a1aaf 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -2,328 +2,325 @@ var helpers = require('../helpers/index'); -module.exports = function(Chart) { - - var arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; +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; + } - /** - * 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] } + }); - Object.defineProperty(array, '_chartjs', { + 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: { - listeners: [listener] + 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; + } - 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; - } - }); - }); + 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) +var DatasetController = module.exports = function(chart, datasetIndex) { + this.initialize(chart, datasetIndex); +}; + +helpers.extend(DatasetController.prototype, { + /** - * 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. + * Element type used to generate a meta dataset (e.g. Chart.element.Line). + * @type {Chart.core.element} */ - function unlistenArrayEvents(array, listener) { - var stub = array._chartjs; - if (!stub) { - return; - } + datasetElementType: null, - var listeners = stub.listeners; - var index = listeners.indexOf(listener); - if (index !== -1) { - listeners.splice(index, 1); + /** + * Element type used to generate a meta data (e.g. Chart.element.Point). + * @type {Chart.core.element} + */ + dataElementType: null, + + initialize: function(chart, datasetIndex) { + var me = this; + me.chart = chart; + me.index = datasetIndex; + me.linkScales(); + me.addElements(); + }, + + updateIndex: function(datasetIndex) { + this.index = datasetIndex; + }, + + linkScales: function() { + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { + meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; } - - if (listeners.length > 0) { - return; + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { + meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; } + }, + + getDataset: function() { + return this.chart.data.datasets[this.index]; + }, - arrayEvents.forEach(function(key) { - delete array[key]; + getMeta: function() { + return this.chart.getDatasetMeta(this.index); + }, + + getScaleForId: function(scaleID) { + return this.chart.scales[scaleID]; + }, + + reset: function() { + this.update(true); + }, + + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + + createMetaDataset: function() { + var me = this; + var type = me.datasetElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index + }); + }, + + createMetaData: function(index) { + var me = this; + var type = me.dataElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index, + _index: index }); + }, - delete array._chartjs; - } + addElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data || []; + var metaData = meta.data; + var i, ilen; - // Base class for all dataset controllers (line, bar, etc) - Chart.DatasetController = function(chart, datasetIndex) { - this.initialize(chart, datasetIndex); - }; - - helpers.extend(Chart.DatasetController.prototype, { - - /** - * Element type used to generate a meta dataset (e.g. Chart.element.Line). - * @type {Chart.core.element} - */ - datasetElementType: null, - - /** - * Element type used to generate a meta data (e.g. Chart.element.Point). - * @type {Chart.core.element} - */ - dataElementType: null, - - initialize: function(chart, datasetIndex) { - var me = this; - me.chart = chart; - me.index = datasetIndex; - me.linkScales(); - me.addElements(); - }, - - updateIndex: function(datasetIndex) { - this.index = datasetIndex; - }, - - linkScales: function() { - var me = this; - var meta = me.getMeta(); - var dataset = me.getDataset(); - - if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { - meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; - } - if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { - meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; - } - }, - - getDataset: function() { - return this.chart.data.datasets[this.index]; - }, - - getMeta: function() { - return this.chart.getDatasetMeta(this.index); - }, - - getScaleForId: function(scaleID) { - return this.chart.scales[scaleID]; - }, - - reset: function() { - this.update(true); - }, - - /** - * @private - */ - destroy: function() { - if (this._data) { - unlistenArrayEvents(this._data, this); - } - }, - - createMetaDataset: function() { - var me = this; - var type = me.datasetElementType; - return type && new type({ - _chart: me.chart, - _datasetIndex: me.index - }); - }, - - createMetaData: function(index) { - var me = this; - var type = me.dataElementType; - return type && new type({ - _chart: me.chart, - _datasetIndex: me.index, - _index: index - }); - }, - - addElements: function() { - var me = this; - var meta = me.getMeta(); - var data = me.getDataset().data || []; - var metaData = meta.data; - var i, ilen; - - for (i = 0, ilen = data.length; i < ilen; ++i) { - metaData[i] = metaData[i] || me.createMetaData(i); - } + for (i = 0, ilen = data.length; i < ilen; ++i) { + metaData[i] = metaData[i] || me.createMetaData(i); + } - meta.dataset = meta.dataset || me.createMetaDataset(); - }, - - addElementAndReset: function(index) { - var element = this.createMetaData(index); - this.getMeta().data.splice(index, 0, element); - this.updateElement(element, index, true); - }, - - buildOrUpdateElements: function() { - var me = this; - var dataset = me.getDataset(); - var data = dataset.data || (dataset.data = []); - - // 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; + meta.dataset = meta.dataset || me.createMetaDataset(); + }, + + addElementAndReset: function(index) { + var element = this.createMetaData(index); + this.getMeta().data.splice(index, 0, element); + this.updateElement(element, index, true); + }, + + buildOrUpdateElements: function() { + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // 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); } - // 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(); - }, + listenArrayEvents(data, me); + me._data = data; + } - update: helpers.noop, + // 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(); + }, - transition: function(easingValue) { - var meta = this.getMeta(); - var elements = meta.data || []; - var ilen = elements.length; - var i = 0; + update: helpers.noop, - for (; i < ilen; ++i) { - elements[i].transition(easingValue); - } + transition: function(easingValue) { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; - if (meta.dataset) { - meta.dataset.transition(easingValue); - } - }, + for (; i < ilen; ++i) { + elements[i].transition(easingValue); + } - draw: function() { - var meta = this.getMeta(); - var elements = meta.data || []; - var ilen = elements.length; - var i = 0; + if (meta.dataset) { + meta.dataset.transition(easingValue); + } + }, - if (meta.dataset) { - meta.dataset.draw(); - } + draw: function() { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; - for (; i < ilen; ++i) { - elements[i].draw(); - } - }, - - removeHoverStyle: function(element) { - helpers.merge(element._model, element.$previousStyle || {}); - delete element.$previousStyle; - }, - - setHoverStyle: function(element) { - var dataset = this.chart.data.datasets[element._datasetIndex]; - var index = element._index; - var custom = element.custom || {}; - var valueOrDefault = helpers.valueAtIndexOrDefault; - var getHoverColor = helpers.getHoverColor; - var model = element._model; - - element.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth - }; - - 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 < count; ++i) { - this.addElementAndReset(start + i); - } - }, - - /** - * @private - */ - onDataPush: function() { - this.insertElements(this.getDataset().data.length - 1, arguments.length); - }, - - /** - * @private - */ - onDataPop: function() { - this.getMeta().data.pop(); - }, - - /** - * @private - */ - onDataShift: function() { - this.getMeta().data.shift(); - }, - - /** - * @private - */ - onDataSplice: function(start, count) { - this.getMeta().data.splice(start, count); - this.insertElements(start, arguments.length - 2); - }, - - /** - * @private - */ - onDataUnshift: function() { - this.insertElements(0, arguments.length); + if (meta.dataset) { + meta.dataset.draw(); } - }); - Chart.DatasetController.extend = helpers.inherits; -}; + for (; i < ilen; ++i) { + elements[i].draw(); + } + }, + + removeHoverStyle: function(element) { + helpers.merge(element._model, element.$previousStyle || {}); + delete element.$previousStyle; + }, + + setHoverStyle: function(element) { + var dataset = this.chart.data.datasets[element._datasetIndex]; + var index = element._index; + var custom = element.custom || {}; + var valueOrDefault = helpers.valueAtIndexOrDefault; + var getHoverColor = helpers.getHoverColor; + var model = element._model; + + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth + }; + + 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 < count; ++i) { + this.addElementAndReset(start + i); + } + }, + + /** + * @private + */ + onDataPush: function() { + this.insertElements(this.getDataset().data.length - 1, arguments.length); + }, + + /** + * @private + */ + onDataPop: function() { + this.getMeta().data.pop(); + }, + + /** + * @private + */ + onDataShift: function() { + this.getMeta().data.shift(); + }, + + /** + * @private + */ + onDataSplice: function(start, count) { + this.getMeta().data.splice(start, count); + this.insertElements(start, arguments.length - 2); + }, + + /** + * @private + */ + onDataUnshift: function() { + this.insertElements(0, arguments.length); + } +}); + +DatasetController.extend = helpers.inherits; diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index 0e843e1435f..62ab0acd87a 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -1,6 +1,11 @@ describe('Chart.controllers.bar', function() { describe('auto', jasmine.fixture.specs('controller.bar')); + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.bar).toBe('function'); + expect(typeof Chart.controllers.horizontalBar).toBe('function'); + }); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'bar', diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index a5f5b89a5de..9591e65973c 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -1,6 +1,10 @@ describe('Chart.controllers.bubble', function() { describe('auto', jasmine.fixture.specs('controller.bubble')); + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.bubble).toBe('function'); + }); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'bubble', diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 2f9602bda73..e7bc951fd57 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -1,4 +1,9 @@ describe('Chart.controllers.doughnut', function() { + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.doughnut).toBe('function'); + expect(Chart.controllers.doughnut).toBe(Chart.controllers.pie); + }); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'doughnut', diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index 25a9ed6b0f5..f3a3351de9c 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -1,6 +1,10 @@ describe('Chart.controllers.line', function() { describe('auto', jasmine.fixture.specs('controller.line')); + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.line).toBe('function'); + }); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'line', diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index 1023bf0789b..1feda5134b8 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -1,6 +1,10 @@ -describe('auto', jasmine.fixture.specs('controller.polarArea')); - describe('Chart.controllers.polarArea', function() { + describe('auto', jasmine.fixture.specs('controller.polarArea')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.polarArea).toBe('function'); + }); + it('should be constructed', function() { var chart = window.acquireChart({ type: 'polarArea', diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 6a447a1ccdb..a013274bb78 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -1,6 +1,10 @@ describe('Chart.controllers.radar', function() { describe('auto', jasmine.fixture.specs('controller.radar')); + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.radar).toBe('function'); + }); + it('Should be constructed', function() { var chart = window.acquireChart({ type: 'radar', diff --git a/test/specs/controller.scatter.test.js b/test/specs/controller.scatter.test.js index 435750928d9..6063caed766 100644 --- a/test/specs/controller.scatter.test.js +++ b/test/specs/controller.scatter.test.js @@ -1,4 +1,8 @@ describe('Chart.controllers.scatter', function() { + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.scatter).toBe('function'); + }); + describe('showLines option', function() { it('should not draw a line if undefined', function() { var chart = window.acquireChart({ From 3cb2d7050e0976f92383beb48bee07e5d579a2c0 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 1 Dec 2018 08:35:43 +0100 Subject: [PATCH 469/685] Remove gulp-connect and add jsdelivr/unpkg paths (#5875) --- .travis.yml | 2 +- gulpfile.js | 20 +------------------- package.json | 23 ++++++++++++++++------- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b38ab3c146..574bfe9a27a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "6" + - lts/* before_install: - "export CHROME_BIN=/usr/bin/google-chrome" diff --git a/gulpfile.js b/gulpfile.js index 06229d6f89c..c0a18cda225 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,6 +1,5 @@ var gulp = require('gulp'); var concat = require('gulp-concat'); -var connect = require('gulp-connect'); var eslint = require('gulp-eslint'); var file = require('gulp-file'); var insert = require('gulp-insert'); @@ -26,7 +25,7 @@ var argv = yargs .option('force-output', {default: false}) .option('silent-errors', {default: false}) .option('verbose', {default: false}) - .argv + .argv; var srcDir = './src/'; var outDir = './dist/'; @@ -53,15 +52,12 @@ gulp.task('lint-html', lintHtmlTask); gulp.task('lint-js', lintJsTask); gulp.task('lint', gulp.parallel('lint-html', 'lint-js')); gulp.task('docs', docsTask); -gulp.task('server', serverTask); gulp.task('unittest', unittestTask); gulp.task('test', gulp.parallel('lint', 'unittest')); gulp.task('library-size', librarySizeTask); gulp.task('module-sizes', moduleSizesTask); gulp.task('size', gulp.parallel('library-size', 'module-sizes')); -gulp.task('_open', _openTask); gulp.task('default', gulp.parallel('build', 'watch')); -gulp.task('dev', gulp.parallel('server', 'default')); /** * Generates the bower.json manifest file which will be pushed along release tags. @@ -136,7 +132,6 @@ function buildTask() { .pipe(gulp.dest(outDir)); return merge(bundled, nonBundled); - } function packageTask() { @@ -235,16 +230,3 @@ function moduleSizesTask() { function watchTask() { return gulp.watch('./src/**', gulp.parallel('build')); } - -function serverTask() { - connect.server({ - port: 8000 - }); -} - -// Convenience task for opening the project straight from the command line - -function _openTask() { - exec('open http://localhost:8000'); - exec('subl .'); -} diff --git a/package.json b/package.json index b8c72f99afb..23b75ffd67b 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,27 @@ { "name": "chart.js", - "homepage": "http://www.chartjs.org", + "homepage": "https://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", "version": "2.7.3", "license": "MIT", + "jsdelivr": "dist/Chart.min.js", + "unpkg": "dist/Chart.min.js", "main": "src/chart.js", + "keywords": [ + "canvas", + "charts", + "data", + "graphs", + "html5", + "responsive" + ], "repository": { "type": "git", "url": "https://github.com/chartjs/Chart.js.git" }, + "bugs": { + "url": "https://github.com/chartjs/Chart.js/issues" + }, "devDependencies": { "browserify": "^16.2.3", "browserify-istanbul": "^3.0.1", @@ -21,7 +34,6 @@ "gitbook-cli": "^2.3.2", "gulp": "^4.0.0", "gulp-concat": "^2.6.0", - "gulp-connect": "^5.6.1", "gulp-eslint": "^5.0.0", "gulp-file": "^0.4.0", "gulp-htmllint": "^0.0.16", @@ -39,16 +51,13 @@ "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", - "karma-jasmine": "^2.0.0", + "karma-jasmine": "^2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "merge-stream": "^1.0.1", "pixelmatch": "^4.0.2", "vinyl-source-stream": "^2.0.0", "watchify": "^3.9.0", - "yargs": "^12.0.2" - }, - "spm": { - "main": "Chart.js" + "yargs": "^12.0.5" }, "dependencies": { "chartjs-color": "^2.1.0", From 5797e034218bebb5c9330407c391c87e812ae839 Mon Sep 17 00:00:00 2001 From: generic-github-user <40661852+generic-github-user@users.noreply.github.com> Date: Tue, 4 Dec 2018 03:07:35 -0500 Subject: [PATCH 470/685] Refactor data generation in scatter basic example (#5877) Replace repeated function call with compact function, generateData --- samples/charts/scatter/basic.html | 57 +++++++------------------------ 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/samples/charts/scatter/basic.html b/samples/charts/scatter/basic.html index 6ac227c3b95..ccf1059f8b8 100644 --- a/samples/charts/scatter/basic.html +++ b/samples/charts/scatter/basic.html @@ -21,59 +21,28 @@ - - - - - -

    - - - - - diff --git a/samples/advanced/progress-bar.html b/samples/advanced/progress-bar.html index b9c72446e59..4736546ba48 100644 --- a/samples/advanced/progress-bar.html +++ b/samples/advanced/progress-bar.html @@ -2,7 +2,7 @@ Animation Callbacks - + diff --git a/samples/charts/area/line-datasets.html b/samples/charts/area/line-datasets.html index 127726e7fb7..3796459d2ab 100644 --- a/samples/charts/area/line-datasets.html +++ b/samples/charts/area/line-datasets.html @@ -6,7 +6,7 @@ area > datasets | Chart.js sample - + diff --git a/samples/charts/area/line-stacked.html b/samples/charts/area/line-stacked.html index 11a143b0b63..50ad6c98acb 100644 --- a/samples/charts/area/line-stacked.html +++ b/samples/charts/area/line-stacked.html @@ -3,7 +3,7 @@ Line Chart - + diff --git a/samples/charts/bar/horizontal.html b/samples/charts/bar/horizontal.html index e174ad2ca35..114a6f93d05 100644 --- a/samples/charts/bar/horizontal.html +++ b/samples/charts/bar/horizontal.html @@ -3,7 +3,7 @@ Horizontal Bar Chart - + diff --git a/samples/charts/polar-area.html b/samples/charts/polar-area.html index 1c40a645181..a667f443bd1 100644 --- a/samples/charts/polar-area.html +++ b/samples/charts/polar-area.html @@ -3,7 +3,7 @@ Polar Area Chart - + diff --git a/samples/scriptable/bubble.html b/samples/scriptable/bubble.html index a8ce1f4743c..52d2fd8e13a 100644 --- a/samples/scriptable/bubble.html +++ b/samples/scriptable/bubble.html @@ -6,7 +6,7 @@ Scriptable > Bubble | Chart.js sample - + diff --git a/samples/tooltips/border.html b/samples/tooltips/border.html index 0742be0c5f5..1a062d9499e 100644 --- a/samples/tooltips/border.html +++ b/samples/tooltips/border.html @@ -3,7 +3,7 @@ Tooltip Border - + + +
    +
    + In order to support a strict content security policy (default-src 'self'), + this page manually loads Chart.min.css and turns off the automatic style + injection by setting Chart.platform.disableCSSInjection = true;. +
    +
    + +
    +
    + + diff --git a/samples/advanced/content-security-policy.js b/samples/advanced/content-security-policy.js new file mode 100644 index 00000000000..a185332f74b --- /dev/null +++ b/samples/advanced/content-security-policy.js @@ -0,0 +1,54 @@ +var utils = Samples.utils; + +// CSP: disable automatic style injection +Chart.platform.disableCSSInjection = true; + +utils.srand(110); + +function generateData() { + var DATA_COUNT = 16; + var MIN_XY = -150; + var MAX_XY = 100; + var data = []; + var i; + + for (i = 0; i < DATA_COUNT; ++i) { + data.push({ + x: utils.rand(MIN_XY, MAX_XY), + y: utils.rand(MIN_XY, MAX_XY), + v: utils.rand(0, 1000) + }); + } + + return data; +} + +window.addEventListener('load', function() { + new Chart('chart-0', { + type: 'bubble', + data: { + datasets: [{ + backgroundColor: utils.color(0), + data: generateData() + }, { + backgroundColor: utils.color(1), + data: generateData() + }] + }, + options: { + aspectRatio: 1, + legend: false, + tooltip: false, + elements: { + point: { + radius: function(context) { + var value = context.dataset.data[context.dataIndex]; + var size = context.chart.width; + var base = Math.abs(value.v) / 1000; + return (size / 24) * base; + } + } + } + } + }); +}); diff --git a/samples/samples.js b/samples/samples.js index c6cc71c7af9..b6ffe762682 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -187,6 +187,9 @@ items: [{ title: 'Progress bar', path: 'advanced/progress-bar.html' + }, { + title: 'Content Security Policy', + path: 'advanced/content-security-policy.html' }] }]; diff --git a/samples/style.css b/samples/style.css index 8224e2c3fdb..db92f0c6016 100644 --- a/samples/style.css +++ b/samples/style.css @@ -1,4 +1,3 @@ -@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'); @import url('https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900'); body, html { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index b0edddc7f99..35ae4d4e473 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -41,7 +41,7 @@ cd $TARGET_DIR git checkout $TARGET_BRANCH # Copy dist files -deploy_files '../dist/*.js' './dist' +deploy_files '../dist/*.css ../dist/*.js' './dist' # Copy generated documentation deploy_files '../dist/docs/*' './docs' diff --git a/scripts/release.sh b/scripts/release.sh index 03c7c6462b6..71f588034f2 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -21,7 +21,7 @@ git remote add auth-origin https://$GITHUB_AUTH_TOKEN@github.com/$TRAVIS_REPO_SL git config --global user.email "$GITHUB_AUTH_EMAIL" git config --global user.name "Chart.js" git checkout --detach --quiet -git add -f dist/*.js bower.json +git add -f dist/*.css dist/*.js bower.json git commit -m "Release $VERSION" git tag -a "v$VERSION" -m "Version $VERSION" git push -q auth-origin refs/tags/v$VERSION 2>/dev/null diff --git a/src/platforms/platform.dom.css b/src/platforms/platform.dom.css new file mode 100644 index 00000000000..e0b99a4aad4 --- /dev/null +++ b/src/platforms/platform.dom.css @@ -0,0 +1,46 @@ +/* + * DOM element rendering detection + * https://davidwalsh.name/detect-node-insertion + */ +@keyframes chartjs-render-animation { + from { opacity: 0.99; } + to { opacity: 1; } +} + +.chartjs-render-monitor { + animation: chartjs-render-animation 0.001s; +} + +/* + * DOM element resizing detection + * https://github.com/marcj/css-element-queries + */ +.chartjs-size-monitor, +.chartjs-size-monitor-expand, +.chartjs-size-monitor-shrink { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + pointer-events: none; + visibility: hidden; + z-index: -1; +} + +.chartjs-size-monitor-expand > div { + position: absolute; + width: 1000000px; + height: 1000000px; + left: 0; + top: 0; +} + +.chartjs-size-monitor-shrink > div { + position: absolute; + width: 200%; + height: 200%; + left: 0; + top: 0; +} diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 053db20d2e2..777833afe5e 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -5,9 +5,11 @@ 'use strict'; var helpers = require('../helpers/index'); +var stylesheet = require('./platform.dom.css'); var EXPANDO_KEY = '$chartjs'; var CSS_PREFIX = 'chartjs-'; +var CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor'; var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor'; var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation'; var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart']; @@ -166,48 +168,24 @@ function throttled(fn, thisArg) { }; } -function createDiv(cls, style) { +function createDiv(cls) { var el = document.createElement('div'); - el.style.cssText = style || ''; el.className = cls || ''; return el; } // Implementation based on https://github.com/marcj/css-element-queries function createResizer(handler) { - var cls = CSS_PREFIX + 'size-monitor'; var maxSize = 1000000; - var style = - 'position:absolute;' + - 'left:0;' + - 'top:0;' + - 'right:0;' + - 'bottom:0;' + - 'overflow:hidden;' + - 'pointer-events:none;' + - 'visibility:hidden;' + - 'z-index:-1;'; // NOTE(SB) Don't use innerHTML because it could be considered unsafe. // https://github.com/chartjs/Chart.js/issues/5902 - var resizer = createDiv(cls, style); - var expand = createDiv(cls + '-expand', style); - var shrink = createDiv(cls + '-shrink', style); - - expand.appendChild(createDiv('', - 'position:absolute;' + - 'height:' + maxSize + 'px;' + - 'width:' + maxSize + 'px;' + - 'left:0;' + - 'top:0;' - )); - shrink.appendChild(createDiv('', - 'position:absolute;' + - 'height:200%;' + - 'width:200%;' + - 'left:0;' + - 'top:0;' - )); + var resizer = createDiv(CSS_SIZE_MONITOR); + var expand = createDiv(CSS_SIZE_MONITOR + '-expand'); + var shrink = createDiv(CSS_SIZE_MONITOR + '-shrink'); + + expand.appendChild(createDiv()); + shrink.appendChild(createDiv()); resizer.appendChild(expand); resizer.appendChild(shrink); @@ -330,6 +308,15 @@ function injectCSS(platform, css) { } module.exports = { + /** + * When `true`, prevents the automatic injection of the stylesheet required to + * correctly detect when the chart is added to the DOM and then resized. This + * switch has been added to allow external stylesheet (`dist/Chart(.min)?.js`) + * to be manually imported to make this library compatible with any CSP. + * See https://github.com/chartjs/Chart.js/issues/5208 + */ + disableCSSInjection: false, + /** * This property holds whether this platform is enabled for the current environment. * Currently used by platform.js to select the proper implementation. @@ -337,19 +324,20 @@ module.exports = { */ _enabled: typeof window !== 'undefined' && typeof document !== 'undefined', - initialize: function() { - var keyframes = 'from{opacity:0.99}to{opacity:1}'; - - injectCSS(this, - // DOM rendering detection - // https://davidwalsh.name/detect-node-insertion - '@-webkit-keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' + - '@keyframes ' + CSS_RENDER_ANIMATION + '{' + keyframes + '}' + - '.' + CSS_RENDER_MONITOR + '{' + - '-webkit-animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' + - 'animation:' + CSS_RENDER_ANIMATION + ' 0.001s;' + - '}' - ); + /** + * @private + */ + _ensureLoaded: function() { + if (this._loaded) { + return; + } + + this._loaded = true; + + // https://github.com/chartjs/Chart.js/issues/5208 + if (!this.disableCSSInjection) { + injectCSS(this, stylesheet); + } }, acquireContext: function(item, config) { @@ -370,6 +358,10 @@ module.exports = { // https://github.com/chartjs/Chart.js/issues/2807 var context = item && item.getContext && item.getContext('2d'); + // Load platform resources on first chart creation, to make possible to change + // platform options after importing the library (e.g. `disableCSSInjection`). + this._ensureLoaded(); + // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the item is // inside an iframe or when running in a protected environment. We could guess the // types from their toString() value but let's keep things flexible and assume it's From 0ed652b39f7fc96cfcbfd916081d5d930850a231 Mon Sep 17 00:00:00 2001 From: James Bedford <32302360+JABedford@users.noreply.github.com> Date: Sat, 9 Feb 2019 07:19:57 +0000 Subject: [PATCH 537/685] Fix typo in radial linear scale docs (#6054) --- docs/axes/radial/linear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index af1674aa518..911b735e56d 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -1,6 +1,6 @@ # Linear Radial Axis -The linear scale is use to chart numerical data. As the name suggests, linear interpolation is used to determine where a value lies in relation the center of the axis. +The linear scale is used to chart numerical data. As the name suggests, linear interpolation is used to determine where a value lies in relation the center of the axis. The following additional configuration options are provided by the radial linear scale. From 8b07cc2f28b042ab0706d8ee9f5a8d5adb3ca2a5 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sun, 10 Feb 2019 06:07:54 +0800 Subject: [PATCH 538/685] Implement scriptable options for points in radar charts (#6041) --- docs/charts/radar.md | 116 +++++--- src/controllers/controller.line.js | 16 +- src/controllers/controller.radar.js | 198 ++++++++----- .../backgroundColor/indexable.js | 58 ++++ .../backgroundColor/indexable.png | Bin 0 -> 13114 bytes .../backgroundColor/scriptable.js | 56 ++++ .../backgroundColor/scriptable.png | Bin 0 -> 12508 bytes .../controller.radar/backgroundColor/value.js | 44 +++ .../backgroundColor/value.png | Bin 0 -> 12403 bytes .../controller.radar/borderColor/indexable.js | 58 ++++ .../borderColor/indexable.png | Bin 0 -> 15373 bytes .../borderColor/scriptable.js | 56 ++++ .../borderColor/scriptable.png | Bin 0 -> 14650 bytes .../controller.radar/borderColor/value.js | 44 +++ .../controller.radar/borderColor/value.png | Bin 0 -> 14526 bytes .../controller.radar/borderWidth/indexable.js | 50 ++++ .../borderWidth/indexable.png | Bin 0 -> 14295 bytes .../borderWidth/scriptable.js | 56 ++++ .../borderWidth/scriptable.png | Bin 0 -> 14107 bytes .../controller.radar/borderWidth/value.js | 46 +++ .../controller.radar/borderWidth/value.png | Bin 0 -> 13944 bytes .../controller.radar/pointStyle/indexable.js | 62 ++++ .../controller.radar/pointStyle/indexable.png | Bin 0 -> 10783 bytes .../controller.radar/pointStyle/scriptable.js | 60 ++++ .../pointStyle/scriptable.png | Bin 0 -> 10878 bytes .../controller.radar/pointStyle/value.js | 46 +++ .../controller.radar/pointStyle/value.png | Bin 0 -> 11268 bytes .../controller.radar/radius/indexable.js | 49 ++++ .../controller.radar/radius/indexable.png | Bin 0 -> 10267 bytes .../controller.radar/radius/scriptable.js | 55 ++++ .../controller.radar/radius/scriptable.png | Bin 0 -> 11034 bytes .../fixtures/controller.radar/radius/value.js | 45 +++ .../controller.radar/radius/value.png | Bin 0 -> 10490 bytes .../controller.radar/rotation/indexable.js | 51 ++++ .../controller.radar/rotation/indexable.png | Bin 0 -> 19221 bytes .../controller.radar/rotation/scriptable.js | 57 ++++ .../controller.radar/rotation/scriptable.png | Bin 0 -> 10351 bytes .../controller.radar/rotation/value.js | 47 ++++ .../controller.radar/rotation/value.png | Bin 0 -> 9857 bytes test/specs/controller.radar.tests.js | 264 ++++++++---------- 40 files changed, 1261 insertions(+), 273 deletions(-) create mode 100644 test/fixtures/controller.radar/backgroundColor/indexable.js create mode 100644 test/fixtures/controller.radar/backgroundColor/indexable.png create mode 100644 test/fixtures/controller.radar/backgroundColor/scriptable.js create mode 100644 test/fixtures/controller.radar/backgroundColor/scriptable.png create mode 100644 test/fixtures/controller.radar/backgroundColor/value.js create mode 100644 test/fixtures/controller.radar/backgroundColor/value.png create mode 100644 test/fixtures/controller.radar/borderColor/indexable.js create mode 100644 test/fixtures/controller.radar/borderColor/indexable.png create mode 100644 test/fixtures/controller.radar/borderColor/scriptable.js create mode 100644 test/fixtures/controller.radar/borderColor/scriptable.png create mode 100644 test/fixtures/controller.radar/borderColor/value.js create mode 100644 test/fixtures/controller.radar/borderColor/value.png create mode 100644 test/fixtures/controller.radar/borderWidth/indexable.js create mode 100644 test/fixtures/controller.radar/borderWidth/indexable.png create mode 100644 test/fixtures/controller.radar/borderWidth/scriptable.js create mode 100644 test/fixtures/controller.radar/borderWidth/scriptable.png create mode 100644 test/fixtures/controller.radar/borderWidth/value.js create mode 100644 test/fixtures/controller.radar/borderWidth/value.png create mode 100644 test/fixtures/controller.radar/pointStyle/indexable.js create mode 100644 test/fixtures/controller.radar/pointStyle/indexable.png create mode 100644 test/fixtures/controller.radar/pointStyle/scriptable.js create mode 100644 test/fixtures/controller.radar/pointStyle/scriptable.png create mode 100644 test/fixtures/controller.radar/pointStyle/value.js create mode 100644 test/fixtures/controller.radar/pointStyle/value.png create mode 100644 test/fixtures/controller.radar/radius/indexable.js create mode 100644 test/fixtures/controller.radar/radius/indexable.png create mode 100644 test/fixtures/controller.radar/radius/scriptable.js create mode 100644 test/fixtures/controller.radar/radius/scriptable.png create mode 100644 test/fixtures/controller.radar/radius/value.js create mode 100644 test/fixtures/controller.radar/radius/value.png create mode 100644 test/fixtures/controller.radar/rotation/indexable.js create mode 100644 test/fixtures/controller.radar/rotation/indexable.png create mode 100644 test/fixtures/controller.radar/rotation/scriptable.js create mode 100644 test/fixtures/controller.radar/rotation/scriptable.png create mode 100644 test/fixtures/controller.radar/rotation/value.js create mode 100644 test/fixtures/controller.radar/rotation/value.png diff --git a/docs/charts/radar.md b/docs/charts/radar.md index b1e31d78b07..1b1c7ef0714 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -64,46 +64,80 @@ var myRadarChart = new Chart(ctx, { The radar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. -All `point*` properties can be specified as an array. If these are set to an array value, the first value applies to the first point, the second value to the second point, and so on. - -| Name | Type | Description -| ---- | ---- | ----------- -| `label` | `string` | The label for the dataset which appears in the legend and tooltips. -| `backgroundColor` | `Color` | The fill color under the line. See [Colors](../general/colors.md#colors). -| `borderColor` | `Color` | The color of the line. See [Colors](../general/colors.md#colors). -| `borderWidth` | `number` | The width of the line in pixels. -| `borderDash` | `number[]` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). -| `borderDashOffset` | `number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). -| `borderCapStyle` | `string` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). -| `borderJoinStyle` | `string` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). -| `fill` | boolean|string | How to fill the area under the line. See [area charts](area.md). -| `lineTension` | `number` | Bezier curve tension of the line. Set to 0 to draw straightlines. -| `pointBackgroundColor` | Color|Color[] | The fill color for points. -| `pointBorderColor` | Color|Color[] | The border color for points. -| `pointBorderWidth` | number|number[] | The width of the point border in pixels. -| `pointRadius` | number|number[] | The radius of the point shape. If set to 0, the point is not rendered. -| `pointRotation` | number|number[] | The rotation of the point in degrees. -| `pointStyle` | string|string[]|Image|Image[] | Style of the point. [more...](#pointstyle) -| `pointHitRadius` | number|number[] | The pixel size of the non-displayed point that reacts to mouse events. -| `pointHoverBackgroundColor` | Color|Color[] | Point background color when hovered. -| `pointHoverBorderColor` | Color|Color[] | Point border color when hovered. -| `pointHoverBorderWidth` | number|number[] | Border width of point when hovered. -| `pointHoverRadius` | number|number[] | The radius of the point when hovered. - -### pointStyle -The style of point. Options are: -* `'circle'` -* `'cross'` -* `'crossRot'` -* `'dash'.` -* `'line'` -* `'rect'` -* `'rectRounded'` -* `'rectRot'` -* `'star'` -* `'triangle'` - -If the option is an image, that image is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | - | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderCapStyle`](#line-styling) | `string` | - | - | `'butt'` +| [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | - | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderDash`](#line-styling) | `number[]` | - | - | `[]` +| [`borderDashOffset`](#line-styling) | `number` | - | - | `0.0` +| [`borderJoinStyle`](#line-styling) | `string` | - | - | `'miter'` +| [`borderWidth`](#line-styling) | `number` | - | - | `3` +| [`fill`](#line-styling) | boolean|string | - | - | `true` +| [`label`](#general) | `string` | - | - | `''` +| [`lineTension`](#line-styling) | `number` | - | - | `0.4` +| [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` +| [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` +| [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` +| [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` +| [`pointStyle`](#point-styling) | string|Image | Yes | Yes | `'circle'` + +### General + +| Name | Description +| ---- | ---- +| `label` | The label for the dataset which appears in the legend and tooltips. + +### Point Styling + +The style of each point can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `pointBackgroundColor` | The fill color for points. +| `pointBorderColor` | The border color for points. +| `pointBorderWidth` | The width of the point border in pixels. +| `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. +| `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointRotation` | The rotation of the point in degrees. +| `pointStyle` | Style of the point. [more...](../configuration/elements#point-styles) + +All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +### Line Styling + +The style of the line can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | The line fill color. +| `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). +| `borderColor` | The line color. +| `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `borderWidth` | The line width (in pixels). +| `fill` | How to fill the area under the line. See [area charts](area.md). +| `lineTension` | Bezier curve tension of the line. Set to 0 to draw straightlines. + +All these values, if `undefined`, fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. + +### Interactions + +The interaction with each point can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `pointHoverBackgroundColor` | Point background color when hovered. +| `pointHoverBorderColor` | Point border color when hovered. +| `pointHoverBorderWidth` | Border width of point when hovered. +| `pointHoverRadius` | The radius of the point when hovered. ## Configuration Options @@ -128,7 +162,7 @@ It is common to want to apply a configuration setting to all created radar chart ## Data Structure -The `data` property of a dataset for a radar chart is specified as an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. +The `data` property of a dataset for a radar chart is specified as an array of numbers. Each point in the data array corresponds to the label at the same index. ```javascript data: [20, 10] diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 5341ac1e6dd..49665e2299a 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -91,6 +91,7 @@ module.exports = DatasetController.extend({ var value = dataset.data[index]; var yScale = me.getScaleForId(meta.yAxisID); var xScale = me.getScaleForId(meta.xAxisID); + var lineModel = meta.dataset._model; var x, y; var options = me._resolvePointOptions(point, index); @@ -117,10 +118,10 @@ module.exports = DatasetController.extend({ backgroundColor: options.backgroundColor, borderColor: options.borderColor, borderWidth: options.borderWidth, - tension: meta.dataset._model ? meta.dataset._model.tension : 0, - steppedLine: meta.dataset._model ? meta.dataset._model.steppedLine : false, + tension: valueOrDefault(custom.tension, lineModel ? lineModel.tension : 0), + steppedLine: lineModel ? lineModel.steppedLine : false, // Tooltip - hitRadius: options.hitRadius, + hitRadius: options.hitRadius }; }, @@ -155,7 +156,7 @@ module.exports = DatasetController.extend({ hoverRadius: 'pointHoverRadius', pointStyle: 'pointStyle', radius: 'pointRadius', - rotation: 'pointRotation', + rotation: 'pointRotation' }; var keys = Object.keys(ELEMENT_OPTIONS); @@ -210,7 +211,7 @@ module.exports = DatasetController.extend({ // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 // This option gives lines the ability to span gaps values.spanGaps = valueOrDefault(dataset.spanGaps, options.spanGaps); - values.tension = resolve([custom.tension, dataset.lineTension, elementOptions.tension]); + values.tension = valueOrDefault(dataset.lineTension, elementOptions.tension); values.steppedLine = resolve([custom.steppedLine, dataset.steppedLine, elementOptions.stepped]); return values; @@ -256,7 +257,7 @@ module.exports = DatasetController.extend({ var lineModel = meta.dataset._model; var area = chart.chartArea; var points = meta.data || []; - var i, ilen, point, model, controlPoints; + var i, ilen, model, controlPoints; // Only consider points that are drawn in case the spanGaps option is used if (lineModel.spanGaps) { @@ -273,8 +274,7 @@ module.exports = DatasetController.extend({ helpers.splineCurveMonotone(points); } else { for (i = 0, ilen = points.length; i < ilen; ++i) { - point = points[i]; - model = point._model; + model = points[i]._model; controlPoints = helpers.splineCurve( helpers.previousItem(points, i)._model, model, diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 500a40d3446..770ddc51afe 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -5,6 +5,7 @@ var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); +var valueOrDefault = helpers.valueOrDefault; var resolve = helpers.options.resolve; defaults._set('radar', { @@ -31,10 +32,8 @@ module.exports = DatasetController.extend({ var meta = me.getMeta(); var line = meta.dataset; var points = meta.data || []; - var custom = line.custom || {}; - var dataset = me.getDataset(); - var lineElementOptions = me.chart.options.elements.line; var scale = me.chart.scale; + var dataset = me.getDataset(); var i, ilen; // Compatibility: If the properties are defined with only the old name, use those values @@ -42,32 +41,19 @@ module.exports = DatasetController.extend({ dataset.lineTension = dataset.tension; } - helpers.extend(meta.dataset, { - // Utility - _datasetIndex: me.index, - _scale: scale, - // Data - _children: points, - _loop: true, - // Model - _model: { - // Appearance - tension: resolve([custom.tension, dataset.lineTension, lineElementOptions.tension]), - backgroundColor: resolve([custom.backgroundColor, dataset.backgroundColor, lineElementOptions.backgroundColor]), - borderWidth: resolve([custom.borderWidth, dataset.borderWidth, lineElementOptions.borderWidth]), - borderColor: resolve([custom.borderColor, dataset.borderColor, lineElementOptions.borderColor]), - fill: resolve([custom.fill, dataset.fill, lineElementOptions.fill]), - borderCapStyle: resolve([custom.borderCapStyle, dataset.borderCapStyle, lineElementOptions.borderCapStyle]), - borderDash: resolve([custom.borderDash, dataset.borderDash, lineElementOptions.borderDash]), - borderDashOffset: resolve([custom.borderDashOffset, dataset.borderDashOffset, lineElementOptions.borderDashOffset]), - borderJoinStyle: resolve([custom.borderJoinStyle, dataset.borderJoinStyle, lineElementOptions.borderJoinStyle]), - } - }); - - meta.dataset.pivot(); + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + line._loop = true; + // Model + line._model = me._resolveLineOptions(line); + + line.pivot(); // Update Points - for (i = 0, ilen = points.length; i < ilen; i++) { + for (i = 0, ilen = points.length; i < ilen; ++i) { me.updateElement(points[i], i, reset); } @@ -75,7 +61,7 @@ module.exports = DatasetController.extend({ me.updateBezierControlPoints(); // Now pivot the point for animation - for (i = 0, ilen = points.length; i < ilen; i++) { + for (i = 0, ilen = points.length; i < ilen; ++i) { points[i].pivot(); } }, @@ -85,43 +71,120 @@ module.exports = DatasetController.extend({ var custom = point.custom || {}; var dataset = me.getDataset(); var scale = me.chart.scale; - var pointElementOptions = me.chart.options.elements.point; var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); + var options = me._resolvePointOptions(point, index); + var lineModel = me.getMeta().dataset._model; + var x = reset ? scale.xCenter : pointPosition.x; + var y = reset ? scale.yCenter : pointPosition.y; + + // Utility + point._scale = scale; + point._options = options; + point._datasetIndex = me.index; + point._index = index; + + // Desired view properties + point._model = { + x: x, // value not used in dataset scale, but we want a consistent API between scales + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: options.radius, + pointStyle: options.pointStyle, + rotation: options.rotation, + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + tension: valueOrDefault(custom.tension, lineModel ? lineModel.tension : 0), + + // Tooltip + hitRadius: options.hitRadius + }; + }, - // Compatibility: If the properties are defined with only the old name, use those values - if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { - dataset.pointRadius = dataset.radius; + /** + * @private + */ + _resolvePointOptions: function(element, index) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options.elements.point; + var values = {}; + var i, ilen, key; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + var ELEMENT_OPTIONS = { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }; + var keys = Object.keys(ELEMENT_OPTIONS); + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[ELEMENT_OPTIONS[key]], + dataset[key], + options[key] + ], context, index); } - if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { - dataset.pointHitRadius = dataset.hitRadius; + + return values; + }, + + /** + * @private + */ + _resolveLineOptions: function(element) { + var me = this; + var chart = me.chart; + var dataset = chart.data.datasets[me.index]; + var custom = element.custom || {}; + var options = chart.options.elements.line; + var values = {}; + var i, ilen, key; + + var keys = [ + 'backgroundColor', + 'borderWidth', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'fill' + ]; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + dataset[key], + options[key] + ]); } - helpers.extend(point, { - // Utility - _datasetIndex: me.index, - _index: index, - _scale: scale, - - // Desired view properties - _model: { - x: reset ? scale.xCenter : pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales - y: reset ? scale.yCenter : pointPosition.y, - - // Appearance - tension: resolve([custom.tension, dataset.lineTension, me.chart.options.elements.line.tension]), - radius: resolve([custom.radius, dataset.pointRadius, pointElementOptions.radius], undefined, index), - backgroundColor: resolve([custom.backgroundColor, dataset.pointBackgroundColor, pointElementOptions.backgroundColor], undefined, index), - borderColor: resolve([custom.borderColor, dataset.pointBorderColor, pointElementOptions.borderColor], undefined, index), - borderWidth: resolve([custom.borderWidth, dataset.pointBorderWidth, pointElementOptions.borderWidth], undefined, index), - pointStyle: resolve([custom.pointStyle, dataset.pointStyle, pointElementOptions.pointStyle], undefined, index), - rotation: resolve([custom.rotation, dataset.pointRotation, pointElementOptions.rotation], undefined, index), - - // Tooltip - hitRadius: resolve([custom.hitRadius, dataset.pointHitRadius, pointElementOptions.hitRadius], undefined, index) - } - }); - - point._model.skip = custom.skip || isNaN(point._model.x) || isNaN(point._model.y); + values.tension = valueOrDefault(dataset.lineTension, options.tension); + + return values; }, updateBezierControlPoints: function() { @@ -135,7 +198,7 @@ module.exports = DatasetController.extend({ return Math.max(Math.min(pt, max), min); } - for (i = 0, ilen = points.length; i < ilen; i++) { + for (i = 0, ilen = points.length; i < ilen; ++i) { model = points[i]._model; controlPoints = helpers.splineCurve( helpers.previousItem(points, i, true)._model, @@ -153,11 +216,8 @@ module.exports = DatasetController.extend({ }, setHoverStyle: function(point) { - // Point - var dataset = this.chart.data.datasets[point._datasetIndex]; - var custom = point.custom || {}; - var index = point._index; var model = point._model; + var options = point._options; var getHoverColor = helpers.getHoverColor; point.$previousStyle = { @@ -167,9 +227,9 @@ module.exports = DatasetController.extend({ radius: model.radius }; - model.radius = resolve([custom.hoverRadius, dataset.pointHoverRadius, this.chart.options.elements.point.hoverRadius], undefined, index); - model.backgroundColor = resolve([custom.hoverBackgroundColor, dataset.pointHoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index); - model.borderColor = resolve([custom.hoverBorderColor, dataset.pointHoverBorderColor, getHoverColor(model.borderColor)], undefined, index); - model.borderWidth = resolve([custom.hoverBorderWidth, dataset.pointHoverBorderWidth, model.borderWidth], undefined, index); + model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault(options.hoverRadius, options.radius); } }); diff --git a/test/fixtures/controller.radar/backgroundColor/indexable.js b/test/fixtures/controller.radar/backgroundColor/indexable.js new file mode 100644 index 00000000000..cd59ce44a13 --- /dev/null +++ b/test/fixtures/controller.radar/backgroundColor/indexable.js @@ -0,0 +1,58 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + radius: 10 + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/backgroundColor/indexable.png b/test/fixtures/controller.radar/backgroundColor/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..6141ca8015bdd04a8aa6e4a8745481323b7bdcf9 GIT binary patch literal 13114 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 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/backgroundColor/scriptable.png b/test/fixtures/controller.radar/backgroundColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e6d6e5aee23b8b42c90284c4cf8ded91b81706 GIT binary patch literal 12508 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 I%b0%tKUF4iz5oCK literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/backgroundColor/value.js b/test/fixtures/controller.radar/backgroundColor/value.js new file mode 100644 index 00000000000..0a592c61595 --- /dev/null +++ b/test/fixtures/controller.radar/backgroundColor/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/backgroundColor/value.png b/test/fixtures/controller.radar/backgroundColor/value.png new file mode 100644 index 0000000000000000000000000000000000000000..dd7f6de6dc2f5a1b957377b667d9b5ffacc65eb7 GIT binary patch literal 12403 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 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderColor/scriptable.png b/test/fixtures/controller.radar/borderColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9bb5c303186545e1a31e22b542aa233b4427de GIT binary patch literal 14650 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 SgnknV0LiKSSNY!j>;D5Fdg3Ag literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/borderColor/value.js b/test/fixtures/controller.radar/borderColor/value.js new file mode 100644 index 00000000000..0ff69e08b87 --- /dev/null +++ b/test/fixtures/controller.radar/borderColor/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderColor/value.png b/test/fixtures/controller.radar/borderColor/value.png new file mode 100644 index 0000000000000000000000000000000000000000..715c582e38a253a580240eb89fdd54aacc04c93e GIT binary patch literal 14526 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/borderWidth/indexable.js b/test/fixtures/controller.radar/borderWidth/indexable.js new file mode 100644 index 00000000000..262d9d56d87 --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/indexable.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointBorderWidth: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: [ + 6, 5, 4, 3, 2, 1 + ], + radius: 10 + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/indexable.png b/test/fixtures/controller.radar/borderWidth/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..73d3493e24841e58ed5cbe7e7c8e5160748379c7 GIT binary patch literal 14295 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 HF5>?IPB8qw literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/borderWidth/scriptable.js b/test/fixtures/controller.radar/borderWidth/scriptable.js new file mode 100644 index 00000000000..8d321b5fcad --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/scriptable.js @@ -0,0 +1,56 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/scriptable.png b/test/fixtures/controller.radar/borderWidth/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..2fccea7488dddb700ec6f7d5a04b91be7478ec7f GIT binary patch literal 14107 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/borderWidth/value.js b/test/fixtures/controller.radar/borderWidth/value.js new file mode 100644 index 00000000000..8c59eccfd2a --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/value.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + borderWidth: 3, + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/value.png b/test/fixtures/controller.radar/borderWidth/value.png new file mode 100644 index 0000000000000000000000000000000000000000..ebaf659e3eba265f0621bddfaa406b97222dfb57 GIT binary patch literal 13944 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 Q{p%d|R#~Q6$~5@@0OyYyAOHXW literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/pointStyle/indexable.js b/test/fixtures/controller.radar/pointStyle/indexable.js new file mode 100644 index 00000000000..835cd2350cd --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/indexable.js @@ -0,0 +1,62 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: [ + 'circle', + 'cross', + 'crossRot', + 'dash', + 'line', + 'rect', + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + borderColor: '#00ff00', + pointStyle: [ + 'line', + 'rect', + 'rectRounded', + 'rectRot', + 'star', + 'triangle' + ], + radius: 10 + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/indexable.png b/test/fixtures/controller.radar/pointStyle/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..3182e522a9e83ec867b43cad9ae9256cc92b4779 GIT binary patch literal 10783 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� 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 G7yk!CR{vc9 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/pointStyle/scriptable.js b/test/fixtures/controller.radar/pointStyle/scriptable.js new file mode 100644 index 00000000000..9301672fbb2 --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/scriptable.js @@ -0,0 +1,60 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'rect' + : value > 0 ? 'star' + : value > -8 ? 'cross' + : 'triangle'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#0000ff', + borderColor: '#0000ff', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'triangle' + : value > 0 ? 'cross' + : value > -8 ? 'star' + : 'rect'; + }, + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/scriptable.png b/test/fixtures/controller.radar/pointStyle/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..35179a3032db957c6218ae6e1e58324696d7160c GIT binary patch literal 10878 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 Jy1P!#{~rp39$^3g literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/pointStyle/value.js b/test/fixtures/controller.radar/pointStyle/value.js new file mode 100644 index 00000000000..31c2ff941a4 --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/value.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000', + pointStyle: 'star', + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + pointStyle: 'rect', + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/value.png b/test/fixtures/controller.radar/pointStyle/value.png new file mode 100644 index 0000000000000000000000000000000000000000..654d4ed825742b622588b89dd774567afe1df677 GIT binary patch literal 11268 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/radius/indexable.js b/test/fixtures/controller.radar/radius/indexable.js new file mode 100644 index 00000000000..7a106b58735 --- /dev/null +++ b/test/fixtures/controller.radar/radius/indexable.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#00ff00', + pointRadius: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: [ + 6, 5, 4, 3, 2, 1 + ], + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/radius/indexable.png b/test/fixtures/controller.radar/radius/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..7d735618a59269613b15b8ce3052d312f06b7b20 GIT binary patch literal 10267 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 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/radius/scriptable.png b/test/fixtures/controller.radar/radius/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..8b58d8881638ff8e63b7bec729a7001edea5097c GIT binary patch literal 11034 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/rotation/indexable.js b/test/fixtures/controller.radar/rotation/indexable.js new file mode 100644 index 00000000000..f516ce75f57 --- /dev/null +++ b/test/fixtures/controller.radar/rotation/indexable.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointRotation: [ + 0, 30, 60, 90, 120, 150 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + pointStyle: 'line', + rotation: [ + 150, 120, 90, 60, 30, 0 + ], + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/indexable.png b/test/fixtures/controller.radar/rotation/indexable.png new file mode 100644 index 0000000000000000000000000000000000000000..403aa7f869fd35274d0cc4fa1616e4e978ac9d15 GIT binary patch literal 19221 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

    ZMJ-9SQIZs6*?np~|V9 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 HrvCpAsE$Tq literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/rotation/scriptable.js b/test/fixtures/controller.radar/rotation/scriptable.js new file mode 100644 index 00000000000..d85b7c97e82 --- /dev/null +++ b/test/fixtures/controller.radar/rotation/scriptable.js @@ -0,0 +1,57 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 120 + : value > -4 ? 60 + : 0; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + rotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 0 + : value > -4 ? 60 + : 120; + }, + pointStyle: 'line', + radius: 10, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/scriptable.png b/test/fixtures/controller.radar/rotation/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..69958a77f2d7c431c09cb15142a51b9f5ae7858e GIT binary patch literal 10351 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 E2gYz>>;M1& literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.radar/rotation/value.js b/test/fixtures/controller.radar/rotation/value.js new file mode 100644 index 00000000000..4b9b17d081d --- /dev/null +++ b/test/fixtures/controller.radar/rotation/value.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: 90 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + pointStyle: 'line', + radius: 10, + rotation: 0, + } + }, + scale: { + display: false, + ticks: { + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/value.png b/test/fixtures/controller.radar/rotation/value.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5cd5977099a3f002b58e19c2dcbfc76b63cb2f GIT binary patch literal 9857 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 literal 0 HcmV?d00001 diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index a911f93020e..6b2c754547c 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -127,7 +127,6 @@ describe('Chart.controllers.radar', function() { borderJoinStyle: 'bevel', borderWidth: 1.2, fill: true, - tension: 0.1, })); [ @@ -210,7 +209,6 @@ describe('Chart.controllers.radar', function() { borderJoinStyle: 'miter', borderWidth: 0.55, fill: false, - tension: 0, })); // Since tension is now 0, we don't care about the control points @@ -237,7 +235,6 @@ describe('Chart.controllers.radar', function() { // Use custom styles for lines & first point meta.dataset.custom = { - tension: 0.25, backgroundColor: 'rgb(55, 55, 54)', borderColor: 'rgb(8, 7, 6)', borderWidth: 0.3, @@ -270,7 +267,6 @@ describe('Chart.controllers.radar', function() { borderJoinStyle: 'round', borderWidth: 0.3, fill: true, - tension: 0.25, })); expect(meta.data[0]._model.x).toBeCloseToPixel(256); @@ -290,165 +286,123 @@ describe('Chart.controllers.radar', function() { })); }); - it('should set point hover styles', function() { - var chart = window.acquireChart({ - type: 'radar', - data: { - datasets: [{ - data: [10, 15, 0, 4] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - showLines: true, - elements: { - line: { - backgroundColor: 'rgb(255, 0, 0)', - borderCapStyle: 'round', - borderColor: 'rgb(0, 255, 0)', - borderDash: [], - borderDashOffset: 0.1, - borderJoinStyle: 'bevel', - borderWidth: 1.2, - fill: true, - skipNull: true, - tension: 0.1, - }, - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3, + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'radar', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [10, 15, 0, 4] + }] + }, + options: { + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } } } - } + }); }); - var meta = chart.getDatasetMeta(0); - - meta.controller.update(); // reset first - - var point = meta.data[0]; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(229, 230, 0)'); - expect(point._model.borderColor).toBe('rgb(230, 230, 230)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(4); - - // Can set hover style per dataset - chart.data.datasets[0].pointHoverRadius = 3.3; - chart.data.datasets[0].pointHoverBackgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].pointHoverBorderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].pointHoverBorderWidth = 2.1; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(3.3); - - // Custom style - point.custom = { - hoverRadius: 4.4, - hoverBorderWidth: 5.5, - hoverBackgroundColor: 'rgb(0, 0, 0)', - hoverBorderColor: 'rgb(10, 10, 10)' - }; - - meta.controller.setHoverStyle(point); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); - }); - - - it('should remove hover styles', function() { - var chart = window.acquireChart({ - type: 'radar', - data: { - datasets: [{ - data: [10, 15, 0, 4] - }], - labels: ['label1', 'label2', 'label3', 'label4'] - }, - options: { - showLines: true, - elements: { - line: { - backgroundColor: 'rgb(255, 0, 0)', - borderCapStyle: 'round', - borderColor: 'rgb(0, 255, 0)', - borderDash: [], - borderDashOffset: 0.1, - borderJoinStyle: 'bevel', - borderWidth: 1.2, - fill: true, - skipNull: true, - tension: 0.1, - }, - point: { - backgroundColor: 'rgb(255, 255, 0)', - borderWidth: 1, - borderColor: 'rgb(255, 255, 255)', - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - radius: 3, - } - } - } + it ('should handle default hover styles', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); + expect(point._model.borderWidth).toBe(1); + expect(point._model.radius).toBe(4); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(3); }); - var meta = chart.getDatasetMeta(0); - - meta.controller.update(); // reset first - - var point = meta.data[0]; - - chart.options.elements.point.backgroundColor = 'rgb(45, 46, 47)'; - chart.options.elements.point.borderColor = 'rgb(50, 51, 52)'; - chart.options.elements.point.borderWidth = 10.1; - chart.options.elements.point.radius = 1.01; + it ('should handle hover styles defined via dataset properties', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(3); + }); - meta.controller.removeHoverStyle(point); - chart.update(); - expect(point._model.backgroundColor).toBe('rgb(45, 46, 47)'); - expect(point._model.borderColor).toBe('rgb(50, 51, 52)'); - expect(point._model.borderWidth).toBe(10.1); - expect(point._model.radius).toBe(1.01); - - // Can set hover style per dataset - chart.data.datasets[0].pointRadius = 3.3; - chart.data.datasets[0].pointBackgroundColor = 'rgb(77, 79, 81)'; - chart.data.datasets[0].pointBorderColor = 'rgb(123, 125, 127)'; - chart.data.datasets[0].pointBorderWidth = 2.1; - - meta.controller.removeHoverStyle(point); - chart.update(); - expect(point._model.backgroundColor).toBe('rgb(77, 79, 81)'); - expect(point._model.borderColor).toBe('rgb(123, 125, 127)'); - expect(point._model.borderWidth).toBe(2.1); - expect(point._model.radius).toBe(3.3); - - // Custom style - point.custom = { - radius: 4.4, - borderWidth: 5.5, - backgroundColor: 'rgb(0, 0, 0)', - borderColor: 'rgb(10, 10, 10)' - }; + it ('should handle hover styles defined via element options', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(3); + }); - meta.controller.removeHoverStyle(point); - chart.update(); - expect(point._model.backgroundColor).toBe('rgb(0, 0, 0)'); - expect(point._model.borderColor).toBe('rgb(10, 10, 10)'); - expect(point._model.borderWidth).toBe(5.5); - expect(point._model.radius).toBe(4.4); + it ('should handle hover styles defined via element custom', function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + point.custom = { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }; + + chart.update(); + + jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); + expect(point._model.borderWidth).toBe(8.4); + expect(point._model.radius).toBe(4.2); + + jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); + expect(point._model.borderWidth).toBe(2); + expect(point._model.radius).toBe(3); + }); }); it('should allow pointBorderWidth to be set to 0', function() { From 409fdde3b0df8407e901c6ce5c456bd01e26c632 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sun, 10 Feb 2019 16:12:55 +0200 Subject: [PATCH 539/685] Fix randomly failing tooltip test (#6061) Use utility to trigger the event in the center of element rather than x/y -based coordinates (its the edge for rectangles) --- test/specs/core.tooltip.tests.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index df8ce9ca6dd..a55e80eb952 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -732,20 +732,8 @@ describe('Core.Tooltip', function() { // Trigger an event over top of the element var pointIndex = 1; var datasetIndex = 0; - var meta = chart.getDatasetMeta(datasetIndex); - var point = meta.data[pointIndex]; - var node = chart.canvas; - var rect = node.getBoundingClientRect(); - var evt = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: Math.round(rect.left + point._model.x), - clientY: Math.round(rect.top + point._model.y) - }); - - // Manually trigger rather than having an async test - node.dispatchEvent(evt); + var point = chart.getDatasetMeta(datasetIndex).data[pointIndex]; + jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; From af464f8a85e5b1c411d53f680888916f62bda1cc Mon Sep 17 00:00:00 2001 From: Alfie Hopkin Date: Sun, 10 Feb 2019 18:04:29 +0000 Subject: [PATCH 540/685] Enhance legend label color point when usePointStyle is true (#6006) --- docs/configuration/legend.md | 2 +- src/plugins/plugin.legend.js | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 9655a67167a..30e3283b40d 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -36,7 +36,7 @@ The legend label configuration is nested below the legend configuration using th | `padding` | `number` | `10` | Padding between rows of colored boxes. | `generateLabels` | `function` | | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#legend-item-interface) for details. | `filter` | `function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#legend-item-interface) and the chart data. -| `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). +| `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on the mimimum value between boxWidth and fontSize). ## Legend Item Interface diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 7d13ea13ed4..03bf8040baf 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -90,8 +90,8 @@ defaults._set('global', { * @return {Number} width of the color box area */ function getBoxWidth(labelOpts, fontSize) { - return labelOpts.usePointStyle ? - fontSize * Math.SQRT2 : + return labelOpts.usePointStyle && labelOpts.boxWidth > fontSize ? + fontSize : labelOpts.boxWidth; } @@ -369,10 +369,9 @@ var Legend = Element.extend({ if (opts.labels && opts.labels.usePointStyle) { // Recalculate x and y for drawPoint() because its expecting // x and y to be center of figure (instead of top left) - var radius = fontSize * Math.SQRT2 / 2; - var offSet = radius / Math.SQRT2; - var centerX = x + offSet; - var centerY = y + offSet; + var radius = boxWidth * Math.SQRT2 / 2; + var centerX = x + boxWidth / 2; + var centerY = y + fontSize / 2; // Draw pointStyle as legend symbol helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); From 2f874fde622ebd2c3b45c668861659f17e1254e9 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 10 Feb 2019 23:51:39 -0800 Subject: [PATCH 541/685] Use lowercase for primitives in jsdocs (#6033) --- .gitignore | 2 + src/controllers/controller.bar.js | 10 +- src/controllers/controller.horizontalBar.js | 1 - src/core/core.animations.js | 4 +- src/core/core.controller.js | 8 +- src/core/core.helpers.js | 22 ++-- src/core/core.interaction.js | 55 ++++---- src/core/core.layouts.js | 34 ++--- src/core/core.plugins.js | 132 ++++++++++---------- src/core/core.scale.js | 6 +- src/core/core.ticks.js | 10 +- src/core/core.tooltip.js | 22 ++-- src/helpers/helpers.canvas.js | 16 +-- src/helpers/helpers.core.js | 50 ++++---- src/helpers/helpers.options.js | 24 ++-- src/platforms/platform.dom.js | 2 +- src/platforms/platform.js | 20 +-- src/plugins/plugin.legend.js | 8 +- src/scales/scale.linearbase.js | 2 +- src/scales/scale.logarithmic.js | 6 +- src/scales/scale.time.js | 4 +- 21 files changed, 223 insertions(+), 215 deletions(-) diff --git a/.gitignore b/.gitignore index 0a65be9b282..f275dac0706 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ /package-lock.json .DS_Store .idea +.project +.settings .vscode bower.json *.log diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index aef53b68afe..992333a25c2 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -190,8 +190,8 @@ module.exports = DatasetController.extend({ /** * Returns the stacks based on groups and bar visibility. - * @param {Number} [last] - The dataset index - * @returns {Array} The stack list + * @param {number} [last] - The dataset index + * @returns {string[]} The list of stack IDs * @private */ _getStacks: function(last) { @@ -226,9 +226,9 @@ module.exports = DatasetController.extend({ /** * Returns the stack index for the given dataset based on groups and bar visibility. - * @param {Number} [datasetIndex] - The dataset index - * @param {String} [name] - The stack name to find - * @returns {Number} The stack index + * @param {number} [datasetIndex] - The dataset index + * @param {string} [name] - The stack name to find + * @returns {number} The stack index * @private */ getStackIndex: function(datasetIndex, name) { diff --git a/src/controllers/controller.horizontalBar.js b/src/controllers/controller.horizontalBar.js index 62bd07d3b83..ebfc6d84ae1 100644 --- a/src/controllers/controller.horizontalBar.js +++ b/src/controllers/controller.horizontalBar.js @@ -1,4 +1,3 @@ - 'use strict'; var BarController = require('./controller.bar'); diff --git a/src/core/core.animations.js b/src/core/core.animations.js index c307f499562..54645ddd996 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -20,8 +20,8 @@ module.exports = { /** * @param {Chart} chart - The chart to animate. * @param {Chart.Animation} animation - The animation that we will animate. - * @param {Number} duration - The animation duration in ms. - * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + * @param {number} duration - The animation duration in ms. + * @param {boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions */ addAnimation: function(chart, animation, duration, lazy) { var animations = this.animations; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index a294b160031..92cfcd01794 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -736,8 +736,10 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { plugins.notify(me, 'afterTooltipDraw', [args]); }, - // 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 + /** + * 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) { return Interaction.modes.single(this, e); }, @@ -970,7 +972,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * Handle an event * @private * @param {IEvent} event the event to handle - * @return {Boolean} true if the chart needs to re-render + * @return {boolean} true if the chart needs to re-render */ handleEvent: function(e) { var me = this; diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index a30b3723b07..5b4aec34c7e 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -123,8 +123,8 @@ module.exports = function() { /** * Returns the number of decimal places * i.e. the number of digits after the decimal point, of the value of this Number. - * @param {Number} x - A number. - * @returns {Number} The number of decimal places. + * @param {number} x - A number. + * @returns {number} The number of decimal places. */ helpers.decimalPlaces = function(x) { if (!helpers.isFinite(x)) { @@ -173,9 +173,9 @@ module.exports = function() { /** * Returns the aligned pixel value to avoid anti-aliasing blur * @param {Chart} chart - The chart instance. - * @param {Number} pixel - A pixel value. - * @param {Number} width - The width of the element. - * @returns {Number} The aligned pixel value. + * @param {number} pixel - A pixel value. + * @param {number} width - The width of the element. + * @returns {number} The aligned pixel value. * @private */ helpers._alignPixel = function(chart, pixel, width) { @@ -430,11 +430,13 @@ module.exports = function() { return value !== undefined && value !== null && value !== 'none'; } - // Private helper to get a constraint dimension - // @param domNode : the node to check the constraint on - // @param maxStyle : the style that defines the maximum for the direction we are using (maxWidth / maxHeight) - // @param percentageProperty : property of parent to use when calculating width as a percentage - // @see https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser + /** + * Returns the max width or height of the given DOM node in a cross-browser compatible fashion + * @param {HTMLElement} domNode - the node to check the constraint on + * @param {string} maxStyle - the style that defines the maximum for the direction we are using ('max-width' / 'max-height') + * @param {string} percentageProperty - property of parent to use when calculating width as a percentage + * @see {@link https://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser} + */ function getConstraintDimension(domNode, maxStyle, percentageProperty) { var view = document.defaultView; var parentNode = helpers._getParentNode(domNode); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 9b99e53bb1b..e163f1182a3 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -6,7 +6,7 @@ var helpers = require('../helpers/index'); * Helper function to get relative position for an event * @param {Event|IEvent} event - The event to get the position for * @param {Chart} chart - The chart - * @returns {Point} the event position + * @returns {object} the event position */ function getRelativePosition(e, chart) { if (e.native) { @@ -21,8 +21,8 @@ function getRelativePosition(e, chart) { /** * 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 + * @param {Chart} chart - the chart + * @param {function} handler - the callback to execute for each visible item */ function parseVisibleItems(chart, handler) { var datasets = chart.data.datasets; @@ -45,8 +45,8 @@ function parseVisibleItems(chart, handler) { /** * 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 + * @param {ChartElement[]} items - elements to filter + * @param {object} position - the point to be nearest to * @return {ChartElement[]} the nearest items */ function getIntersectItems(chart, position) { @@ -63,10 +63,10 @@ function getIntersectItems(chart, position) { /** * 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 - * @param distanceMetric {Function} function to provide the distance between points + * @param {Chart} chart - the chart to look at elements from + * @param {object} position - the point to be nearest to + * @param {boolean} intersect - if true, only consider items that intersect the position + * @param {function} distanceMetric - function to provide the distance between points * @return {ChartElement[]} the nearest items */ function getNearestItems(chart, position, intersect, distanceMetric) { @@ -80,7 +80,6 @@ function getNearestItems(chart, position, intersect, distanceMetric) { var center = element.getCenterPoint(); var distance = distanceMetric(position, center); - if (distance < minDistance) { nearestItems = [element]; minDistance = distance; @@ -96,7 +95,7 @@ function getNearestItems(chart, position, intersect, distanceMetric) { /** * Get a distance metric function for two points based on the * axis mode setting - * @param {String} axis the axis mode. x|y|xy + * @param {string} axis - the axis mode. x|y|xy */ function getDistanceMetricForAxis(axis) { var useX = axis.indexOf('x') !== -1; @@ -179,9 +178,9 @@ module.exports = { * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - 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, @@ -190,9 +189,9 @@ module.exports = { * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - 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) { @@ -222,8 +221,8 @@ module.exports = { * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - 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) { @@ -234,9 +233,9 @@ module.exports = { /** * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - 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) { @@ -249,9 +248,9 @@ module.exports = { /** * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - 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, options) { @@ -280,9 +279,9 @@ module.exports = { /** * 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 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - 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, options) { diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js index 96887770b51..fbaf96a952d 100644 --- a/src/core/core.layouts.js +++ b/src/core/core.layouts.js @@ -67,19 +67,19 @@ defaults._set('global', { /** * @interface ILayoutItem - * @prop {String} position - The position of the item in the chart layout. Possible values are + * @prop {string} position - The position of the item in the chart layout. Possible values are * 'left', 'top', 'right', 'bottom', and 'chartArea' - * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area - * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down - * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) - * @prop {Function} update - Takes two parameters: width and height. Returns size of item - * @prop {Function} getPadding - Returns an object with padding on the edges - * @prop {Number} width - Width of item. Must be valid after update() - * @prop {Number} height - Height of item. Must be valid after update() - * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update - * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update - * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update - * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {function} update - Takes two parameters: width and height. Returns size of item + * @prop {function} getPadding - Returns an object with padding on the edges + * @prop {number} width - Width of item. Must be valid after update() + * @prop {number} height - Height of item. Must be valid after update() + * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update */ // The layout service is very self explanatory. It's responsible for the layout within a chart. @@ -110,7 +110,7 @@ module.exports = { /** * Remove a layoutItem from a chart * @param {Chart} chart - the chart to remove the box from - * @param {Object} layoutItem - the item to remove from the layout + * @param {ILayoutItem} layoutItem - the item to remove from the layout */ removeBox: function(chart, layoutItem) { var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; @@ -122,8 +122,8 @@ module.exports = { /** * Sets (or updates) options on the given `item`. * @param {Chart} chart - the chart in which the item lives (or will be added to) - * @param {Object} item - the item to configure with the given options - * @param {Object} options - the new item options. + * @param {ILayoutItem} item - the item to configure with the given options + * @param {object} options - the new item options. */ configure: function(chart, item, options) { var props = ['fullWidth', 'position', 'weight']; @@ -143,8 +143,8 @@ module.exports = { * Fits boxes of the given chart into the given size by having each box measure itself * then running a fitting algorithm * @param {Chart} chart - the chart - * @param {Number} width - the width to fit into - * @param {Number} height - the height to fit into + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into */ update: function(chart, width, height) { if (!chart) { diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index f2fbcadec31..cf44af0ace3 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -29,7 +29,7 @@ module.exports = { /** * Registers the given plugin(s) if not already registered. - * @param {Array|Object} plugins plugin instance(s). + * @param {IPlugin[]|IPlugin} plugins plugin instance(s). */ register: function(plugins) { var p = this._plugins; @@ -44,7 +44,7 @@ module.exports = { /** * Unregisters the given plugin(s) only if registered. - * @param {Array|Object} plugins plugin instance(s). + * @param {IPlugin[]|IPlugin} plugins plugin instance(s). */ unregister: function(plugins) { var p = this._plugins; @@ -69,7 +69,7 @@ module.exports = { /** * Returns the number of registered plugins? - * @returns {Number} + * @returns {number} * @since 2.1.5 */ count: function() { @@ -78,7 +78,7 @@ module.exports = { /** * Returns all registered plugin instances. - * @returns {Array} array of plugin objects. + * @returns {IPlugin[]} array of plugin objects. * @since 2.1.5 */ getAll: function() { @@ -89,10 +89,10 @@ module.exports = { * Calls enabled plugins for `chart` on the specified hook and with the given args. * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. - * @param {Object} chart - The chart instance for which plugins should be called. - * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Chart} chart - The chart instance for which plugins should be called. + * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). * @param {Array} [args] - Extra arguments to apply to the hook call. - * @returns {Boolean} false if any of the plugins return false, else returns true. + * @returns {boolean} false if any of the plugins return false, else returns true. */ notify: function(chart, hook, args) { var descriptors = this.descriptors(chart); @@ -117,7 +117,7 @@ module.exports = { /** * Returns descriptors of enabled plugins for the given chart. - * @returns {Array} [{ plugin, options }] + * @returns {object[]} [{ plugin, options }] * @private */ descriptors: function(chart) { @@ -179,36 +179,36 @@ module.exports = { * @method IPlugin#beforeInit * @desc Called before initializing `chart`. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#afterInit * @desc Called after `chart` has been initialized and before the first update. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeUpdate * @desc Called before updating `chart`. If any plugin returns `false`, the update * is cancelled (and thus subsequent render(s)) until another `update` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart update. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart update. */ /** * @method IPlugin#afterUpdate * @desc Called after `chart` has been updated and before rendering. Note that this * hook will not be called if the chart update has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeDatasetsUpdate * @desc Called before updating the `chart` datasets. If any plugin returns `false`, * the datasets update is cancelled until another `update` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} false to cancel the datasets update. + * @param {object} options - The plugin options. + * @returns {boolean} false to cancel the datasets update. * @since version 2.1.5 */ /** @@ -216,7 +216,7 @@ module.exports = { * @desc Called after the `chart` datasets have been updated. Note that this hook * will not be called if the datasets update has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. * @since version 2.1.5 */ /** @@ -224,51 +224,51 @@ module.exports = { * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin * returns `false`, the datasets update is cancelled until another `update` is triggered. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. */ /** * @method IPlugin#afterDatasetUpdate * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note * that this hook will not be called if the datasets update has been previously cancelled. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeLayout * @desc Called before laying out `chart`. If any plugin returns `false`, * the layout update is cancelled until another `update` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart layout. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart layout. */ /** * @method IPlugin#afterLayout * @desc Called after the `chart` has been layed out. Note that this hook will not * be called if the layout update has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeRender * @desc Called before rendering `chart`. If any plugin returns `false`, * the rendering is cancelled until another `render` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart rendering. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart rendering. */ /** * @method IPlugin#afterRender * @desc Called after the `chart` has been fully rendered (and animation completed). Note * that this hook will not be called if the rendering has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeDraw @@ -276,34 +276,34 @@ module.exports = { * easing value. If any plugin returns `false`, the frame drawing is cancelled until * another `render` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart drawing. + * @param {number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart drawing. */ /** * @method IPlugin#afterDraw * @desc Called after the `chart` has been drawn for the specific easing value. Note * that this hook will not be called if the drawing has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. + * @param {number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeDatasetsDraw * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, * the datasets drawing is cancelled until another `render` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. + * @param {number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. */ /** * @method IPlugin#afterDatasetsDraw * @desc Called after the `chart` datasets have been drawn. Note that this hook * will not be called if the datasets drawing has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. + * @param {number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeDatasetDraw @@ -311,12 +311,12 @@ module.exports = { * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing * is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. */ /** * @method IPlugin#afterDatasetDraw @@ -324,32 +324,32 @@ module.exports = { * (datasets are drawn in the reverse order). Note that this hook will not be called * if the datasets drawing has been previously cancelled. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeTooltipDraw * @desc Called before drawing the `tooltip`. If any plugin returns `false`, * the tooltip drawing is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart tooltip drawing. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart tooltip drawing. */ /** * @method IPlugin#afterTooltipDraw * @desc Called after drawing the `tooltip`. Note that this hook will not * be called if the tooltip drawing has been previously cancelled. * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Object} args.tooltip - The tooltip. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {object} options - The plugin options. */ /** * @method IPlugin#beforeEvent @@ -357,7 +357,7 @@ module.exports = { * the event will be discarded. * @param {Chart.Controller} chart - The chart instance. * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#afterEvent @@ -365,18 +365,18 @@ module.exports = { * will not be called if the `event` has been previously discarded. * @param {Chart.Controller} chart - The chart instance. * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ /** * @method IPlugin#resize * @desc Called after the chart as been resized. * @param {Chart.Controller} chart - The chart instance. - * @param {Number} size - The new canvas display size (eq. canvas.style width & height). - * @param {Object} options - The plugin options. + * @param {number} size - The new canvas display size (eq. canvas.style width & height). + * @param {object} options - The plugin options. */ /** * @method IPlugin#destroy * @desc Called after the chart as been destroyed. * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. + * @param {object} options - The plugin options. */ diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 5bbf6ebcbdd..5384e323613 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -717,8 +717,10 @@ module.exports = Element.extend({ return false; }, - // Actually draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + /** + * Actually draw the scale on the canvas + * @param {object} chartArea - the area of the chart to draw full grid lines on + */ draw: function(chartArea) { var me = this; var options = me.options; diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index f63e525983a..c9e75181fb2 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -16,7 +16,7 @@ module.exports = { * Formatter for value labels * @method Chart.Ticks.formatters.values * @param value the value to display - * @return {String|Array} the label to display + * @return {string|string[]} the label to display */ values: function(value) { return helpers.isArray(value) ? value : '' + value; @@ -25,10 +25,10 @@ module.exports = { /** * 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 + * @param tickValue {number} the value to be formatted + * @param index {number} the position of the tickValue parameter in the ticks array + * @param ticks {number[]} 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 diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index a3757e2047a..f85055b2e6c 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -107,7 +107,7 @@ var positioners = { * Average mode places the tooltip at the average position of the elements shown * @function Chart.Tooltip.positioners.average * @param elements {ChartElement[]} the elements being displayed in the tooltip - * @returns {Point} tooltip position + * @returns {object} tooltip position */ average: function(elements) { if (!elements.length) { @@ -139,8 +139,8 @@ var positioners = { * Gets the tooltip position nearest of the item nearest to the event position * @function Chart.Tooltip.positioners.nearest * @param elements {Chart.Element[]} the tooltip elements - * @param eventPosition {Point} the position of the event in canvas coordinates - * @returns {Point} the tooltip position + * @param eventPosition {object} the position of the event in canvas coordinates + * @returns {object} the tooltip position */ nearest: function(elements, eventPosition) { var x = eventPosition.x; @@ -190,8 +190,8 @@ function pushOrConcat(base, toPush) { /** * Returns array of strings split by newline - * @param {String} value - The value to split by newline. - * @returns {Array} value if newline present - Returned from String split() method + * @param {string} value - The value to split by newline. + * @returns {string[]} value if newline present - Returned from String split() method * @function */ function splitNewlines(str) { @@ -202,9 +202,11 @@ function splitNewlines(str) { } -// Private helper to create a tooltip item model -// @param element : the chart element (point, arc, bar) to create the tooltip item for -// @return : new tooltip item +/** + * Private helper to create a tooltip item model + * @param element - the chart element (point, arc, bar) to create the tooltip item for + * @return new tooltip item + */ function createTooltipItem(element) { var xScale = element._xScale; var yScale = element._yScale || element._scale; // handle radar || polarArea charts @@ -228,7 +230,7 @@ function createTooltipItem(element) { /** * Helper to get the reset model for the tooltip - * @param tooltipOpts {Object} the tooltip options + * @param tooltipOpts {object} the tooltip options */ function getBaseModel(tooltipOpts) { var globalDefaults = defaults.global; @@ -953,7 +955,7 @@ var exports = Element.extend({ * Handle an event * @private * @param {IEvent} event - The event to handle - * @returns {Boolean} true if the tooltip changed + * @returns {boolean} true if the tooltip changed */ handleEvent: function(e) { var me = this; diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 1513386a8cb..b23c2ff8213 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -25,11 +25,11 @@ var exports = { * Creates a "path" for a rectangle with rounded corners at position (x, y) with a * given size (width, height) and the same `radius` for all corners. * @param {CanvasRenderingContext2D} ctx - The canvas 2D Context. - * @param {Number} x - The x axis of the coordinate for the rectangle starting point. - * @param {Number} y - The y axis of the coordinate for the rectangle starting point. - * @param {Number} width - The rectangle's width. - * @param {Number} height - The rectangle's height. - * @param {Number} radius - The rounded amount (in pixels) for the four corners. + * @param {number} x - The x axis of the coordinate for the rectangle starting point. + * @param {number} y - The y axis of the coordinate for the rectangle starting point. + * @param {number} width - The rectangle's width. + * @param {number} height - The rectangle's height. + * @param {number} radius - The rounded amount (in pixels) for the four corners. * @todo handle `radius` as top-left, top-right, bottom-right, bottom-left array/object? */ roundedRect: function(ctx, x, y, width, height, radius) { @@ -174,9 +174,9 @@ var exports = { /** * Returns true if the point is inside the rectangle - * @param {Object} point - The point to test - * @param {Object} area - The rectangle - * @returns {Boolean} + * @param {object} point - The point to test + * @param {object} area - The rectangle + * @returns {boolean} * @private */ _isPointInArea: function(point, area) { diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 1b798c925c8..cc8d888e653 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -11,7 +11,7 @@ var helpers = { /** * Returns a unique id, sequentially generated from a global variable. - * @returns {Number} + * @returns {number} * @function */ uid: (function() { @@ -24,7 +24,7 @@ var helpers = { /** * Returns true if `value` is neither null nor undefined, else returns false. * @param {*} value - The value to test. - * @returns {Boolean} + * @returns {boolean} * @since 2.7.0 */ isNullOrUndef: function(value) { @@ -34,7 +34,7 @@ var helpers = { /** * Returns true if `value` is an array (including typed arrays), else returns false. * @param {*} value - The value to test. - * @returns {Boolean} + * @returns {boolean} * @function */ isArray: function(value) { @@ -51,7 +51,7 @@ var helpers = { /** * Returns true if `value` is an object (excluding null), else returns false. * @param {*} value - The value to test. - * @returns {Boolean} + * @returns {boolean} * @since 2.7.0 */ isObject: function(value) { @@ -61,7 +61,7 @@ var helpers = { /** * Returns true if `value` is a finite number, else returns false * @param {*} value - The value to test. - * @returns {Boolean} + * @returns {boolean} */ isFinite: function(value) { return (typeof value === 'number' || value instanceof Number) && isFinite(value); @@ -80,7 +80,7 @@ var helpers = { /** * Returns value at the given `index` in array if defined, else returns `defaultValue`. * @param {Array} value - The array to lookup for value at `index`. - * @param {Number} index - The index in `value` to lookup for value. + * @param {number} index - The index in `value` to lookup for value. * @param {*} defaultValue - The value to return if `value[index]` is undefined. * @returns {*} */ @@ -91,9 +91,9 @@ var helpers = { /** * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the * value returned by `fn`. If `fn` is not a function, this method returns undefined. - * @param {Function} fn - The function to call. + * @param {function} fn - The function to call. * @param {Array|undefined|null} args - The arguments with which `fn` should be called. - * @param {Object} [thisArg] - The value of `this` provided for the call to `fn`. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. * @returns {*} */ callback: function(fn, args, thisArg) { @@ -106,10 +106,10 @@ var helpers = { * Note(SB) for performance sake, this method should only be used when loopable type * is unknown or in none intensive code (not called often and small loopable). Else * it's preferable to use a regular for() loop and save extra function calls. - * @param {Object|Array} loopable - The object or array to be iterated. - * @param {Function} fn - The function to call for each item. - * @param {Object} [thisArg] - The value of `this` provided for the call to `fn`. - * @param {Boolean} [reverse] - If true, iterates backward on the loopable. + * @param {object|Array} loopable - The object or array to be iterated. + * @param {function} fn - The function to call for each item. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. + * @param {boolean} [reverse] - If true, iterates backward on the loopable. */ each: function(loopable, fn, thisArg, reverse) { var i, len, keys; @@ -138,7 +138,7 @@ var helpers = { * @see https://stackoverflow.com/a/14853974 * @param {Array} a0 - The array to compare * @param {Array} a1 - The array to compare - * @returns {Boolean} + * @returns {boolean} */ arrayEquals: function(a0, a1) { var i, ilen, v0, v1; @@ -224,11 +224,11 @@ var helpers = { /** * Recursively deep copies `source` properties into `target` with the given `options`. * IMPORTANT: `target` is not cloned and will be updated with `source` properties. - * @param {Object} target - The target object in which all sources are merged into. - * @param {Object|Array(Object)} source - Object(s) to merge into `target`. - * @param {Object} [options] - Merging options: - * @param {Function} [options.merger] - The merge method (key, target, source, options) - * @returns {Object} The `target` object. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @param {object} [options] - Merging options: + * @param {function} [options.merger] - The merge method (key, target, source, options) + * @returns {object} The `target` object. */ merge: function(target, source, options) { var sources = helpers.isArray(source) ? source : [source]; @@ -260,9 +260,9 @@ var helpers = { /** * Recursively deep copies `source` properties into `target` *only* if not defined in target. * IMPORTANT: `target` is not cloned and will be updated with `source` properties. - * @param {Object} target - The target object in which all sources are merged into. - * @param {Object|Array(Object)} source - Object(s) to merge into `target`. - * @returns {Object} The `target` object. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @returns {object} The `target` object. */ mergeIf: function(target, source) { return helpers.merge(target, source, {merger: helpers._mergerIf}); @@ -270,10 +270,10 @@ var helpers = { /** * Applies the contents of two or more objects together into the first object. - * @param {Object} target - The target object in which all objects are merged into. - * @param {Object} arg1 - Object containing additional properties to merge in target. - * @param {Object} argN - Additional objects containing properties to merge in target. - * @returns {Object} The `target` object. + * @param {object} target - The target object in which all objects are merged into. + * @param {object} arg1 - Object containing additional properties to merge in target. + * @param {object} argN - Additional objects containing properties to merge in target. + * @returns {object} The `target` object. */ extend: function(target) { var setFn = function(value, key) { diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js index b1ff4c199a6..0e96a71079c 100644 --- a/src/helpers/helpers.options.js +++ b/src/helpers/helpers.options.js @@ -7,8 +7,8 @@ var valueOrDefault = helpers.valueOrDefault; /** * Converts the given font object into a CSS font string. - * @param {Object} font - A font object. - * @return {String} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + * @param {object} font - A font object. + * @return {string} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font * @private */ function toFontString(font) { @@ -29,9 +29,9 @@ function toFontString(font) { module.exports = { /** * Converts the given line height `value` in pixels for a specific font `size`. - * @param {Number|String} value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). - * @param {Number} size - The font size (in pixels) used to resolve relative `value`. - * @returns {Number} The effective line height in pixels (size * 1.2 if value is invalid). + * @param {number|string} value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). + * @param {number} size - The font size (in pixels) used to resolve relative `value`. + * @returns {number} The effective line height in pixels (size * 1.2 if value is invalid). * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height * @since 2.7.0 */ @@ -58,9 +58,9 @@ module.exports = { /** * Converts the given value into a padding object with pre-computed width/height. - * @param {Number|Object} value - If a number, set the value to all TRBL component, + * @param {number|object} value - If a number, set the value to all TRBL component, * else, if and object, use defined properties and sets undefined ones to 0. - * @returns {Object} The padding values (top, right, bottom, left, width, height) + * @returns {object} The padding values (top, right, bottom, left, width, height) * @since 2.7.0 */ toPadding: function(value) { @@ -87,8 +87,8 @@ module.exports = { /** * Parses font options and returns the font object. - * @param {Object} options - A object that contains font options to be parsed. - * @return {Object} The font object. + * @param {object} options - A object that contains font options to be parsed. + * @return {object} The font object. * @todo Support font.* options and renamed to toFont(). * @private */ @@ -110,10 +110,10 @@ module.exports = { /** * Evaluates the given `inputs` sequentially and returns the first defined value. - * @param {Array[]} inputs - An array of values, falling back to the last value. - * @param {Object} [context] - If defined and the current value is a function, the value + * @param {Array} inputs - An array of values, falling back to the last value. + * @param {object} [context] - If defined and the current value is a function, the value * is called with `context` as first argument and the result becomes the new input. - * @param {Number} [index] - If defined and the current value is an array, the value + * @param {number} [index] - If defined and the current value is an array, the value * at `index` become the new input. * @since 2.7.0 */ diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index 777833afe5e..66f95668997 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -38,7 +38,7 @@ var EVENT_TYPES = { * `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. * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value - * @returns {Number} Size in pixels or undefined if unknown. + * @returns {number} Size in pixels or undefined if unknown. */ function readUsedSize(element, property) { var value = helpers.getStyle(element, property); diff --git a/src/platforms/platform.js b/src/platforms/platform.js index c755f81fdb1..159077e4959 100644 --- a/src/platforms/platform.js +++ b/src/platforms/platform.js @@ -22,7 +22,7 @@ module.exports = helpers.extend({ * Called at chart construction time, returns a context2d instance implementing * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. * @param {*} item - The native item from which to acquire context (platform specific) - * @param {Object} options - The chart options + * @param {object} options - The chart options * @returns {CanvasRenderingContext2D} context2d instance */ acquireContext: function() {}, @@ -31,24 +31,24 @@ module.exports = helpers.extend({ * Called at chart destruction time, releases any resources associated to the context * previously returned by the acquireContext() method. * @param {CanvasRenderingContext2D} context - The context2d instance - * @returns {Boolean} true if the method succeeded, else false + * @returns {boolean} true if the method succeeded, else false */ releaseContext: function() {}, /** * Registers the specified listener on the given chart. * @param {Chart} chart - Chart from which to listen for event - * @param {String} type - The ({@link IEvent}) type to listen for - * @param {Function} listener - Receives a notification (an object that implements + * @param {string} type - The ({@link IEvent}) type to listen for + * @param {function} listener - Receives a notification (an object that implements * the {@link IEvent} interface) when an event of the specified type occurs. */ addEventListener: function() {}, /** * Removes the specified listener previously registered with addEventListener. - * @param {Chart} chart -Chart from which to remove the listener - * @param {String} type - The ({@link IEvent}) type to remove - * @param {Function} listener - The listener function to remove from the event target. + * @param {Chart} chart - Chart from which to remove the listener + * @param {string} type - The ({@link IEvent}) type to remove + * @param {function} listener - The listener function to remove from the event target. */ removeEventListener: function() {} @@ -65,10 +65,10 @@ module.exports = helpers.extend({ /** * @interface IEvent - * @prop {String} type - The event type name, possible values are: + * @prop {string} type - The event type name, possible values are: * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout', * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize' * @prop {*} native - The original native event (null for emulated events, e.g. 'resize') - * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events) - * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events) + * @prop {number} x - The mouse x position, relative to the canvas (null for incompatible events) + * @prop {number} y - The mouse y position, relative to the canvas (null for incompatible events) */ diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 03bf8040baf..2e832f98afd 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -85,9 +85,9 @@ defaults._set('global', { /** * 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 + * @param {object} labelopts - the label options on the legend + * @param {number} fontSize - the label font size + * @return {number} width of the color box area */ function getBoxWidth(labelOpts, fontSize) { return labelOpts.usePointStyle && labelOpts.boxWidth > fontSize ? @@ -462,7 +462,7 @@ var Legend = Element.extend({ * Handle an event * @private * @param {IEvent} event - The event to handle - * @return {Boolean} true if a change occured + * @return {boolean} true if a change occured */ handleEvent: function(e) { var me = this; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 057e68ffc3d..24816e9225e 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -10,7 +10,7 @@ var isNullOrUndef = helpers.isNullOrUndef; * Generate a set of linear ticks * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data - * @returns {Array} array of tick values + * @returns {number[]} array of tick values */ function generateTicks(generationOptions, dataRange) { var ticks = []; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 91f90845da3..e52f714a81f 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -11,7 +11,7 @@ var valueOrDefault = helpers.valueOrDefault; * Generate a set of logarithmic ticks * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data - * @returns {Array} array of tick values + * @returns {number[]} array of tick values */ function generateTicks(generationOptions, dataRange) { var ticks = []; @@ -251,8 +251,8 @@ module.exports = Scale.extend({ /** * Returns the value of the first tick. - * @param {Number} value - The minimum not zero value. - * @return {Number} The first tick value. + * @param {number} value - The minimum not zero value. + * @return {number} The first tick value. * @private */ _getFirstTickValue: function(value) { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 8dfa4e36b00..d2a5e4844df 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -88,8 +88,8 @@ function arrayUnique(items) { * store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need * to create the lookup table. The table ALWAYS contains at least two items: min and max. * - * @param {Number[]} timestamps - timestamps sorted from lowest to highest. - * @param {String} distribution - If 'linear', timestamps will be spread linearly along the min + * @param {number[]} timestamps - timestamps sorted from lowest to highest. + * @param {string} distribution - If 'linear', timestamps will be spread linearly along the min * and max range, so basically, the table will contains only two items: {min, 0} and {max, 1}. * If 'series', timestamps will be positioned at the same distance from each other. In this * case, only timestamps that break the time linearity are registered, meaning that in the From ef507e11bd6936a0a60cbd66822aa6aef23df828 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 11 Feb 2019 13:31:26 -0500 Subject: [PATCH 542/685] Handle inextensible `dataset.data` array (#6060) --- src/core/core.datasetController.js | 4 +- test/specs/core.datasetController.tests.js | 80 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 6a7d36f196b..afd021c1db1 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -225,7 +225,9 @@ helpers.extend(DatasetController.prototype, { unlistenArrayEvents(me._data, me); } - listenArrayEvents(data, me); + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } me._data = data; } diff --git a/test/specs/core.datasetController.tests.js b/test/specs/core.datasetController.tests.js index 5ab9dd5f31f..8b5d6dec2e0 100644 --- a/test/specs/core.datasetController.tests.js +++ b/test/specs/core.datasetController.tests.js @@ -38,6 +38,86 @@ describe('Chart.DatasetController', function() { }); }); + describe('inextensible data', function() { + it('should handle a frozen data object', function() { + function createChart() { + var data = Object.freeze([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.freeze([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + + it('should handle a sealed data object', function() { + function createChart() { + var data = Object.seal([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.seal([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + + it('should handle an unextendable data object', function() { + function createChart() { + var data = Object.preventExtensions([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.preventExtensions([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + }); + it('should synchronize metadata when data are inserted or removed', function() { var data = [0, 1, 2, 3, 4, 5]; var chart = acquireChart({ From 5fc934eae1237e78f6db3ab065c71ee9412b9047 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Wed, 13 Feb 2019 18:08:38 +0200 Subject: [PATCH 543/685] Fix responsive resize on rtl page (#6063) --- src/platforms/platform.dom.css | 1 + test/specs/core.controller.tests.js | 40 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/platforms/platform.dom.css b/src/platforms/platform.dom.css index e0b99a4aad4..5e749593eeb 100644 --- a/src/platforms/platform.dom.css +++ b/src/platforms/platform.dom.css @@ -19,6 +19,7 @@ .chartjs-size-monitor-expand, .chartjs-size-monitor-shrink { position: absolute; + direction: ltr; left: 0; top: 0; right: 0; diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 819074393b1..ef41f5942b4 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -379,6 +379,46 @@ describe('Chart', function() { }); }); + it('should resize the canvas when parent is RTL and width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative; direction: rtl' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + wrapper.style.width = '455px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + wrapper.style.width = '150px'; + waitForResize(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: { From 58d7891ba23f154d3d799445c7ccb6dda2926468 Mon Sep 17 00:00:00 2001 From: Janelle deMent Date: Wed, 13 Feb 2019 22:12:52 -0400 Subject: [PATCH 544/685] Add examples of scriptable charts (#6042) * Add example of scriptable pie chart * Add example of scriptable line chart * Add example of scriptable polar area chart * Add example of scriptable radar chart --- samples/samples.js | 12 ++++ samples/scriptable/line.html | 115 ++++++++++++++++++++++++++++++++++ samples/scriptable/pie.html | 110 ++++++++++++++++++++++++++++++++ samples/scriptable/polar.html | 98 +++++++++++++++++++++++++++++ samples/scriptable/radar.html | 112 +++++++++++++++++++++++++++++++++ 5 files changed, 447 insertions(+) create mode 100644 samples/scriptable/line.html create mode 100644 samples/scriptable/pie.html create mode 100644 samples/scriptable/polar.html create mode 100644 samples/scriptable/radar.html diff --git a/samples/samples.js b/samples/samples.js index b6ffe762682..29ed1ff8190 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -181,6 +181,18 @@ }, { title: 'Bubble Chart', path: 'scriptable/bubble.html' + }, { + title: 'Pie Chart', + path: 'scriptable/pie.html' + }, { + title: 'Line Chart', + path: 'scriptable/line.html' + }, { + title: 'Polar Area Chart', + path: 'scriptable/polar.html' + }, { + title: 'Radar Chart', + path: 'scriptable/radar.html' }] }, { title: 'Advanced', diff --git a/samples/scriptable/line.html b/samples/scriptable/line.html new file mode 100644 index 00000000000..eb5a940d79d --- /dev/null +++ b/samples/scriptable/line.html @@ -0,0 +1,115 @@ + + + + + + + Scriptable > Line | Chart.js sample + + + + + +

    +
    +
    + + + +
    +
    + + + + diff --git a/samples/scriptable/pie.html b/samples/scriptable/pie.html new file mode 100644 index 00000000000..7ee105b7a2f --- /dev/null +++ b/samples/scriptable/pie.html @@ -0,0 +1,110 @@ + + + + + + + Scriptable > Pie | Chart.js sample + + + + + +
    +
    +
    + + + + +
    +
    + + + diff --git a/samples/scriptable/polar.html b/samples/scriptable/polar.html new file mode 100644 index 00000000000..3eb290b8b4e --- /dev/null +++ b/samples/scriptable/polar.html @@ -0,0 +1,98 @@ + + + + + + + Scriptable > Polar Area | Chart.js sample + + + + + +
    +
    +
    + + + +
    +
    + + + diff --git a/samples/scriptable/radar.html b/samples/scriptable/radar.html new file mode 100644 index 00000000000..8b56c868245 --- /dev/null +++ b/samples/scriptable/radar.html @@ -0,0 +1,112 @@ + + + + + + + Scriptable > Radar | Chart.js sample + + + + + +
    +
    +
    + + + +
    +
    + + + + From 32aeeac82cdc371b6eb15e0b3442d307e2f36df7 Mon Sep 17 00:00:00 2001 From: Abel Heinsbroek Date: Mon, 18 Feb 2019 10:09:37 +0100 Subject: [PATCH 545/685] Add crosshair plugin to extensions page (#6070) --- docs/notes/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index f8f53f3d564..af0534bb4c0 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -15,6 +15,7 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co -
    chartjs-plugin-annotation - Draws lines and boxes on chart area. - chartjs-plugin-colorschemes - Enables automatic coloring using predefined color schemes. + - chartjs-plugin-crosshair - Adds a data crosshair to line and scatter charts - chartjs-plugin-datalabels - Displays labels on data for any type of charts. - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. From 3e187081837888fcf8ca724050e773d104a2181b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 18 Feb 2019 10:45:38 -0800 Subject: [PATCH 546/685] Use `datetime` as default time scale tooltip format (#6019) Remove the logic that computed an "optimal" tooltip format. Instead, always fallback to the `datetime` adapter format which is more efficient and stable. Additionally, remove the adapter `presets` API, which is not needed anymore. --- src/adapters/adapter.moment.js | 11 +----- src/core/core.adapters.js | 12 ++----- src/scales/scale.time.js | 27 +-------------- test/specs/scale.time.tests.js | 62 +++------------------------------- 4 files changed, 8 insertions(+), 104 deletions(-) diff --git a/src/adapters/adapter.moment.js b/src/adapters/adapter.moment.js index 04c03155924..2ae611dfe2a 100644 --- a/src/adapters/adapter.moment.js +++ b/src/adapters/adapter.moment.js @@ -7,6 +7,7 @@ var adapter = require('../core/core.adapters')._date; var helpers = require('../helpers/helpers.core'); var FORMATS = { + datetime: 'MMM D, YYYY, h:mm:ss a', millisecond: 'h:mm:ss.SSS a', second: 'h:mm:ss a', minute: 'h:mm a', @@ -18,12 +19,6 @@ var FORMATS = { year: 'YYYY' }; -var PRESETS = { - full: 'MMM D, YYYY h:mm:ss.SSS a', - time: 'MMM D, YYYY h:mm:ss a', - date: 'MMM D, YYYY' -}; - helpers.merge(adapter, moment ? { _id: 'moment', // DEBUG ONLY @@ -31,10 +26,6 @@ helpers.merge(adapter, moment ? { return FORMATS; }, - presets: function() { - return PRESETS; - }, - parse: function(value, format) { if (typeof value === 'string' && typeof format === 'string') { value = moment(value, format); diff --git a/src/core/core.adapters.js b/src/core/core.adapters.js index 5aa78e0765b..246f3b70d3e 100644 --- a/src/core/core.adapters.js +++ b/src/core/core.adapters.js @@ -30,20 +30,12 @@ function abstract() { /** @lends Chart._adapters._date */ module.exports._date = { /** - * Returns a map of time formats for the supported units. + * Returns a map of time formats for the supported formatting units defined + * in Unit as well as 'datetime' representing a detailed date/time string. * @returns {{string: string}} */ formats: abstract, - /** - * Returns a map of date/time formats for the following presets: - * 'full': date + time + millisecond - * 'time': date + time - * 'date': date - * @returns {{string: string}} - */ - presets: abstract, - /** * Parses the given `value` and return the associated timestamp. * @param {any} value - the value to parse (usually comes from the data) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index d2a5e4844df..3f9a616992a 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -405,29 +405,6 @@ function ticksFromTimestamps(values, majorUnit) { return ticks; } -/** - * Return the time format for the label with the most parts (milliseconds, second, etc.) - */ -function determineLabelFormat(timestamps) { - var presets = adapter.presets(); - var ilen = timestamps.length; - var i, ts, hasTime; - - for (i = 0; i < ilen; i++) { - ts = timestamps[i]; - if (ts % INTERVALS.second.size !== 0) { - return presets.full; - } - if (!hasTime && adapter.startOf(ts, 'day') !== ts) { - hasTime = true; - } - } - if (hasTime) { - return presets.time; - } - return presets.date; -} - var defaultConfig = { position: 'bottom', @@ -637,7 +614,6 @@ module.exports = Scale.extend({ me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); - me._labelFormat = determineLabelFormat(me._timestamps.data); if (options.ticks.reverse) { ticks.reverse(); @@ -662,8 +638,7 @@ module.exports = Scale.extend({ if (typeof label === 'string') { return label; } - - return adapter.format(toTimestamp(label, timeOpts), me._labelFormat); + return adapter.format(toTimestamp(label, timeOpts), timeOpts.displayFormats.datetime); }, /** diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index c5be682d95d..8c516297554 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -599,7 +599,7 @@ describe('Time scale tests', function() { expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); }); - it('should get the correct label for a timestamp with milliseconds', function() { + it('should get the correct label for a timestamp', function() { var chart = window.acquireChart({ type: 'line', data: { @@ -624,63 +624,7 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; var label = xScale.getLabelForIndex(0, 0); - expect(label).toEqual('Jan 8, 2018 5:14:23.234 am'); - }); - - it('should get the correct label for a timestamp with time', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale0', - data: [ - {t: +new Date('2018-01-08 05:14:23'), y: 10}, - {t: +new Date('2018-01-09 06:17:43'), y: 3} - ] - }], - }, - options: { - scales: { - xAxes: [{ - id: 'xScale0', - type: 'time', - position: 'bottom' - }], - } - } - }); - - var xScale = chart.scales.xScale0; - var label = xScale.getLabelForIndex(0, 0); - expect(label).toEqual('Jan 8, 2018 5:14:23 am'); - }); - - it('should get the correct label for a timestamp representing a date', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale0', - data: [ - {t: +new Date('2018-01-08 00:00:00'), y: 10}, - {t: +new Date('2018-01-09 00:00:00'), y: 3} - ] - }], - }, - options: { - scales: { - xAxes: [{ - id: 'xScale0', - type: 'time', - position: 'bottom' - }], - } - } - }); - - var xScale = chart.scales.xScale0; - var label = xScale.getLabelForIndex(0, 0); - expect(label).toEqual('Jan 8, 2018'); + expect(label).toEqual('Jan 8, 2018, 5:14:23 am'); }); it('should get the correct pixel for only one data in the dataset', function() { @@ -1532,6 +1476,7 @@ describe('Time scale tests', function() { // NOTE: built-in adapter uses moment var expected = { + datetime: 'MMM D, YYYY, h:mm:ss a', millisecond: 'h:mm:ss.SSS a', second: 'h:mm:ss a', minute: 'h:mm a', @@ -1570,6 +1515,7 @@ describe('Time scale tests', function() { // NOTE: built-in adapter uses moment var expected = { + datetime: 'MMM D, YYYY, h:mm:ss a', millisecond: 'foo', second: 'h:mm:ss a', minute: 'h:mm a', From 1c01272c9af8261a114524e6781e5e67105735fb Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 20 Feb 2019 00:28:07 -0800 Subject: [PATCH 547/685] Improve autoSkip documentation (#6079) --- docs/axes/cartesian/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 97d2eac1182..86611b2fdb4 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -26,8 +26,8 @@ The following options are common to all cartesian axes but do not apply to other | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels that can be shown and hides labels accordingly. Turn it off to show all labels no matter what. -| `autoSkipPadding` | `number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. *Note: Only applicable to horizontal scales.* +| `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what. +| `autoSkipPadding` | `number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. | `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* | `maxRotation` | `number` | `90` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* | `minRotation` | `number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* From 20c26455ba9d3861f0c19b6d8906070168c6d4b9 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Wed, 20 Feb 2019 23:13:41 +0800 Subject: [PATCH 548/685] Add a link to chartjs-plugin-rough to extensions.md (#6081) --- docs/notes/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index af0534bb4c0..d526ff0c4fd 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -19,6 +19,7 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co - chartjs-plugin-datalabels - Displays labels on data for any type of charts. - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. + - chartjs-plugin-rough - Draws charts in a sketchy, hand-drawn-like style. - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. - chartjs-plugin-streaming - Enables to create live streaming charts. - chartjs-plugin-style - Provides more styling options. From f2b099b835bf5d6dfe3a7d3097997ec11983c3ed Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 20 Feb 2019 23:11:32 -0800 Subject: [PATCH 549/685] Initialize date adapter with chart options (#6016) --- docs/axes/cartesian/time.md | 1 + docs/getting-started/integration.md | 2 +- src/adapters/adapter.moment.js | 5 ++- src/core/core.adapters.js | 18 ++++++++-- src/scales/scale.time.js | 55 +++++++++++++++++------------ test/specs/scale.time.tests.js | 1 + 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index 6349af4fcb4..a3d3e016cab 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -28,6 +28,7 @@ The following options are provided by the time scale. You may also set options p | Name | Type | Default | Description | ---- | ---- | ------- | ----------- +| `adapters.date` | `object` | `{}` | Options for adapter for external date library if that adapter needs or supports options | `distribution` | `string` | `'linear'` | How data is plotted. [more...](#scale-distribution) | `bounds` | `string` | `'data'` | Determines the scale bounds. [more...](#scale-bounds) | `ticks.source` | `string` | `'auto'` | How ticks are generated. [more...](#ticks-source) diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md index 4070955edb1..e07a4c74783 100644 --- a/docs/getting-started/integration.md +++ b/docs/getting-started/integration.md @@ -25,7 +25,7 @@ import Chart from 'chart.js'; var myChart = new Chart(ctx, {...}); ``` -**Note:** Moment.js is installed along Chart.js as dependency. If you don't want to use Momemt.js (either because you use a different date adapter or simply because don't need time functionalities), you will have to configure your bundler to exclude this dependency (e.g. using [`externals` for Webpack](https://webpack.js.org/configuration/externals/) or [`external` for Rollup](https://rollupjs.org/guide/en#peer-dependencies)). +**Note:** Moment.js is installed along Chart.js as dependency. If you don't want to use Moment.js (either because you use a different date adapter or simply because don't need time functionalities), you will have to configure your bundler to exclude this dependency (e.g. using [`externals` for Webpack](https://webpack.js.org/configuration/externals/) or [`external` for Rollup](https://rollupjs.org/guide/en#peer-dependencies)). ```javascript // Webpack diff --git a/src/adapters/adapter.moment.js b/src/adapters/adapter.moment.js index 2ae611dfe2a..e028b52f3ae 100644 --- a/src/adapters/adapter.moment.js +++ b/src/adapters/adapter.moment.js @@ -3,8 +3,7 @@ 'use strict'; var moment = require('moment'); -var adapter = require('../core/core.adapters')._date; -var helpers = require('../helpers/helpers.core'); +var adapters = require('../core/core.adapters'); var FORMATS = { datetime: 'MMM D, YYYY, h:mm:ss a', @@ -19,7 +18,7 @@ var FORMATS = { year: 'YYYY' }; -helpers.merge(adapter, moment ? { +adapters._date.override(moment ? { _id: 'moment', // DEBUG ONLY formats: function() { diff --git a/src/core/core.adapters.js b/src/core/core.adapters.js index 246f3b70d3e..7073412d824 100644 --- a/src/core/core.adapters.js +++ b/src/core/core.adapters.js @@ -6,6 +6,8 @@ 'use strict'; +var helpers = require('../helpers/index'); + function abstract() { throw new Error( 'This method is not implemented: either no adapter can ' + @@ -27,8 +29,14 @@ function abstract() { * @name Unit */ -/** @lends Chart._adapters._date */ -module.exports._date = { +/** + * @class + */ +function DateAdapter(options) { + this.options = options || {}; +} + +helpers.extend(DateAdapter.prototype, /** @lends DateAdapter */ { /** * Returns a map of time formats for the supported formatting units defined * in Unit as well as 'datetime' representing a detailed date/time string. @@ -104,4 +112,10 @@ module.exports._date = { _create: function(value) { return value; } +}); + +DateAdapter.override = function(members) { + helpers.extend(DateAdapter.prototype, members); }; + +module.exports._date = DateAdapter; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 3f9a616992a..7d1df171e0f 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -1,6 +1,6 @@ 'use strict'; -var adapter = require('../core/core.adapters')._date; +var adapters = require('../core/core.adapters'); var defaults = require('../core/core.defaults'); var helpers = require('../helpers/index'); var Scale = require('../core/core.scale'); @@ -177,7 +177,9 @@ function interpolate(table, skey, sval, tkey) { return prev[tkey] + offset; } -function toTimestamp(input, options) { +function toTimestamp(scale, input) { + var adapter = scale._adapter; + var options = scale.options.time; var parser = options.parser; var format = parser || options.format; var value = input; @@ -211,19 +213,19 @@ function toTimestamp(input, options) { return value; } -function parse(input, scale) { +function parse(scale, input) { if (helpers.isNullOrUndef(input)) { return null; } var options = scale.options.time; - var value = toTimestamp(scale.getRightValue(input), options); + var value = toTimestamp(scale, scale.getRightValue(input)); if (value === null) { return value; } if (options.round) { - value = +adapter.startOf(value, options.round); + value = +scale._adapter.startOf(value, options.round); } return value; @@ -276,13 +278,13 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) { /** * Figures out what unit to format a set of ticks with */ -function determineUnitForFormatting(ticks, minUnit, min, max) { +function determineUnitForFormatting(scale, ticks, minUnit, min, max) { var ilen = UNITS.length; var i, unit; for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { unit = UNITS[i]; - if (INTERVALS[unit].common && adapter.diff(max, min, unit) >= ticks.length) { + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= ticks.length) { return unit; } } @@ -304,7 +306,9 @@ function determineMajorUnit(unit) { * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. */ -function generate(min, max, capacity, options) { +function generate(scale, min, max, capacity) { + var adapter = scale._adapter; + var options = scale.options; var timeOpts = options.time; var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); var major = determineMajorUnit(minor); @@ -388,13 +392,13 @@ function computeOffsets(table, ticks, min, max, options) { return {start: start, end: end}; } -function ticksFromTimestamps(values, majorUnit) { +function ticksFromTimestamps(scale, values, majorUnit) { var ticks = []; var i, ilen, value, major; for (i = 0, ilen = values.length; i < ilen; ++i) { value = values[i]; - major = majorUnit ? value === +adapter.startOf(value, majorUnit) : false; + major = majorUnit ? value === +scale._adapter.startOf(value, majorUnit) : false; ticks.push({ value: value, @@ -426,6 +430,7 @@ var defaultConfig = { */ bounds: 'data', + adapters: {}, time: { parser: false, // false == a pattern string from https://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from https://momentjs.com/docs/#/parsing/string-format/ @@ -465,6 +470,7 @@ module.exports = Scale.extend({ var me = this; var options = me.options; var time = options.time || (options.time = {}); + var adapter = me._adapter = new adapters._date(options.adapters.date); // DEPRECATIONS: output a message only one time per update if (time.format) { @@ -493,6 +499,7 @@ module.exports = Scale.extend({ determineDataLimits: function() { var me = this; var chart = me.chart; + var adapter = me._adapter; var timeOpts = me.options.time; var unit = timeOpts.unit || 'day'; var min = MAX_INTEGER; @@ -505,7 +512,7 @@ module.exports = Scale.extend({ // Convert labels to timestamps for (i = 0, ilen = dataLabels.length; i < ilen; ++i) { - labels.push(parse(dataLabels[i], me)); + labels.push(parse(me, dataLabels[i])); } // Convert data to timestamps @@ -518,7 +525,7 @@ module.exports = Scale.extend({ datasets[i] = []; for (j = 0, jlen = data.length; j < jlen; ++j) { - timestamp = parse(data[j], me); + timestamp = parse(me, data[j]); timestamps.push(timestamp); datasets[i][j] = timestamp; } @@ -546,8 +553,8 @@ module.exports = Scale.extend({ max = Math.max(max, timestamps[timestamps.length - 1]); } - min = parse(timeOpts.min, me) || min; - max = parse(timeOpts.max, me) || max; + min = parse(me, timeOpts.min) || min; + max = parse(me, timeOpts.max) || max; // In case there is no valid min/max, set limits based on unit time option min = min === MAX_INTEGER ? +adapter.startOf(Date.now(), unit) : min; @@ -586,7 +593,7 @@ module.exports = Scale.extend({ break; case 'auto': default: - timestamps = generate(min, max, me.getLabelCapacity(min), options); + timestamps = generate(me, min, max, me.getLabelCapacity(min), options); } if (options.bounds === 'ticks' && timestamps.length) { @@ -595,8 +602,8 @@ module.exports = Scale.extend({ } // Enforce limits with user min/max options - min = parse(timeOpts.min, me) || min; - max = parse(timeOpts.max, me) || max; + min = parse(me, timeOpts.min) || min; + max = parse(me, timeOpts.max) || max; // Remove ticks outside the min/max range for (i = 0, ilen = timestamps.length; i < ilen; ++i) { @@ -610,7 +617,7 @@ module.exports = Scale.extend({ me.max = max; // PRIVATE - me._unit = timeOpts.unit || determineUnitForFormatting(ticks, timeOpts.minUnit, me.min, me.max); + me._unit = timeOpts.unit || determineUnitForFormatting(me, ticks, timeOpts.minUnit, me.min, me.max); me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); @@ -619,11 +626,12 @@ module.exports = Scale.extend({ ticks.reverse(); } - return ticksFromTimestamps(ticks, me._majorUnit); + return ticksFromTimestamps(me, ticks, me._majorUnit); }, getLabelForIndex: function(index, datasetIndex) { var me = this; + var adapter = me._adapter; var data = me.chart.data; var timeOpts = me.options.time; var label = data.labels && index < data.labels.length ? data.labels[index] : ''; @@ -633,12 +641,12 @@ module.exports = Scale.extend({ label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - return adapter.format(toTimestamp(label, timeOpts), timeOpts.tooltipFormat); + return adapter.format(toTimestamp(me, label), timeOpts.tooltipFormat); } if (typeof label === 'string') { return label; } - return adapter.format(toTimestamp(label, timeOpts), timeOpts.displayFormats.datetime); + return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime); }, /** @@ -647,6 +655,7 @@ module.exports = Scale.extend({ */ tickFormatFunction: function(time, index, ticks, format) { var me = this; + var adapter = me._adapter; var options = me.options; var formats = options.time.displayFormats; var minorFormat = formats[me._unit]; @@ -696,7 +705,7 @@ module.exports = Scale.extend({ } if (time === null) { - time = parse(value, me); + time = parse(me, value); } if (time !== null) { @@ -719,7 +728,7 @@ module.exports = Scale.extend({ var time = interpolate(me._table, 'pos', pos, 'time'); // DEPRECATION, we should return time directly - return adapter._create(time); + return me._adapter._create(time); }, /** diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 8c516297554..5258fba5d38 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -76,6 +76,7 @@ describe('Time scale tests', function() { scaleLabel: Chart.defaults.scale.scaleLabel, bounds: 'data', distribution: 'linear', + adapters: {}, ticks: { beginAtZero: false, minRotation: 0, From f3b18373e67c8e0aab6b29379787725a88dd7391 Mon Sep 17 00:00:00 2001 From: Jon Rimmer Date: Sun, 24 Feb 2019 09:58:22 +0000 Subject: [PATCH 550/685] Add onLeave callback to legend (#6059) --- samples/legend/callbacks.html | 124 ++++++++++++++++++++++++++++++ samples/samples.js | 3 + src/plugins/plugin.legend.js | 73 +++++++++++------- test/specs/plugin.legend.tests.js | 50 ++++++++++++ 4 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 samples/legend/callbacks.html diff --git a/samples/legend/callbacks.html b/samples/legend/callbacks.html new file mode 100644 index 00000000000..aa71857489e --- /dev/null +++ b/samples/legend/callbacks.html @@ -0,0 +1,124 @@ + + + + Legend Callbacks + + + + + + +
    +
    + +
    + + + + diff --git a/samples/samples.js b/samples/samples.js index 29ed1ff8190..bb0463f7e6e 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -148,6 +148,9 @@ }, { title: 'Point style', path: 'legend/point-style.html' + }, { + title: 'Callbacks', + path: 'legend/callbacks.html' }] }, { title: 'Tooltip', diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 2e832f98afd..2c6870b279d 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -30,6 +30,7 @@ defaults._set('global', { }, onHover: null, + onLeave: null, labels: { boxWidth: 40, @@ -106,6 +107,11 @@ var Legend = Element.extend({ // Contains hit boxes for each dataset (in dataset order) this.legendHitBoxes = []; + /** + * @private + */ + this._hoveredItem = null; + // Are we in doughnut mode which has a different data type this.doughnutMode = false; }, @@ -458,20 +464,42 @@ var Legend = Element.extend({ } }, + /** + * @private + */ + _getLegendItemAt: function(x, y) { + var me = this; + var i, hitBox, lh; + + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + lh = me.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + return me.legendItems[i]; + } + } + } + + return null; + }, + /** * Handle an event * @private * @param {IEvent} event - The event to handle - * @return {boolean} true if a change occured */ handleEvent: function(e) { var me = this; var opts = me.options; var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; + var hoveredItem; if (type === 'mousemove') { - if (!opts.onHover) { + if (!opts.onHover && !opts.onLeave) { return; } } else if (type === 'click') { @@ -483,33 +511,26 @@ var Legend = Element.extend({ } // Chart event already has relative position in it - var x = e.x; - var y = e.y; + hoveredItem = me._getLegendItemAt(e.x, e.y); - if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { - // See if we are touching one of the dataset boxes - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; - - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + if (type === 'click') { + if (hoveredItem && opts.onClick) { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, hoveredItem); + } + } else { + if (opts.onLeave && hoveredItem !== me._hoveredItem) { + if (me._hoveredItem) { + opts.onLeave.call(me, e.native, me._hoveredItem); } + me._hoveredItem = hoveredItem; } - } - return changed; + if (opts.onHover && hoveredItem) { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, hoveredItem); + } + } } }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index fee715bdb13..e970c596a82 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -11,6 +11,7 @@ describe('Legend block tests', function() { // a callback that will handle onClick: jasmine.any(Function), onHover: null, + onLeave: null, labels: { boxWidth: 40, @@ -653,4 +654,53 @@ describe('Legend block tests', function() { expect(chart.legend.options).toEqual(jasmine.objectContaining(Chart.defaults.global.legend)); }); }); + + describe('callbacks', function() { + it('should call onClick, onHover and onLeave at the correct times', function() { + var clickItem = null; + var hoverItem = null; + var leaveItem = null; + + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: { + onClick: function(_, item) { + clickItem = item; + }, + onHover: function(_, item) { + hoverItem = item; + }, + onLeave: function(_, item) { + leaveItem = item; + } + } + } + }); + + var hb = chart.legend.legendHitBoxes[0]; + var el = { + x: hb.left + (hb.width / 2), + y: hb.top + (hb.height / 2) + }; + + jasmine.triggerMouseEvent(chart, 'click', el); + + expect(clickItem).toBe(chart.legend.legendItems[0]); + + jasmine.triggerMouseEvent(chart, 'mousemove', el); + + expect(hoverItem).toBe(chart.legend.legendItems[0]); + + jasmine.triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]); + + expect(leaveItem).toBe(chart.legend.legendItems[0]); + }); + }); }); From 317cae11dc3358e816528229d05bea7c835c4ad3 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 24 Feb 2019 01:59:21 -0800 Subject: [PATCH 551/685] Ignore invalid log scale min and max (#6058) --- src/scales/scale.logarithmic.js | 13 ++++-- test/specs/scale.logarithmic.tests.js | 62 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index e52f714a81f..06b206df27c 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -62,6 +62,11 @@ var defaultConfig = { } }; +// TODO(v3): change this to positiveOrDefault +function nonNegativeOrDefault(value, defaultValue) { + return helpers.isFinite(value) && value >= 0 ? value : defaultValue; +} + module.exports = Scale.extend({ determineDataLimits: function() { var me = this; @@ -174,8 +179,8 @@ module.exports = Scale.extend({ var DEFAULT_MIN = 1; var DEFAULT_MAX = 10; - me.min = valueOrDefault(tickOpts.min, me.min); - me.max = valueOrDefault(tickOpts.max, me.max); + me.min = nonNegativeOrDefault(tickOpts.min, me.min); + me.max = nonNegativeOrDefault(tickOpts.max, me.max); if (me.min === me.max) { if (me.min !== 0 && me.min !== null) { @@ -211,8 +216,8 @@ module.exports = Scale.extend({ var reverse = !me.isHorizontal(); var generationOptions = { - min: tickOpts.min, - max: tickOpts.max + min: nonNegativeOrDefault(tickOpts.min), + max: nonNegativeOrDefault(tickOpts.max) }; var ticks = me.ticks = generateTicks(generationOptions, me); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index cb44f108d36..dd7c7cce94a 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -477,6 +477,68 @@ describe('Logarithmic Scale tests', function() { expect(yScale.ticks[tickCount - 1]).toBe(10); }); + it('should ignore negative min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: [] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale', + type: 'logarithmic', + ticks: { + min: -10, + max: -1010, + callback: function(value) { + return value; + } + } + }] + } + } + }); + + var yScale = chart.scales.yScale; + expect(yScale.min).toBe(0); + expect(yScale.max).toBe(2); + }); + + it('should ignore invalid min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: [] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale', + type: 'logarithmic', + ticks: { + min: '', + max: false, + callback: function(value) { + return value; + } + } + }] + } + } + }); + + var yScale = chart.scales.yScale; + expect(yScale.min).toBe(0); + expect(yScale.max).toBe(2); + }); + it('should generate tick marks', function() { var chart = window.acquireChart({ type: 'bar', From b36d55d0936bf591d5ab58ae3d6a076f8f2d62e1 Mon Sep 17 00:00:00 2001 From: Jon Rimmer Date: Mon, 25 Feb 2019 07:59:48 +0000 Subject: [PATCH 552/685] Add onLeave to legend config docs (#6088) --- docs/configuration/legend.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 30e3283b40d..c7d124868ed 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -12,6 +12,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | `fullWidth` | `boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. | `onClick` | `function` | | A callback that is called when a click event is registered on a label item. | `onHover` | `function` | | A callback that is called when a 'mousemove' event is registered on top of a label item. +| `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. | `reverse` | `boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. From 0ec3f5569e6cf45b2d86886554bea200a71bd9c9 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Mon, 25 Feb 2019 10:03:12 +0200 Subject: [PATCH 553/685] Add support for per side border width for rectangle (#6077) --- docs/charts/bar.md | 11 +- src/elements/element.rectangle.js | 239 +++++++++--------- .../controller.bar/borderSkipped/value.js | 5 + .../controller.bar/borderSkipped/value.png | Bin 4889 -> 1989 bytes .../borderWidth/indexable-object.js | 56 ++++ .../borderWidth/indexable-object.png | Bin 0 -> 1871 bytes .../controller.bar/borderWidth/indexable.png | Bin 4969 -> 1855 bytes .../controller.bar/borderWidth/negative.js | 49 ++++ .../controller.bar/borderWidth/negative.png | Bin 0 -> 1766 bytes .../controller.bar/borderWidth/object.js | 42 +++ .../controller.bar/borderWidth/object.png | Bin 0 -> 2113 bytes .../borderWidth/scriptable-object.js | 54 ++++ .../borderWidth/scriptable-object.png | Bin 0 -> 1640 bytes .../controller.bar/borderWidth/value.png | Bin 5109 -> 2064 bytes .../controller.bar/chart-area-clip.js | 42 +++ .../controller.bar/chart-area-clip.png | Bin 0 -> 1711 bytes .../controller.bar/horizontal-borders.js | 42 +++ .../controller.bar/horizontal-borders.png | Bin 0 -> 1522 bytes test/specs/element.rectangle.tests.js | 183 +------------- 19 files changed, 420 insertions(+), 303 deletions(-) create mode 100644 test/fixtures/controller.bar/borderWidth/indexable-object.js create mode 100644 test/fixtures/controller.bar/borderWidth/indexable-object.png create mode 100644 test/fixtures/controller.bar/borderWidth/negative.js create mode 100644 test/fixtures/controller.bar/borderWidth/negative.png create mode 100644 test/fixtures/controller.bar/borderWidth/object.js create mode 100644 test/fixtures/controller.bar/borderWidth/object.png create mode 100644 test/fixtures/controller.bar/borderWidth/scriptable-object.js create mode 100644 test/fixtures/controller.bar/borderWidth/scriptable-object.png create mode 100644 test/fixtures/controller.bar/chart-area-clip.js create mode 100644 test/fixtures/controller.bar/chart-area-clip.png create mode 100644 test/fixtures/controller.bar/horizontal-borders.js create mode 100644 test/fixtures/controller.bar/horizontal-borders.png diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 3f6e5cdd202..02f080614a1 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -71,7 +71,7 @@ the color of the bars is generally set this way. | [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'bottom'` -| [`borderWidth`](#styling) | `number` | Yes | Yes | `0` +| [`borderWidth`](#borderwidth) | number|object | Yes | Yes | `0` | [`data`](#data-structure) | `object[]` | - | - | **required** | [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | - | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | - | Yes | `undefined` @@ -97,7 +97,7 @@ The style of each bar can be controlled with the following properties: | `backgroundColor` | The bar background color. | `borderColor` | The bar border color. | [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar. -| `borderWidth` | The bar border width (in pixels). +| [`borderWidth`](#borderwidth) | The bar border width (in pixels). All these values, if `undefined`, fallback to the associated [`elements.rectangle.*`](../configuration/elements.md#rectangle-configuration) options. @@ -107,11 +107,18 @@ This setting is used to avoid drawing the bar stroke at the base of the fill. In general, this does not need to be changed except when creating chart types that derive from a bar chart. +**Note:** for negative bars in vertical chart, `top` and `bottom` are flipped. Same goes for `left` and `right` in horizontal chart. + Options are: * `'bottom'` * `'left'` * `'top'` * `'right'` +* `false` + +#### borderWidth + +If this value is a number, it is applied to all sides of the rectangle (left, top, right, bottom), except [`borderSkipped`](#borderskipped). If this value is an object, the `left` property defines the left border width. Similarly the `right`, `top` and `bottom` properties can also be specified. Omitted borders and [`borderSkipped`](#borderskipped) are skipped. ### Interactions diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index fe3702cda4e..5e5a2eac459 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -2,6 +2,7 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); +var helpers = require('../helpers/index'); var defaultColor = defaults.global.defaultColor; @@ -16,8 +17,8 @@ defaults._set('global', { } }); -function isVertical(bar) { - return bar._view.width !== undefined; +function isVertical(vm) { + return vm && vm.width !== undefined; } /** @@ -26,24 +27,21 @@ function isVertical(bar) { * @return {Bounds} bounds of the bar * @private */ -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; +function getBarBounds(vm) { + var x1, x2, y1, y2, half; + + if (isVertical(vm)) { + half = vm.width / 2; + x1 = vm.x - half; + x2 = vm.x + half; y1 = Math.min(vm.y, vm.base); y2 = Math.max(vm.y, vm.base); } else { - // horizontal bar - var halfHeight = vm.height / 2; + half = 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; + y1 = vm.y - half; + y2 = vm.y + half; } return { @@ -54,96 +52,107 @@ function getBarBounds(bar) { }; } -module.exports = Element.extend({ - draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; - var left, right, top, bottom, signX, signY, borderSkipped; - var borderWidth = vm.borderWidth; - - if (!vm.horizontal) { - // bar - left = vm.x - vm.width / 2; - right = vm.x + vm.width / 2; - top = vm.y; - bottom = vm.base; - signX = 1; - signY = bottom > top ? 1 : -1; - borderSkipped = vm.borderSkipped || 'bottom'; - } else { - // horizontal bar - left = vm.base; - right = vm.x; - top = vm.y - vm.height / 2; - bottom = vm.y + vm.height / 2; - signX = right > left ? 1 : -1; - signY = 1; - borderSkipped = vm.borderSkipped || 'left'; - } +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} - // 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 (borderWidth) { - // borderWidth shold be less than bar width and bar height. - var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom)); - borderWidth = borderWidth > barSize ? barSize : borderWidth; - var halfStroke = borderWidth / 2; - // Adjust borderWidth when bar top position is near vm.base(zero). - var borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0); - var borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0); - var borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0); - var borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0); - // not become a vertical line? - if (borderLeft !== borderRight) { - top = borderTop; - bottom = borderBottom; - } - // not become a horizontal line? - if (borderTop !== borderBottom) { - left = borderLeft; - right = borderRight; - } - } +function parseBorderSkipped(vm) { + var edge = vm.borderSkipped; + var res = {}; - ctx.beginPath(); - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [left, bottom], - [left, top], - [right, top], - [right, bottom] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } + if (!edge) { + return res; + } - function cornerAt(index) { - return corners[(startCorner + index) % 4]; + if (vm.horizontal) { + if (vm.base > vm.x) { + edge = swap(edge, 'left', 'right'); } + } else if (vm.base < vm.y) { + edge = swap(edge, 'bottom', 'top'); + } + + res[edge] = true; + return res; +} + +function parseBorderWidth(vm, maxW, maxH) { + var value = vm.borderWidth; + var skip = parseBorderSkipped(vm); + var t, r, b, l; + + if (helpers.isObject(value)) { + t = +value.top || 0; + r = +value.right || 0; + b = +value.bottom || 0; + l = +value.left || 0; + } else { + t = r = b = l = +value || 0; + } + + return { + t: skip.top || (t < 0) ? 0 : t > maxH ? maxH : t, + r: skip.right || (r < 0) ? 0 : r > maxW ? maxW : r, + b: skip.bottom || (b < 0) ? 0 : b > maxH ? maxH : b, + l: skip.left || (l < 0) ? 0 : l > maxW ? maxW : l + }; +} - // Draw rectangle from 'startCorner' - var corner = cornerAt(0); - ctx.moveTo(corner[0], corner[1]); +function boundingRects(vm) { + var bounds = getBarBounds(vm); + var width = bounds.right - bounds.left; + var height = bounds.bottom - bounds.top; + var border = parseBorderWidth(vm, width / 2, height / 2); - for (var i = 1; i < 4; i++) { - corner = cornerAt(i); - ctx.lineTo(corner[0], corner[1]); + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b } + }; +} + +function inRange(vm, x, y) { + var skipX = x === null; + var skipY = y === null; + var bounds = !vm || (skipX && skipY) ? false : getBarBounds(vm); + + return bounds + && (skipX || x >= bounds.left && x <= bounds.right) + && (skipY || y >= bounds.top && y <= bounds.bottom); +} + +module.exports = Element.extend({ + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + var rects = boundingRects(vm); + var outer = rects.outer; + var inner = rects.inner; + + ctx.fillStyle = vm.backgroundColor; + ctx.fillRect(outer.x, outer.y, outer.w, outer.h); - ctx.fill(); - if (borderWidth) { - ctx.stroke(); + if (outer.w === inner.w && outer.h === inner.h) { + return; } + + ctx.save(); + ctx.beginPath(); + ctx.rect(outer.x, outer.y, outer.w, outer.h); + ctx.clip(); + ctx.fillStyle = vm.borderColor; + ctx.rect(inner.x, inner.y, inner.w, inner.h); + ctx.fill('evenodd'); + ctx.restore(); }, height: function() { @@ -152,48 +161,28 @@ module.exports = Element.extend({ }, inRange: function(mouseX, mouseY) { - 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; + return inRange(this._view, mouseX, mouseY); }, 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; + var vm = this._view; + return isVertical(vm) + ? inRange(vm, mouseX, null) + : inRange(vm, null, mouseY); }, inXRange: function(mouseX) { - var bounds = getBarBounds(this); - return mouseX >= bounds.left && mouseX <= bounds.right; + return inRange(this._view, mouseX, null); }, inYRange: function(mouseY) { - var bounds = getBarBounds(this); - return mouseY >= bounds.top && mouseY <= bounds.bottom; + return inRange(this._view, null, mouseY); }, getCenterPoint: function() { var vm = this._view; var x, y; - if (isVertical(this)) { + if (isVertical(vm)) { x = vm.x; y = (vm.y + vm.base) / 2; } else { @@ -207,7 +196,7 @@ module.exports = Element.extend({ getArea: function() { var vm = this._view; - return isVertical(this) + return isVertical(vm) ? vm.width * Math.abs(vm.y - vm.base) : vm.height * Math.abs(vm.x - vm.base); }, diff --git a/test/fixtures/controller.bar/borderSkipped/value.js b/test/fixtures/controller.bar/borderSkipped/value.js index 139a2c68905..fed9592636b 100644 --- a/test/fixtures/controller.bar/borderSkipped/value.js +++ b/test/fixtures/controller.bar/borderSkipped/value.js @@ -22,6 +22,11 @@ module.exports = { { // option in element (fallback) data: [0, 5, -10, null], + }, + { + // option in dataset + data: [0, 5, -10, null], + borderSkipped: false } ] }, diff --git a/test/fixtures/controller.bar/borderSkipped/value.png b/test/fixtures/controller.bar/borderSkipped/value.png index 56ef05a41fe65a38ffee53a6b4a25964e0eee2e7..7f4179c87b13b21345439be6f2f7266b2b9b46d4 100644 GIT binary patch literal 1989 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@odpunnLn`LHy|yt|#*@M2 zqOZrc8rB0F%GBg;vee$>@_6piFlYDqqq>d@p4l$g>liMb{O2|=P&+=*@c*sNdHv^= z|9)*RmN|ZY|GwJp{QZCbz588X`~5)W-n)6*fB*gW)${w`UH9L|*YE%M)w8^AKT!0s z#Xj5Dz0WJ_e)iNeHgF39o!h`ILZt8X`?=R|zpZ1a`2JIv;d$jgyO-xkEtwJy+pDp-9^zS) RGZR#~db;|#taD0e0sxW#R#N}~ literal 4889 zcmeHLe^3*57Js{4=mvPjx)5>9LDQ#i#pklRzUaO^6Nw5|b5V%tz2?Cp< zpyvtpYz>t|&})kfsMwLh90>7%k+yPzs89kVskJ48A>oH4m_Y8k@&CKInRcdsB>!Z; zFQ0ku?fbs>c`xr+V!{T#ho1)k@HfWC{t*C2j~I~d=&S4Gzjp(8>o&&5ye&&M4!@E1 zo3DM?zUR&byduhI_5a7YzHOrJA$AkKuWL{Ji0@+m)iH^S+p9LJ>g~04-Fd-cab0-u z_noV))EAbQ9ZP721q`nc3}#JqF51jJ6XZjJ4SXW^UrK?0>wyg+P86>8qByyd??JNs zHPh84KWFW(QJK$3%iiEWd@jBTnZ4(w-;k-dVK0aPy zv)KkLQPIoG+GlO^k`=4$ANUqa%F4=eYx)g0&hty7XTrTYjHb&0Wj9pw1&)LEkLI)y zl?k~e9r`(g!SJm{qq#MgRX)=>9_IDpsW!tV4XrVPQRAH<8qyH8c!W z6uemOX%KY2Clwn+8Kw18U5VUqEKxRQ+ujG?}C1&i}rvhamNkZ_Icm_x|uIPRsPTVF*R`kxB z>A^Sycg`fWtYmamIIiBj%9{dQ+NYBp1*Hhb>7FfjhK>RSjUo3kd6ZcQYbm6iV7h5J z55wztR!?=>Ory{Tc{59BUd50Oum7PV3vMz;S)?|{=)*59A*uLa z8g6foyLf_&N%~IXX}}!rYOlk{7Dk1~C$DGKY4`%(;FoVa;>N;-SFh)Yoi%~7iG?Il z#PT49KEXo#{$-8pf=~c19&3CLC+z;NfVcwkE6$Mjv26X`Hm?rz_~hi|#DU29X7i3h z1xL0T?TY13xb;!d!OW5rncOs8Qd08MNT$UozI8UCrlmt~w$IGWJTMxKKZRC4IOtnk zAyzaEc+fPecECM?09R_YLo1S-YMWw4@SY{uFCE6CYYObMb zuj5x8;ZfugRQG??jYm~fzJ|&_wIdaAQ&b!Shdk09YdDT`Je#{7 z&qo~BX0r4rgd$@#e3ei@_r@|HO8JUZwh=@%JWM~|B=^G_dh&A9a3K%b&+Y?x#1T}m zf8wu;sXoo|4aZ?x0R=yP2+Hp`^0>L&Z}DeHV7Sy-O#UE5%tt#u9@3Yy;47GV3Bz}D zAnUt#&@Ycny*yIHL8|9^-woT2nCFxtP`TiS?tZNB8(Z`_7;}_Xo@AQUNw8fa(GF|1 z?QKowNtwM-YjQl6z9lJ6u|Bd|Wx0`+1$ILoYIAeh5yLwa&G#p*UusS51?IDw6yM}g zn_aKh%bPU$Hfb5x?JT)I4$HjFQc+h|*B&jq&h@+FLbEOvb??;6qt|(>kXO@)y0$Bt z1fUv|&jw?dG>W9kS0kcUKRh?dL}J*%IhQ-ah7a9tx%SojXfrYSAkt5H3;kd3-{o9lY}>#<{r$`>_~&RcygDx678dCN zXlqm2vW54t55ip%bIXZ~2hj>4a$@Vs#BB_0Zi#YkO75Y#e7yO+Pnf+J+KN0z8!lz52WZ^)Hvj+t diff --git a/test/fixtures/controller.bar/borderWidth/indexable-object.js b/test/fixtures/controller.bar/borderWidth/indexable-object.js new file mode 100644 index 00000000000..97f9af5d46f --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/indexable-object.js @@ -0,0 +1,56 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: [ + {}, + {bottom: 1, left: 1, top: 1, right: 1}, + {bottom: 1, left: 2, top: 1, right: 2}, + {bottom: 1, left: 3, top: 1, right: 3}, + {bottom: 1, left: 4, top: 1, right: 4}, + {bottom: 1, left: 5, top: 1, right: 5} + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#80808080', + borderSkipped: false, + borderWidth: [ + {bottom: 1, left: 5, top: 1, right: 5}, + {bottom: 1, left: 4, top: 1, right: 4}, + {bottom: 1, left: 3, top: 1, right: 3}, + {bottom: 1, left: 2, top: 1, right: 2}, + {bottom: 1, left: 1, top: 1, right: 1}, + {} + ] + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/indexable-object.png b/test/fixtures/controller.bar/borderWidth/indexable-object.png new file mode 100644 index 0000000000000000000000000000000000000000..75471f52679ce285a93d1a4c780c7da6d9e4db48 GIT binary patch literal 1871 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@o#XMacLn`LHy|FMaCSAns z;)G3moO_#2JM}i7a$eJ9<2du_#o8}T+g1hN>)^*me|1v%hId0GR;WuyozfafChBH67y*=MPw)pVl$B)l5F*N8f zDljl`2mu|^pfJj$PB=U`Yu)$wpT+sRn`O(@-|djE3kHSky={-rGwiT``0nt>`-~q7 zj^6&xlrWEhA;N`$qd}92AwdwRXc-4XgDXn|!we+`1~EYfri89h1;aEPCW_bb`5pfKFOSR1?pXxPp2t`i7!E{#0fL`P9BhCY`pfse#};;TO&`5{`SSCZ-SfWc z9vA&@$@)OHQjuX_%Z~w0|N^u zu#j<>Fv_GtI5ga5JU%1?%h?bk_L0KNS1FiU9~bUHx3vIVCg!0QSyLm;e9( literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/indexable.png b/test/fixtures/controller.bar/borderWidth/indexable.png index 30011d3b722e474f7ae790f8a1c477f3679195b8..d3f1b85c8675193c6deceae1afe9da63a6bab7ff 100644 GIT binary patch literal 1855 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 ZVSYys`@d?L^aJp-ZL_y~=_c0EUjgZ(L^ diff --git a/test/fixtures/controller.bar/borderWidth/negative.js b/test/fixtures/controller.bar/borderWidth/negative.js new file mode 100644 index 00000000000..5a5189d7fbe --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/negative.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: -2 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + }, + { + data: [0, 5, 10, null, -10, -5], + borderWidth: {left: -5, top: -5, bottom: -5, right: -5}, + borderSkipped: false + }, + { + data: [0, 5, 10, null, -10, -5], + borderWidth: {} + }, + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: '#888', + borderColor: '#f00', + borderWidth: -4 + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/negative.png b/test/fixtures/controller.bar/borderWidth/negative.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2a445d99250cb59e9bc4d06b00b762801bfad4 GIT binary patch literal 1766 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@o)_S@)hE&XXd)+tpu!Dr_ z#RiVHAdc1q&DMA6X^C7%gSeIkaH&qnYf^aEf3>>r$=m1~lS=nr=KMEt>$T@mufLrz zeqOULzvj)2ikOFe%~;&Eor$1TN=H|5^0Jb!G?=U=h!Lm4<28k~h0m>d`wSU4FN6&M(r zSQ;1v7#JLx96)jkj0y}a3=R{3LQD(_9zY>R1_2cYmIg?KIgDx>fFNNIc*Ib^U@;n3 tLogjS%;#iaurOg%7%lN=QS$da;9h%i_KZWT^k*{wfv2mV%Q~loCID${;hF#d literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/object.js b/test/fixtures/controller.bar/borderWidth/object.js new file mode 100644 index 00000000000..9133b30aec5 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/object.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: {bottom: 1, left: 2, top: 3, right: 4} + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#888', + borderSkipped: false, + borderWidth: {bottom: 4, left: 3, top: 2, right: 1} + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/object.png b/test/fixtures/controller.bar/borderWidth/object.png new file mode 100644 index 0000000000000000000000000000000000000000..04576006af463429104cd7595d23318d113cf6cd GIT binary patch literal 2113 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is>yI6Pe(Ln`LHy|pnjHC&`M z@#wk(XCnT@ zzVF|&pJ9b61B2KPj)qtL5BTSO)nmxwWMKFw&Zxk!ij$#%m8F4Ug&I(S00Yy4Squyj zz^H1-Vq!>O991xM!lAK~@xzPBLJa(Nz?d@v#vBVIqXDS}{R}%^9R6Li{qb_{hVFNt zzSea!Ffcrb&)a_c`Rb&vWxH#4-TwIUknDWvd#^+78SbgSV@QyIn6RLevEba#Z!3P! zFW$F=#o?=L{gXMLbr=}heseSwpPT+JeQxo-rK}EL@Bh8`{l7IZmHqH%X<&G9Lj|Zr z7?_-rCj+y-A22=j`2w9~0W{Rg44C8|jw%}NLEzxWVDpIs64k&$2~^O7DTjQ9n7p#~W!Hm(3=AoPh`RKJGJ~EKQ^WFlhNn_zA9%NJ23ETa N44$rjF6*2UngBaj#$^Bi literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/scriptable-object.js b/test/fixtures/controller.bar/borderWidth/scriptable-object.js new file mode 100644 index 00000000000..5bd2903efa7 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/scriptable-object.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return {top: Math.abs(value)}; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + rectangle: { + backgroundColor: 'transparent', + borderColor: '#80808080', + borderSkipped: false, + borderWidth: function(ctx) { + return {left: ctx.dataIndex * 2}; + } + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/scriptable-object.png b/test/fixtures/controller.bar/borderWidth/scriptable-object.png new file mode 100644 index 0000000000000000000000000000000000000000..10e65f5ced56ec31adbd1f15db2e98ef96bd1f45 GIT binary patch literal 1640 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@o^gUf1Ln`LHy=K^z94^sv z(Q{6vpzv9-IaQqJ=4!n1ogr5Ka`KG}Y>Bscmxix6$GKEol3UjO;{ z@wDH6|6Mq6r1H*>r91ILMidh&K-dF-% z@J)u1fuS0xjA5M$P{jnGdPkrz8z%#U#4r?iu(0^t`{!2o?cevbQifFZ#z z%lP3E9{MVaLmLCbf>{hF1Q-}Xgc&567#gye6r33t0(==#fZ@4{vw??^!NHVqf+7RM z3N;2Jpkbq$s2mUs4fU1v_5a_-JU?!4ea_PU-|NG`y~#i|T?$_z%R znk#bW*LJKfXZn!B^k8f4H^p`LI1fzbY`FgIX8VpRbq0SmhPd15@{eB4Ww4pa08?z> zHJS|Qk{=ihJb;Cn0AFaA4j1DR-sJY;>jO0ZVTNPgg&ebxsLQ02i2Y AmH+?% literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/borderWidth/value.png b/test/fixtures/controller.bar/borderWidth/value.png index 3c81aff7f2dfb863751bc423a7c8db002e959871..af89232e9723602b3b02106ba12e409b2a81ac8b 100644 GIT binary patch literal 2064 zcmeAS@N?(olHy`uVBq!ia0y~yU;;83893O0)X@p&(is@ouY0;UhE&XXdwuW9m~e*H zz@zI9oRyfen$vuNoLW_Sz$&&oEk%v93t9c%y70^K_ZYtUdGlWOLC^I0GZWsG+t)Pw zpQs#yh8?>_y`{`U{V2MHzz1_px{rVQsS>+fWLp7VJ% z149G55YQnFj^|| zfq{(^==uW+j0y}45-OvL@q~u|`#Hsx-;dXBFFyD4$1^#3`HFt;Pv)Y)aDVU;sBHo; z1W)Z@bNHqBLx1*tR)=W}3=ixD8JH9t85pKiUSqI$UbFwnVK zFYkU|x%u~<&#Zh642%j4lYTQXENB7d(v!7}3;|9I91TzQF)(N-GO#!l#RC(O05CD_ z)MsGu;sB<$JK_ussw@o*9`Cq;)`HU0cUA@m&QW8AL)dI%dJyN%z+htuj7}C%>0EG| zq2f#P_sDmQ2POl{6bEoYzMYeSp&|>!F&Hh%sZp^lNat;^mIqdcVvGvF>hOyc1HTop gdY5wqW<~ek(#vKsK5(4q;K=|4p00i_>zopr0OWP4(f|Me literal 5109 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 Date: Mon, 25 Feb 2019 00:17:37 -0800 Subject: [PATCH 554/685] Add instructions for image-based tests to the contributors guide (#6073) --- docs/developers/contributing.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index 71cb5533793..15fc938ae09 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -42,6 +42,26 @@ The following commands are now available from the repository root: More information can be found in [gulpfile.js](https://github.com/chartjs/Chart.js/blob/master/gulpfile.js). +### Image-Based Tests + +Some display-related functionality is difficult to test via typical Jasmine units. For this reason, we introduced image-based tests ([#3988](https://github.com/chartjs/Chart.js/pull/3988) and [#5777](https://github.com/chartjs/Chart.js/pull/5777)) to assert that a chart is drawn pixel-for-pixel matching an expected image. + +Generated charts in image-based tests should be **as minimal as possible** and focus only on the tested feature to prevent failure if another feature breaks (e.g. disable the title and legend when testing scales). + +You can create a new image-based test by following the steps below: +- Create a JS file ([example](https://github.com/chartjs/Chart.js/blob/f7b671006a86201808402c3b6fe2054fe834fd4a/test/fixtures/controller.bubble/radius-scriptable.js)) or JSON file ([example](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/fixtures/plugin.filler/fill-line-dataset.json)) that defines chart config and generation options. +- Add this file in `test/fixtures/{spec.name}/{feature-name}.json`. +- Add a [describe line](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/specs/plugin.filler.tests.js#L10) to the beginning of `test/specs/{spec.name}.tests.js` if it doesn't exist yet. +- Run `gulp unittest --watch --inputs=test/specs/{spec.name}.tests.js`. +- Click the *"Debug"* button (top/right): a test should fail with the associated canvas visible. +- Right click on the chart and *"Save image as..."* `test/fixtures/{spec.name}/{feature-name}.png` making sure not to activate the tooltip or any hover functionality +- Refresh the browser page (`CTRL+R`): test should now pass +- Verify test relevancy by changing the feature values *slightly* in the JSON file. + +Tests should pass in both browsers. In general, we've hidden all text in image tests since it's quite difficult to get them passing between different browsers. As a result, it is recommended to hide all scales in image-based tests. It is also recommended to disable animations. If tests still do not pass, adjust [`tolerance` and/or `threshold`](https://github.com/chartjs/Chart.js/blob/1ca0ffb5d5b6c2072176fd36fa85a58c483aa434/test/jasmine.matchers.js) at the beginning of the JSON file keeping them **as low as possible**. + +When a test fails, the expected and actual images are shown. If you'd like to see the images even when the tests pass, set `"debug": true` in the JSON file. + ## Bugs and Issues Please report these on the GitHub page - at github.com/chartjs/Chart.js. Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](https://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow. From 93f4e6e4e8431fdfbdec8654ec325ab9b51cb501 Mon Sep 17 00:00:00 2001 From: Vincent-Ip <40588938+Vincent-Ip@users.noreply.github.com> Date: Wed, 27 Feb 2019 17:06:54 -0500 Subject: [PATCH 555/685] New weight option for pie and doughnut charts (#5951) Add functionality to give pie & doughnut datasets a weight attribute, which affects the relative thickness of the dataset when there are multiple datasets in pie & doughnut charts. The default weight of each dataset is 1, providing any other numerical value will allow the pie or doughnut dataset to be drawn with a thickness relative to its default size. For example a weight of 2 will allow the dataset to be drawn double its typical dataset thickness. Note that the weight attribute will only affect a pie or doughnut chart if there is more than one visible dataset. Using weight on a pie or doughnut dataset when there is only one dataset on the chart will have no affect. --- docs/charts/doughnut.md | 2 + src/controllers/controller.doughnut.js | 40 ++++++++++++-- .../controller.doughnut/doughnut-weight.json | 50 +++++++++++++++++ .../controller.doughnut/doughnut-weight.png | Bin 0 -> 35512 bytes .../controller.doughnut/pie-weight.json | 52 ++++++++++++++++++ .../controller.doughnut/pie-weight.png | Bin 0 -> 28314 bytes 6 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/controller.doughnut/doughnut-weight.json create mode 100644 test/fixtures/controller.doughnut/doughnut-weight.png create mode 100644 test/fixtures/controller.doughnut/pie-weight.json create mode 100644 test/fixtures/controller.doughnut/pie-weight.png diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index ad1af3106ef..65c8767fa9f 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -63,6 +63,7 @@ The doughnut/pie chart allows a number of properties to be specified for each da | [`hoverBackgroundColor`](#interations) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` +| [`weight`](#styling) | `number` | - | - | `1` ### Styling @@ -73,6 +74,7 @@ The style of each arc can be controlled with the following properties: | `backgroundColor` | arc background color. | `borderColor` | arc border color. | `borderWidth` | arc border width (in pixels). +| `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 21e23ebcae8..a6d4a63775a 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -6,6 +6,7 @@ var elements = require('../elements/index'); var helpers = require('../helpers/index'); var resolve = helpers.options.resolve; +var valueOrDefault = helpers.valueOrDefault; defaults._set('doughnut', { animation: { @@ -152,6 +153,7 @@ module.exports = DatasetController.extend({ var arcs = meta.data; var cutoutPercentage = opts.cutoutPercentage; var circumference = opts.circumference; + var chartWeight = me._getRingWeight(me.index); var i, ilen; // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc @@ -180,14 +182,14 @@ module.exports = DatasetController.extend({ chart.borderWidth = me.getMaxBorderWidth(); chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); - chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / (me._getVisibleDatasetWeightTotal() || 1); chart.offsetX = offset.x * chart.outerRadius; chart.offsetY = offset.y * chart.outerRadius; meta.total = me.calculateTotal(); - me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); + me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index); + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength * chartWeight, 0); for (i = 0, ilen = arcs.length; i < ilen; ++i) { me.updateElement(arcs[i], i, reset); @@ -322,7 +324,6 @@ module.exports = DatasetController.extend({ var model = arc._model; var options = arc._options; var getHoverColor = helpers.getHoverColor; - var valueOrDefault = helpers.valueOrDefault; arc.$previousStyle = { backgroundColor: model.backgroundColor, @@ -375,5 +376,36 @@ module.exports = DatasetController.extend({ } return values; + }, + + /** + * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly + * @private + */ + _getRingWeightOffset: function(datasetIndex) { + var ringWeightOffset = 0; + + for (var i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + + return ringWeightOffset; + }, + + /** + * @private + */ + _getRingWeight: function(dataSetIndex) { + return Math.max(valueOrDefault(this.chart.data.datasets[dataSetIndex].weight, 1), 0); + }, + + /** + * Returns the sum of all visibile data set weights. This value can be 0. + * @private + */ + _getVisibleDatasetWeightTotal: function() { + return this._getRingWeightOffset(this.chart.data.datasets.length); } }); diff --git a/test/fixtures/controller.doughnut/doughnut-weight.json b/test/fixtures/controller.doughnut/doughnut-weight.json new file mode 100644 index 00000000000..769814b0ccb --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-weight.json @@ -0,0 +1,50 @@ +{ + "config": { + "type": "doughnut", + "data": { + "datasets": [{ + "data": [ 1, 1 ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 2, 1 ], + "hidden": true, + "borderWidth": 0 + }, + { + "data": [ 3, 3 ], + "weight": 3, + "backgroundColor": [ + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 4, 0 ], + "weight": 0, + "borderWidth": 0 + }, + { + "data": [ 5, 0 ], + "weight": -2, + "borderWidth": 0 + }], + "labels": [ "label0", "label1" ] + }, + "options": { + "legend": false, + "title": false + } + }, + "options": { + "canvas": { + "height": 500, + "width": 500 + } + } +} \ No newline at end of file diff --git a/test/fixtures/controller.doughnut/doughnut-weight.png b/test/fixtures/controller.doughnut/doughnut-weight.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ab34c8a9af80411801c8bc1cd856413811e1e6 GIT binary patch literal 35512 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.doughnut/pie-weight.json b/test/fixtures/controller.doughnut/pie-weight.json new file mode 100644 index 00000000000..91d6c110ba9 --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-weight.json @@ -0,0 +1,52 @@ +{ + "config": { + "type": "pie", + "data": { + "datasets": [ + { + "data": [ 1, 1 ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 2, 1 ], + "hidden": true, + "borderWidth": 0 + }, + { + "data": [ 3, 3 ], + "weight": 3, + "backgroundColor": [ + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 4, 0 ], + "weight": 0, + "borderWidth": 0 + }, + { + "data": [ 5, 0 ], + "weight": -2, + "borderWidth": 0 + } + ], + "labels": [ "label0", "label1" ] + }, + "options": { + "legend": false, + "title": false + } + }, + "options": { + "canvas": { + "height": 500, + "width": 500 + } + } +} \ No newline at end of file diff --git a/test/fixtures/controller.doughnut/pie-weight.png b/test/fixtures/controller.doughnut/pie-weight.png new file mode 100644 index 0000000000000000000000000000000000000000..606ae0ebc59fe78c76af278b60b36029424024e4 GIT binary patch literal 28314 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%0A!_F%|a?ZY_F(f^&-Yv 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

    !MM(KVXyqLl1E~uhg5&Z<8SsG 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 literal 0 HcmV?d00001 From b2f7adc2b4539ee8d5208ccef8e0f6ab1f9c8558 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 1 Mar 2019 09:13:21 +0100 Subject: [PATCH 556/685] Revamp the README.md and add link to the awesome list (#6096) Integrate the upcoming awesome list and make our README.md welcome page more user friendly by displaying the Chart.js logo, adding the docs TOC and removing instructions that was already in the docs. --- README.md | 69 ++++++++++++-------------------------- docs/SUMMARY.md | 2 +- docs/notes/extensions.md | 72 ---------------------------------------- 3 files changed, 23 insertions(+), 120 deletions(-) delete mode 100644 docs/notes/extensions.md diff --git a/README.md b/README.md index 8673e30cb6a..5a522a5e915 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,31 @@ -# Chart.js - -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-chartjs-blue.svg?style=flat-square&maxAge=3600)](https://chartjs-slack.herokuapp.com/) - -*Simple HTML5 Charts using the canvas element* [chartjs.org](https://www.chartjs.org) - -## Installation - -You can download the latest version of Chart.js from the [GitHub releases](https://github.com/chartjs/Chart.js/releases/latest) or use a [Chart.js CDN](https://cdnjs.com/libraries/Chart.js). - -To install via npm: - -```bash -npm install chart.js --save -``` - -To install via bower: -```bash -bower install chart.js --save -``` - -### Selecting the Correct Build - -Chart.js provides two different builds for you to choose: `Stand-Alone Build`, `Bundled Build`. - -#### Stand-Alone Build -Files: -* `dist/Chart.js` -* `dist/Chart.min.js` - -The stand-alone build includes Chart.js as well as the color parsing library. If this version is used, you are required to include [Moment.js](https://momentjs.com/) before Chart.js for the functionality of the time axis. - -#### Bundled Build -Files: -* `dist/Chart.bundle.js` -* `dist/Chart.bundle.min.js` - -The bundled build includes Moment.js in a single file. You should use this version if you require time axes and want to include a single file. You should not use this build if your application already included Moment.js. Otherwise, Moment.js will be included twice which results in increasing page load time and possible version compatibility issues. The Moment.js version in the bundled build is private to Chart.js so if you want to use Moment.js yourself, it's better to use Chart.js (non bundled) and import Moment.js manually. +

    +
    + Simple yet flexible JavaScript charting for designers & developers +

    + +
    ## Documentation -You can find documentation at [www.chartjs.org/docs](https://www.chartjs.org/docs). The markdown files that build the site are available under `/docs`. Previous version documentation is available at [www.chartjs.org/docs/latest/developers/#previous-versions](https://www.chartjs.org/docs/latest/developers/#previous-versions). +- [Introduction](https://www.chartjs.org/docs/latest/) +- [Getting Started](https://www.chartjs.org/docs/latest/getting-started/) +- [General](https://www.chartjs.org/docs/latest/general/) +- [Configuration](https://www.chartjs.org/docs/latest/configuration/) +- [Charts](https://www.chartjs.org/docs/latest/charts/) +- [Axes](https://www.chartjs.org/docs/latest/axes/) +- [Developers](https://www.chartjs.org/docs/latest/developers/) +- [Popular Extensions](https://github.com/chartjs/awesome) +- [Samples](https://www.chartjs.org/samples/) ## Contributing -Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md) first. For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](https://stackoverflow.com/questions/tagged/chartjs). - -## Building -Instructions on building and testing Chart.js can be found in [the documentation](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md#building-and-testing). - -## Thanks -- [BrowserStack](https://browserstack.com) for allowing our team to test on thousands of browsers. -- [@n8agrin](https://twitter.com/n8agrin) for the Twitter handle donation. +Instructions on building and testing Chart.js can be found in [the documentation](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://github.com/chartjs/Chart.js/blob/master/docs/developers/contributing.md) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chartjs) with the `chartjs` tag. ## License diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 15265bd2b5a..f0beb678cdc 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -51,5 +51,5 @@ * [Contributing](developers/contributing.md) * [Additional Notes](notes/README.md) * [Comparison Table](notes/comparison.md) - * [Popular Extensions](notes/extensions.md) + * [Popular Extensions](https://github.com/chartjs/awesome) * [License](notes/license.md) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md deleted file mode 100644 index d526ff0c4fd..00000000000 --- a/docs/notes/extensions.md +++ /dev/null @@ -1,72 +0,0 @@ -# Popular Extensions - -Many extensions can be found on the [Chart.js GitHub organization](https://github.com/chartjs) or on the [npm registry](https://www.npmjs.com/search?q=chartjs-). - -## Charts - - - chartjs-chart-financial - Adds financial chart types such as a candlestick. - - Chart.BarFunnel.js - Adds a bar funnel chart type. - - Chart.LinearGauge.js - Adds a linear gauge chart type. - - Chart.Smith.js - Adds a smith chart type. - -In addition, many charts can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-chart-). - -## Plugins - - - chartjs-plugin-annotation - Draws lines and boxes on chart area. - - chartjs-plugin-colorschemes - Enables automatic coloring using predefined color schemes. - - chartjs-plugin-crosshair - Adds a data crosshair to line and scatter charts - - chartjs-plugin-datalabels - Displays labels on data for any type of charts. - - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. - - chartjs-plugin-rough - Draws charts in a sketchy, hand-drawn-like style. - - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. - - chartjs-plugin-streaming - Enables to create live streaming charts. - - chartjs-plugin-style - Provides more styling options. - - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - - chartjs-plugin-zoom - Enables zooming and panning on charts. - -In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). - -## Integrations - -### Angular (v2+) - - - emn178/angular2-chartjs - - valor-software/ng2-charts - -### Angular (v1) - - angular-chart.js - - tc-angular-chartjs - - angular-chartjs - - Angular Chart-js Directive - -### React - - react-chartjs2 - - react-chartjs-2 - -### Django - - Django JChart - - Django Chartjs - -### Ruby on Rails - - chartjs-ror - -### Laravel - - laravel-chartjs - -### Vue.js - - vue-chartjs - -### Java - - Chart.java - - Wicked-Charts - -### GWT (Google Web toolkit) - - Charba - -### Ember.js - - ember-cli-chart - -### Omi (v5+) - - omi-chart From 946c6d0617bfa0eb21e86f6f75d7f34b2bb1dc85 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 2 Mar 2019 00:03:20 +0100 Subject: [PATCH 557/685] Fix document errors related to ticks (#6099) --- docs/axes/cartesian/README.md | 4 ++-- docs/axes/styling.md | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 86611b2fdb4..2c4ab87ef9d 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -29,10 +29,10 @@ The following options are common to all cartesian axes but do not apply to other | `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what. | `autoSkipPadding` | `number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. | `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* -| `maxRotation` | `number` | `90` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* +| `maxRotation` | `number` | `50` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* | `minRotation` | `number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* | `mirror` | `boolean` | `false` | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* -| `padding` | `number` | `10` | Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. +| `padding` | `number` | `0` | Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. ### Axis ID The properties `dataset.xAxisID` or `dataset.yAxisID` have to match the scale properties `scales.xAxes.id` or `scales.yAxes.id`. This is especially needed if multi-axes charts are used. diff --git a/docs/axes/styling.md b/docs/axes/styling.md index e0089cb3725..23b212348da 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -54,10 +54,11 @@ The minorTick configuration is nested under the ticks configuration in the `mino | `lineHeight` | number|string | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). ## Major Tick Configuration -The majorTick configuration is nested under the ticks configuration in the `major` key. It defines options for the major tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. +The majorTick configuration is nested under the ticks configuration in the `major` key. It defines options for the major tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. These options are disabled by default. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- +| `enabled` | `boolean` | `false` | If true, major tick options are used to show major ticks. | `callback` | `function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). | `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. From 653e9a954eae83a8fcf157e6d402bcacaac51821 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 3 Mar 2019 01:26:40 -0800 Subject: [PATCH 558/685] Add a note about how to include an example against master (#6107) --- .github/ISSUE_TEMPLATE/BUG.md | 11 +++++++++-- docs/developers/contributing.md | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG.md b/.github/ISSUE_TEMPLATE/BUG.md index 90ebd643e2b..1d9ca8c2754 100644 --- a/.github/ISSUE_TEMPLATE/BUG.md +++ b/.github/ISSUE_TEMPLATE/BUG.md @@ -21,13 +21,20 @@ labels: 'type: bug' ## Possible Solution - - + ## Steps to Reproduce (for bugs) ## Context diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md index 15fc938ae09..6180a3139bc 100644 --- a/docs/developers/contributing.md +++ b/docs/developers/contributing.md @@ -72,6 +72,6 @@ Guidelines for reporting bugs: - Check the issue search to see if it has already been reported - Isolate the problem to a simple test case - - Please include a demonstration of the bug on a website such as [JS Bin](https://jsbin.com/), [JS Fiddle](https://jsfiddle.net/), or [Codepen](https://codepen.io/pen/). ([Template](https://codepen.io/pen?template=JXVYzq)) + - Please include a demonstration of the bug on a website such as [JS Bin](https://jsbin.com/), [JS Fiddle](https://jsfiddle.net/), or [Codepen](https://codepen.io/pen/). ([Template](https://codepen.io/pen?template=JXVYzq)). If filing a bug against `master`, you may reference the latest code via https://www.chartjs.org/dist/master/Chart.min.js (changing the filename to point at the file you need as appropriate). Do not rely on these files for production purposes as they may be removed at any time. Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. From 0ac215b56a14373d3e49d927d39e9e4ccaff52cd Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sun, 3 Mar 2019 06:00:24 -0800 Subject: [PATCH 559/685] Improve financial sample tooltips and interactions (#6089) --- samples/scales/time/financial.html | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index bce7d587b93..875bb60e158 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -33,8 +33,8 @@ } function randomBar(date, lastClose) { - var open = randomNumber(lastClose * 0.95, lastClose * 1.05); - var close = randomNumber(open * 0.95, open * 1.05); + var open = randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2); + var close = randomNumber(open * 0.95, open * 1.05).toFixed(2); return { t: date.valueOf(), y: close @@ -44,12 +44,10 @@ var dateFormat = 'MMMM DD YYYY'; var date = moment('April 01 2017', dateFormat); var data = [randomBar(date, 30)]; - var labels = [date]; while (data.length < 60) { date = date.clone().add(1, 'd'); if (date.isoWeekday() <= 5) { data.push(randomBar(date, data[data.length - 1].y)); - labels.push(date); } } @@ -61,7 +59,6 @@ var cfg = { type: 'bar', data: { - labels: labels, datasets: [{ label: 'CHRT - Chart.js Corporation', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), @@ -80,7 +77,8 @@ type: 'time', distribution: 'series', ticks: { - source: 'labels' + source: 'data', + autoSkip: true } }], yAxes: [{ @@ -89,9 +87,24 @@ labelString: 'Closing price ($)' } }] + }, + tooltips: { + intersect: false, + mode: 'index', + callbacks: { + label: function(tooltipItem, myData) { + var label = myData.datasets[tooltipItem.datasetIndex].label || ''; + if (label) { + label += ': '; + } + label += parseFloat(tooltipItem.value).toFixed(2); + return label; + } + } } } }; + var chart = new Chart(ctx, cfg); document.getElementById('update').addEventListener('click', function() { From 35273ee9488b9764485e6a0adfe92390fa01b130 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 3 Mar 2019 15:19:11 +0100 Subject: [PATCH 560/685] Optimize the npm package by removing useless files (#6105) Explicitly target files that should be included in the npm package, making it 10x smaller by removing the docs, samples, scripts, sources, tests and other useless files. --- .npmignore | 13 ------------- karma.conf.js | 4 ++-- package.json | 6 ++++++ rollup.config.js | 2 +- src/{chart.js => index.js} | 0 src/scales/scale.category.js | 2 +- src/scales/scale.linear.js | 2 +- src/scales/scale.logarithmic.js | 2 +- src/scales/scale.radialLinear.js | 2 +- src/scales/scale.time.js | 2 +- 10 files changed, 14 insertions(+), 21 deletions(-) delete mode 100644 .npmignore rename src/{chart.js => index.js} (100%) diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 47b4948ed27..00000000000 --- a/.npmignore +++ /dev/null @@ -1,13 +0,0 @@ -/.git -/.github -/coverage -/custom -/dist/*.zip -/docs/index.md -/node_modules - -.codeclimate.yml -.DS_Store -.gitignore -.idea -.travis.yml diff --git a/karma.conf.js b/karma.conf.js index 02878567571..d7aae642a2e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -49,12 +49,12 @@ module.exports = function(karma) { {pattern: 'test/fixtures/**/*.png', included: false}, 'node_modules/moment/min/moment.min.js', 'test/index.js', - 'src/chart.js' + 'src/index.js' ].concat(args.inputs), preprocessors: { 'test/index.js': ['rollup'], - 'src/chart.js': ['sources'] + 'src/index.js': ['sources'] }, rollupPreprocessor: { diff --git a/package.json b/package.json index 72c359debf4..5d16f9323cc 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,12 @@ "bugs": { "url": "https://github.com/chartjs/Chart.js/issues" }, + "files": [ + "bower.json", + "composer.json", + "dist/*.css", + "dist/*.js" + ], "devDependencies": { "clean-css": "^4.2.1", "coveralls": "^3.0.0", diff --git a/rollup.config.js b/rollup.config.js index 1f0dbbacac5..fc53f1a7460 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,7 @@ const optional = require('./rollup.plugins').optional; const stylesheet = require('./rollup.plugins').stylesheet; const pkg = require('./package.json'); -const input = 'src/chart.js'; +const input = 'src/index.js'; const banner = `/*! * Chart.js v${pkg.version} * ${pkg.homepage} diff --git a/src/chart.js b/src/index.js similarity index 100% rename from src/chart.js rename to src/index.js diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index d837a7365da..b9e51bcb162 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -130,5 +130,5 @@ module.exports = Scale.extend({ } }); -// INTERNAL: static default options, registered in src/chart.js +// INTERNAL: static default options, registered in src/index.js module.exports._defaults = defaultConfig; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 41a4e4168a3..a2199b66e78 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -186,5 +186,5 @@ module.exports = LinearScaleBase.extend({ } }); -// INTERNAL: static default options, registered in src/chart.js +// INTERNAL: static default options, registered in src/index.js module.exports._defaults = defaultConfig; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 06b206df27c..fd67f0b19a4 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -345,5 +345,5 @@ module.exports = Scale.extend({ } }); -// INTERNAL: static default options, registered in src/chart.js +// INTERNAL: static default options, registered in src/index.js module.exports._defaults = defaultConfig; diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index d19c03e99bc..ce605bde404 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -530,5 +530,5 @@ module.exports = LinearScaleBase.extend({ } }); -// INTERNAL: static default options, registered in src/chart.js +// INTERNAL: static default options, registered in src/index.js module.exports._defaults = defaultConfig; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 7d1df171e0f..0db15bb7859 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -764,5 +764,5 @@ module.exports = Scale.extend({ } }); -// INTERNAL: static default options, registered in src/chart.js +// INTERNAL: static default options, registered in src/index.js module.exports._defaults = defaultConfig; From 344628ba9c5fc95a1edbc9cf94488461d24a4b32 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Mon, 4 Mar 2019 10:11:57 +0200 Subject: [PATCH 561/685] Fix animation regression introduced by #5331 (#6108) --- src/core/core.animations.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/core.animations.js b/src/core/core.animations.js index 54645ddd996..df6100f5b1e 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -93,20 +93,24 @@ module.exports = { */ advance: function() { var animations = this.animations; - var animation, chart; + var animation, chart, numSteps, nextStep; var i = 0; + // 1 animation per chart, so we are looping charts here while (i < animations.length) { animation = animations[i]; chart = animation.chart; + numSteps = animation.numSteps; - animation.currentStep = Math.floor((Date.now() - animation.startTime) / animation.duration * animation.numSteps); - animation.currentStep = Math.min(animation.currentStep, animation.numSteps); + // Make sure that currentStep starts at 1 + // https://github.com/chartjs/Chart.js/issues/6104 + nextStep = Math.floor((Date.now() - animation.startTime) / animation.duration * numSteps) + 1; + animation.currentStep = Math.min(nextStep, numSteps); helpers.callback(animation.render, [chart, animation], chart); helpers.callback(animation.onAnimationProgress, [animation], chart); - if (animation.currentStep >= animation.numSteps) { + if (animation.currentStep >= numSteps) { helpers.callback(animation.onAnimationComplete, [animation], chart); chart.animating = false; animations.splice(i, 1); From 858cc80a1fbbf33e70808e75aaae94c21968d325 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 4 Mar 2019 00:15:29 -0800 Subject: [PATCH 562/685] Properly initialize variables if ticks aren't being displayed (#6100) --- src/core/core.scale.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 5384e323613..1f65bc564b6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -171,6 +171,9 @@ module.exports = Element.extend({ top: 0, bottom: 0 }, margins); + + me._maxLabelLines = 0; + me.longestLabelWidth = 0; me.longestTextCache = me.longestTextCache || {}; // Dimensions @@ -419,20 +422,18 @@ module.exports = Element.extend({ } } - // Don't bother fitting the ticks if we are not showing them + // Don't bother fitting the ticks if we are not showing the labels if (tickOpts.display && display) { var largestTextWidth = helpers.longestText(me.ctx, tickFont.string, labels, me.longestTextCache); var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels); var lineSpace = tickFont.size * 0.5; var tickPadding = me.options.ticks.padding; - // Store max number of lines used in labels for _autoSkip + // Store max number of lines and widest label for _autoSkip me._maxLabelLines = tallestLabelHeightInLines; + me.longestLabelWidth = largestTextWidth; if (isHorizontal) { - // A horizontal axis is more constrained by the height. - me.longestLabelWidth = largestTextWidth; - var angleRadians = helpers.toRadians(me.labelRotation); var cosRotation = Math.cos(angleRadians); var sinRotation = Math.sin(angleRadians); @@ -679,11 +680,11 @@ module.exports = Element.extend({ var cos = Math.abs(Math.cos(rot)); var sin = Math.abs(Math.sin(rot)); - var padding = optionTicks.autoSkipPadding; - var w = me.longestLabelWidth + padding || 0; + var padding = optionTicks.autoSkipPadding || 0; + var w = (me.longestLabelWidth + padding) || 0; var tickFont = helpers.options._parseFont(optionTicks); - var h = me._maxLabelLines * tickFont.lineHeight + padding; + var h = (me._maxLabelLines * tickFont.lineHeight + padding) || 0; // Calculate space needed for 1 tick in axis direction. return isHorizontal @@ -744,7 +745,7 @@ module.exports = Element.extend({ var isHorizontal = me.isHorizontal(); var parseFont = helpers.options._parseFont; - var ticks = optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); + var ticks = optionTicks.display && optionTicks.autoSkip ? me._autoSkip(me.getTicks()) : me.getTicks(); var tickFontColor = valueOrDefault(optionTicks.fontColor, defaultFontColor); var tickFont = parseFont(optionTicks); var lineHeight = tickFont.lineHeight; From 31aebf3bab8a6a45edce853360bc469ed42d386e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 4 Mar 2019 09:57:08 +0100 Subject: [PATCH 563/685] Include generated CSS in the GitHub releases --- .travis.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 574bfe9a27a..0be85778214 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,13 +42,12 @@ deploy: branch: release - provider: releases api_key: $GITHUB_AUTH_TOKEN - file: - - "./dist/Chart.bundle.js" - - "./dist/Chart.bundle.min.js" - - "./dist/Chart.js" - - "./dist/Chart.min.js" - - "./dist/Chart.js.zip" skip_cleanup: true + file_glob: true + file: + - ./dist/*.css + - ./dist/*.js + - ./dist/*.zip on: tags: true - provider: npm From f9f048a5c5ff460e300eec43d7ed6a1fb4f29809 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 4 Mar 2019 09:58:08 +0100 Subject: [PATCH 564/685] Bump version to 2.8.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d16f9323cc..38990b0aec2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "https://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.3", + "version": "2.8.0-rc.1", "license": "MIT", "jsdelivr": "dist/Chart.min.js", "unpkg": "dist/Chart.min.js", From d3b7559b9b2e1ccb62cd315bd103cafbb34118a2 Mon Sep 17 00:00:00 2001 From: Jon Rimmer Date: Wed, 6 Mar 2019 08:11:24 +0000 Subject: [PATCH 565/685] Tighten check for detecting if Moment is installed (#6113) --- src/adapters/adapter.moment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/adapter.moment.js b/src/adapters/adapter.moment.js index e028b52f3ae..5d7c1f3103d 100644 --- a/src/adapters/adapter.moment.js +++ b/src/adapters/adapter.moment.js @@ -18,7 +18,7 @@ var FORMATS = { year: 'YYYY' }; -adapters._date.override(moment ? { +adapters._date.override(typeof moment === 'function' ? { _id: 'moment', // DEBUG ONLY formats: function() { From 87a74f99a1e73fef3ebd314a11c322829c3d7cd1 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Wed, 6 Mar 2019 09:12:29 +0100 Subject: [PATCH 566/685] Fix missing Chart.Chart (deprecated) alias (#6112) --- src/index.js | 9 +++++++++ test/specs/global.deprecations.tests.js | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/index.js b/src/index.js index 586dcc38295..a8286b302c1 100644 --- a/src/index.js +++ b/src/index.js @@ -51,6 +51,15 @@ if (typeof window !== 'undefined') { // DEPRECATIONS +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Chart + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +Chart.Chart = Chart; + /** * Provided for backward compatibility, not available anymore * @namespace Chart.Legend diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index d9e705a1c32..437a316fe0b 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -24,6 +24,12 @@ describe('Deprecations', function() { }); }); + describe('Chart.Chart', function() { + it('should be defined as an alias to Chart', function() { + expect(Chart.Chart).toBe(Chart); + }); + }); + describe('Chart.helpers.aliasPixel', function() { it('should be defined as a function', function() { expect(typeof Chart.helpers.aliasPixel).toBe('function'); From f5ff45693e79093279aa8e1463721976c86bea5c Mon Sep 17 00:00:00 2001 From: Roman Borovik <31778230+Starmordar@users.noreply.github.com> Date: Mon, 11 Mar 2019 11:06:50 +0300 Subject: [PATCH 567/685] Correct typo in a comment in test/index.js (#6122) --- test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.js b/test/index.js index b3fa6cf3b60..c0ed1526801 100644 --- a/test/index.js +++ b/test/index.js @@ -34,7 +34,7 @@ var utils = require('./utils'); window.waitForResize = utils.waitForResize; window.createMockContext = createMockContext; - // some style initialization to limit differences between browsers across different plateforms. + // some style initialization to limit differences between browsers across different platforms. utils.injectCSS( '.chartjs-wrapper, .chartjs-wrapper canvas {' + 'border: 0;' + From eddd1f14ba657d64675c6e23a2451b807c7a609b Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 12 Mar 2019 08:35:39 +0100 Subject: [PATCH 568/685] Keep the previous extensions page link alive (#6127) Instead of a direct link, restore the extensions.md file which now redirects /notes/extensions.html to https://github.com/chartjs/awesome in case anyone bookmarked it / there were links to it. --- book.json | 10 +++++++++- docs/SUMMARY.md | 2 +- docs/notes/extensions.md | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/notes/extensions.md diff --git a/book.json b/book.json index 8b0f73970df..22acbde968c 100644 --- a/book.json +++ b/book.json @@ -3,7 +3,15 @@ "title": "Chart.js documentation", "author": "chartjs", "gitbook": "3.2.2", - "plugins": ["-lunr", "-search", "search-plus", "anchorjs", "chartjs", "ga"], + "plugins": [ + "-lunr", + "-search", + "search-plus", + "anchorjs", + "chartjs", + "ga", + "redirect" + ], "pluginsConfig": { "anchorjs": { "icon": "#", diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f0beb678cdc..15265bd2b5a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -51,5 +51,5 @@ * [Contributing](developers/contributing.md) * [Additional Notes](notes/README.md) * [Comparison Table](notes/comparison.md) - * [Popular Extensions](https://github.com/chartjs/awesome) + * [Popular Extensions](notes/extensions.md) * [License](notes/license.md) diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md new file mode 100644 index 00000000000..d66989b1592 --- /dev/null +++ b/docs/notes/extensions.md @@ -0,0 +1 @@ +!REDIRECT "https://github.com/chartjs/awesome" From 152f1d9725eaf38973687899d69c21f296d5d3f2 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Tue, 12 Mar 2019 11:29:11 +0100 Subject: [PATCH 569/685] Bump version to 2.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38990b0aec2..9d72920c053 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "https://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.8.0-rc.1", + "version": "2.8.0", "license": "MIT", "jsdelivr": "dist/Chart.min.js", "unpkg": "dist/Chart.min.js", From 75e76cffe5270a9d4cece59871d65ba758ec3dd5 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Wed, 13 Mar 2019 11:36:10 +0200 Subject: [PATCH 570/685] Make decimalPlaces private and update CDN links (#6131) --- docs/README.md | 2 +- docs/getting-started/README.md | 2 +- src/core/core.helpers.js | 3 ++- src/scales/scale.linearbase.js | 2 +- test/specs/core.helpers.tests.js | 16 ++++++++-------- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index d0b0544ab0c..9592ecf2c10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ ## Installation -You can download the latest version of Chart.js from the [GitHub releases](https://github.com/chartjs/Chart.js/releases/latest) or use a [Chart.js CDN](https://cdnjs.com/libraries/Chart.js). Detailed installation instructions can be found on the [installation](./getting-started/installation.md) page. +You can download the latest version of Chart.js from the [GitHub releases](https://github.com/chartjs/Chart.js/releases/latest) or use a [Chart.js CDN](https://www.jsdelivr.com/package/npm/chart.js). Detailed installation instructions can be found on the [installation](./getting-started/installation.md) page. ## Creating a Chart diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 5a879c4ce51..9dd7ed684d0 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -11,7 +11,7 @@ First, we need to have a canvas in our page. Now that we have a canvas we can use, we need to include Chart.js in our page. ```html - + ``` Now, we can create a chart. We add a script to our page: diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 5b4aec34c7e..7922747dee9 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -125,8 +125,9 @@ module.exports = function() { * i.e. the number of digits after the decimal point, of the value of this Number. * @param {number} x - A number. * @returns {number} The number of decimal places. + * @private */ - helpers.decimalPlaces = function(x) { + helpers._decimalPlaces = function(x) { if (!helpers.isFinite(x)) { return; } diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 24816e9225e..7f68279db6c 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -44,7 +44,7 @@ function generateTicks(generationOptions, dataRange) { if (stepSize || isNullOrUndef(precision)) { // If a precision is not specified, calculate factor based on spacing - factor = Math.pow(10, helpers.decimalPlaces(spacing)); + factor = Math.pow(10, helpers._decimalPlaces(spacing)); } else { // If the user specified a precision, round to that number of decimal places factor = Math.pow(10, precision); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index b0c7431c859..a075c0e8d85 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -73,14 +73,14 @@ describe('Core helper tests', function() { }); it('should get the correct number of decimal places', function() { - expect(helpers.decimalPlaces(100)).toBe(0); - expect(helpers.decimalPlaces(1)).toBe(0); - expect(helpers.decimalPlaces(0)).toBe(0); - expect(helpers.decimalPlaces(0.01)).toBe(2); - expect(helpers.decimalPlaces(-0.01)).toBe(2); - expect(helpers.decimalPlaces('1')).toBe(undefined); - expect(helpers.decimalPlaces('')).toBe(undefined); - expect(helpers.decimalPlaces(undefined)).toBe(undefined); + expect(helpers._decimalPlaces(100)).toBe(0); + expect(helpers._decimalPlaces(1)).toBe(0); + expect(helpers._decimalPlaces(0)).toBe(0); + expect(helpers._decimalPlaces(0.01)).toBe(2); + expect(helpers._decimalPlaces(-0.01)).toBe(2); + expect(helpers._decimalPlaces('1')).toBe(undefined); + expect(helpers._decimalPlaces('')).toBe(undefined); + expect(helpers._decimalPlaces(undefined)).toBe(undefined); }); it('should get an angle from a point', function() { From 6c9f8999116782bbe289d0481e3d1ec6b49ce808 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Mon, 18 Mar 2019 16:05:27 +0900 Subject: [PATCH 571/685] Add the description of the tooltip alignment options (#6139) --- docs/configuration/tooltip.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 35f2e291fa3..2ae32ac91fb 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -19,17 +19,20 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `titleFontSize` | `number` | `12` | Title font size. | `titleFontStyle` | `string` | `'bold'` | Title font style. | `titleFontColor` | `Color` | `'#fff'` | Title font color. +| `titleAlign` | `string` | `'left'` | Horizontal alignment of the title text lines. [more...](#alignment) | `titleSpacing` | `number` | `2` | Spacing to add to top and bottom of each title line. | `titleMarginBottom` | `number` | `6` | Margin to add on bottom of title section. | `bodyFontFamily` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Body line font. | `bodyFontSize` | `number` | `12` | Body font size. | `bodyFontStyle` | `string` | `'normal'` | Body font style. | `bodyFontColor` | `Color` | `'#fff'` | Body font color. +| `bodyAlign` | `string` | `'left'` | Horizontal alignment of the body text lines. [more...](#alignment) | `bodySpacing` | `number` | `2` | Spacing to add to top and bottom of each tooltip item. | `footerFontFamily` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Footer font. | `footerFontSize` | `number` | `12` | Footer font size. | `footerFontStyle` | `string` | `'bold'` | Footer font style. | `footerFontColor` | `Color` | `'#fff'` | Footer font color. +| `footerAlign` | `string` | `'left'` | Horizontal alignment of the footer text lines. [more...](#alignment) | `footerSpacing` | `number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `number` | `6` | Margin to add before drawing the footer. | `xPadding` | `number` | `6` | Padding to add on left and right of tooltip. @@ -74,6 +77,16 @@ Chart.Tooltip.positioners.custom = function(elements, eventPosition) { }; ``` +### Alignment + +The `titleAlign`, `bodyAlign` and `footerAlign` options define the horizontal position of the text lines with respect to the tooltip box. The following values are supported. + +* `'left'` (default) +* `'right'` +* `'center'` + +These options are only applied to text lines. Color boxes are always aligned to the left edge. + ### Sort Callback Allows sorting of [tooltip items](#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. From 390a8d7b413a5ba557f8b73888bad0b19aba8ed4 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 18 Mar 2019 00:14:01 -0700 Subject: [PATCH 572/685] Add docs for the date adapter used by the time scale (#6134) --- docs/axes/cartesian/time.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index a3d3e016cab..61e04da749b 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -2,6 +2,10 @@ The time scale is used to display times and dates. When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale. +## Date Adapters + +The time scale requires both a date library and corresponding adapter to be present. By default, Chart.js includes an adapter for Moment.js. You may wish to [exclude moment](../../getting-started/integration.md) and choose from [other available adapters](https://github.com/chartjs/awesome#adapters) instead. + ## Data Sets ### Input Data From 4941aa0217ea1cd7029863d08b52fc59aa918af1 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 19 Mar 2019 03:42:41 -0700 Subject: [PATCH 573/685] Improvements to helpers.almostWhole (#6120) --- src/core/core.helpers.js | 2 +- test/specs/core.helpers.tests.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 7922747dee9..52c8aea68a8 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -71,7 +71,7 @@ module.exports = function() { }; helpers.almostWhole = function(x, epsilon) { var rounded = Math.round(x); - return (((rounded - epsilon) < x) && ((rounded + epsilon) > x)); + return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); }; helpers.max = function(array) { return array.reduce(function(max, value) { diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js index a075c0e8d85..1f2bc29d5a1 100644 --- a/test/specs/core.helpers.tests.js +++ b/test/specs/core.helpers.tests.js @@ -46,6 +46,8 @@ describe('Core helper tests', function() { it('should correctly determine if a numbers are essentially whole', function() { expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true); expect(helpers.almostWhole(0.9, 0.0001)).toBe(false); + expect(helpers.almostWhole(1234567890123, 0.0001)).toBe(true); + expect(helpers.almostWhole(1234567890123.001, 0.0001)).toBe(false); }); it('should generate integer ids', function() { @@ -81,6 +83,8 @@ describe('Core helper tests', function() { expect(helpers._decimalPlaces('1')).toBe(undefined); expect(helpers._decimalPlaces('')).toBe(undefined); expect(helpers._decimalPlaces(undefined)).toBe(undefined); + expect(helpers._decimalPlaces(12345678.1234)).toBe(4); + expect(helpers._decimalPlaces(1234567890.1234567)).toBe(7); }); it('should get an angle from a point', function() { From 0d8b8f637f336dfb68ea8b2088fe980bdd0f358e Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 19 Mar 2019 14:25:45 +0200 Subject: [PATCH 574/685] Replace helpers.extend with Object.assign when available or use helpers.merge (#6148) --- src/helpers/helpers.core.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index cc8d888e653..a97b2f7e814 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -275,14 +275,12 @@ var helpers = { * @param {object} argN - Additional objects containing properties to merge in target. * @returns {object} The `target` object. */ - extend: function(target) { - var setFn = function(value, key) { - target[key] = value; - }; - for (var i = 1, ilen = arguments.length; i < ilen; ++i) { - helpers.each(arguments[i], setFn); - } - return target; + extend: Object.assign || function(target) { + return helpers.merge(target, [].slice.call(arguments, 1), { + merger: function(key, dst, src) { + dst[key] = src[key]; + } + }); }, /** From 86ed35446d394ec505d879e79fd29e6ecb199b87 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Thu, 21 Mar 2019 16:03:39 +0800 Subject: [PATCH 575/685] Fix hover animations and optimize pivot() (#6129) --- src/core/core.element.js | 4 +-- test/specs/core.element.tests.js | 7 ++++++ test/specs/core.tooltip.tests.js | 42 +++++++++++++++++--------------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/core/core.element.js b/src/core/core.element.js index 665f20c4302..ecfec2c994d 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -66,7 +66,7 @@ helpers.extend(Element.prototype, { pivot: function() { var me = this; if (!me._view) { - me._view = helpers.clone(me._model); + me._view = helpers.extend({}, me._model); } me._start = {}; return me; @@ -80,7 +80,7 @@ helpers.extend(Element.prototype, { // No animation -> No Transition if (!model || ease === 1) { - me._view = model; + me._view = helpers.extend({}, model); me._start = null; return me; } diff --git a/test/specs/core.element.tests.js b/test/specs/core.element.tests.js index ca8f9b03e7c..8b72a0ccc5a 100644 --- a/test/specs/core.element.tests.js +++ b/test/specs/core.element.tests.js @@ -18,6 +18,7 @@ describe('Core element tests', function() { element.transition(0.25); expect(element._view).toEqual(element._model); + expect(element._view).not.toBe(element._model); expect(element._view.objectProp).toBe(element._model.objectProp); // not cloned element._model.numberProp = 100; @@ -40,5 +41,11 @@ describe('Core element tests', function() { }, colorProp: 'rgb(64, 64, 0)', }); + + // Final transition clones model into view + element.transition(1); + + expect(element._view).toEqual(element._model); + expect(element._view).not.toBe(element._model); }); }); diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index a55e80eb952..6fb4880cb65 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -137,11 +137,11 @@ describe('Core.Tooltip', function() { footer: [], caretPadding: 2, labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }, { - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); @@ -338,8 +338,8 @@ describe('Core.Tooltip', function() { caretPadding: 2, labelTextColors: ['#fff'], labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); @@ -488,11 +488,11 @@ describe('Core.Tooltip', function() { caretPadding: 2, labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }, { - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); @@ -547,6 +547,7 @@ describe('Core.Tooltip', function() { // Check and see if tooltip was displayed var tooltip = chart.tooltip; + var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ // Positioning @@ -568,11 +569,11 @@ describe('Core.Tooltip', function() { afterBody: [], footer: [], labelColors: [{ - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }, { - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); @@ -629,6 +630,7 @@ describe('Core.Tooltip', function() { // Check and see if tooltip was displayed var tooltip = chart.tooltip; + var globalDefaults = Chart.defaults.global; expect(tooltip._view).toEqual(jasmine.objectContaining({ // Positioning @@ -646,8 +648,8 @@ describe('Core.Tooltip', function() { afterBody: [], footer: [], labelColors: [{ - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); }); @@ -1088,11 +1090,11 @@ describe('Core.Tooltip', function() { caretPadding: 2, labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ - borderColor: 'rgb(255, 0, 0)', - backgroundColor: 'rgb(0, 255, 0)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }, { - borderColor: 'rgb(0, 0, 255)', - backgroundColor: 'rgb(0, 255, 255)' + borderColor: globalDefaults.defaultColor, + backgroundColor: globalDefaults.defaultColor }] })); }); From b9290a20de0ac051b80eca3509163d597ecec476 Mon Sep 17 00:00:00 2001 From: Janelle deMent Date: Thu, 21 Mar 2019 04:06:39 -0400 Subject: [PATCH 576/685] Make line options scriptable (#6128) --- docs/charts/line.md | 18 ++--- samples/scriptable/line.html | 20 +++--- src/controllers/controller.line.js | 20 ++++-- .../backgroundColor/scriptable.js | 33 ++++----- .../backgroundColor/scriptable.png | Bin 5213 -> 13867 bytes .../controller.line/backgroundColor/value.js | 10 +-- .../controller.line/backgroundColor/value.png | Bin 5160 -> 14765 bytes .../borderCapStyle/scriptable.js | 68 ++++++++++++++++++ .../borderCapStyle/scriptable.png | Bin 0 -> 6575 bytes .../controller.line/borderCapStyle/value.js | 52 ++++++++++++++ .../controller.line/borderCapStyle/value.png | Bin 0 -> 6586 bytes .../controller.line/borderColor/scriptable.js | 38 +++++----- .../borderColor/scriptable.png | Bin 6276 -> 19662 bytes .../controller.line/borderColor/value.js | 8 ++- .../controller.line/borderColor/value.png | Bin 6231 -> 14015 bytes .../controller.line/borderDash/scriptable.js | 56 +++++++++++++++ .../controller.line/borderDash/scriptable.png | Bin 0 -> 13585 bytes .../controller.line/borderDash/value.js | 47 ++++++++++++ .../controller.line/borderDash/value.png | Bin 0 -> 12670 bytes .../borderDashOffset/scriptable.js | 53 ++++++++++++++ .../borderDashOffset/scriptable.png | Bin 0 -> 7382 bytes .../controller.line/borderDashOffset/value.js | 49 +++++++++++++ .../borderDashOffset/value.png | Bin 0 -> 8601 bytes .../borderJoinStyle/scriptable.js | 61 ++++++++++++++++ .../borderJoinStyle/scriptable.png | Bin 0 -> 16194 bytes .../controller.line/borderJoinStyle/value.js | 52 ++++++++++++++ .../controller.line/borderJoinStyle/value.png | Bin 0 -> 16247 bytes .../controller.line/borderWidth/scriptable.js | 36 +++++----- .../borderWidth/scriptable.png | Bin 11687 -> 15115 bytes .../controller.line/borderWidth/value.js | 11 +-- .../controller.line/borderWidth/value.png | Bin 6145 -> 14299 bytes .../cubicInterpolationMode/scriptable.js | 49 +++++++++++++ .../cubicInterpolationMode/scriptable.png | Bin 0 -> 16162 bytes .../cubicInterpolationMode/value.js | 45 ++++++++++++ .../cubicInterpolationMode/value.png | Bin 0 -> 16162 bytes .../controller.line/fill/scriptable.js | 47 ++++++++++++ .../controller.line/fill/scriptable.png | Bin 0 -> 11922 bytes test/fixtures/controller.line/fill/value.js | 43 +++++++++++ test/fixtures/controller.line/fill/value.png | Bin 0 -> 12152 bytes .../indexable.js | 0 .../indexable.png | Bin .../pointBackgroundColor/scriptable.js | 61 ++++++++++++++++ .../pointBackgroundColor/scriptable.png | Bin 0 -> 5213 bytes .../pointBackgroundColor/value.js | 42 +++++++++++ .../pointBackgroundColor/value.png | Bin 0 -> 5160 bytes .../indexable.js | 0 .../indexable.png | Bin .../pointBorderColor/scriptable.js | 61 ++++++++++++++++ .../pointBorderColor/scriptable.png | Bin 0 -> 6276 bytes .../controller.line/pointBorderColor/value.js | 42 +++++++++++ .../pointBorderColor/value.png | Bin 0 -> 6231 bytes .../indexable.js | 0 .../indexable.png | Bin .../pointBorderWidth/scriptable.js | 61 ++++++++++++++++ .../pointBorderWidth/scriptable.png | Bin 0 -> 11687 bytes .../controller.line/pointBorderWidth/value.js | 44 ++++++++++++ .../pointBorderWidth/value.png | Bin 0 -> 6145 bytes test/specs/core.controller.tests.js | 14 +++- test/specs/plugin.filler.tests.js | 2 - 59 files changed, 1054 insertions(+), 89 deletions(-) create mode 100644 test/fixtures/controller.line/borderCapStyle/scriptable.js create mode 100644 test/fixtures/controller.line/borderCapStyle/scriptable.png create mode 100644 test/fixtures/controller.line/borderCapStyle/value.js create mode 100644 test/fixtures/controller.line/borderCapStyle/value.png create mode 100644 test/fixtures/controller.line/borderDash/scriptable.js create mode 100644 test/fixtures/controller.line/borderDash/scriptable.png create mode 100644 test/fixtures/controller.line/borderDash/value.js create mode 100644 test/fixtures/controller.line/borderDash/value.png create mode 100644 test/fixtures/controller.line/borderDashOffset/scriptable.js create mode 100644 test/fixtures/controller.line/borderDashOffset/scriptable.png create mode 100644 test/fixtures/controller.line/borderDashOffset/value.js create mode 100644 test/fixtures/controller.line/borderDashOffset/value.png create mode 100644 test/fixtures/controller.line/borderJoinStyle/scriptable.js create mode 100644 test/fixtures/controller.line/borderJoinStyle/scriptable.png create mode 100644 test/fixtures/controller.line/borderJoinStyle/value.js create mode 100644 test/fixtures/controller.line/borderJoinStyle/value.png create mode 100644 test/fixtures/controller.line/cubicInterpolationMode/scriptable.js create mode 100644 test/fixtures/controller.line/cubicInterpolationMode/scriptable.png create mode 100644 test/fixtures/controller.line/cubicInterpolationMode/value.js create mode 100644 test/fixtures/controller.line/cubicInterpolationMode/value.png create mode 100644 test/fixtures/controller.line/fill/scriptable.js create mode 100644 test/fixtures/controller.line/fill/scriptable.png create mode 100644 test/fixtures/controller.line/fill/value.js create mode 100644 test/fixtures/controller.line/fill/value.png rename test/fixtures/controller.line/{backgroundColor => pointBackgroundColor}/indexable.js (100%) rename test/fixtures/controller.line/{backgroundColor => pointBackgroundColor}/indexable.png (100%) create mode 100644 test/fixtures/controller.line/pointBackgroundColor/scriptable.js create mode 100644 test/fixtures/controller.line/pointBackgroundColor/scriptable.png create mode 100644 test/fixtures/controller.line/pointBackgroundColor/value.js create mode 100644 test/fixtures/controller.line/pointBackgroundColor/value.png rename test/fixtures/controller.line/{borderColor => pointBorderColor}/indexable.js (100%) rename test/fixtures/controller.line/{borderColor => pointBorderColor}/indexable.png (100%) create mode 100644 test/fixtures/controller.line/pointBorderColor/scriptable.js create mode 100644 test/fixtures/controller.line/pointBorderColor/scriptable.png create mode 100644 test/fixtures/controller.line/pointBorderColor/value.js create mode 100644 test/fixtures/controller.line/pointBorderColor/value.png rename test/fixtures/controller.line/{borderWidth => pointBorderWidth}/indexable.js (100%) rename test/fixtures/controller.line/{borderWidth => pointBorderWidth}/indexable.png (100%) create mode 100644 test/fixtures/controller.line/pointBorderWidth/scriptable.js create mode 100644 test/fixtures/controller.line/pointBorderWidth/scriptable.png create mode 100644 test/fixtures/controller.line/pointBorderWidth/value.js create mode 100644 test/fixtures/controller.line/pointBorderWidth/value.png diff --git a/docs/charts/line.md b/docs/charts/line.md index 557cc8e9b03..6b7385d74c0 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -43,15 +43,15 @@ The line chart allows a number of properties to be specified for each dataset. T | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- -| [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | - | - | `'rgba(0, 0, 0, 0.1)'` -| [`borderCapStyle`](#line-styling) | `string` | - | - | `'butt'` -| [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | - | - | `'rgba(0, 0, 0, 0.1)'` -| [`borderDash`](#line-styling) | `number[]` | - | - | `[]` -| [`borderDashOffset`](#line-styling) | `number` | - | - | `0.0` -| [`borderJoinStyle`](#line-styling) | `string` | - | - | `'miter'` -| [`borderWidth`](#line-styling) | `number` | - | - | `3` -| [`cubicInterpolationMode`](#cubicinterpolationmode) | `string` | - | - | `''` -| [`fill`](#line-styling) | boolean|string | - | - | `true` +| [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` +| [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` +| [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` +| [`borderJoinStyle`](#line-styling) | `string` | Yes | - | `'miter'` +| [`borderWidth`](#line-styling) | `number` | Yes | - | `3` +| [`cubicInterpolationMode`](#cubicinterpolationmode) | `string` | Yes | - | `''` +| [`fill`](#line-styling) | boolean|string | Yes | - | `true` | [`label`](#general) | `string` | - | - | `''` | [`lineTension`](#line-styling) | `number` | - | - | `0.4` | [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` diff --git a/samples/scriptable/line.html b/samples/scriptable/line.html index eb5a940d79d..880f0e6fb49 100644 --- a/samples/scriptable/line.html +++ b/samples/scriptable/line.html @@ -26,14 +26,17 @@ utils.srand(110); + function getLineColor(ctx) { + return utils.color(ctx.datasetIndex); + } + function alternatePointStyles(ctx) { var index = ctx.dataIndex; return index % 2 === 0 ? 'circle' : 'rect'; } function makeHalfAsOpaque(ctx) { - var c = ctx.dataset.backgroundColor; - return utils.transparentize(c); + return utils.transparentize(getLineColor(ctx)); } function adjustRadiusBasedOnData(ctx) { @@ -56,9 +59,7 @@ var data = { labels: utils.months({count: DATA_COUNT}), datasets: [{ - data: generateData(), - backgroundColor: '#4dc9f6', - borderColor: '#4dc9f6', + data: generateData() }] }; @@ -68,8 +69,11 @@ elements: { line: { fill: false, + backgroundColor: getLineColor, + borderColor: getLineColor, }, point: { + backgroundColor: getLineColor, hoverBackgroundColor: makeHalfAsOpaque, radius: adjustRadiusBasedOnData, pointStyle: alternatePointStyles, @@ -87,12 +91,8 @@ // eslint-disable-next-line no-unused-vars function addDataset() { - var newColor = utils.color(chart.data.datasets.length); - chart.data.datasets.push({ - data: generateData(), - backgroundColor: newColor, - borderColor: newColor + data: generateData() }); chart.update(); } diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 49665e2299a..8bc14c3decd 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -179,23 +179,31 @@ module.exports = DatasetController.extend({ _resolveLineOptions: function(element) { var me = this; var chart = me.chart; - var dataset = chart.data.datasets[me.index]; + var datasetIndex = me.index; + var dataset = chart.data.datasets[datasetIndex]; var custom = element.custom || {}; var options = chart.options; var elementOptions = options.elements.line; var values = {}; var i, ilen, key; + // Scriptable options + var context = { + chart: chart, + dataset: dataset, + datasetIndex: datasetIndex + }; + var keys = [ 'backgroundColor', - 'borderWidth', - 'borderColor', 'borderCapStyle', + 'borderColor', 'borderDash', 'borderDashOffset', 'borderJoinStyle', - 'fill', - 'cubicInterpolationMode' + 'borderWidth', + 'cubicInterpolationMode', + 'fill' ]; for (i = 0, ilen = keys.length; i < ilen; ++i) { @@ -204,7 +212,7 @@ module.exports = DatasetController.extend({ custom[key], dataset[key], elementOptions[key] - ]); + ], context); } // The default behavior of lines is to break at null values, according diff --git a/test/fixtures/controller.line/backgroundColor/scriptable.js b/test/fixtures/controller.line/backgroundColor/scriptable.js index 6ede2cb0c15..2171e6696b9 100644 --- a/test/fixtures/controller.line/backgroundColor/scriptable.js +++ b/test/fixtures/controller.line/backgroundColor/scriptable.js @@ -6,18 +6,17 @@ module.exports = { datasets: [ { // option in dataset - data: [0, 5, 10, null, -10, -5], - pointBackgroundColor: function(ctx) { - var value = ctx.dataset.data[ctx.dataIndex] || 0; - return value > 8 ? '#ff0000' - : value > 0 ? '#00ff00' - : value > -8 ? '#0000ff' + data: [4, 5, 10, null, -10, -5], + backgroundColor: function(ctx) { + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' : '#ff00ff'; } }, { // option in element (fallback) - data: [4, -5, -10, null, 10, 5], + data: [-4, -5, -10, null, 10, 5], } ] }, @@ -26,19 +25,21 @@ module.exports = { title: false, elements: { line: { - fill: false, + backgroundColor: function(ctx) { + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#ff00ff'; + } }, point: { - backgroundColor: function(ctx) { - var value = ctx.dataset.data[ctx.dataIndex] || 0; - return value > 8 ? '#ff00ff' - : value > 0 ? '#0000ff' - : value > -8 ? '#ff0000' - : '#00ff00'; - }, - radius: 10, + backgroundColor: '#0000ff', + radius: 10 } }, + layout: { + padding: 32 + }, scales: { xAxes: [{display: false}], yAxes: [ diff --git a/test/fixtures/controller.line/backgroundColor/scriptable.png b/test/fixtures/controller.line/backgroundColor/scriptable.png index c366b6cfadfc7c83395b015d927cb639a60068b0..00f11aa55b09237184a89f898a07250210826a57 100644 GIT binary patch literal 13867 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 literal 5213 zcmXX~cR&+M6Ti@f(4_ZvNE0c7G$Ek03rGnd&q5PH1!>ZI=%6$Y6#Zx_0f~s_NfkkY zpwf%do**TFD2OyEk@j79@2}grot^#7?9A?OH_gV%jGa}O6#xKs$lMqQ0CY$b1+Xw7 zpZD3l*8qSQgp7^s!wQzZ_V_ss#iXpxZ!5> zrbJIfId6TL|2ZF6&DW9E+`k0tdfX=FiLa^N zPW^JLM>SAXKG8Riu=~%gml=%Ykrjh}i3VHTQ%m*mu%vsnLHiZY`Pl#n`?dXacWueK zOVW3{Fxj94f|c%vzn}gJc&W@tW>p#hb5hRMK{zX?#dMDDSHK=M$>Yy=tM4@AbwOQAJX;e~Ye0`m3ml)}MoKxuQouUYu_Z z+54%q?R2v6AZvB}VBnE5jN3fs_p7;QX|ac6ueoO|#r!Sv7LUMOwA7d^QH!1K4HPq# zPJapiPJraj-&=XApZd~J!)N|w!5{tRgZ1!l*MxuU=3Q_uxVq-(jk&_J`uX6`)~o?V z+6lgRUb5(#s2tU(Dt}F9r=@Rt`{;3l=L4&l)RA55J-NngUpvB0otCuKuFD3¯v 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 NUCThTOal}3e*li#D0Kh; literal 5160 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/borderCapStyle/value.js b/test/fixtures/controller.line/borderCapStyle/value.js new file mode 100644 index 00000000000..d428462467f --- /dev/null +++ b/test/fixtures/controller.line/borderCapStyle/value.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [null, 3, 3], + borderCapStyle: 'round', + }, + { + // option in dataset + data: [null, 2, 2], + borderCapStyle: 'square', + }, + { + // option in element (fallback) + data: [null, 1, 1], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + borderCapStyle: 'butt', + borderColor: '#00ff00', + borderWidth: 32, + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderCapStyle/value.png b/test/fixtures/controller.line/borderCapStyle/value.png new file mode 100644 index 0000000000000000000000000000000000000000..20f3116bb32e90d0a8efa8fc0fede41b324dd54d GIT binary patch literal 6586 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 fx# 8 ? '#ff0000' - : value > 0 ? '#00ff00' - : value > -8 ? '#0000ff' - : '#ff00ff'; + data: [4, 5, 10, null, -10, -5], + borderColor: function(ctx) { + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; } }, { // option in element (fallback) - data: [4, -5, -10, null, 10, 5], + data: [-4, -5, -10, null, 10, 5] } ] }, @@ -26,19 +25,24 @@ module.exports = { title: false, elements: { line: { - fill: false, - }, - point: { borderColor: function(ctx) { - var value = ctx.dataset.data[ctx.dataIndex] || 0; - return value > 8 ? '#ff00ff' - : value > 0 ? '#0000ff' - : value > -8 ? '#ff0000' - : '#00ff00'; + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; }, - radius: 10, + borderWidth: 10, + fill: false + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + radius: 16 } }, + layout: { + padding: 32 + }, scales: { xAxes: [{display: false}], yAxes: [ diff --git a/test/fixtures/controller.line/borderColor/scriptable.png b/test/fixtures/controller.line/borderColor/scriptable.png index 6366828ecfc5316b9953dff892bf64e15fbcb224..5df5acb7f76428fcab4b50a3f93b1103d939742a 100644 GIT binary patch literal 19662 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 diff --git a/test/fixtures/controller.line/borderColor/value.js b/test/fixtures/controller.line/borderColor/value.js index 84e4d6adfb9..2b43635490c 100644 --- a/test/fixtures/controller.line/borderColor/value.js +++ b/test/fixtures/controller.line/borderColor/value.js @@ -7,7 +7,7 @@ module.exports = { { // option in dataset data: [0, 5, 10, null, -10, -5], - pointBorderColor: '#ff0000' + borderColor: '#ff0000' }, { // option in element (fallback) @@ -20,13 +20,17 @@ module.exports = { title: false, elements: { line: { + borderColor: '#0000ff', fill: false, }, point: { - borderColor: '#00ff00', + borderColor: '#0000ff', radius: 10, } }, + layout: { + padding: 32 + }, scales: { xAxes: [{display: false}], yAxes: [{display: false}] diff --git a/test/fixtures/controller.line/borderColor/value.png b/test/fixtures/controller.line/borderColor/value.png index 6bfde92f541608c606a5ea1b4988de691a6827e3..53a2dafb048709b15477e15478f6d16daefb1f9f 100644 GIT binary patch literal 14015 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 E01+qrh5!Hn literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/borderDash/value.js b/test/fixtures/controller.line/borderDash/value.js new file mode 100644 index 00000000000..3e6404f0d49 --- /dev/null +++ b/test/fixtures/controller.line/borderDash/value.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000', + borderDash: [5] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + borderColor: '#00ff00', + borderDash: [10], + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderDash/value.png b/test/fixtures/controller.line/borderDash/value.png new file mode 100644 index 0000000000000000000000000000000000000000..0704b19bf8b491d29cc583dea760565f6aa132ea GIT binary patch literal 12670 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
      mrtV!EYx2r?uUy}%>HUe>Jkxm^;!x%<@u=<5 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/borderJoinStyle/value.js b/test/fixtures/controller.line/borderJoinStyle/value.js new file mode 100644 index 00000000000..784800c5994 --- /dev/null +++ b/test/fixtures/controller.line/borderJoinStyle/value.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2], + datasets: [ + { + // option in dataset + data: [6, 18, 6], + borderColor: '#ff0000', + borderJoinStyle: 'round', + }, + { + // option in element (fallback) + data: [2, 14, 2], + borderColor: '#0000ff', + borderJoinStyle: 'bevel', + }, + { + // option in element (fallback) + data: [-2, 10, -2] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + borderColor: '#00ff00', + borderJoinStyle: 'miter', + borderWidth: 25, + fill: false, + tension: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderJoinStyle/value.png b/test/fixtures/controller.line/borderJoinStyle/value.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce1bfdae2eb123d985fde305e746ab1c993af53 GIT binary patch literal 16247 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 ZCYo2MsV6?lj|0FTuwnatr6Tgh{{iHPC_exI literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/borderWidth/scriptable.js b/test/fixtures/controller.line/borderWidth/scriptable.js index a40ac1cb0a4..34d736a191f 100644 --- a/test/fixtures/controller.line/borderWidth/scriptable.js +++ b/test/fixtures/controller.line/borderWidth/scriptable.js @@ -6,18 +6,17 @@ module.exports = { datasets: [ { // option in dataset - data: [0, 5, 10, null, -10, -5], - pointBorderColor: '#0000ff', - pointBorderWidth: function(ctx) { - var value = ctx.dataset.data[ctx.dataIndex] || 0; - return value > 4 ? 10 - : value > -4 ? 5 - : 2; - } + data: [4, 5, 10, null, -10, -5], + borderColor: '#0000ff', + borderWidth: function(ctx) { + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index % 2 ? 10 : 20; + }, + pointBorderColor: '#00ff00' }, { // option in element (fallback) - data: [4, -5, -10, null, 10, 5], + data: [-4, -5, -10, null, 10, 5], } ] }, @@ -26,19 +25,22 @@ module.exports = { title: false, elements: { line: { - fill: false, - }, - point: { borderColor: '#ff0000', borderWidth: function(ctx) { - var value = ctx.dataset.data[ctx.dataIndex] || 0; - return value > 4 ? 2 - : value > -4 ? 5 - : 10; + var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + return index % 2 ? 10 : 20; }, - radius: 10, + fill: false, + }, + point: { + borderColor: '#00ff00', + borderWidth: 5, + radius: 10 } }, + layout: { + padding: 32 + }, scales: { xAxes: [{display: false}], yAxes: [ diff --git a/test/fixtures/controller.line/borderWidth/scriptable.png b/test/fixtures/controller.line/borderWidth/scriptable.png index 7bb6b1b3c779607db82bf4d1a8825986a0c64559..ab54b5a99a3bfbb4c63865663019dd4e95e113d5 100644 GIT binary patch literal 15115 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 Xx0H3^E?3}?D@avQOQHO}W$^z2<6PrL literal 11687 zcmZ9yc|6qL7eD@*mlZfY84% z0Lcvfd6U-R4ggrd+(`dK@Y$IhRE6Zpo5l51St|1{Wc8BprJq6T;W86NY>GwptPCOjmw`jT*Y$l!paF8Q;xCw84&m|B{%mZZGbl4;qDXl(g$V= z=9)P~dh%h+#; zPtZs*qke@S3uYz_%CBzpm4;fmtHZxe$@75p;>xtwELLZ_o-K%=gq@}u)bst(WM<1J zh7+-bz4hd;ZIL6^d5z1t%p>jFXX*FaI%|Vr*%4fYFV07t3FA(G$@`z!e{3U71LjFR ze^`r@7w#%Xe07oWwlclnW1N(m_qc$b{(`7Lzl+NLe^{)u5<5ZbN2Im`r5m>?!5g!s zO^3+V_mB%$eWvQU)5T2@i0boPD7M=KbQ1ZDZ(OQ3YWrxZIxA(3HaUCGpYGQgtqiQd zreuohU!ak)fIQiY=dxtlYfgM+aObTpy{ct4{EU0)82{XAopZrAeeM>Nv0@q@%Ins!Or0gRIw(^)kL(>s=gRuf0}jDP_2~z=RiYRU6Jfa~=>;A92vFrxO^E<2H9@P> zocnTbuAKvCFaAAJJS-`oPP(Pa;b6)92*__U>bYitY`(&2J<1{TC~FYuSXI^QH&;7#sd^4uL4QVAJs?`W5X-TvGKFW8gF#X{+Peq_Iguue&(Kg zu!<47KZf=MB+ge|5~_iT*!jk5et)Z440wDvp_wKO`f+Lqr&gDU4X{r=;CwCg-@DbV|#vH?FWnlYZ{&BW>8`NSN#gvWBk1n5-VeQyr<#KDTK|JhX4>w zh7l^RI`G&l^gB#*7(G|@KU6LHDPevkoH~B_wK!9XR!0ftoQvm)3|bY&%-M-3N;jt& zUKp579lu+~%5TRK3D3@=y%Ux$W0lQ& zmG@I{e?kuVHXBiu3pb86>kuNGu@$5ENn@P0%KX(!Sfi|>nC$s>CFAYNB3CdA%Oih3$iX-?f#E1FfWkV@kY{rmLWMhbIxb z8g4n(O-JV}X$2H-bh2!vlrr3kESbWsiZUb4!QC!#PY;iU&WQ+h@8ts?YWgqNWYO5H zBFbNx-0%_o(?&rGSMbb@A31UwwaKw6IOl3seF=`&=T0M-!vna5G4===nc&dgh2$m8 z3posV*@N7&sbs6D@^b~i9VvpSs61s~_GVn4wa{%c{GAwXfE8Lq1SCZeVL~V)v6{2Lme@t4RPuevm*$pG(}xiP=53N z{g=+DWL{VfoYm?!NX5z~v0o!wMHm|THgOAF0N0T}kK}^HN8f^eXw7nxe&qGQET|{I z*MtvbJDAO6U-`T)sw(!I3|=&Y41Xd?=NjR)=LNPZz9f>Zv@d6}Z-`UexaL+4e+!cb zGGEY4LsaD#kKWVSP@;SkqjR_(vO|+Uo~p2%dFmcunRBW|igJZVAXHQWL8#l~@i|m| zS&wX)OKg#$Wbz11?ZF@j?RM^lHDmmz4Q0MMn-h&t$v?RsfkY#_sf0jBnQX2ce9eB! z0W2=CJ{R)F(r2Cro6Ao0s5$$pOJXS_@V9R9$6z*LN9~{Mfv(|izlt8rO((-I5~zQ4 zEzsn>h0mFT?*Gvvu;0^)mxiGT51oj-fYP6X$mDi@2f?|*;~;mw2*qTdvpjI|Wrs<% zp|?zC?3ssEoc3&m!V@2RLm(Vvqf~#dWP!x&+Y7{&*Gi>axV(pb8^)xUBGv=9)kb6e9c3SB!dwrKuVPCQUxVue>?&_`qB#nwI#NhZ1yWT zLJNCMh>x7eya2o7H9q#d1CB(#KTxPLqxQm0=8-30UDL)I;~%e`c~q#+RG|y~UIsV$ zz48#GmU;TonS{(^wMb}NSr{au8z@n3_zWP)jd2M~s1(;Wti6Im>Pbg_88o$k*?jt- z{;01{nI1<96o9~~g8H(Kuz+=aFjntBdhG-MS1*>y1@1I5TpnxlUg89TfKbshuuRV8 zd546@ESEOA5|! z)(}}5;{498D0kno;08n;OPNG1Ry1Wb#?e?DUb}vnnJxyY>1~JA0VPD`{*qTxV@xi|!&_j6NB z%l2i0SLbc73AHpYmPFsbQW=$3c1)lqj%za~$=HW-g?WnfTt;lJDF!dSrz;QqJqg^< zjh@&byB_xaMPXi!9<4?9m&@&Z!!4jo>9&eeZxnhN6ZE)r(0z+Ws$i>yx`=o%Ft(nY+$}^~vO>`ljm1qe6 zaI|{#xg`zjL8b^L2qy%v9h=rTMeZDAO*#~1S3;iS@o=05zw_zB{&1qn$Huh7VI((- z^sR=c*3owHC4{_oCUG~K!(3fnLwSm_jU8`@q~teVMNbAexjm2+-q)T9R>4CY-R_53aRK;u5qgl9a`u6I~Si_Uq-}35yCK(~VZlXoC@L#;<;uV~d zdTqYnxObr|3Cf)o^4n65y{*lmlzqYLIEoy&774J(%q>VaJPlNwC@?Vl`SPw({KK<= zXti!l!K=Q*3l#Ur4Yr=OFPqiU7TSB$_5N?o!-NBPgkSQ7J#m6zI^UdXT;RdK6!Vhb zXHu;)*Y9#osg7B9-eHw?((~5##P7M$&th`v#<&#Lukq)uQ90kNJO6{~Nl-4vaBa-4 zVaYl!Z|h*qyn1)ATOZ8hcB*oEIQQyAJ}Br@{~49~Au8OS<7GCR-D$x6EFxZyzfuP{ zgs-449G#v@6aHivl4QYvtReDT@3xD2NVFqe?@)ZIrFzBa3Td^T#=%O=LlkK@T)$Y_ z@52&kMSmNIdqxttfL}4*`}Wy1Us|^9N!LmV227X4(;E9Pg#RhowC-_#rVV`jQN>)< z_jG!rbm8;xsK#8HT--v&BPmS9J)~kk$Jc8Mt5&mH_b!g#Cw>3b!=K)*K?x21`e0Y{ z0C+(m?#OiC>??5Vl9F&BCMJ4YGD{K|$uJ~tT_C-?VuZV^=2Z_rqIx`xa&9eib`|Hv z%RxEc|6WX^ZrY^Ki4o@}EwQofl*8o`;}<@|O8knZWqTI?l8%BxLF|7vP}5Gc&fgvc z`6Y8tKR1hr{}8Y8n$`cMa#_%Deh66uu#oiX-Vw$mfzUUvR|E8jRo8*VwX90jg8Vs% zdIwh)s!pZCFik|PqeN>zH_|J+Q1FaLOxlgU?(PYJZ^@%UpTQoNqta~51Bq1^N@XNZ z(Wv@%zTu1m;;zCE=FwVZh0LR*V{hl$0?h=whM%66=Wb=4v$mi|ns6Dlg{RoR7qaAP zoBfR({&rC-TzoECQF?(|&BG;}3$_ihoV>sEU%YvQ>Ak?2>=;f9}9G`RRM@kG+~Dw%^TmmO-a4WYj4t>u2s@mM>`Yd)FLGG|9C$G(y%q2?am z!2%hsiHZSw%5M7HQhROW6TRXKmdqF|cbtn0jdhJv_n&u^7@8kZO!%aV|FmD+$=}Fx zGQ&=iMH7YRPEV}u$Yg{EGwL2t3o|Shpz4uYcp3iy(vp1y!ZETm-SRYLR*5ESpKBnda`oX?icWndTzNd)7 zk{0J@Q%kMQcaBFp2mXoWxBKj(P~+|AQ|IMmKN~-HTaG-FM(cJ(UaFfetk*i@8=o;G zaLliD*H-E9J}dHY-M+Ka&Sl|stGRQ(+Ub^O9#$U^OkU`dZ9s1)(0M@W=O=#iLXYqF z+U+J2J22-ZAWpLMGG6cw{scRoyAY{#VL(cF&(!BO-_vt=tKDrU!5k2~75X`8*f=#> zBBPiL>s?!R<-Q|+W+tB_vM++OwXMYOv~b#P(2b!%mf`yqfeZ{r*M`D2`&C^xNZ_n@ zYgL|*iMdq-d)}`(MkpkXBAcb=3&t8HElbv`6;2^I3n`3tpu-dnBnuZ1Rk?b z*;-R8O?sVib!U4{cSh!HnuKxnhwo}Uhn5V|b*nZX&b@m$d`q!#W>NrFr+)IiOu&4> z&x)SiKqQ+*3jm3Nr0IR6Pj50xSO#_d6Z)UmXqB#Co);3ZP=IP-sBAbY*|i_=LA+)b zzD;BAC2u_r*TEh3;yJ&b9IPH-q`Af(j-KAx2t$3L5Rl}`Z`*aWk_@%y-?*GIuXE!E zGn;HbYI2XiyJQzX{Go>>XY-<-ai9$YtIxzSPQQ&68;9bM$oN;Kt%N;Vu!ls=wQG%v zy{3$VZK=8bQvGWT$E zZuMQOh|5`A8@qw+g?Lv1kkH}Vb5jTB!`xg%(YymK-$88bB%PhAvMUux(0_B3;J!?o zW3fLl`q3m(+%tFLO1w7LA?4lJDAQE}ysi?& zn8L1`RP_hUPdPU#$}{BS7JyR@A_$AVDny^JAFBjOtfkK${(vy$!e3;#D@K8i^@3r-6P4E>e%^0# zSLZp!x_SdQw?%00&T9IeVt@*YlIs#Su|UGTSY;ajU032Xy4f)Tk9b=~*bJ&&^JM51 zB6h-viiTmFR-xdV0{(LUadrPz*1=pd2KFyZ?%Wn+9f1I?wKuOK;cJOiz|Tcig~d;x39VsZxZ$sry1a#+T94H}Cvp_#pud=T7imDRSsExiMJ45zkUzqj1>Cc4;P$_Qdpj$CW>E zuWLIGxnDVwN8_eCJZ7N9jG9G=2RtGdi%gsRttc9t6Y{m_h(E!>BR{2{Om)k3sr`d# zKAFY~Vt@YE6s+oIsXxffz7eT|#{5Oc7OkZa#hT?PiBv<;s2=+st_{qrLsC*``^Ix5E(VEB%BmMQH zi;0f!ZE!I=v~MI|+=(#F9An%^Hxm`MO8TFd>0s`d0NHgq+S}U1oZ{+^s7L*BrSZSq z&Zw>n=|}x?Q!DylMCkQHxefR!Fcb!*qD07G^lZ}xm=k2ydk-Fl8q61naT2X==~juz zkxm5)<3@dYYj;H_1|S#H*b+(%FPT}!@oVvLNnqDS1A~_HqzB4{{-N;-kL(?eJD%hL zu~$VVO{da~ab7`k*`TZX|C|{cPXI3ruC2Ax_NAS6GRzb@ACmmg^FRs-%~lWm&#eWK zqbTxWG7}cXWn;&hN2@G%`Hn<#VbtMJ*KpTAo`~UcfP|t&S4yh>u@&`667%QwADJ8tB=k;p2x4HVwyN%XMH2s=Q62mt4n$eFcaV{Ryc$d+apZq>r+^mtLlST) zybk4FrY7+g{RDEYSAy$HS>aN~r>eO$Jc@j$UE?k?!86KPn&aZu-k_LWM?z5icmN8m zS$|8GVHR2skANu*lQ9TYKk3ZI^Zx~E96$W+NuYT&(228_$NnIH`ETrQSul-CE(a3Y zQnC6A&rf1H$VrsPuJ!Wp9akZM*JN;`A(AZM;2dQLkMSqk zPs3x7_P^)<0b2DY8KkO}`|T%vR^lQ#23nRr^`EU`cpfnsB@67kvx$L~@pN(U@_qyj z;Q|MPlOqYj)euq26K)&e$h}@9FRwUui-VO56T&2Zk@`VQg^pTfC~NuWfZ_wwE~#O}+<^s@&WfwbgTOve(wG=&1zEceh}Ft1yQiO7QitaEns z7TOx>^nq0+$A=6?$wH7qHB|ONu4P5YntkRagWWpoEho$t#vM%J9HRNLrj&EuPW6m6Mx&k%kn&IMG z|9N#Q@{ym`4AZa{^M{${2S7gY`lJ2vw8Iqd!(PAZDPz)@t$+1dvJOR6_(LQx{l%bc z#G-1-pla8pq!RfM5co8@r*11q zB`o0Z>WQfjajnull~u)lGN)>{rAU)u!6uYp;Y?aD;@zp;8_Ql}?6pnY6zzV0L+|mK z?asF6?ZuZ^sLhGi!#p&{`OGT8l%eN0`EZKGtbuOa$&ocI_Mf7GU_a2Q-Sb-~!{MnH z;Gd+5ZnwDxQ65U{Pdqot6v=6JV%-sP?s_|#u~(D z98DOpDU*P`HZa=RbhWz4RDe!PmhOI06 zzQbfgJ{?D{UXLr24>bn!PIlj!VPsIkeq{4X$air^K)sfuG6lD&FdW?({cuE+IA%pN zisu8QIvx8gV4E#UV@b~)>g0}b$f(3d$QPWS+GXj?g>3mVC-^>lGtfbZKZj{bo8NX5 z(V@e}adq~-`4cD*R3BSqn_EX`UI_v7?7C7ccN6y=o)yIe5q9rM9)~Ci!a|N)VqhPC|7KEu!w>8=Ig%wG@;2cDf4lGEl|)win}>J1#uv~Fzh zr9=jv;I-41mg33umeAepL>NkS4Uei%)Px$}Qg{3Lq3*DR)(nNFyVE&r6(L_8Q;E$X zFOZ8^x&nk1v=t~--(H#DuJ&~2O@4EKw|_RyJ$`9EOTg@0(lzz1*Qw#qv!gvm&grzej#O{-+9l~qGIH-po;wX~zT!u}Pu}?R zH{b}Yi}M9cC&L)6{p4m%$frFk0sD=v(4huk1AOG&&OsGE&rS;zKzioH=Teo=SpMM^CLio;or~B*C0T$ow z9T!R#pq$cNThetv5N1D^%p^`^n~st76rx z_r&`NV^SoIQ}{CV8;|ZFDW8*aEN62*?kUhJ@ujd+LMC99IlwgK>Hs-$F>TkmhV~?P+vhCG`tLPxdn0QS`E{c>1R*C8h9t!!?;X(Y#M)*a|l0$ z-Wvr#!ysr^9Aj7bzNABeOq$s&?tAQ#Hiu>E zwL5lbL=1MwK}zwjYrff#AUF3^H5HususV! zl>ClXT#cy95Pkpq5dG1y0l13{F&yi|Ua?;sRq4xm$Bw23wfj`>(pvFrKP`8(o0e;B zOw@G0-Ge~NH1zN>3PjaJ!p6#AW3m1)?&l564_a@GDWL37-d`;mktf9E9V z4KM_5@~KUkGoC?aj~GTz__`go@~x)#sb8}Bkp!-*x1c-pB=r6c;ejX*8G9{68UEzb zRa7oHK@_uwNEYp4wR{g@t4R_!4@o`83xvdM*4lxe*H{E>tL)NmLG7wIed>Y#Z_NoD z7R89gO3aY*-uOYc;O1gRy+c)kn<%9Zc{RuZZilL?)DTqGbLB4#L7=}!SnpA^dcGbZLH@I$U)Nr|aZ<6x1)Lj8w3xQ9?S%}+1no<@3 z8QOxus3e`RnfA83;dgTMS&_m^{9cDas9y?^Sry*DZ-ssiP5!00D1GhRv*hRluSpoT zZ6JeP?LwtU($2g~PR3BqSv?<^9dM(3uJAGrE1@L`Jq5HHf`wsNc;4)e`|#t z&fN0@BUEDH@W3F0CctI(W6Z;eZeFqJQ3>}!Y@tu1Pj{_{9B#CBm;T&VGJ5hMAS+nf z{Vj@sM?t z&!OHlfKL!dll5&XQjTIVo^>;_RaXYKnFeb$YU`R|-(lK^D4vJCnx0AF&Vduqh+OZB z?>9?Fwe4$>#i!u8ufem+$es}Oo&a_G)gxvGY4PF6^p*45IvE*GlE2&f4+tEL{^)rY zhP&eLmg;Vbg$5FDch-I$zOEe0n%Am^tKt6|w%&r$_U^jr01e0KW5cCL7YmdNievpb zqsQV@rd5~A_S4yOW0#gcpu^DVLFeQew1Or^1oo###&)LIg3z@MWm9xX5?Sc4oUOyj zk)QuERAztKeRHZlT)OgaVa}z=^D}}msIB3AHA#sm!jQLC2@&RS?a#kgHC@j9Z5VZ%(UpTK)0Ty zcsmWCdzP-t&}w`?j0DC2T^7)yzAV#V1tU`aS*P>L-r5-svJqaMK8TzkAump0xKgo~05BRS!|M1&le|U+_=% zvyr~u12WfTBs#^VO;=8S4x>f$i<$^~VLmuzgRV0c^nB*vhEbfEe|E05lNOXaE2J diff --git a/test/fixtures/controller.line/borderWidth/value.js b/test/fixtures/controller.line/borderWidth/value.js index 291f422e527..68b16e52e06 100644 --- a/test/fixtures/controller.line/borderWidth/value.js +++ b/test/fixtures/controller.line/borderWidth/value.js @@ -7,8 +7,8 @@ module.exports = { { // option in dataset data: [0, 5, 10, null, -10, -5], - pointBorderColor: '#0000ff', - pointBorderWidth: 6 + borderColor: '#0000ff', + borderWidth: 6 }, { // option in element (fallback) @@ -21,14 +21,17 @@ module.exports = { title: false, elements: { line: { + borderColor: '#00ff00', + borderWidth: 3, fill: false, }, point: { - borderColor: '#00ff00', - borderWidth: 3, radius: 10, } }, + layout: { + padding: 32 + }, scales: { xAxes: [{display: false}], yAxes: [{display: false}] diff --git a/test/fixtures/controller.line/borderWidth/value.png b/test/fixtures/controller.line/borderWidth/value.png index 0b8f38c1d62bd16f190a519710df3009cc801d0d..715ea7200e2cf2ba7b45d3b47b0ce8d237439215 100644 GIT binary patch literal 14299 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 F{twWWuS);` literal 6145 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�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 Q_Z^_DpdpWZ{OtAr2hziOV*mgE literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/cubicInterpolationMode/value.js b/test/fixtures/controller.line/cubicInterpolationMode/value.js new file mode 100644 index 00000000000..01b49951deb --- /dev/null +++ b/test/fixtures/controller.line/cubicInterpolationMode/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 4, 2, 6, 4, 8], + borderColor: '#ff0000', + cubicInterpolationMode: 'monotone' + }, + { + // option in element (fallback) + data: [2, 6, 4, 8, 6, 10] + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 20, + cubicInterpolationMode: 'default', + fill: false, + } + }, + layout: { + padding: 32 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/cubicInterpolationMode/value.png b/test/fixtures/controller.line/cubicInterpolationMode/value.png new file mode 100644 index 0000000000000000000000000000000000000000..4a68926f66e849f5f9f68084bdd29daf9d494117 GIT binary patch literal 16162 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�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 Q_Z^_DpdpWZ{OtAr2hziOV*mgE literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/fill/scriptable.js b/test/fixtures/controller.line/fill/scriptable.js new file mode 100644 index 00000000000..84c93bfd197 --- /dev/null +++ b/test/fixtures/controller.line/fill/scriptable.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [-2, -6, -4, -8, -6, -10], + backgroundColor: '#ff0000', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + backgroundColor: '#00ff00', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + } + }, + layout: { + padding: 32 + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/fill/scriptable.png b/test/fixtures/controller.line/fill/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..bd93389a65da0cdbb2b78ad6d572c92d59cea98c GIT binary patch literal 11922 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 KS9}){_ 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBackgroundColor/scriptable.png b/test/fixtures/controller.line/pointBackgroundColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..c366b6cfadfc7c83395b015d927cb639a60068b0 GIT binary patch literal 5213 zcmXX~cR&+M6Ti@f(4_ZvNE0c7G$Ek03rGnd&q5PH1!>ZI=%6$Y6#Zx_0f~s_NfkkY zpwf%do**TFD2OyEk@j79@2}grot^#7?9A?OH_gV%jGa}O6#xKs$lMqQ0CY$b1+Xw7 zpZD3l*8qSQgp7^s!wQzZ_V_ss#iXpxZ!5> zrbJIfId6TL|2ZF6&DW9E+`k0tdfX=FiLa^N zPW^JLM>SAXKG8Riu=~%gml=%Ykrjh}i3VHTQ%m*mu%vsnLHiZY`Pl#n`?dXacWueK zOVW3{Fxj94f|c%vzn}gJc&W@tW>p#hb5hRMK{zX?#dMDDSHK=M$>Yy=tM4@AbwOQAJX;e~Ye0`m3ml)}MoKxuQouUYu_Z z+54%q?R2v6AZvB}VBnE5jN3fs_p7;QX|ac6ueoO|#r!Sv7LUMOwA7d^QH!1K4HPq# zPJapiPJraj-&=XApZd~J!)N|w!5{tRgZ1!l*MxuU=3Q_uxVq-(jk&_J`uX6`)~o?V z+6lgRUb5(#s2tU(Dt}F9r=@Rt`{;3l=L4&l)RA55J-NngUpvB0otCuKuFD3¯v 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 U(W;&r 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderColor/scriptable.png b/test/fixtures/controller.line/pointBorderColor/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..6366828ecfc5316b9953dff892bf64e15fbcb224 GIT binary patch literal 6276 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/pointBorderColor/value.js b/test/fixtures/controller.line/pointBorderColor/value.js new file mode 100644 index 00000000000..84e4d6adfb9 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderColor/value.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + radius: 10, + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderColor/value.png b/test/fixtures/controller.line/pointBorderColor/value.png new file mode 100644 index 0000000000000000000000000000000000000000..6bfde92f541608c606a5ea1b4988de691a6827e3 GIT binary patch literal 6231 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 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + radius: 10, + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [ + { + display: false, + ticks: { + beginAtZero: true + } + } + ] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderWidth/scriptable.png b/test/fixtures/controller.line/pointBorderWidth/scriptable.png new file mode 100644 index 0000000000000000000000000000000000000000..7bb6b1b3c779607db82bf4d1a8825986a0c64559 GIT binary patch literal 11687 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.line/pointBorderWidth/value.js b/test/fixtures/controller.line/pointBorderWidth/value.js new file mode 100644 index 00000000000..291f422e527 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderWidth/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + legend: false, + title: false, + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + borderWidth: 3, + radius: 10, + } + }, + scales: { + xAxes: [{display: false}], + yAxes: [{display: false}] + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderWidth/value.png b/test/fixtures/controller.line/pointBorderWidth/value.png new file mode 100644 index 0000000000000000000000000000000000000000..0b8f38c1d62bd16f190a519710df3009cc801d0d GIT binary patch literal 6145 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 Date: Sat, 23 Mar 2019 02:25:17 -0700 Subject: [PATCH 577/685] Implement legend.align: 'start', 'center', 'end' (#6141) New `options.legend.align`config option for controlling alignment of legend blocks in horizontal/vertical legends. --- docs/configuration/legend.md | 9 + src/plugins/plugin.legend.js | 45 ++- ...end-doughnut-bottom-center-mulitiline.json | 25 ++ ...gend-doughnut-bottom-center-mulitiline.png | Bin 0 -> 12591 bytes .../legend-doughnut-bottom-center-single.json | 25 ++ .../legend-doughnut-bottom-center-single.png | Bin 0 -> 9368 bytes ...legend-doughnut-bottom-end-mulitiline.json | 25 ++ .../legend-doughnut-bottom-end-mulitiline.png | Bin 0 -> 11705 bytes ...gend-doughnut-bottom-start-mulitiline.json | 25 ++ ...egend-doughnut-bottom-start-mulitiline.png | Bin 0 -> 11619 bytes ...egend-doughnut-left-center-mulitiline.json | 25 ++ ...legend-doughnut-left-center-mulitiline.png | Bin 0 -> 14654 bytes .../legend-doughnut-left-center-single.json | 25 ++ .../legend-doughnut-left-center-single.png | Bin 0 -> 10147 bytes .../legend-doughnut-left-default-center.json | 24 ++ .../legend-doughnut-left-default-center.png | Bin 0 -> 11389 bytes .../legend-doughnut-left-end-mulitiline.json | 25 ++ .../legend-doughnut-left-end-mulitiline.png | Bin 0 -> 14613 bytes ...legend-doughnut-left-start-mulitiline.json | 25 ++ .../legend-doughnut-left-start-mulitiline.png | Bin 0 -> 14652 bytes ...gend-doughnut-right-center-mulitiline.json | 25 ++ ...egend-doughnut-right-center-mulitiline.png | Bin 0 -> 15047 bytes .../legend-doughnut-right-center-single.json | 25 ++ .../legend-doughnut-right-center-single.png | Bin 0 -> 10231 bytes .../legend-doughnut-right-default-center.json | 24 ++ .../legend-doughnut-right-default-center.png | Bin 0 -> 11375 bytes .../legend-doughnut-right-end-mulitiline.json | 25 ++ .../legend-doughnut-right-end-mulitiline.png | Bin 0 -> 15012 bytes ...egend-doughnut-right-start-mulitiline.json | 25 ++ ...legend-doughnut-right-start-mulitiline.png | Bin 0 -> 15000 bytes ...legend-doughnut-top-center-mulitiline.json | 25 ++ .../legend-doughnut-top-center-mulitiline.png | Bin 0 -> 12544 bytes .../legend-doughnut-top-center-single.json | 25 ++ .../legend-doughnut-top-center-single.png | Bin 0 -> 9360 bytes .../legend-doughnut-top-end-mulitiline.json | 25 ++ .../legend-doughnut-top-end-mulitiline.png | Bin 0 -> 11695 bytes .../legend-doughnut-top-start-mulitiline.json | 25 ++ .../legend-doughnut-top-start-mulitiline.png | Bin 0 -> 11604 bytes test/specs/plugin.legend.tests.js | 356 +----------------- 39 files changed, 491 insertions(+), 367 deletions(-) create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-center-single.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-center-single.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-default-center.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-default-center.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-center-single.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-center-single.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-default-center.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-default-center.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-center-single.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-center-single.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json create mode 100644 test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index c7d124868ed..f8ce7eb5a17 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -9,6 +9,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | ---- | ---- | ------- | ----------- | `display` | `boolean` | `true` | Is the legend shown? | `position` | `string` | `'top'` | Position of the legend. [more...](#position) +| `align` | `string` | `'center'` | Alignment of the legend. [more...](#align) | `fullWidth` | `boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. | `onClick` | `function` | | A callback that is called when a click event is registered on a label item. | `onHover` | `function` | | A callback that is called when a 'mousemove' event is registered on top of a label item. @@ -23,6 +24,14 @@ Position of the legend. Options are: * `'bottom'` * `'right'` +## Align +Alignment of the legend. Options are: +* `'start'` +* `'center'` +* `'end'` + +Defaults to `'center'` for unrecognized values. + ## Legend Label Configuration The legend label configuration is nested below the legend configuration using the `labels` key. diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 2c6870b279d..d3fd5e35f04 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -12,6 +12,7 @@ defaults._set('global', { legend: { display: true, position: 'top', + align: 'center', fullWidth: true, reverse: false, weight: 1000, @@ -102,18 +103,19 @@ function getBoxWidth(labelOpts, fontSize) { var Legend = Element.extend({ initialize: function(config) { - helpers.extend(this, config); + var me = this; + helpers.extend(me, config); // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; + me.legendHitBoxes = []; /** * @private */ - this._hoveredItem = null; + me._hoveredItem = null; // Are we in doughnut mode which has a different data type - this.doughnutMode = false; + me.doughnutMode = false; }, // These methods are ordered by lifecycle. Utilities then follow. @@ -253,9 +255,9 @@ var Legend = Element.extend({ var boxWidth = getBoxWidth(labelOpts, fontSize); var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - if (i === 0 || lineWidths[lineWidths.length - 1] + width + labelOpts.padding > minSize.width) { + if (i === 0 || lineWidths[lineWidths.length - 1] + width + 2 * labelOpts.padding > minSize.width) { totalHeight += fontSize + labelOpts.padding; - lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = labelOpts.padding; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; } // Store the hitbox width and height here. Final position will be updated in `draw` @@ -274,27 +276,27 @@ var Legend = Element.extend({ } else { var vPadding = labelOpts.padding; var columnWidths = me.columnWidths = []; + var columnHeights = me.columnHeights = []; var totalWidth = labelOpts.padding; var currentColWidth = 0; var currentColHeight = 0; - var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { var boxWidth = getBoxWidth(labelOpts, fontSize); var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column - if (i > 0 && currentColHeight + itemHeight > minSize.height - vPadding) { + if (i > 0 && currentColHeight + fontSize + 2 * vPadding > minSize.height) { totalWidth += currentColWidth + labelOpts.padding; columnWidths.push(currentColWidth); // previous column width - + columnHeights.push(currentColHeight); currentColWidth = 0; currentColHeight = 0; } // Get max width currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; + currentColHeight += fontSize + vPadding; // Store the hitbox width and height here. Final position will be updated in `draw` hitboxes[i] = { @@ -307,6 +309,7 @@ var Legend = Element.extend({ totalWidth += currentColWidth; columnWidths.push(currentColWidth); + columnHeights.push(currentColHeight); minSize.width += totalWidth; } } @@ -329,6 +332,8 @@ var Legend = Element.extend({ var globalDefaults = defaults.global; var defaultColor = globalDefaults.defaultColor; var lineDefault = globalDefaults.elements.line; + var legendHeight = me.height; + var columnHeights = me.columnHeights; var legendWidth = me.width; var lineWidths = me.lineWidths; @@ -408,18 +413,29 @@ var Legend = Element.extend({ } }; + var alignmentOffset = function(dimension, blockSize) { + switch (opts.align) { + case 'start': + return labelOpts.padding; + case 'end': + return dimension - blockSize; + default: // center + return (dimension - blockSize + labelOpts.padding) / 2; + } + }; + // Horizontal var isHorizontal = me.isHorizontal(); if (isHorizontal) { cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2) + labelOpts.padding, + x: me.left + alignmentOffset(legendWidth, lineWidths[0]), y: me.top + labelOpts.padding, line: 0 }; } else { cursor = { x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, + y: me.top + alignmentOffset(legendHeight, columnHeights[0]), line: 0 }; } @@ -438,12 +454,12 @@ var Legend = Element.extend({ if (i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) { y = cursor.y += itemHeight; cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2) + labelOpts.padding; + x = cursor.x = me.left + alignmentOffset(legendWidth, lineWidths[cursor.line]); } } else if (i > 0 && y + itemHeight > me.top + me.minSize.height) { x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; cursor.line++; + y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]); } drawLegendBox(x, y, legendItem); @@ -459,7 +475,6 @@ var Legend = Element.extend({ } else { cursor.y += itemHeight; } - }); } }, diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json new file mode 100644 index 00000000000..22837c7bc47 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 20, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "bottom", + "align": "center" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..bad507684b41eb35d27479b0d3ed800cf572212a GIT binary patch literal 12591 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json new file mode 100644 index 00000000000..82e462f7da3 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "bottom", + "align": "center" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png new file mode 100644 index 0000000000000000000000000000000000000000..139a16b863da35889d3d0d43aee09df43ead3cf0 GIT binary patch literal 9368 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 O_^~jtHLfzGUi?3^HanyM literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json new file mode 100644 index 00000000000..630b81ee048 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "bottom", + "align": "end" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..e7fc84b85f05fe4c2ab6c31d4e3749c2fdbf29b3 GIT binary patch literal 11705 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 P4N_53S1h|{9`e5cHQpCS literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json new file mode 100644 index 00000000000..4544602a147 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "bottom", + "align": "start" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..32700c17d8188ca27603439a78a2117127fa9974 GIT binary patch literal 11619 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json new file mode 100644 index 00000000000..6665193f637 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "left", + "align": "center" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..5160e356f74438a99b9511d4e5ce8a70166b6357 GIT binary patch literal 14654 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json new file mode 100644 index 00000000000..4723a860258 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "left", + "align": "center" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.png new file mode 100644 index 0000000000000000000000000000000000000000..72072bfbefd6344a2b6d9b4c2fa1995a2e4ffb0d GIT binary patch literal 10147 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json new file mode 100644 index 00000000000..bb48a2778b2 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json @@ -0,0 +1,24 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "left" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-default-center.png b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.png new file mode 100644 index 0000000000000000000000000000000000000000..3d9829833e2fbdf84c6cef698b122e8a1dd641d0 GIT binary patch literal 11389 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json new file mode 100644 index 00000000000..662b16725f4 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "left", + "align": "end" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..46c6014eb6a94f07934ebd602b5104c209be6325 GIT binary patch literal 14613 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json new file mode 100644 index 00000000000..1d4dc1f7e6f --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "left", + "align": "start" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..91c0c31b0a59c479635e606f7fdfb24e4a44f94e GIT binary patch literal 14652 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json new file mode 100644 index 00000000000..5131989bdcd --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "right", + "align": "center" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..72cbe903dc4bf1a6f3842a0871dc72516b2fbcf2 GIT binary patch literal 15047 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 Q5BgwcM%IS)r@hnu2geoYkN^Mx literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json new file mode 100644 index 00000000000..83762072036 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json @@ -0,0 +1,24 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "right" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-default-center.png b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.png new file mode 100644 index 0000000000000000000000000000000000000000..1ba7052ea0b130f31d333525a95a7e1ea92db04b GIT binary patch literal 11375 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 C`>iwp literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json new file mode 100644 index 00000000000..183959b69e7 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "right", + "align": "end" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..00e366460ea7efc3ae6d66cf095c137140c70adc GIT binary patch literal 15012 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

      37(%UpZ%b|(L{gF%eOL&{Ei!qQt-K$q$N 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json new file mode 100644 index 00000000000..b150ef789d0 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "top", + "align": "end" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e56e4701fbbe279bad14d581a878f1f4a1ae61 GIT binary patch literal 11695 zcmd72hgVZk^DdkQ1dtMtB8J|i2`D80P8ISVB=ig`jkhCLN?o3nGFb zMd>|)^dd@;&UfPLd++bN|G>R#owXqA>^XbSJkQLYnZ2X+bZ*j6u~C6QAevhmY6c(> z1o#&Wf|CP(IuiPvK_D*BEw$@LL3cLNDXYcQ1O7zt&%qeb`s4^_G#S%**m-)SG9v}d zM@K3JB|3*mq1WS$$@~OXrtrVeA;&P35cMwn`m-EzGMKs^;ZmU*m?Di##BFrrsOhKg zPq}GpLxOM&e zC>I6V?>}N_n1BYFmH>NJ#vS`tEDD$~UObqszRQe;ZMI@D>E!PA=<(3;ms{xB*4O?E zpk<0nOQT*b?qSz=F=@(LPl~e--X6$Jt*6fV{Wwm9a#7fx^7WYW`a*b*7OML~$Ez|q z61JT`drM0x*FYp*VmP?k!{kbx0ddfUK=Qs#>utkrzmMC7A|PMTlk}NG>@eMW#Z^68 z^tp(W@J|D=UKbCJgecIAsBnf)8c}J42}l$+Gplr6km7QiRJVZ2wxK0x0@Ag&8^cl} zUdX}q>)oYHSg&qxf9N}rJH9`Tho~?npgBRir;oZQOPDaM`;-&i;lXHuS2xDhT*6(+ z&?2DmmOH;9cgcpKeBsOsFS7uBUV8<0XvL*vT7UYasMc6auyP) z5oG?HPZv}6g@~{aDi&4u2BE%0A=JB&-hR0tO_>=U|Ci7Tc=y_$Y!WQ(eQ3Et$>ZIl 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 TBMP{P4Z5YSqgJM38~OhM1jDOF literal 0 HcmV?d00001 diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json new file mode 100644 index 00000000000..ca2111922ab --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json @@ -0,0 +1,25 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "legend": { + "position": "top", + "align": "start" + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7e7fe88aa591e4ee42f6efdc20a9935bb88b54 GIT binary patch literal 11604 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 l Date: Wed, 27 Mar 2019 23:56:37 -0700 Subject: [PATCH 578/685] Demonstrate multiple units on financial timeseries sample (#6119) --- samples/scales/time/financial.html | 54 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 875bb60e158..02d3a8a3b2e 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -26,29 +26,41 @@ +

    + Downloads + Builds + Coverage + Awesome + Slack +