From b7025fedbec8fbb2d77c8c52a3c566270d5ca4d9 Mon Sep 17 00:00:00 2001 From: Akihiko Kusanagi Date: Sat, 3 Mar 2018 09:13:41 +0800 Subject: [PATCH 01/48] 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 02/48] 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 03/48] 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 04/48] 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 05/48] 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 06/48] 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 07/48] 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 08/48] 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 09/48] 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 10/48] 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 11/48] 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 12/48] 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 13/48] 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 14/48] 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 15/48] 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 16/48] 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 17/48] 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 18/48] 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 19/48] 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 24/48] 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 25/48] 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 26/48] 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 27/48] 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 28/48] 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 29/48] 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 30/48] 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 31/48] 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 32/48] 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 33/48] 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 34/48] 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 35/48] 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 36/48] 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 37/48] 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 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 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 38/48] 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 39/48] 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 40/48] 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 41/48] 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 42/48] 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 43/48] 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 44/48] 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 45/48] 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 46/48] 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 47/48] 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 48/48] 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": {