From b83f64b16e3ae51c1c2a2e9feeed48cf8b2c7395 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sat, 28 Dec 2019 00:13:24 +0200 Subject: [PATCH] Rewrite animation logic (#6845) * Rewrite animation logic * Review update 1 * Review update 2 * Review update 3 * Add 'none' to api.md --- .eslintrc.yml | 1 + docs/configuration/animations.md | 42 +- docs/configuration/tooltip.md | 2 +- docs/developers/api.md | 35 +- docs/developers/charts.md | 4 +- docs/developers/updates.md | 2 +- docs/general/interactions/README.md | 1 - docs/general/performance.md | 8 +- docs/general/responsive.md | 1 - docs/getting-started/v3-migration.md | 16 +- samples/animations/delay.html | 122 +++ samples/animations/drop.html | 175 ++++ samples/animations/loop.html | 181 ++++ samples/charts/bubble.html | 3 - samples/samples.js | 15 + src/controllers/controller.bar.js | 53 +- src/controllers/controller.bubble.js | 108 ++- src/controllers/controller.doughnut.js | 94 +-- src/controllers/controller.line.js | 146 +--- src/controllers/controller.polarArea.js | 105 +-- src/controllers/controller.radar.js | 115 +-- src/core/core.animation.js | 94 ++- src/core/core.animations.js | 225 +++-- src/core/core.animator.js | 211 +++++ src/core/core.controller.js | 250 ++---- src/core/core.datasetController.js | 257 ++++-- src/core/core.element.js | 104 +-- src/core/core.interaction.js | 6 +- src/core/core.plugins.js | 17 +- src/core/core.tooltip.js | 770 +++++++++--------- src/elements/element.arc.js | 101 ++- src/elements/element.line.js | 166 +++- src/elements/element.point.js | 55 +- src/elements/element.rectangle.js | 89 +- src/helpers/helpers.curve.js | 2 +- src/index.js | 1 + src/plugins/plugin.filler.js | 46 +- src/plugins/plugin.legend.js | 2 +- .../controller.line/clip/default-y-max.png | Bin 14590 -> 14351 bytes .../controller.line/clip/default-y.png | Bin 14464 -> 14217 bytes test/fixtures/core.tooltip/opacity.js | 9 +- test/specs/controller.bar.tests.js | 190 ++--- test/specs/controller.bubble.tests.js | 62 +- test/specs/controller.doughnut.tests.js | 89 +- test/specs/controller.line.tests.js | 214 ++--- test/specs/controller.polarArea.tests.js | 78 +- test/specs/controller.radar.tests.js | 97 ++- ...er.test.js => controller.scatter.tests.js} | 4 +- test/specs/core.controller.tests.js | 67 +- test/specs/core.element.tests.js | 51 -- test/specs/core.interaction.tests.js | 64 +- test/specs/core.tooltip.tests.js | 411 +++++----- test/specs/element.arc.tests.js | 27 +- test/specs/element.point.tests.js | 60 +- test/specs/element.rectangle.tests.js | 43 +- test/specs/global.defaults.tests.js | 16 +- test/specs/helpers.curve.tests.js | 228 +++--- test/specs/plugin.legend.tests.js | 4 +- test/utils.js | 2 - 59 files changed, 2832 insertions(+), 2509 deletions(-) create mode 100644 samples/animations/delay.html create mode 100644 samples/animations/drop.html create mode 100644 samples/animations/loop.html create mode 100644 src/core/core.animator.js rename test/specs/{controller.scatter.test.js => controller.scatter.tests.js} (92%) delete mode 100644 test/specs/core.element.tests.js diff --git a/.eslintrc.yml b/.eslintrc.yml index ace7ae7a4..2a65bd042 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,6 +1,7 @@ extends: chartjs env: + es6: true browser: true node: true diff --git a/docs/configuration/animations.md b/docs/configuration/animations.md index 40dd8ec3f..8131941d7 100644 --- a/docs/configuration/animations.md +++ b/docs/configuration/animations.md @@ -10,12 +10,33 @@ The following animation options are available. The global options for are define | ---- | ---- | ------- | ----------- | `duration` | `number` | `1000` | The number of milliseconds an animation takes. | `easing` | `string` | `'easeOutQuart'` | Easing function to use. [more...](#easing) +| `debug` | `boolean` | `undefined` | Running animation count + FPS display in upper left corner of the chart. | `onProgress` | `function` | `null` | Callback called on each step of an animation. [more...](#animation-callbacks) -| `onComplete` | `function` | `null` | Callback called at the end of an animation. [more...](#animation-callbacks) +| `onComplete` | `function` | `null` | Callback called when all animations are completed. [more...](#animation-callbacks) +| `delay` | `number` | `undefined` | Delay before starting the animations. +| `loop` | `boolean` | `undefined` | If set to `true`, loop the animations loop endlessly. +| `type` | `string` | `typeof property` | Type of property, determines the interpolator used. Possible values: `'number'`, '`color`'. +| `from` | number|Color | `undefined` | Start value for the animation. Current value is used when `undefined` +| `active` | `object` | `{ duration: 400 }` | Option overrides for `active` animations (hover) +| `resize` | `object` | `{ duration: 0 }` | Option overrides for `resize` animations. +| [property] | `object` | `undefined` | Option overrides for [property]. +| [collection] | `object` | `undefined` | Option overrides for multiple properties, identified by `properties` array. + +Default collections: +| Name | option | value +| `numbers` | `type` | `'number'` +| | `properties` | `['x', 'y', 'borderWidth', 'radius', 'tension']` +| `colors` | `type` | `'color'` +| | `properties` | `['borderColor', 'backgroundColor']` + +Direct property configuration overrides configuration of same property in a collection. + +These defaults can be overridden in `options.animation` and `dataset.animation`. ## Easing Available options are: + * `'linear'` * `'easeInQuad'` * `'easeOutQuad'` @@ -52,34 +73,23 @@ See [Robert Penner's easing equations](http://robertpenner.com/easing/). ## Animation Callbacks -The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed a `Chart.Animation` instance: +The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed following object: ```javascript { // Chart object chart: Chart, - // Current Animation frame number + // Number of animations still in progress currentStep: number, - // Number of animation frames + // Total number of animations at the start of current animation numSteps: number, - - // Animation easing to use - easing: string, - - // Function that renders the chart - render: function, - - // User callback - onAnimationProgress: function, - - // User callback - onAnimationComplete: function } ``` The following example fills a progress bar during the chart animation. + ```javascript var chart = new Chart(ctx, { type: 'line', diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 26b1d6a71..6e086a167 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -356,7 +356,7 @@ The tooltip model contains parameters that can be used to render the tooltip. // 0 opacity is a hidden tooltip opacity: number, - legendColorBackground: Color, + multiKeyBackground: Color, displayColors: boolean, borderColor: Color, borderWidth: number diff --git a/docs/developers/api.md b/docs/developers/api.md index e248db3a5..01b10801c 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -17,32 +17,23 @@ This must be called before the canvas is reused for a new chart. myLineChart.destroy(); ``` -## .update(config) +## .update(mode) Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. ```javascript -// duration is the time for the animation of the redraw in milliseconds -// lazy is a boolean. if true, the animation can be interrupted by other animations myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 myLineChart.update(); // Calling update now animates the position of March from 90 to 50. ``` > **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. -A `config` object can be provided with additional configuration for the update process. This is useful when `update` is manually called inside an event handler and some different animation is desired. - -The following properties are supported: -* **duration** (number): Time for the animation of the redraw in milliseconds -* **lazy** (boolean): If true, the animation can be interrupted by other animations -* **easing** (string): The animation easing function. See [Animation Easing](../configuration/animations.md) for possible values. +A `mode` string can be provided to indicate what should be updated and what animation configuration should be used. Core calls this method using any of `undefined`, `'reset'`, `'resize'` or `'active'`. `'none'` is also a supported mode for skipping animations for single update. Example: + ```javascript -myChart.update({ - duration: 800, - easing: 'easeOutBounce' -}); +myChart.update(); ``` See [Updating Charts](updates.md) for more details. @@ -55,25 +46,13 @@ Reset the chart to it's state before the initial animation. A new animation can myLineChart.reset(); ``` -## .render(config) +## .render() Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use `.update()` in that case. -See `.update(config)` for more details on the config object. - -```javascript -// duration is the time for the animation of the redraw in milliseconds -// lazy is a boolean. if true, the animation can be interrupted by other animations -myLineChart.render({ - duration: 800, - lazy: false, - easing: 'easeOutBounce' -}); -``` - ## .stop() -Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. +Use this to stop any current animation. This will pause the chart during any current animation frame. Call `.render()` to re-animate. ```javascript // Stops the charts animation loop at its current frame @@ -175,5 +154,5 @@ Extensive examples of usage are available in the [Chart.js tests](https://github ```javascript var meta = myChart.getDatasetMeta(0); -var x = meta.data[0]._model.x; +var x = meta.data[0].x; ``` diff --git a/docs/developers/charts.md b/docs/developers/charts.md index 2e2e5c231..c96350373 100644 --- a/docs/developers/charts.md +++ b/docs/developers/charts.md @@ -94,13 +94,13 @@ var custom = Chart.controllers.bubble.extend({ // Now we can do some custom drawing for this dataset. Here we'll draw a red box around the first point in each dataset var meta = this.getMeta(); var pt0 = meta.data[0]; - var radius = pt0._view.radius; + var radius = pt0.radius; var ctx = this.chart.chart.ctx; ctx.save(); ctx.strokeStyle = 'red'; ctx.lineWidth = 1; - ctx.strokeRect(pt0._view.x - radius, pt0._view.y - radius, 2 * radius, 2 * radius); + ctx.strokeRect(pt0.x - radius, pt0.y - radius, 2 * radius, 2 * radius); ctx.restore(); } }); diff --git a/docs/developers/updates.md b/docs/developers/updates.md index e50b26004..8b7da7b0e 100644 --- a/docs/developers/updates.md +++ b/docs/developers/updates.md @@ -97,4 +97,4 @@ Code sample for updating options can be found in [toggle-scale-type.html](../../ ## Preventing Animations -Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with a duration of `0`. This will render the chart synchronously and without an animation. +Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with `'none'` as mode. diff --git a/docs/general/interactions/README.md b/docs/general/interactions/README.md index 9b5ad6d32..be5ec8e23 100644 --- a/docs/general/interactions/README.md +++ b/docs/general/interactions/README.md @@ -7,4 +7,3 @@ The hover configuration is passed into the `options.hover` namespace. The global | `mode` | `string` | `'nearest'` | Sets which elements appear in the tooltip. See [Interaction Modes](./modes.md#interaction-modes) for details. | `intersect` | `boolean` | `true` | if true, the hover mode only applies when the mouse position intersects an item on the chart. | `axis` | `string` | `'x'` | Can be set to `'x'`, `'y'`, or `'xy'` to define which directions are used in calculating distances. Defaults to `'x'` for `'index'` mode and `'xy'` in `dataset` and `'nearest'` modes. -| `animationDuration` | `number` | `400` | Duration in milliseconds it takes to animate hover style changes. diff --git a/docs/general/performance.md b/docs/general/performance.md index 8c0dc11c7..1d970a6f9 100644 --- a/docs/general/performance.md +++ b/docs/general/performance.md @@ -23,13 +23,7 @@ new Chart(ctx, { type: 'line', data: data, options: { - animation: { - duration: 0 // general animation time - }, - hover: { - animationDuration: 0 // duration of animations when hovering an item - }, - responsiveAnimationDuration: 0 // animation duration after a resize + animation: false } }); ``` diff --git a/docs/general/responsive.md b/docs/general/responsive.md index 319709a5a..81569476b 100644 --- a/docs/general/responsive.md +++ b/docs/general/responsive.md @@ -14,7 +14,6 @@ Chart.js provides a [few options](#configuration-options) to enable responsivene | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `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. diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index b55ad5381..ee719fe98 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -49,6 +49,13 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `scales.[x/y]Axes.time.max` was renamed to `scales[id].max` * `scales.[x/y]Axes.time.min` was renamed to `scales[id].min` +### Animations + +Animation system was completely rewritten in Chart.js v3. Each property can now be animated separately. Please see [animations](../configuration/animations.md) docs for details. + +* `hover.animationDuration` is now configured in `animation.active.duration` +* `responsiveAnimationDuration` is now configured in `animation.resize.duration` + ## Developer migration ### Removed @@ -90,10 +97,8 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `Chart.data.datasets[datasetIndex]._meta` * `Element._ctx` -* `Element._model.datasetLabel` -* `Element._model.label` -* `Point._model.tension` -* `Point._model.steppedLine` +* `Element._model` +* `Element._view` * `TimeScale._getPixelForOffset` * `TimeScale.getLabelWidth` @@ -108,7 +113,6 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `helpers.log10` was renamed to `helpers.math.log10` * `helpers.almostEquals` was renamed to `helpers.math.almostEquals` * `helpers.almostWhole` was renamed to `helpers.math.almostWhole` -* `helpers._decimalPlaces` was renamed to `helpers.math._decimalPlaces` * `helpers.distanceBetweenPoints` was renamed to `helpers.math.distanceBetweenPoints` * `helpers.isNumber` was renamed to `helpers.math.isNumber` * `helpers.sign` was renamed to `helpers.math.sign` @@ -129,10 +133,12 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `TimeScale.getLabelCapacity` was renamed to `TimeScale._getLabelCapacity` * `TimeScale.tickFormatFunction` was renamed to `TimeScale._tickFormatFunction` * `TimeScale.getPixelForOffset` was renamed to `TimeScale._getPixelForOffset` +* `Tooltip.options.legendColorBackgroupd` was renamed to `Tooltip.options.multiKeyBackground` #### Renamed private APIs * `helpers._alignPixel` was renamed to `helpers.canvas._alignPixel` +* `helpers._decimalPlaces` was renamed to `helpers.math._decimalPlaces` ### Changed diff --git a/samples/animations/delay.html b/samples/animations/delay.html new file mode 100644 index 000000000..38f24c8bb --- /dev/null +++ b/samples/animations/delay.html @@ -0,0 +1,122 @@ + + + + + Stacked Bar Chart + + + + + + +
+ +
+ + + + + diff --git a/samples/animations/drop.html b/samples/animations/drop.html new file mode 100644 index 000000000..c4db7ae9b --- /dev/null +++ b/samples/animations/drop.html @@ -0,0 +1,175 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/samples/animations/loop.html b/samples/animations/loop.html new file mode 100644 index 000000000..15d743a85 --- /dev/null +++ b/samples/animations/loop.html @@ -0,0 +1,181 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/samples/charts/bubble.html b/samples/charts/bubble.html index b604a5ac1..19cae60bb 100644 --- a/samples/charts/bubble.html +++ b/samples/charts/bubble.html @@ -29,9 +29,6 @@ var addedCount = 0; var color = Chart.helpers.color; var bubbleChartData = { - animation: { - duration: 10000 - }, datasets: [{ label: 'My First dataset', backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), diff --git a/samples/samples.js b/samples/samples.js index d1cae5672..d95a3fe7e 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -65,6 +65,9 @@ }, { title: 'Other charts', items: [{ + title: 'Bubble', + path: 'charts/bubble.html' + }, { title: 'Scatter', path: 'charts/scatter/basic.html' }, { @@ -209,6 +212,18 @@ title: 'Radar Chart', path: 'scriptable/radar.html' }] + }, { + title: 'Animations', + items: [{ + title: 'Delay', + path: 'animations/delay.html' + }, { + title: 'Drop', + path: 'animations/drop.html' + }, { + title: 'Loop', + path: 'animations/loop.html' + }] }, { title: 'Advanced', items: [{ diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 7b00ea00f..7755cd14f 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -30,7 +30,13 @@ defaults._set('global', { datasets: { bar: { categoryPercentage: 0.8, - barPercentage: 0.9 + barPercentage: 0.9, + animation: { + numbers: { + type: 'number', + properties: ['x', 'y', 'base', 'width', 'height'] + } + } } } }); @@ -267,50 +273,53 @@ module.exports = DatasetController.extend({ meta.bar = true; }, - update: function(reset) { + update: function(mode) { const me = this; const rects = me._cachedMeta.data; - me.updateElements(rects, 0, rects.length, reset); + me.updateElements(rects, 0, rects.length, mode); }, - updateElements: function(rectangles, start, count, reset) { + updateElements: function(rectangles, start, count, mode) { const me = this; + const reset = mode === 'reset'; const vscale = me._cachedMeta.vScale; const base = vscale.getBasePixel(); const horizontal = vscale.isHorizontal(); const ruler = me.getRuler(); + const firstOpts = me._resolveDataElementOptions(start, mode); + const sharedOptions = me._getSharedOptions(mode, rectangles[start], firstOpts); + const includeOptions = me._includeOptions(mode, sharedOptions); + let i; for (i = 0; i < start + count; i++) { - const rectangle = rectangles[i]; - const options = me._resolveDataElementOptions(i); + const options = me._resolveDataElementOptions(i, mode); const vpixels = me.calculateBarValuePixels(i, options); const ipixels = me.calculateBarIndexPixels(i, ruler, options); - rectangle._model = { - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderSkipped: options.borderSkipped, - borderWidth: options.borderWidth + const properties = { + horizontal, + base: reset ? base : vpixels.base, + x: horizontal ? reset ? base : vpixels.head : ipixels.center, + y: horizontal ? ipixels.center : reset ? base : vpixels.head, + height: horizontal ? ipixels.size : undefined, + width: horizontal ? undefined : ipixels.size }; - const model = rectangle._model; - // all borders are drawn for floating bar + /* TODO: float bars border skipping magic if (me._getParsed(i)._custom) { model.borderSkipped = null; } - - model.horizontal = horizontal; - model.base = reset ? base : vpixels.base; - model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; - model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; - model.height = horizontal ? ipixels.size : undefined; - model.width = horizontal ? undefined : ipixels.size; - - rectangle.pivot(me.chart._animationsDisabled); + */ + if (includeOptions) { + properties.options = options; + } + me._updateElement(rectangles[i], i, properties, mode); } + + me._updateSharedOptions(sharedOptions, mode); }, /** diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 1cf947b24..21cacfb78 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -1,14 +1,18 @@ 'use strict'; -var DatasetController = require('../core/core.datasetController'); -var defaults = require('../core/core.defaults'); -var elements = require('../elements/index'); -var helpers = require('../helpers/index'); +const DatasetController = require('../core/core.datasetController'); +const defaults = require('../core/core.defaults'); +const elements = require('../elements/index'); +const helpers = require('../helpers/index'); -var valueOrDefault = helpers.valueOrDefault; -var resolve = helpers.options.resolve; +const resolve = helpers.options.resolve; defaults._set('bubble', { + animation: { + numbers: { + properties: ['x', 'y', 'borderWidth', 'radius'] + } + }, scales: { x: { type: 'linear', @@ -43,11 +47,8 @@ module.exports = DatasetController.extend({ 'backgroundColor', 'borderColor', 'borderWidth', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', - 'hoverRadius', 'hitRadius', + 'radius', 'pointStyle', 'rotation' ], @@ -77,15 +78,14 @@ module.exports = DatasetController.extend({ * @private */ _getMaxOverflow: function() { - var me = this; - var meta = me._cachedMeta; - var data = meta.data || []; - if (!data.length) { - return false; + const me = this; + const meta = me._cachedMeta; + let i = (meta.data || []).length - 1; + let max = 0; + for (; i >= 0; --i) { + max = Math.max(max, me.getStyle(i, true).radius); } - var firstPoint = data[0].size(); - var lastPoint = data[data.length - 1].size(); - return Math.max(firstPoint, lastPoint) / 2; + return max > 0 && max; }, /** @@ -109,72 +109,56 @@ module.exports = DatasetController.extend({ /** * @protected */ - update: function(reset) { + update: function(mode) { const me = this; const points = me._cachedMeta.data; // Update Points - me.updateElements(points, 0, points.length, reset); + me.updateElements(points, 0, points.length, mode); }, /** * @protected */ - updateElements: function(points, start, count, reset) { + updateElements: function(points, start, count, mode) { const me = this; + const reset = mode === 'reset'; const {xScale, yScale} = me._cachedMeta; + const firstOpts = me._resolveDataElementOptions(start, mode); + const sharedOptions = me._getSharedOptions(mode, points[start], firstOpts); + const includeOptions = me._includeOptions(mode, sharedOptions); let i; for (i = start; i < start + count; i++) { const point = points[i]; - const options = me._resolveDataElementOptions(i); const parsed = !reset && me._getParsed(i); const x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(parsed[xScale.id]); const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed[yScale.id]); - - point._options = options; - point._model = { - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - hitRadius: options.hitRadius, - pointStyle: options.pointStyle, - rotation: options.rotation, - radius: reset ? 0 : options.radius, - skip: isNaN(x) || isNaN(y), - x: x, - y: y, + const properties = { + x, + y, + skip: isNaN(x) || isNaN(y) }; - point.pivot(me.chart._animationsDisabled); - } - }, + if (includeOptions) { + properties.options = i === start ? firstOpts + : me._resolveDataElementOptions(i, mode); - /** - * @protected - */ - setHoverStyle: function(point) { - var model = point._model; - var options = point._options; - var getHoverColor = helpers.getHoverColor; - - point.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; + if (reset) { + properties.options.radius = 0; + } + } - model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); - model.radius = options.radius + options.hoverRadius; + me._updateElement(point, i, properties, mode); + } + + me._updateSharedOptions(sharedOptions, mode); }, /** * @private */ - _resolveDataElementOptions: function(index) { + _resolveDataElementOptions: function(index, mode) { var me = this; var chart = me.chart; var dataset = me.getDataset(); @@ -190,12 +174,16 @@ module.exports = DatasetController.extend({ }; // In case values were cached (and thus frozen), we need to clone the values - if (me._cachedDataOpts === values) { - values = helpers.extend({}, values); + if (values.$shared) { + values = helpers.extend({}, values, {$shared: false}); } + // Custom radius resolution - values.radius = resolve([ + if (mode !== 'active') { + values.radius = 0; + } + values.radius += resolve([ parsed && parsed._custom, me._config.radius, chart.options.elements.point.radius diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index ad5fa86ea..954c2ec79 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -13,6 +13,10 @@ var HALF_PI = PI / 2; defaults._set('doughnut', { animation: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, // Boolean - Whether we animate the rotation of the Doughnut animateRotate: true, // Boolean - Whether we animate scaling the Doughnut from the centre @@ -160,7 +164,7 @@ module.exports = DatasetController.extend({ return ringIndex; }, - update: function(reset) { + update: function(mode) { var me = this; var chart = me.chart; var chartArea = chart.chartArea; @@ -200,7 +204,7 @@ module.exports = DatasetController.extend({ } for (i = 0, ilen = arcs.length; i < ilen; ++i) { - arcs[i]._options = me._resolveDataElementOptions(i); + arcs[i]._options = me._resolveDataElementOptions(i, mode); } chart.borderWidth = me.getMaxBorderWidth(); @@ -217,57 +221,45 @@ module.exports = DatasetController.extend({ me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index); me.innerRadius = Math.max(me.outerRadius - chart.radiusLength * chartWeight, 0); - me.updateElements(arcs, 0, arcs.length, reset); + me.updateElements(arcs, 0, arcs.length, mode); }, - updateElements: function(arcs, start, count, reset) { + updateElements: function(arcs, start, count, mode) { const me = this; + const reset = mode === 'reset'; const chart = me.chart; const chartArea = chart.chartArea; const opts = chart.options; const animationOpts = opts.animation; const centerX = (chartArea.left + chartArea.right) / 2; const centerY = (chartArea.top + chartArea.bottom) / 2; - const startAngle = opts.rotation; // non reset case handled later - const endAngle = opts.rotation; // non reset case handled later const meta = me.getMeta(); const innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; const outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; + let startAngle = opts.rotation; let i; for (i = 0; i < start + count; ++i) { const arc = arcs[i]; const circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(meta._parsed[i] * opts.circumference / DOUBLE_PI); const options = arc._options || {}; - const model = { - // Desired view properties - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - borderAlign: options.borderAlign, + if (i < start) { + startAngle += circumference; + continue; + } + const properties = { x: centerX + chart.offsetX, y: centerY + chart.offsetY, - startAngle: startAngle, - endAngle: endAngle, - circumference: circumference, - outerRadius: outerRadius, - innerRadius: innerRadius + startAngle, + endAngle: startAngle + circumference, + circumference, + outerRadius, + innerRadius, + options }; + startAngle += circumference; - arc._model = model; - - // Set correct angles if not resetting - if (!reset || !animationOpts.animateRotate) { - if (i === 0) { - model.startAngle = opts.rotation; - } else { - model.startAngle = me._cachedMeta.data[i - 1]._model.endAngle; - } - - model.endAngle = model.startAngle + model.circumference; - } - - arc.pivot(chart._animationsDisabled); + me._updateElement(arc, i, properties, mode); } }, @@ -304,7 +296,7 @@ module.exports = DatasetController.extend({ var me = this; var max = 0; var chart = me.chart; - var i, ilen, meta, arc, controller, options, borderWidth, hoverWidth; + var i, ilen, meta, controller, options; if (!arcs) { // Find the outmost visible dataset @@ -312,8 +304,9 @@ module.exports = DatasetController.extend({ if (chart.isDatasetVisible(i)) { meta = chart.getDatasetMeta(i); arcs = meta.data; - if (i !== me.index) { - controller = meta.controller; + controller = meta.controller; + if (controller !== me) { + controller._configure(); } break; } @@ -325,43 +318,14 @@ module.exports = DatasetController.extend({ } for (i = 0, ilen = arcs.length; i < ilen; ++i) { - arc = arcs[i]; - if (controller) { - controller._configure(); - options = controller._resolveDataElementOptions(i); - } else { - options = arc._options; - } + options = controller._resolveDataElementOptions(i); if (options.borderAlign !== 'inner') { - borderWidth = options.borderWidth; - hoverWidth = options.hoverBorderWidth; - - max = borderWidth > max ? borderWidth : max; - max = hoverWidth > max ? hoverWidth : max; + max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0); } } return max; }, - /** - * @protected - */ - setHoverStyle: function(arc) { - var model = arc._model; - var options = arc._options; - var getHoverColor = helpers.getHoverColor; - - arc.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - }; - - model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); - }, - /** * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly * @private diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 34d421f98..5277d9cb0 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -7,7 +7,6 @@ const helpers = require('../helpers/index'); const valueOrDefault = helpers.valueOrDefault; const resolve = helpers.options.resolve; -const isPointInArea = helpers.canvas._isPointInArea; defaults._set('line', { showLines: true, @@ -44,6 +43,7 @@ module.exports = DatasetController.extend({ 'borderDashOffset', 'borderJoinStyle', 'borderWidth', + 'capBezierPoints', 'cubicInterpolationMode', 'fill' ], @@ -56,6 +56,7 @@ module.exports = DatasetController.extend({ borderColor: 'pointBorderColor', borderWidth: 'pointBorderWidth', hitRadius: 'pointHitRadius', + hoverHitRadius: 'pointHitRadius', hoverBackgroundColor: 'pointHoverBackgroundColor', hoverBorderColor: 'pointHoverBorderColor', hoverBorderWidth: 'pointHoverBorderWidth', @@ -65,7 +66,7 @@ module.exports = DatasetController.extend({ rotation: 'pointRotation' }, - update: function(reset) { + update: function(mode) { const me = this; const meta = me._cachedMeta; const line = meta.dataset; @@ -73,62 +74,53 @@ module.exports = DatasetController.extend({ const options = me.chart.options; const config = me._config; const showLine = me._showLine = valueOrDefault(config.showLine, options.showLines); - let i, ilen; // Update Line - if (showLine) { - // Data - line._children = points; - // Model - line._model = me._resolveDatasetElementOptions(); + if (showLine && mode !== 'resize') { - line.pivot(); - } - - // Update Points - me.updateElements(points, 0, points.length, reset); + const properties = { + _children: points, + options: me._resolveDatasetElementOptions() + }; - if (showLine && line._model.tension !== 0) { - me.updateBezierControlPoints(); + me._updateElement(line, undefined, properties, mode); } - // Now pivot the point for animation - for (i = 0, ilen = points.length; i < ilen; ++i) { - points[i].pivot(me.chart._animationsDisabled); + // Update Points + if (meta.visible) { + me.updateElements(points, 0, points.length, mode); } }, - updateElements: function(points, start, count, reset) { + updateElements: function(points, start, count, mode) { const me = this; + const reset = mode === 'reset'; const {xScale, yScale, _stacked} = me._cachedMeta; + const firstOpts = me._resolveDataElementOptions(start, mode); + const sharedOptions = me._getSharedOptions(mode, points[start], firstOpts); + const includeOptions = me._includeOptions(mode, sharedOptions); let i; for (i = start; i < start + count; ++i) { const point = points[i]; const parsed = me._getParsed(i); - const options = me._resolveDataElementOptions(i); const x = xScale.getPixelForValue(parsed[xScale.id]); const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me._applyStack(yScale, parsed) : parsed[yScale.id]); + const properties = { + x, + y, + skip: isNaN(x) || isNaN(y) + }; - // Utility - point._options = options; + if (includeOptions) { + properties.options = i === start ? firstOpts + : me._resolveDataElementOptions(i, mode); + } - // Desired view properties - point._model = { - x: x, - y: y, - skip: isNaN(x) || isNaN(y), - // Appearance - radius: options.radius, - pointStyle: options.pointStyle, - rotation: options.rotation, - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - // Tooltip - hitRadius: options.hitRadius - }; + me._updateElement(point, i, properties, mode); } + + me._updateSharedOptions(sharedOptions, mode); }, /** @@ -161,67 +153,12 @@ module.exports = DatasetController.extend({ if (!data.length) { return false; } - const border = me._showLine ? meta.dataset._model.borderWidth : 0; + const border = me._showLine && meta.dataset.options.borderWidth || 0; const firstPoint = data[0].size(); const lastPoint = data[data.length - 1].size(); return Math.max(border, firstPoint, lastPoint) / 2; }, - updateBezierControlPoints: function() { - const me = this; - const chart = me.chart; - const meta = me._cachedMeta; - const lineModel = meta.dataset._model; - const area = chart.chartArea; - let points = meta.data || []; - let i, ilen; - - // Only consider points that are drawn in case the spanGaps option is used - if (lineModel.spanGaps) { - points = points.filter(function(pt) { - return !pt._model.skip; - }); - } - - function capControlPoint(pt, min, max) { - return Math.max(Math.min(pt, max), min); - } - - if (lineModel.cubicInterpolationMode === 'monotone') { - helpers.curve.splineCurveMonotone(points); - } else { - for (i = 0, ilen = points.length; i < ilen; ++i) { - const model = points[i]._model; - const controlPoints = helpers.curve.splineCurve( - points[Math.max(0, i - 1)]._model, - model, - points[Math.min(i + 1, ilen - 1)]._model, - lineModel.tension - ); - model.controlPointPreviousX = controlPoints.previous.x; - model.controlPointPreviousY = controlPoints.previous.y; - model.controlPointNextX = controlPoints.next.x; - model.controlPointNextY = controlPoints.next.y; - } - } - - if (chart.options.elements.line.capBezierPoints) { - for (i = 0, ilen = points.length; i < ilen; ++i) { - const model = points[i]._model; - if (isPointInArea(model, area)) { - if (i > 0 && isPointInArea(points[i - 1]._model, area)) { - model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); - model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); - } - if (i < points.length - 1 && isPointInArea(points[i + 1]._model, area)) { - model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); - model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); - } - } - } - } - }, - draw: function() { const me = this; const ctx = me._ctx; @@ -233,7 +170,7 @@ module.exports = DatasetController.extend({ let i = 0; if (me._showLine) { - meta.dataset.draw(ctx); + meta.dataset.draw(ctx, area); } // Draw the points @@ -241,25 +178,4 @@ module.exports = DatasetController.extend({ points[i].draw(ctx, area); } }, - - /** - * @protected - */ - setHoverStyle: function(point) { - const model = point._model; - const options = point._options; - const getHoverColor = helpers.getHoverColor; - - point.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; - - model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); - model.radius = valueOrDefault(options.hoverRadius, options.radius); - }, }); diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index fd6bc877c..c47278389 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -8,6 +8,14 @@ var helpers = require('../helpers/index'); var resolve = helpers.options.resolve; defaults._set('polarArea', { + animation: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, + animateRotate: true, + animateScale: true + }, scales: { r: { type: 'radialLinear', @@ -24,12 +32,6 @@ defaults._set('polarArea', { } }, - // Boolean - Whether to animate the rotation of the chart - animation: { - animateRotate: true, - animateScale: true - }, - startAngle: -0.5 * Math.PI, legendCallback: function(chart) { var list = document.createElement('ul'); @@ -135,28 +137,14 @@ module.exports = DatasetController.extend({ return this._cachedMeta.rAxisID; }, - update: function(reset) { + update: function(mode) { var me = this; - var dataset = me.getDataset(); var meta = me._cachedMeta; - var start = me.chart.options.startAngle || 0; - var starts = me._starts = []; - var angles = me._angles = []; var arcs = meta.data; - var i, ilen, angle; me._updateRadius(); - meta.count = me.countVisibleElements(); - - for (i = 0, ilen = dataset.data.length; i < ilen; i++) { - starts[i] = start; - angle = me._computeAngle(i); - angles[i] = angle; - start += angle; - } - - me.updateElements(arcs, 0, arcs.length, reset); + me.updateElements(arcs, 0, arcs.length, mode); }, /** @@ -177,8 +165,9 @@ module.exports = DatasetController.extend({ me.innerRadius = me.outerRadius - chart.radiusLength; }, - updateElements: function(arcs, start, count, reset) { + updateElements: function(arcs, start, count, mode) { const me = this; + const reset = mode === 'reset'; const chart = me.chart; const dataset = me.getDataset(); const opts = chart.options; @@ -186,33 +175,43 @@ module.exports = DatasetController.extend({ const scale = chart.scales.r; const centerX = scale.xCenter; const centerY = scale.yCenter; - var i; + const datasetStartAngle = opts.startAngle || 0; + let angle = datasetStartAngle; + let i; + + me._cachedMeta.count = me.countVisibleElements(); - for (i = 0; i < start + count; i++) { + for (i = 0; i < start; ++i) { + angle += me._computeAngle(i); + } + for (; i < start + count; i++) { const arc = arcs[i]; - // var negHalfPI = -0.5 * Math.PI; - const datasetStartAngle = opts.startAngle; - const distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[i]); - const startAngle = me._starts[i]; - const endAngle = startAngle + (arc.hidden ? 0 : me._angles[i]); - - const resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[i]); - const options = arc._options = me._resolveDataElementOptions(i); - - arc._model = { - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - borderAlign: options.borderAlign, + let startAngle = angle; + let endAngle = angle + me._computeAngle(i); + let outerRadius = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[i]); + angle = endAngle; + + if (reset) { + if (animationOpts.animateScale) { + outerRadius = 0; + } + if (animationOpts.animateRotate) { + startAngle = datasetStartAngle; + endAngle = datasetStartAngle; + } + } + + const properties = { x: centerX, y: centerY, innerRadius: 0, - outerRadius: reset ? resetRadius : distance, - startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, - endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle + outerRadius, + startAngle, + endAngle, + options: me._resolveDataElementOptions(i) }; - arc.pivot(chart._animationsDisabled); + me._updateElement(arc, i, properties, mode); } }, @@ -230,26 +229,6 @@ module.exports = DatasetController.extend({ return count; }, - /** - * @protected - */ - setHoverStyle: function(arc) { - var model = arc._model; - var options = arc._options; - var getHoverColor = helpers.getHoverColor; - var valueOrDefault = helpers.valueOrDefault; - - arc.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - }; - - model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); - }, - /** * @private */ diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index c52f871de..9f3ae118e 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -21,14 +21,6 @@ defaults._set('radar', { } }); -function nextItem(collection, index) { - return index >= collection.length - 1 ? collection[0] : collection[index + 1]; -} - -function previousItem(collection, index) { - return index <= 0 ? collection[collection.length - 1] : collection[index - 1]; -} - module.exports = DatasetController.extend({ datasetElementType: elements.Line, @@ -93,66 +85,49 @@ module.exports = DatasetController.extend({ }; }, - update: function(reset) { + update: function(mode) { var me = this; var meta = me._cachedMeta; var line = meta.dataset; var points = meta.data || []; - var animationsDisabled = me.chart._animationsDisabled; - var i, ilen; - // Data - line._children = points; - line._loop = true; - // Model - line._model = me._resolveDatasetElementOptions(); + const properties = { + _children: points, + _loop: true, + options: me._resolveDatasetElementOptions() + }; - line.pivot(animationsDisabled); + me._updateElement(line, undefined, properties, mode); // Update Points - me.updateElements(points, 0, points.length, reset); - - // Update bezier control points - me.updateBezierControlPoints(); + me.updateElements(points, 0, points.length, mode); - // Now pivot the point for animation - for (i = 0, ilen = points.length; i < ilen; ++i) { - points[i].pivot(animationsDisabled); - } + line.updateControlPoints(me.chart.chartArea); }, - updateElements: function(points, start, count, reset) { + updateElements: function(points, start, count, mode) { const me = this; const dataset = me.getDataset(); const scale = me.chart.scales.r; + const reset = mode === 'reset'; var i; for (i = start; i < start + count; i++) { const point = points[i]; - const pointPosition = scale.getPointPositionForValue(i, dataset.data[i]); const options = me._resolveDataElementOptions(i); + const pointPosition = scale.getPointPositionForValue(i, dataset.data[i]); + const x = reset ? scale.xCenter : pointPosition.x; const y = reset ? scale.yCenter : pointPosition.y; - // Utility - point._options = options; - - // Desired view properties - point._model = { - x: x, // value not used in dataset scale, but we want a consistent API between scales + const properties = { + x: x, y: y, skip: isNaN(x) || isNaN(y), - // Appearance - radius: options.radius, - pointStyle: options.pointStyle, - rotation: options.rotation, - backgroundColor: options.backgroundColor, - borderColor: options.borderColor, - borderWidth: options.borderWidth, - - // Tooltip - hitRadius: options.hitRadius + options, }; + + me._updateElement(point, i, properties, mode); } }, @@ -169,59 +144,5 @@ module.exports = DatasetController.extend({ values.tension = valueOrDefault(config.lineTension, options.elements.line.tension); return values; - }, - - updateBezierControlPoints: function() { - var me = this; - var meta = me._cachedMeta; - var lineModel = meta.dataset._model; - var area = me.chart.chartArea; - var points = meta.data || []; - var i, ilen, model, controlPoints; - - // Only consider points that are drawn in case the spanGaps option is used - if (meta.dataset._model.spanGaps) { - points = points.filter(function(pt) { - return !pt._model.skip; - }); - } - - function capControlPoint(pt, min, max) { - return Math.max(Math.min(pt, max), min); - } - - for (i = 0, ilen = points.length; i < ilen; ++i) { - model = points[i]._model; - controlPoints = helpers.curve.splineCurve( - previousItem(points, i)._model, - model, - nextItem(points, i)._model, - lineModel.tension - ); - - // Prevent the bezier going outside of the bounds of the graph - model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); - model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); - model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); - model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); - } - }, - - setHoverStyle: function(point) { - var model = point._model; - var options = point._options; - var getHoverColor = helpers.getHoverColor; - - point.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth, - radius: model.radius - }; - - model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); - model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); - model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); - model.radius = valueOrDefault(options.hoverRadius, options.radius); } }); diff --git a/src/core/core.animation.js b/src/core/core.animation.js index ce47f6d73..1e97684da 100644 --- a/src/core/core.animation.js +++ b/src/core/core.animation.js @@ -1,24 +1,92 @@ 'use strict'; -const Element = require('./core.element'); const helpers = require('../helpers/index'); -class Animation extends Element { +const transparent = 'transparent'; +const interpolators = { + number: function(from, to, factor) { + return from + (to - from) * factor; + }, + color: function(from, to, factor) { + var c0 = helpers.color(from || transparent); + var c1 = c0.valid && helpers.color(to || transparent); + return c1 && c1.valid + ? c1.mix(c0, factor).rgbaString() + : to; + } +}; + +class Animation { + constructor(cfg, target, prop, to) { + const me = this; + let from = cfg.from; + + if (from === undefined) { + from = target[prop]; + } + if (to === undefined) { + to = target[prop]; + } + + if (from === undefined) { + from = to; + } else if (to === undefined) { + to = from; + } - constructor(props) { - super({ - 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 + me._active = true; + me._fn = cfg.fn || interpolators[cfg.type || typeof from]; + me._easing = helpers.easing.effects[cfg.easing || 'linear']; + me._start = Math.floor(Date.now() + (cfg.delay || 0)); + me._duration = Math.floor(cfg.duration); + me._loop = !!cfg.loop; + me._target = target; + me._prop = prop; + me._from = from; + me._to = to; + } + + active() { + return this._active; + } - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null, // user specified callback to fire when the animation finishes - }); - helpers.extend(this, props); + cancel() { + const me = this; + if (me._active) { + // update current evaluated value, for smoother animations + me.tick(Date.now()); + me._active = false; + } } + tick(date) { + const me = this; + const elapsed = date - me._start; + const duration = me._duration; + const prop = me._prop; + const from = me._from; + const loop = me._loop; + const to = me._to; + let factor; + + me._active = from !== to && (loop || (elapsed < duration)); + + if (!me._active) { + me._target[prop] = to; + return; + } + + if (elapsed < 0) { + me._target[prop] = from; + return; + } + + factor = (elapsed / duration) % 2; + factor = loop && factor > 1 ? 2 - factor : factor; + factor = me._easing(Math.min(1, Math.max(0, factor))); + + me._target[prop] = me._fn(from, to, factor); + } } module.exports = Animation; diff --git a/src/core/core.animations.js b/src/core/core.animations.js index 5e7222a77..86b022af9 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -1,121 +1,166 @@ 'use strict'; -var defaults = require('./core.defaults'); -var helpers = require('../helpers/index'); +const Animator = require('./core.animator'); +const Animation = require('./core.animation'); +const helpers = require('../helpers/index'); +const defaults = require('./core.defaults'); defaults._set('global', { animation: { duration: 1000, easing: 'easeOutQuart', + active: { + duration: 400 + }, + resize: { + duration: 0 + }, + numbers: { + type: 'number', + properties: ['x', 'y', 'borderWidth', 'radius', 'tension'] + }, + colors: { + type: 'color', + properties: ['borderColor', 'backgroundColor'] + }, onProgress: helpers.noop, onComplete: helpers.noop } }); -module.exports = { - animations: [], - 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; - animation.startTime = Date.now(); - animation.duration = duration; +function copyOptions(target, values) { + let oldOpts = target.options; + let newOpts = values.options; + if (!oldOpts || !newOpts || newOpts.$shared) { + return; + } + if (oldOpts.$shared) { + target.options = helpers.extend({}, oldOpts, newOpts, {$shared: false}); + } else { + helpers.extend(oldOpts, newOpts); + } + delete values.options; +} + +class Animations { + constructor(chart, animations) { + this._chart = chart; + this._properties = new Map(); + this.configure(animations); + } - if (!lazy) { - chart.animating = true; - } + configure(animations) { + const animatedProps = this._properties; + const animDefaults = Object.fromEntries(Object.entries(animations).filter(({1: value}) => !helpers.isObject(value))); - for (i = 0, ilen = animations.length; i < ilen; ++i) { - if (animations[i].chart === chart) { - animations[i] = animation; - return; + for (let [key, cfg] of Object.entries(animations)) { + if (!helpers.isObject(cfg)) { + continue; + } + for (let prop of cfg.properties || [key]) { + // Can have only one config per animation. + if (!animatedProps.has(prop)) { + animatedProps.set(prop, helpers.extend({}, animDefaults, cfg)); + } else if (prop === key) { + // Single property targetting config wins over multi-targetting. + animatedProps.set(prop, helpers.extend({}, animatedProps.get(prop), cfg)); + } } } + } - 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; - }); + /** + * Utility to handle animation of `options`. + * This should not be called, when animating $shared options to $shared new options. + * @private + * @todo if new options are $shared, target.options should be replaced with those new shared + * options after all animations have completed + */ + _animateOptions(target, values) { + const newOptions = values.options; + let animations = []; - if (index !== -1) { - this.animations.splice(index, 1); - chart.animating = false; + if (!newOptions) { + return animations; } - }, - - 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(); - }); + let options = target.options; + if (options) { + if (options.$shared) { + // If the current / old options are $shared, meaning other elements are + // using the same options, we need to clone to become unique. + target.options = options = helpers.extend({}, options, {$shared: false, $animations: {}}); + } + animations = this._createAnimations(options, newOptions); + } else { + target.options = newOptions; } - }, + return animations; + } /** * @private */ - startDigest: function() { - var me = this; + _createAnimations(target, values) { + const animatedProps = this._properties; + const animations = []; + const running = target.$animations || (target.$animations = {}); + const props = Object.keys(values); + let i; + + for (i = props.length - 1; i >= 0; --i) { + let prop = props[i]; + if (prop.charAt(0) === '$') { + continue; + } + + if (prop === 'options') { + animations.push.apply(animations, this._animateOptions(target, values)); + continue; + } + let value = values[prop]; - me.advance(); + const cfg = animatedProps.get(prop); + if (!cfg || !cfg.duration) { + // not animated, set directly to new value + target[prop] = value; + continue; + } - // Do we have more stuff to animate? - if (me.animations.length > 0) { - me.requestAnimationFrame(); + let animation = running[prop]; + if (animation) { + animation.cancel(); + } + running[prop] = animation = new Animation(cfg, target, prop, value); + animations.push(animation); } - }, + return animations; + } + /** - * @private - */ - advance: function() { - var animations = this.animations; - var animation, chart, numSteps, nextStep; - var i = 0; - - // 1 animation per chart, so we are looping charts here - while (i < animations.length) { - animation = animations[i]; - chart = animation.chart; - numSteps = animation.numSteps; - - // Make sure that currentStep starts at 1 - // https://github.com/chartjs/Chart.js/issues/6104 - nextStep = Math.floor((Date.now() - animation.startTime) / animation.duration * numSteps) + 1; - animation.currentStep = Math.min(nextStep, numSteps); - - helpers.callback(animation.render, [chart, animation], chart); - helpers.callback(animation.onAnimationProgress, [animation], chart); - - if (animation.currentStep >= numSteps) { - helpers.callback(animation.onAnimationComplete, [animation], chart); - chart.animating = false; - animations.splice(i, 1); - } else { - ++i; - } + * Update `target` properties to new values, using configured animations + * @param {object} target - object to update + * @param {object} values - new target properties + * @returns {boolean|undefined} - `true` if animations were started + **/ + update(target, values) { + if (this._properties.size === 0) { + // Nothing is animated, just apply the new values. + // Options can be shared, need to account for that. + copyOptions(target, values); + // copyOptions removes the `options` from `values`, + // unless it can be directly assigned. + helpers.extend(target, values); + return; + } + + const animations = this._createAnimations(target, values); + + if (animations.length) { + Animator.add(this._chart, animations); + return true; } } -}; +} + +module.exports = Animations; diff --git a/src/core/core.animator.js b/src/core/core.animator.js new file mode 100644 index 000000000..29422d5ba --- /dev/null +++ b/src/core/core.animator.js @@ -0,0 +1,211 @@ +'use strict'; + +const helpers = require('../helpers/index'); + +function drawFPS(chart, count, date, lastDate) { + const fps = (1000 / (date - lastDate)) | 0; + const ctx = chart.ctx; + ctx.save(); + ctx.clearRect(0, 0, 50, 24); + ctx.fillStyle = 'black'; + ctx.textAlign = 'right'; + if (count) { + ctx.fillText(count, 50, 8); + ctx.fillText(fps + ' fps', 50, 18); + } + ctx.restore(); +} + +class Animator { + constructor() { + this._request = null; + this._charts = new Map(); + this._running = false; + } + + /** + * @private + */ + _notify(chart, anims, date, type) { + const callbacks = anims.listeners[type] || []; + const numSteps = anims.duration; + + callbacks.forEach(fn => fn({ + chart: chart, + numSteps, + currentStep: date - anims.start + })); + } + + /** + * @private + */ + _refresh() { + const me = this; + + if (me._request) { + return; + } + me._running = true; + + me._request = helpers.requestAnimFrame.call(window, function() { + me._update(); + me._request = null; + + if (me._running) { + me._refresh(); + } + }); + } + + /** + * @private + */ + _update() { + const me = this; + const date = Date.now(); + const charts = me._charts; + let remaining = 0; + + for (let [chart, anims] of charts) { + if (!anims.running || !anims.items.length) { + continue; + } + const items = anims.items; + let i = items.length - 1; + let draw = false; + let item; + + for (; i >= 0; --i) { + item = items[i]; + + if (item._active) { + item.tick(date); + draw = true; + } else { + // Remove the item by replacing it with last item and removing the last + // A lot faster than splice. + items[i] = items[items.length - 1]; + items.pop(); + } + } + + if (draw) { + chart.draw(); + if (chart.options.animation.debug) { + drawFPS(chart, items.length, date, me._lastDate); + } + } + + me._notify(chart, anims, date, 'progress'); + + if (!items.length) { + anims.running = false; + me._notify(chart, anims, date, 'complete'); + } + + remaining += items.length; + } + + this._lastDate = date; + + if (remaining === 0) { + this._running = false; + } + } + + _getAnims(chart) { + const charts = this._charts; + let anims = charts.get(chart); + if (!anims) { + anims = { + running: false, + items: [], + listeners: { + complete: [], + progress: [] + } + }; + charts.set(chart, anims); + } + return anims; + } + + /** + * @param {Chart} chart + * @param {string} event - event name + * @param {Function} cb - callback + */ + listen(chart, event, cb) { + this._getAnims(chart).listeners[event].push(cb); + } + + /** + * Add animations + * @param {Chart} chart + * @param {Animation[]} items - animations + */ + add(chart, items) { + if (!items || !items.length) { + return; + } + this._getAnims(chart).items.push(...items); + } + + /** + * Counts number of active animations for the chart + * @param {Chart} chart + */ + has(chart) { + return this._getAnims(chart).items.length > 0; + } + + /** + * Start animating (all charts) + * @param {Chart} chart + */ + start(chart) { + const anims = this._charts.get(chart); + if (!anims) { + return; + } + anims.running = true; + anims.start = Date.now(); + anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); + this._refresh(); + } + + running(chart) { + if (!this._running) { + return false; + } + const anims = this._charts.get(chart); + if (!anims || !anims.running || !anims.items.length) { + return false; + } + return true; + } + + /** + * Stop all animations for the chart + * @param {Chart} chart + */ + stop(chart) { + const anims = this._charts.get(chart); + if (!anims || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + + for (; i >= 0; --i) { + items[i].cancel(); + } + anims.items = []; + this._notify(chart, anims, Date.now(), 'complete'); + } +} + +const instance = new Animator(); + +module.exports = instance; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 305d56549..b970f05b3 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -1,7 +1,6 @@ 'use strict'; -var Animation = require('./core.animation'); -var animations = require('./core.animations'); +var Animator = require('./core.animator'); var controllers = require('../controllers/index'); var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); @@ -26,13 +25,11 @@ defaults._set('global', { hover: { onHover: null, mode: 'nearest', - intersect: true, - animationDuration: 400 + intersect: true }, onClick: null, maintainAspectRatio: true, - responsive: true, - responsiveAnimationDuration: 0 + responsive: true }); function mergeScaleConfig(config, options) { @@ -115,11 +112,7 @@ function initConfig(config) { } function isAnimationDisabled(config) { - return !config.animation || !( - config.animation.duration || - (config.hover && config.hover.animationDuration) || - config.responsiveAnimationDuration - ); + return !config.animation; } function updateConfig(chart) { @@ -143,8 +136,6 @@ function updateConfig(chart) { chart.ensureScalesHaveIDs(); chart.buildOrUpdateScales(); - // Tooltip - chart.tooltip._options = newOptions.tooltips; chart.tooltip.initialize(); } @@ -161,6 +152,20 @@ function compare2Level(l1, l2) { }; } +function onAnimationsComplete(ctx) { + const chart = ctx.chart; + const animationOptions = chart.options.animation; + + plugins.notify(chart, 'afterRender'); + helpers.callback(animationOptions && animationOptions.onComplete, arguments, chart); +} + +function onAnimationProgress(ctx) { + const chart = ctx.chart; + const animationOptions = chart.options.animation; + helpers.callback(animationOptions && animationOptions.onProgress, arguments, chart); +} + var Chart = function(item, config) { this.construct(item, config); return this; @@ -213,6 +218,9 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { return; } + Animator.listen(me, 'complete', onAnimationsComplete); + Animator.listen(me, 'progress', onAnimationProgress); + me.initialize(); me.update(); }, @@ -249,8 +257,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { }, stop: function() { - // Stops any current animation loop occurring - animations.cancelAnimation(this); + Animator.stop(this); return this; }, @@ -289,9 +296,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { } me.stop(); - me.update({ - duration: options.responsiveAnimationDuration - }); + me.update('resize'); } }, @@ -455,11 +460,11 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { this.tooltip.initialize(); }, - update: function(config) { + update: function(mode) { var me = this; var i, ilen; - config = config || {}; + me._updating = true; updateConfig(me); @@ -471,9 +476,6 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { return; } - // In case the entire data object changed - me.tooltip._data = me.data; - // Make sure dataset controllers are updated and new controllers are reset var newControllers = me.buildOrUpdateControllers(); @@ -485,40 +487,27 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { me.updateLayout(); // Can only reset the new controllers after the scales have been updated - if (me.options.animation && me.options.animation.duration) { + if (me.options.animation) { helpers.each(newControllers, function(controller) { controller.reset(); }); } - me.updateDatasets(); - - // Need to reset tooltip in case it is displayed with elements that are removed - // after update. - me.tooltip.initialize(); - - // Last active contains items that were previously hovered. - me.lastActive = []; + me.updateDatasets(mode); // Do this before render so that any plugins that need final scale updates can use it plugins.notify(me, 'afterUpdate'); me._layers.sort(compare2Level('z', '_idx')); - if (me._bufferedRender) { - me._bufferedRequest = { - duration: config.duration, - easing: config.easing, - lazy: config.lazy - }; - } else { - me.render(config); - } - // Replay last event from before update if (me._lastEvent) { me.eventHandler(me._lastEvent); } + + me.render(); + + me._updating = false; }, /** @@ -557,7 +546,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. * @private */ - updateDatasets: function() { + updateDatasets: function(mode) { var me = this; if (plugins.notify(me, 'beforeDatasetsUpdate') === false) { @@ -565,7 +554,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { } for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { - me.updateDataset(i); + me.updateDataset(i, mode); } plugins.notify(me, 'afterDatasetsUpdate'); @@ -576,91 +565,52 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * hook, in which case, plugins will not be called on `afterDatasetUpdate`. * @private */ - updateDataset: function(index) { - var me = this; - var meta = me.getDatasetMeta(index); - var args = { - meta: meta, - index: index - }; + updateDataset: function(index, mode) { + const me = this; + const meta = me.getDatasetMeta(index); + const args = {meta, index, mode}; if (plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) { return; } - meta.controller._update(); + meta.controller._update(mode); plugins.notify(me, 'afterDatasetUpdate', [args]); }, - render: function(config) { - var me = this; - - if (!config || typeof config !== 'object') { - // backwards compatibility - config = { - duration: config, - lazy: arguments[1] - }; - } - - var animationOptions = me.options.animation; - var duration = valueOrDefault(config.duration, animationOptions && animationOptions.duration); - var lazy = config.lazy; - + render: function() { + const me = this; + const animationOptions = me.options.animation; if (plugins.notify(me, 'beforeRender') === false) { return; } - - var onComplete = function(animation) { + var onComplete = function() { plugins.notify(me, 'afterRender'); - helpers.callback(animationOptions && animationOptions.onComplete, [animation], me); + helpers.callback(animationOptions && animationOptions.onComplete, [], me); }; - if (animationOptions && duration) { - var animation = new Animation({ - numSteps: duration / 16.66, // 60 fps - easing: config.easing || animationOptions.easing, - - render: function(chart, animationObject) { - const easingFunction = helpers.easing.effects[animationObject.easing]; - const stepDecimal = animationObject.currentStep / animationObject.numSteps; - - chart.draw(easingFunction(stepDecimal)); - }, - - onAnimationProgress: animationOptions.onProgress, - onAnimationComplete: onComplete - }); - - animations.addAnimation(me, animation, duration, lazy); + if (Animator.has(me)) { + if (!Animator.running(me)) { + Animator.start(me); + } } else { me.draw(); - - // See https://github.com/chartjs/Chart.js/issues/3781 - onComplete(new Animation({numSteps: 0, chart: me})); + onComplete(); } - - return me; }, - draw: function(easingValue) { + draw: function() { var me = this; var i, layers; me.clear(); - if (helpers.isNullOrUndef(easingValue)) { - easingValue = 1; - } - - me.transition(easingValue); - if (me.width <= 0 || me.height <= 0) { return; } - if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { + if (plugins.notify(me, 'beforeDraw') === false) { return; } @@ -672,41 +622,16 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { layers[i].draw(me.chartArea); } - me.drawDatasets(easingValue); + me.drawDatasets(); // Rest of layers for (; i < layers.length; ++i) { layers[i].draw(me.chartArea); } - me._drawTooltip(easingValue); + me._drawTooltip(); - plugins.notify(me, 'afterDraw', [easingValue]); - }, - - /** - * @private - */ - transition: function(easingValue) { - const me = this; - var i, ilen; - - if (!me._animationsDisabled) { - const metas = me._getSortedDatasetMetas(); - for (i = 0, ilen = metas.length; i < ilen; ++i) { - let meta = metas[i]; - if (meta.visible) { - meta.controller.transition(easingValue); - } - } - } - - me.tooltip.transition(easingValue); - - if (me._lastEvent && me.animating) { - // If, during animation, element under mouse changes, let's react to that. - me.handleEvent(me._lastEvent); - } + plugins.notify(me, 'afterDraw'); }, /** @@ -740,20 +665,20 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * hook, in which case, plugins will not be called on `afterDatasetsDraw`. * @private */ - drawDatasets: function(easingValue) { + drawDatasets: function() { var me = this; var metasets, i; - if (plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) { + if (plugins.notify(me, 'beforeDatasetsDraw') === false) { return; } metasets = me._getSortedVisibleDatasetMetas(); for (i = metasets.length - 1; i >= 0; --i) { - me.drawDataset(metasets[i], easingValue); + me.drawDataset(metasets[i]); } - plugins.notify(me, 'afterDatasetsDraw', [easingValue]); + plugins.notify(me, 'afterDatasetsDraw'); }, /** @@ -761,7 +686,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * hook, in which case, plugins will not be called on `afterDatasetDraw`. * @private */ - drawDataset: function(meta, easingValue) { + drawDataset: function(meta) { var me = this; var ctx = me.ctx; var clip = meta._clip; @@ -770,7 +695,6 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { var args = { meta: meta, index: meta.index, - easingValue: easingValue }; if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { @@ -784,7 +708,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { bottom: clip.bottom === false ? canvas.height : area.bottom + clip.bottom }); - meta.controller.draw(easingValue); + meta.controller.draw(); helpers.canvas.unclipArea(ctx); @@ -796,19 +720,18 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * hook, in which case, plugins will not be called on `afterTooltipDraw`. * @private */ - _drawTooltip: function(easingValue) { + _drawTooltip: function() { var me = this; var tooltip = me.tooltip; var args = { - tooltip: tooltip, - easingValue: easingValue + tooltip: tooltip }; if (plugins.notify(me, 'beforeTooltipDraw', [args]) === false) { return; } - tooltip.draw(); + tooltip.draw(me.ctx); plugins.notify(me, 'afterTooltipDraw', [args]); }, @@ -925,12 +848,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { }, initToolTip: function() { - var me = this; - me.tooltip = new Tooltip({ - _chart: me, - _data: me.data, - _options: me.options.tooltips - }); + this.tooltip = new Tooltip({_chart: this}); }, /** @@ -1020,48 +938,22 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { * @private */ eventHandler: function(e) { - var me = this; - var tooltip = me.tooltip; + const me = this; + const tooltip = me.tooltip; if (plugins.notify(me, 'beforeEvent', [e]) === false) { return; } - // Buffer any update calls so that renders do not occur - me._bufferedRender = true; - me._bufferedRequest = null; + me.handleEvent(e); - var changed = me.handleEvent(e); - // for smooth tooltip animations issue #4989 - // the tooltip should be the source of change - // Animation check workaround: - // tooltip._start will be null when tooltip isn't animating if (tooltip) { - changed = tooltip._start - ? tooltip.handleEvent(e) - : changed | tooltip.handleEvent(e); + tooltip.handleEvent(e); } plugins.notify(me, 'afterEvent', [e]); - var bufferedRequest = me._bufferedRequest; - if (bufferedRequest) { - // If we have an update that was triggered, we need to do a normal render - me.render(bufferedRequest); - } else if (changed && !me.animating) { - // If entering, leaving, or changing elements, animate the change via pivot - me.stop(); - - // We only need to render at this point. Updating will cause scales to be - // recomputed generating flicker & using more memory than necessary. - me.render({ - duration: me.options.hover.animationDuration, - lazy: true - }); - } - - me._bufferedRender = false; - me._bufferedRequest = null; + me.render(); return me; }, @@ -1086,7 +978,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { me._lastEvent = null; } else { me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions); - me._lastEvent = e.type === 'click' ? null : e; + me._lastEvent = e.type === 'click' ? me._lastEvent : e; } // Invoke onHover hook @@ -1100,8 +992,10 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { } } - me._updateHoverStyles(); changed = !helpers._elementsEqual(me.active, me.lastActive); + if (changed) { + me._updateHoverStyles(); + } // Remember Last Actives me.lastActive = me.active; diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 2c5e35e9e..3a818477d 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -1,6 +1,7 @@ 'use strict'; var helpers = require('../helpers/index'); +var Animations = require('./core.animations'); var resolve = helpers.options.resolve; @@ -268,6 +269,8 @@ helpers.extend(DatasetController.prototype, { me.chart = chart; me._ctx = chart.ctx; me.index = datasetIndex; + me._cachedAnimations = {}; + me._cachedDataOpts = {}; me._cachedMeta = meta = me.getMeta(); me._type = meta.type; me._configure(); @@ -347,7 +350,7 @@ helpers.extend(DatasetController.prototype, { }, reset: function() { - this._update(true); + this._update('reset'); }, /** @@ -450,7 +453,7 @@ helpers.extend(DatasetController.prototype, { }, /** - * Returns the merged user-supplied and default dataset-level options + * Merges user-supplied and default dataset-level options * @private */ _configure: function() { @@ -740,33 +743,19 @@ helpers.extend(DatasetController.prototype, { /** * @private */ - _update: function(reset) { + _update: function(mode) { const me = this; const meta = me._cachedMeta; me._configure(); - me._cachedDataOpts = null; - me.update(reset); + me._cachedAnimations = {}; + me._cachedDataOpts = {}; + me.update(mode); meta._clip = toClip(helpers.valueOrDefault(me._config.clip, defaultClip(meta.xScale, meta.yScale, me._getMaxOverflow()))); me._cacheScaleStackStatus(); }, update: helpers.noop, - transition: function(easingValue) { - const meta = this._cachedMeta; - const elements = meta.data || []; - const ilen = elements.length; - let i = 0; - - for (; i < ilen; ++i) { - elements[i].transition(easingValue); - } - - if (meta.dataset) { - meta.dataset.transition(easingValue); - } - }, - draw: function() { const ctx = this._ctx; const meta = this._cachedMeta; @@ -783,30 +772,54 @@ helpers.extend(DatasetController.prototype, { } }, + _addAutomaticHoverColors: function(index, options) { + const me = this; + const getHoverColor = helpers.getHoverColor; + const normalOptions = me.getStyle(index); + const missingColors = Object.keys(normalOptions).filter(key => { + return key.indexOf('Color') !== -1 && !(key in options); + }); + let i = missingColors.length - 1; + let color; + for (; i >= 0; i--) { + color = missingColors[i]; + options[color] = getHoverColor(normalOptions[color]); + } + }, + /** * Returns a set of predefined style properties that should be used to represent the dataset * or the data if the index is specified * @param {number} index - data index * @return {IStyleInterface} style object */ - getStyle: function(index) { + getStyle: function(index, active) { const me = this; const meta = me._cachedMeta; const dataset = meta.dataset; - let style; - if (dataset && index === undefined) { - style = me._resolveDatasetElementOptions(); - } else { - index = index || 0; - style = me._resolveDataElementOptions(index); + if (!me._config) { + me._configure(); } - if (style.fill === false || style.fill === null) { - style.backgroundColor = style.borderColor; + const options = dataset && index === undefined + ? me._resolveDatasetElementOptions(active) + : me._resolveDataElementOptions(index || 0, active && 'active'); + if (active) { + me._addAutomaticHoverColors(index, options); } + return options; + }, + + _getContext(index, active) { + return { + chart: this.chart, + dataIndex: index, + dataset: this.getDataset(), + datasetIndex: this.index, + active + }; - return style; }, /** @@ -819,21 +832,19 @@ helpers.extend(DatasetController.prototype, { const options = chart.options.elements[me.datasetElementType.prototype._type] || {}; const elementOptions = me._datasetElementOptions; const values = {}; - const context = { - chart, - dataset: me.getDataset(), - datasetIndex: me.index, - active - }; - let i, ilen, key, readKey; + const context = me._getContext(undefined, active); + let i, ilen, key, readKey, value; for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { key = elementOptions[i]; readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; - values[key] = resolve([ + value = resolve([ datasetOpts[readKey], options[readKey] ], context); + if (value !== undefined) { + values[key] = value; + } } return values; @@ -842,72 +853,152 @@ helpers.extend(DatasetController.prototype, { /** * @private */ - _resolveDataElementOptions: function(index) { + _resolveDataElementOptions: function(index, mode) { const me = this; + const active = mode === 'active'; const cached = me._cachedDataOpts; - if (cached) { - return cached; + if (cached[mode]) { + return cached[mode]; } const chart = me.chart; const datasetOpts = me._config; const options = chart.options.elements[me.dataElementType.prototype._type] || {}; const elementOptions = me._dataElementOptions; const values = {}; - const context = { - chart: chart, - dataIndex: index, - dataset: me.getDataset(), - datasetIndex: me.index - }; - const info = {cacheable: true}; - let keys, i, ilen, key; + const context = me._getContext(index, active); + const info = {cacheable: !active}; + let keys, i, ilen, key, value, readKey; if (helpers.isArray(elementOptions)) { for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { key = elementOptions[i]; - values[key] = resolve([ - datasetOpts[key], - options[key] + readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; + value = resolve([ + datasetOpts[readKey], + options[readKey] ], context, index, info); + if (value !== undefined) { + values[key] = value; + } } } else { keys = Object.keys(elementOptions); for (i = 0, ilen = keys.length; i < ilen; ++i) { key = keys[i]; - values[key] = resolve([ - datasetOpts[elementOptions[key]], - datasetOpts[key], - options[key] + readKey = active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; + value = resolve([ + datasetOpts[elementOptions[readKey]], + datasetOpts[readKey], + options[readKey] ], context, index, info); + if (value !== undefined) { + values[key] = value; + } } } if (info.cacheable) { - me._cachedDataOpts = Object.freeze(values); + // `$shared` indicades this set of options can be shared between multiple elements. + // Sharing is used to reduce number of properties to change during animation. + values.$shared = true; + + // We cache options by `mode`, which can be 'active' for example. This enables us + // to have the 'active' element options and 'default' options to switch between + // when interacting. + cached[mode] = values; } return values; }, - removeHoverStyle: function(element) { - helpers.merge(element._model, element.$previousStyle || {}); - delete element.$previousStyle; + /** + * @private + */ + _resolveAnimations: function(index, mode, active) { + const me = this; + const chart = me.chart; + const cached = me._cachedAnimations; + mode = mode || 'default'; + + if (cached[mode]) { + return cached[mode]; + } + + const info = {cacheable: true}; + const context = me._getContext(index, active); + const datasetAnim = resolve([me._config.animation], context, index, info); + const chartAnim = resolve([chart.options.animation], context, index, info); + let config = helpers.mergeIf({}, [datasetAnim, chartAnim]); + + if (active && config.active) { + config = helpers.extend({}, config, config.active); + } + if (mode === 'resize' && config.resize) { + config = helpers.extend({}, config, config.resize); + } + + const animations = new Animations(chart, config); + + if (info.cacheable) { + cached[mode] = animations && Object.freeze(animations); + } + + return animations; }, - setHoverStyle: function(element, datasetIndex, index) { - const dataset = this.chart.data.datasets[datasetIndex]; - const model = element._model; - const getHoverColor = helpers.getHoverColor; + /** + * Utility for checking if the options are shared and should be animated separately. + * @private + */ + _getSharedOptions: function(mode, el, options) { + if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) { + return {target: el.options, options}; + } + }, - element.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth - }; + /** + * Utility for determining if `options` should be included in the updated properties + * @private + */ + _includeOptions: function(mode, sharedOptions) { + return mode !== 'resize' && !sharedOptions; + }, + + /** + * Utility for updating a element with new properties, using animations when appropriate. + * @private + */ + _updateElement: function(element, index, properties, mode) { + if (mode === 'reset' || mode === 'none') { + helpers.extend(element, properties); + } else { + this._resolveAnimations(index, mode).update(element, properties); + } + }, + + /** + * Utility to animate the shared options, that are potentially affecting multiple elements. + * @private + */ + _updateSharedOptions: function(sharedOptions, mode) { + if (sharedOptions) { + this._resolveAnimations(undefined, mode).update(sharedOptions.target, sharedOptions.options); + } + }, + + /** + * @private + */ + _setStyle(element, index, mode, active) { + this._resolveAnimations(index, mode, active).update(element, {options: this.getStyle(index, active)}); + }, + + removeHoverStyle: function(element, datasetIndex, index) { + this._setStyle(element, index, 'active', false); + }, - model.backgroundColor = resolve([dataset.hoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index); - model.borderColor = resolve([dataset.hoverBorderColor, getHoverColor(model.borderColor)], undefined, index); - model.borderWidth = resolve([dataset.hoverBorderWidth, model.borderWidth], undefined, index); + setHoverStyle: function(element, datasetIndex, index) { + this._setStyle(element, index, 'active', true); }, /** @@ -917,7 +1008,7 @@ helpers.extend(DatasetController.prototype, { const element = this._cachedMeta.dataset; if (element) { - this.removeHoverStyle(element); + this._setStyle(element, undefined, 'active', false); } }, @@ -926,24 +1017,10 @@ helpers.extend(DatasetController.prototype, { */ _setDatasetHoverStyle: function() { const element = this._cachedMeta.dataset; - const prev = {}; - let i, ilen, key, keys, hoverOptions, model; - if (!element) { - return; - } - - model = element._model; - hoverOptions = this._resolveDatasetElementOptions(true); - - keys = Object.keys(hoverOptions); - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - prev[key] = model[key]; - model[key] = hoverOptions[key]; + if (element) { + this._setStyle(element, undefined, 'active', true); } - - element.$previousStyle = prev; }, /** @@ -986,7 +1063,7 @@ helpers.extend(DatasetController.prototype, { } me._parse(start, count); - me.updateElements(data, start, count); + me.updateElements(data, start, count, 'reset'); }, /** diff --git a/src/core/core.element.js b/src/core/core.element.js index 159e4721c..fb2e442c3 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -1,119 +1,27 @@ 'use strict'; -import color from 'chartjs-color'; -import helpers from '../helpers/index'; +import {extend, inherits} from '../helpers/helpers.core'; import {isNumber} from '../helpers/helpers.math'; -function interpolate(start, view, model, ease) { - var keys = Object.keys(model); - var i, ilen, key, actual, origin, target, type, c0, c1; - - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - - target = model[key]; - - // if a value is added to the model after pivot() has been called, the view - // doesn't contain it, so let's initialize the view to the target value. - if (!Object.prototype.hasOwnProperty.call(view, key)) { - view[key] = target; - } - - actual = view[key]; - - if (actual === target || key[0] === '_') { - continue; - } - - if (!Object.prototype.hasOwnProperty.call(start, key)) { - start[key] = actual; - } - - origin = start[key]; - - type = typeof target; - - if (type === typeof origin) { - if (type === 'string') { - c0 = color(origin); - if (c0.valid) { - c1 = color(target); - if (c1.valid) { - view[key] = c1.mix(c0, ease).rgbString(); - continue; - } - } - } else if (helpers.isFinite(origin) && helpers.isFinite(target)) { - view[key] = origin + (target - origin) * ease; - continue; - } - } - - view[key] = target; - } -} - class Element { constructor(configuration) { - helpers.extend(this, configuration); + extend(this, configuration); // this.hidden = false; we assume Element has an attribute called hidden, but do not initialize to save memory } - pivot(animationsDisabled) { - var me = this; - if (animationsDisabled) { - me._view = me._model; - return me; - } - - if (!me._view) { - me._view = helpers.extend({}, me._model); - } - me._start = {}; - return me; - } - - transition(ease) { - var me = this; - var model = me._model; - var start = me._start; - var view = me._view; - - // No animation -> No Transition - if (!model || ease === 1) { - // _model has to be cloned to _view - // Otherwise, when _model properties are set on hover, _view.* is also set to the same value, and hover animation doesn't occur - me._view = helpers.extend({}, model); - me._start = null; - return me; - } - - if (!view) { - view = me._view = {}; - } - - if (!start) { - start = me._start = {}; - } - - interpolate(start, view, model, ease); - - return me; - } - tooltipPosition() { return { - x: this._model.x, - y: this._model.y + x: this.x, + y: this.y }; } hasValue() { - return isNumber(this._model.x) && isNumber(this._model.y); + return isNumber(this.x) && isNumber(this.y); } } -Element.extend = helpers.inherits; +Element.extend = inherits; export default Element; diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index f1975cd1e..2224d7242 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -33,7 +33,7 @@ function evaluateAllVisibleItems(chart, handler) { ({index, data} = metasets[i]); for (let j = 0, jlen = data.length; j < jlen; ++j) { element = data[j]; - if (!element._view.skip) { + if (!element.skip) { handler(element, index, j); } } @@ -66,7 +66,7 @@ function evaluateItemsAtIndex(chart, axis, position, handler) { const metaset = metasets[i]; const index = indices[i]; const element = metaset.data[index]; - if (!element._view.skip) { + if (!element.skip) { handler(element, metaset.index, index); } } @@ -193,7 +193,7 @@ export default { const element = meta.data[index]; // don't count items that are skipped (null data) - if (element && !element._view.skip) { + if (element && !element.skip) { elements.push({element, datasetIndex: meta.index, index}); } }); diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index cf44af0ac..1d68c20bd 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -272,20 +272,17 @@ module.exports = { */ /** * @method IPlugin#beforeDraw - * @desc Called before drawing `chart` at every animation frame specified by the given - * easing value. If any plugin returns `false`, the frame drawing is cancelled until - * another `render` is triggered. + * @desc Called before drawing `chart` at every animation frame. If any plugin returns `false`, + * the frame drawing is cancelled untilanother `render` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {number} easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart drawing. */ /** * @method IPlugin#afterDraw - * @desc Called after the `chart` has been drawn for the specific easing value. Note - * that this hook will not be called if the drawing has been previously cancelled. + * @desc Called after the `chart` has been drawn. Note that this hook will not be called + * if the drawing has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {number} easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. */ /** @@ -293,7 +290,6 @@ module.exports = { * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, * the datasets drawing is cancelled until another `render` is triggered. * @param {Chart.Controller} chart - The chart instance. - * @param {number} easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart datasets drawing. */ @@ -302,7 +298,6 @@ module.exports = { * @desc Called after the `chart` datasets have been drawn. Note that this hook * will not be called if the datasets drawing has been previously cancelled. * @param {Chart.Controller} chart - The chart instance. - * @param {number} easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. */ /** @@ -314,7 +309,6 @@ module.exports = { * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. - * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart datasets drawing. */ @@ -327,7 +321,6 @@ module.exports = { * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. - * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. */ /** @@ -337,7 +330,6 @@ module.exports = { * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Tooltip} args.tooltip - The tooltip. - * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart tooltip drawing. */ @@ -348,7 +340,6 @@ module.exports = { * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Tooltip} args.tooltip - The tooltip. - * @param {number} args.easingValue - The current animation value, between 0.0 and 1.0. * @param {object} options - The plugin options. */ /** diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 73122f8df..eac403f90 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -3,6 +3,7 @@ const defaults = require('./core.defaults'); const Element = require('./core.element'); const helpers = require('../helpers/index'); +const Animations = require('./core.animations'); const valueOrDefault = helpers.valueOrDefault; const getRtlHelper = helpers.rtl.getRtlAdapter; @@ -37,6 +38,18 @@ defaults._set('global', { displayColors: true, borderColor: 'rgba(0,0,0,0)', borderWidth: 0, + animation: { + duration: 400, + easing: 'easeOutQuart', + numbers: { + type: 'number', + properties: ['x', 'y', 'width', 'height'], + }, + opacity: { + easing: 'linear', + duration: 200 + } + }, callbacks: { // Args are: (tooltipItems, data) beforeTitle: helpers.noop, @@ -76,15 +89,14 @@ defaults._set('global', { }, labelColor: function(tooltipItem, chart) { var meta = chart.getDatasetMeta(tooltipItem.datasetIndex); - var activeElement = meta.data[tooltipItem.index]; - var view = activeElement.$previousStyle || activeElement._view; + var options = meta.controller.getStyle(tooltipItem.index); return { - borderColor: view.borderColor, - backgroundColor: view.backgroundColor + borderColor: options.borderColor, + backgroundColor: options.backgroundColor }; }, labelTextColor: function() { - return this._options.bodyFontColor; + return this.options.bodyFontColor; }, afterLabel: helpers.noop, @@ -218,90 +230,61 @@ function createTooltipItem(chart, item) { /** * Helper to get the reset model for the tooltip - * @param tooltipOpts {object} the tooltip options + * @param options {object} the tooltip options */ -function getBaseModel(tooltipOpts) { +function resolveOptions(options) { var globalDefaults = defaults.global; - return { - // Positioning - xPadding: tooltipOpts.xPadding, - yPadding: tooltipOpts.yPadding, - xAlign: tooltipOpts.xAlign, - yAlign: tooltipOpts.yAlign, - - // Drawing direction and text direction - rtl: tooltipOpts.rtl, - textDirection: tooltipOpts.textDirection, - - // 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 - }; + options = helpers.extend({}, globalDefaults.tooltips, options); + + options.bodyFontFamily = valueOrDefault(options.bodyFontFamily, globalDefaults.defaultFontFamily); + options.bodyFontStyle = valueOrDefault(options.bodyFontStyle, globalDefaults.defaultFontStyle); + options.bodyFontSize = valueOrDefault(options.bodyFontSize, globalDefaults.defaultFontSize); + + options.titleFontFamily = valueOrDefault(options.titleFontFamily, globalDefaults.defaultFontFamily); + options.titleFontStyle = valueOrDefault(options.titleFontStyle, globalDefaults.defaultFontStyle); + options.titleFontSize = valueOrDefault(options.titleFontSize, globalDefaults.defaultFontSize); + + options.footerFontFamily = valueOrDefault(options.footerFontFamily, globalDefaults.defaultFontFamily); + options.footerFontStyle = valueOrDefault(options.footerFontStyle, globalDefaults.defaultFontStyle); + options.footerFontSize = valueOrDefault(options.footerFontSize, globalDefaults.defaultFontSize); + + return options; } /** * Get the size of the tooltip */ -function getTooltipSize(tooltip, model) { - var ctx = tooltip._chart.ctx; +function getTooltipSize(tooltip) { + const ctx = tooltip._chart.ctx; + const {body, footer, options, title} = tooltip; + const {bodyFontSize, footerFontSize, titleFontSize} = options; + const titleLineCount = title.length; + const footerLineCount = footer.length; - var height = model.yPadding * 2; // Tooltip Padding - var width = 0; + let height = options.yPadding * 2; // Tooltip Padding + let 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 + combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length; + + if (titleLineCount) { + height += titleLineCount * titleFontSize + + (titleLineCount - 1) * options.titleSpacing + + options.titleMarginBottom; + } + if (combinedBodyLength) { + height += combinedBodyLength * bodyFontSize + + (combinedBodyLength - 1) * options.bodySpacing; + } + if (footerLineCount) { + height += options.footerMarginTop + + footerLineCount * footerFontSize + + (footerLineCount - 1) * options.footerSpacing; + } // Title width var widthPadding = 0; @@ -309,15 +292,15 @@ function getTooltipSize(tooltip, model) { width = Math.max(width, ctx.measureText(line).width + widthPadding); }; - ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily); - helpers.each(model.title, maxLineWidth); + ctx.font = helpers.fontString(titleFontSize, options.titleFontStyle, options.titleFontFamily); + helpers.each(tooltip.title, maxLineWidth); // Body width - ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily); - helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth); + ctx.font = helpers.fontString(bodyFontSize, options.bodyFontStyle, options.bodyFontFamily); + helpers.each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth); // Body lines may include some extra width due to the color box - widthPadding = model.displayColors ? (bodyFontSize + 2) : 0; + widthPadding = options.displayColors ? (bodyFontSize + 2) : 0; helpers.each(body, function(bodyItem) { helpers.each(bodyItem.before, maxLineWidth); helpers.each(bodyItem.lines, maxLineWidth); @@ -328,31 +311,27 @@ function getTooltipSize(tooltip, model) { widthPadding = 0; // Footer width - ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily); - helpers.each(model.footer, maxLineWidth); + ctx.font = helpers.fontString(footerFontSize, options.footerFontStyle, options.footerFontFamily); + helpers.each(tooltip.footer, maxLineWidth); // Add padding - width += 2 * model.xPadding; + width += 2 * options.xPadding; - return { - width: width, - height: height - }; + return {width, height}; } /** * Helper to get the alignment of a tooltip given the size */ -function determineAlignment(tooltip, size) { - var model = tooltip._model; - var chart = tooltip._chart; +function determineAlignment(chart, options, size) { + const {x, y, width, height} = size; var chartArea = chart.chartArea; var xAlign = 'center'; var yAlign = 'center'; - if (model.y < size.height) { + if (y < height) { yAlign = 'top'; - } else if (model.y > (chart.height - size.height)) { + } else if (y > (chart.height - height)) { yAlign = 'bottom'; } @@ -363,91 +342,80 @@ function determineAlignment(tooltip, size) { var midY = (chartArea.top + chartArea.bottom) / 2; if (yAlign === 'center') { - lf = function(x) { - return x <= midX; - }; - rf = function(x) { - return x > midX; - }; + lf = (value) => value <= midX; + rf = (value) => value > midX; } else { - lf = function(x) { - return x <= (size.width / 2); - }; - rf = function(x) { - return x >= (chart.width - (size.width / 2)); - }; + lf = (value) => value <= (width / 2); + rf = (value) => value >= (chart.width - (width / 2)); } - 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'; - }; + olf = (value) => value + width + options.caretSize + options.caretPadding > chart.width; + orf = (value) => value - width - options.caretSize - options.caretPadding < 0; + yf = (value) => value <= midY ? 'top' : 'bottom'; - if (lf(model.x)) { + if (lf(x)) { xAlign = 'left'; // Is tooltip too wide and goes over the right side of the chart.? - if (olf(model.x)) { + if (olf(x)) { xAlign = 'center'; - yAlign = yf(model.y); + yAlign = yf(y); } - } else if (rf(model.x)) { + } else if (rf(x)) { xAlign = 'right'; // Is tooltip too wide and goes outside left edge of canvas? - if (orf(model.x)) { + if (orf(x)) { xAlign = 'center'; - yAlign = yf(model.y); + yAlign = yf(y); } } - var opts = tooltip._options; return { - xAlign: opts.xAlign ? opts.xAlign : xAlign, - yAlign: opts.yAlign ? opts.yAlign : yAlign + xAlign: options.xAlign ? options.xAlign : xAlign, + yAlign: options.yAlign ? options.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; - +function alignX(size, xAlign, chartWidth) { + let {x, width} = size; if (xAlign === 'right') { - x -= size.width; + x -= width; } else if (xAlign === 'center') { - x -= (size.width / 2); - if (x + size.width > chart.width) { - x = chart.width - size.width; + x -= (width / 2); + if (x + width > chartWidth) { + x = chartWidth - width; } if (x < 0) { x = 0; } } + return x; +} +function alignY(size, yAlign, paddingAndSize) { + let {y, height} = size; if (yAlign === 'top') { y += paddingAndSize; } else if (yAlign === 'bottom') { - y -= size.height + paddingAndSize; + y -= height + paddingAndSize; } else { - y -= (size.height / 2); + y -= (height / 2); } + return 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(options, size, alignment, chart) { + const {caretSize, caretPadding, cornerRadius} = options; + const {xAlign, yAlign} = alignment; + const paddingAndSize = caretSize + caretPadding; + const radiusAndPadding = cornerRadius + caretPadding; + + let x = alignX(size, xAlign, chart.width); + let y = alignY(size, yAlign, paddingAndSize); if (yAlign === 'center') { if (xAlign === 'left') { @@ -461,18 +429,16 @@ function getBackgroundPoint(vm, size, alignment, chart) { x += radiusAndPadding; } - return { - x: x, - y: y - }; + return {x, y}; } -function getAlignedX(vm, align) { +function getAlignedX(tooltip, align) { + const options = tooltip.options; return align === 'center' - ? vm.x + vm.width / 2 + ? tooltip.x + tooltip.width / 2 : align === 'right' - ? vm.x + vm.width - vm.xPadding - : vm.x + vm.xPadding; + ? tooltip.x + tooltip.width - options.xPadding + : tooltip.x + options.xPadding; } /** @@ -486,36 +452,42 @@ class Tooltip extends Element { constructor(config) { super(config); - this.initialize(); + const me = this; + me.opacity = 0; + me._active = []; + me._lastActive = []; + me.initialize(); } initialize() { - var me = this; - me._model = getBaseModel(me._options); - me._view = {}; - me._lastActive = []; + const me = this; + me.options = resolveOptions(me._chart.options.tooltips); } - transition(easingValue) { - var me = this; - var options = me._options; - - if (me._lastEvent && me._chart.animating) { - // Let's react to changes during animation - me._active = me._chart.getElementsAtEventForMode(me._lastEvent, options.mode, options); - me.update(true); - me.pivot(); - me._lastActive = me.active; + /** + * @private + */ + _resolveAnimations() { + const me = this; + const cached = me._cachedAnimations; + + if (cached) { + return cached; } - Element.prototype.transition.call(me, easingValue); + const chart = me._chart; + const opts = chart.options.animation && me.options.animation; + const animations = new Animations(me._chart, opts); + me._cachedAnimations = Object.freeze(animations); + + return animations; } // Get the title // Args are: (tooltipItem, data) getTitle() { var me = this; - var opts = me._options; + var opts = me.options; var callbacks = opts.callbacks; var beforeTitle = callbacks.beforeTitle.apply(me, arguments); @@ -532,13 +504,13 @@ class Tooltip extends Element { // Args are: (tooltipItem, data) getBeforeBody() { - return getBeforeAfterBodyLines(this._options.callbacks.beforeBody.apply(this, arguments)); + return getBeforeAfterBodyLines(this.options.callbacks.beforeBody.apply(this, arguments)); } // Args are: (tooltipItem, data) getBody(tooltipItems, data) { var me = this; - var callbacks = me._options.callbacks; + var callbacks = me.options.callbacks; var bodyItems = []; helpers.each(tooltipItems, function(tooltipItem) { @@ -559,14 +531,14 @@ class Tooltip extends Element { // Args are: (tooltipItem, data) getAfterBody() { - return getBeforeAfterBodyLines(this._options.callbacks.afterBody.apply(this, arguments)); + return getBeforeAfterBodyLines(this.options.callbacks.afterBody.apply(this, arguments)); } // Get the footer and beforeFooter and afterFooter lines // Args are: (tooltipItem, data) getFooter() { var me = this; - var callbacks = me._options.callbacks; + var callbacks = me.options.callbacks; var beforeFooter = callbacks.beforeFooter.apply(me, arguments); var footer = callbacks.footer.apply(me, arguments); @@ -580,138 +552,114 @@ class Tooltip extends Element { return lines; } - update(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; + /** + * @private + */ + _createItems() { + const me = this; + const active = me._active; + const options = me.options; + const data = me._chart.data; + const labelColors = []; + const labelTextColors = []; + let tooltipItems = []; + let i, len; + + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(me._chart, active[i])); + } - if (active.length) { - model.opacity = 1; + // If the user provided a filter function, use it to modify the tooltip items + if (options.filter) { + tooltipItems = tooltipItems.filter(function(a) { + return options.filter(a, data); + }); + } - var labelColors = []; - var labelTextColors = []; - tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition); + // If the user provided a sorting function, use it to modify the tooltip items + if (options.itemSort) { + tooltipItems = tooltipItems.sort(function(a, b) { + return options.itemSort(a, b, data); + }); + } - var tooltipItems = []; - for (i = 0, len = active.length; i < len; ++i) { - tooltipItems.push(createTooltipItem(me._chart, active[i])); - } + // Determine colors for boxes + helpers.each(tooltipItems, function(tooltipItem) { + labelColors.push(options.callbacks.labelColor.call(me, tooltipItem, me._chart)); + labelTextColors.push(options.callbacks.labelTextColor.call(me, tooltipItem, me._chart)); + }); - // 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); - }); - } + me.labelColors = labelColors; + me.labelTextColors = labelTextColors; + me.dataPoints = tooltipItems; + return tooltipItems; + } - // 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); - }); + update(changed) { + const me = this; + const options = me.options; + const active = me._active; + let properties; + + if (!active.length) { + if (me.opacity !== 0) { + properties = { + opacity: 0 + }; } - - // 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 = tooltipPosition.x; - model.y = 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; + const data = me._chart.data; + const position = positioners[options.position].call(me, active, me._eventPosition); + const tooltipItems = me._createItems(); + + me.title = me.getTitle(tooltipItems, data); + me.beforeBody = me.getBeforeBody(tooltipItems, data); + me.body = me.getBody(tooltipItems, data); + me.afterBody = me.getAfterBody(tooltipItems, data); + me.footer = me.getFooter(tooltipItems, data); + + const size = me._size = getTooltipSize(me); + const positionAndSize = helpers.extend({}, position, size); + const alignment = determineAlignment(me._chart, options, positionAndSize); + const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, me._chart); + + me.xAlign = alignment.xAlign; + me.yAlign = alignment.yAlign; + + properties = { + opacity: 1, + x: backgroundPoint.x, + y: backgroundPoint.y, + width: size.width, + height: size.height, + caretX: position.x, + caretY: position.y + }; } - 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; - - me._model = model; - - if (changed && opts.custom) { - opts.custom.call(me, model); + if (properties) { + me._resolveAnimations().update(me, properties); } - return me; + if (changed && options.custom) { + options.custom.call(me); + } } - drawCaret(tooltipPoint, size) { - var ctx = this._chart.ctx; - var vm = this._view; - var caretPosition = this.getCaretPosition(tooltipPoint, size, vm); + drawCaret(tooltipPoint, ctx, size) { + var caretPosition = this.getCaretPosition(tooltipPoint, size); ctx.lineTo(caretPosition.x1, caretPosition.y1); ctx.lineTo(caretPosition.x2, caretPosition.y2); ctx.lineTo(caretPosition.x3, caretPosition.y3); } - getCaretPosition(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; + getCaretPosition(tooltipPoint, size) { + const {xAlign, yAlign, options} = this; + const {cornerRadius, caretSize} = options; + const {x: ptX, y: ptY} = tooltipPoint; + const {width, height} = size; + let x1, x2, x3, y1, y2, y3; if (yAlign === 'center') { y2 = ptY + (height / 2); @@ -719,117 +667,126 @@ class Tooltip extends Element { 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; } + 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; + x2 = this.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; } + y3 = y1; } - return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3}; + return {x1, x2, x3, y1, y2, y3}; } - drawTitle(pt, vm, ctx) { - var title = vm.title; + drawTitle(pt, ctx) { + const me = this; + const options = me.options; + var title = me.title; var length = title.length; var titleFontSize, titleSpacing, i; if (length) { - var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); + var rtlHelper = getRtlHelper(options.rtl, me.x, me.width); - pt.x = getAlignedX(vm, vm._titleAlign); + pt.x = getAlignedX(me, options.titleAlign); - ctx.textAlign = rtlHelper.textAlign(vm._titleAlign); + ctx.textAlign = rtlHelper.textAlign(options.titleAlign); ctx.textBaseline = 'middle'; - titleFontSize = vm.titleFontSize; - titleSpacing = vm.titleSpacing; + titleFontSize = options.titleFontSize; + titleSpacing = options.titleSpacing; - ctx.fillStyle = vm.titleFontColor; - ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + ctx.fillStyle = options.titleFontColor; + ctx.font = helpers.fontString(titleFontSize, options.titleFontStyle, options.titleFontFamily); for (i = 0; i < length; ++i) { ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFontSize / 2); pt.y += titleFontSize + titleSpacing; // Line Height and spacing if (i + 1 === length) { - pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing + pt.y += options.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing } } } } - drawBody(pt, vm, ctx) { - var bodyFontSize = vm.bodyFontSize; - var bodySpacing = vm.bodySpacing; - var bodyAlign = vm._bodyAlign; - var body = vm.body; - var drawColorBoxes = vm.displayColors; + _drawColorBox(ctx, pt, i, rtlHelper) { + const me = this; + const options = me.options; + const labelColors = me.labelColors[i]; + const bodyFontSize = options.bodyFontSize; + const colorX = getAlignedX(me, 'left'); + const rtlColorX = rtlHelper.x(colorX); + + // Fill a white rect so that colours merge nicely if the opacity is < 1 + ctx.fillStyle = options.multiKeyBackground; + ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); + + // Border + ctx.lineWidth = 1; + ctx.strokeStyle = labelColors.borderColor; + ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); + + // Inner square + ctx.fillStyle = labelColors.backgroundColor; + ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + + // restore fillStyle + ctx.fillStyle = me.labelTextColors[i]; + } + + drawBody(pt, ctx) { + const me = this; + const {body, options} = me; + const {bodyFontSize, bodySpacing, bodyAlign, displayColors} = options; var xLinePadding = 0; - var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0; - var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); + var rtlHelper = getRtlHelper(options.rtl, me.x, me.width); var fillLineOfText = function(line) { ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2); pt.y += bodyFontSize + bodySpacing; }; - var bodyItem, textColor, labelColors, lines, i, j, ilen, jlen; var bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); + var bodyItem, textColor, lines, i, j, ilen, jlen; ctx.textAlign = bodyAlign; ctx.textBaseline = 'middle'; - ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + ctx.font = helpers.fontString(bodyFontSize, options.bodyFontStyle, options.bodyFontFamily); - pt.x = getAlignedX(vm, bodyAlignForCalculation); + pt.x = getAlignedX(me, bodyAlignForCalculation); // Before body lines - ctx.fillStyle = vm.bodyFontColor; - helpers.each(vm.beforeBody, fillLineOfText); + ctx.fillStyle = options.bodyFontColor; + helpers.each(me.beforeBody, fillLineOfText); - xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right' + xLinePadding = displayColors && bodyAlignForCalculation !== 'right' ? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2) : 0; // Draw body lines now for (i = 0, ilen = body.length; i < ilen; ++i) { bodyItem = body[i]; - textColor = vm.labelTextColors[i]; - labelColors = vm.labelColors[i]; + textColor = me.labelTextColors[i]; ctx.fillStyle = textColor; helpers.each(bodyItem.before, fillLineOfText); @@ -837,22 +794,8 @@ class Tooltip extends Element { lines = bodyItem.lines; for (j = 0, jlen = lines.length; j < jlen; ++j) { // Draw Legend-like boxes if needed - if (drawColorBoxes) { - var rtlColorX = rtlHelper.x(colorX); - - // Fill a white rect so that colours merge nicely if the opacity is < 1 - ctx.fillStyle = vm.legendColorBackground; - ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); - - // Border - ctx.lineWidth = 1; - ctx.strokeStyle = labelColors.borderColor; - ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize); - - // Inner square - ctx.fillStyle = labelColors.backgroundColor; - ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); - ctx.fillStyle = textColor; + if (displayColors) { + me._drawColorBox(ctx, pt, i, rtlHelper); } fillLineOfText(lines[j]); @@ -865,67 +808,67 @@ class Tooltip extends Element { xLinePadding = 0; // After body lines - helpers.each(vm.afterBody, fillLineOfText); + helpers.each(me.afterBody, fillLineOfText); pt.y -= bodySpacing; // Remove last body spacing } - drawFooter(pt, vm, ctx) { - var footer = vm.footer; + drawFooter(pt, ctx) { + const me = this; + const options = me.options; + var footer = me.footer; var length = footer.length; var footerFontSize, i; if (length) { - var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width); + var rtlHelper = getRtlHelper(options.rtl, me.x, me.width); - pt.x = getAlignedX(vm, vm._footerAlign); - pt.y += vm.footerMarginTop; + pt.x = getAlignedX(me, options.footerAlign); + pt.y += options.footerMarginTop; - ctx.textAlign = rtlHelper.textAlign(vm._footerAlign); + ctx.textAlign = rtlHelper.textAlign(options.footerAlign); ctx.textBaseline = 'middle'; - footerFontSize = vm.footerFontSize; + footerFontSize = options.footerFontSize; - ctx.fillStyle = vm.footerFontColor; - ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + ctx.fillStyle = options.footerFontColor; + ctx.font = helpers.fontString(footerFontSize, options.footerFontStyle, options.footerFontFamily); for (i = 0; i < length; ++i) { ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2); - pt.y += footerFontSize + vm.footerSpacing; + pt.y += footerFontSize + options.footerSpacing; } } } - drawBackground(pt, vm, ctx, tooltipSize) { - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - 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; + drawBackground(pt, ctx, tooltipSize) { + const {xAlign, yAlign, options} = this; + const {x, y} = pt; + const {width, height} = tooltipSize; + const radius = options.cornerRadius; + + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; ctx.beginPath(); ctx.moveTo(x + radius, y); if (yAlign === 'top') { - this.drawCaret(pt, tooltipSize); + this.drawCaret(pt, ctx, 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); + this.drawCaret(pt, ctx, 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); + this.drawCaret(pt, ctx, 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); + this.drawCaret(pt, ctx, tooltipSize); } ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); @@ -933,56 +876,83 @@ class Tooltip extends Element { ctx.fill(); - if (vm.borderWidth > 0) { + if (options.borderWidth > 0) { ctx.stroke(); } } - draw() { - var ctx = this._chart.ctx; - var vm = this._view; + /** + * Update x/y animation targets when _active elements are animating too + * @private + */ + _updateAnimationTarget() { + const me = this; + const chart = me._chart; + const options = me.options; + const anims = me.$animations; + const animX = anims && anims.x; + const animY = anims && anims.y; + if (animX && animX.active() || animY && animY.active()) { + const position = positioners[options.position].call(me, me._active, me._eventPosition); + if (!position) { + return; + } + const positionAndSize = helpers.extend({}, position, me._size); + const alignment = determineAlignment(chart, options, positionAndSize); + const point = getBackgroundPoint(options, positionAndSize, alignment, chart); + if (animX._to !== point.x || animY._to !== point.y) { + me._resolveAnimations().update(me, point); + } + } + } + + draw(ctx) { + const me = this; + const options = me.options; + let opacity = me.opacity; - if (vm.opacity === 0) { + if (!opacity) { return; } + me._updateAnimationTarget(); + var tooltipSize = { - width: vm.width, - height: vm.height + width: me.width, + height: me.height }; var pt = { - x: vm.x, - y: vm.y + x: me.x, + y: me.y }; // IE11/Edge does not like very small opacities, so snap to 0 - var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity; + opacity = Math.abs(opacity < 1e-3) ? 0 : opacity; // Truthy/falsey value for empty tooltip - var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length; + var hasTooltipContent = me.title.length || me.beforeBody.length || me.body.length || me.afterBody.length || me.footer.length; - if (this._options.enabled && hasTooltipContent) { + if (options.enabled && hasTooltipContent) { ctx.save(); ctx.globalAlpha = opacity; // Draw Background - this.drawBackground(pt, vm, ctx, tooltipSize); + me.drawBackground(pt, ctx, tooltipSize); - // Draw Title, Body, and Footer - pt.y += vm.yPadding; + helpers.rtl.overrideTextDirection(ctx, options.textDirection); - helpers.rtl.overrideTextDirection(ctx, vm.textDirection); + pt.y += options.yPadding; // Titles - this.drawTitle(pt, vm, ctx); + me.drawTitle(pt, ctx); // Body - this.drawBody(pt, vm, ctx); + me.drawBody(pt, ctx); // Footer - this.drawFooter(pt, vm, ctx); + me.drawFooter(pt, ctx); - helpers.rtl.restoreTextDirection(ctx, vm.textDirection); + helpers.rtl.restoreTextDirection(ctx, options.textDirection); ctx.restore(); } @@ -996,7 +966,7 @@ class Tooltip extends Element { */ handleEvent(e) { var me = this; - var options = me._options; + var options = me.options; var changed = false; me._lastActive = me._lastActive || []; @@ -1004,12 +974,8 @@ class Tooltip extends Element { // Find Active Elements for tooltips if (e.type === 'mouseout') { me._active = []; - me._lastEvent = null; } else { me._active = me._chart.getElementsAtEventForMode(e, options.mode, options); - if (e.type !== 'click') { - me._lastEvent = e.type === 'click' ? null : e; - } if (options.reverse) { me._active.reverse(); } @@ -1029,7 +995,7 @@ class Tooltip extends Element { }; me.update(true); - me.pivot(); + // me.pivot(); } } diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 000c98bc3..6d1ec447f 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -66,13 +66,14 @@ function drawFullCircleBorders(ctx, vm, arc, inner) { } function drawBorder(ctx, vm, arc) { - var inner = vm.borderAlign === 'inner'; + const options = vm.options; + var inner = options.borderAlign === 'inner'; if (inner) { - ctx.lineWidth = vm.borderWidth * 2; + ctx.lineWidth = options.borderWidth * 2; ctx.lineJoin = 'round'; } else { - ctx.lineWidth = vm.borderWidth; + ctx.lineWidth = options.borderWidth; ctx.lineJoin = 'bevel'; } @@ -98,75 +99,73 @@ class Arc extends Element { } inRange(chartX, chartY) { - var vm = this._view; - - if (vm) { - var pointRelativePosition = getAngleFromPoint(vm, {x: chartX, y: chartY}); - var angle = pointRelativePosition.angle; - var distance = pointRelativePosition.distance; - - // Sanitise angle range - var startAngle = vm.startAngle; - var endAngle = vm.endAngle; - while (endAngle < startAngle) { - endAngle += TAU; - } - while (angle > endAngle) { - angle -= TAU; - } - while (angle < startAngle) { - angle += TAU; - } + var me = this; - // Check if within the range of the open/close angle - var betweenAngles = (angle >= startAngle && angle <= endAngle); - var withinRadius = (distance >= vm.innerRadius && distance <= vm.outerRadius); + var pointRelativePosition = getAngleFromPoint(me, {x: chartX, y: chartY}); + var angle = pointRelativePosition.angle; + var distance = pointRelativePosition.distance; - return (betweenAngles && withinRadius); + // Sanitise angle range + var startAngle = me.startAngle; + var endAngle = me.endAngle; + while (endAngle < startAngle) { + endAngle += TAU; + } + while (angle > endAngle) { + angle -= TAU; } - return false; + while (angle < startAngle) { + angle += TAU; + } + + // Check if within the range of the open/close angle + var betweenAngles = (angle >= startAngle && angle <= endAngle); + var withinRadius = (distance >= me.innerRadius && distance <= me.outerRadius); + + return (betweenAngles && withinRadius); } getCenterPoint() { - var vm = this._view; - var halfAngle = (vm.startAngle + vm.endAngle) / 2; - var halfRadius = (vm.innerRadius + vm.outerRadius) / 2; + var me = this; + var halfAngle = (me.startAngle + me.endAngle) / 2; + var halfRadius = (me.innerRadius + me.outerRadius) / 2; return { - x: vm.x + Math.cos(halfAngle) * halfRadius, - y: vm.y + Math.sin(halfAngle) * halfRadius + x: me.x + Math.cos(halfAngle) * halfRadius, + y: me.y + Math.sin(halfAngle) * halfRadius }; } tooltipPosition() { - var vm = this._view; - var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2); - var rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; + var me = this; + var centreAngle = me.startAngle + ((me.endAngle - me.startAngle) / 2); + var rangeFromCentre = (me.outerRadius - me.innerRadius) / 2 + me.innerRadius; return { - x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), - y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) + x: me.x + (Math.cos(centreAngle) * rangeFromCentre), + y: me.y + (Math.sin(centreAngle) * rangeFromCentre) }; } draw(ctx) { - var vm = this._view; - var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0; + var me = this; + var options = me.options; + var pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; var arc = { - x: vm.x, - y: vm.y, - innerRadius: vm.innerRadius, - outerRadius: Math.max(vm.outerRadius - pixelMargin, 0), + x: me.x, + y: me.y, + innerRadius: me.innerRadius, + outerRadius: Math.max(me.outerRadius - pixelMargin, 0), pixelMargin: pixelMargin, - startAngle: vm.startAngle, - endAngle: vm.endAngle, - fullCircles: Math.floor(vm.circumference / TAU) + startAngle: me.startAngle, + endAngle: me.endAngle, + fullCircles: Math.floor(me.circumference / TAU) }; var i; ctx.save(); - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; if (arc.fullCircles) { arc.endAngle = arc.startAngle + TAU; @@ -177,7 +176,7 @@ class Arc extends Element { for (i = 0; i < arc.fullCircles; ++i) { ctx.fill(); } - arc.endAngle = arc.startAngle + vm.circumference % TAU; + arc.endAngle = arc.startAngle + me.circumference % TAU; } ctx.beginPath(); @@ -186,8 +185,8 @@ class Arc extends Element { ctx.closePath(); ctx.fill(); - if (vm.borderWidth) { - drawBorder(ctx, vm, arc); + if (options.borderWidth) { + drawBorder(ctx, me, arc); } ctx.restore(); diff --git a/src/elements/element.line.js b/src/elements/element.line.js index 533b93fca..67bd08e1e 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -5,6 +5,7 @@ import Element from '../core/core.element'; import helpers from '../helpers'; const defaultColor = defaults.global.defaultColor; +const isPointInArea = helpers.canvas._isPointInArea; defaults._set('global', { elements: { @@ -18,48 +19,71 @@ defaults._set('global', { borderDashOffset: 0.0, borderJoinStyle: 'miter', capBezierPoints: true, - fill: true, // do we fill in the area between the line and its base axis + fill: true } } }); function startAtGap(points, spanGaps) { let closePath = true; - let previous = points.length && points[0]._view; - let index, view; + let previous = points.length && points[0]; + let index, point; for (index = 1; index < points.length; ++index) { // If there is a gap in the (looping) line, start drawing from that gap - view = points[index]._view; - if (!view.skip && previous.skip) { + point = points[index]; + if (!point.skip && previous.skip) { points = points.slice(index).concat(points.slice(0, index)); closePath = spanGaps; break; } - previous = view; + previous = point; } points.closePath = closePath; return points; } -function setStyle(ctx, vm) { - ctx.lineCap = vm.borderCapStyle; - ctx.setLineDash(vm.borderDash); - ctx.lineDashOffset = vm.borderDashOffset; - ctx.lineJoin = vm.borderJoinStyle; - ctx.lineWidth = vm.borderWidth; - ctx.strokeStyle = vm.borderColor; +function setStyle(ctx, options) { + ctx.lineCap = options.borderCapStyle; + ctx.setLineDash(options.borderDash); + ctx.lineDashOffset = options.borderDashOffset; + ctx.lineJoin = options.borderJoinStyle; + ctx.lineWidth = options.borderWidth; + ctx.strokeStyle = options.borderColor; } -function normalPath(ctx, points, spanGaps, vm) { - const steppedLine = vm.steppedLine; - const lineMethod = steppedLine ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo; +function bezierCurveTo(ctx, previous, target, flip) { + ctx.bezierCurveTo( + flip ? previous.controlPointPreviousX : previous.controlPointNextX, + flip ? previous.controlPointPreviousY : previous.controlPointNextY, + flip ? target.controlPointNextX : target.controlPointPreviousX, + flip ? target.controlPointNextY : target.controlPointPreviousY, + target.x, + target.y); +} + +function steppedLineTo(ctx, previous, target, flip, mode) { + if (mode === 'middle') { + const midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, flip ? target.y : previous.y); + ctx.lineTo(midpoint, flip ? previous.y : target.y); + } else if ((mode === 'after' && !flip) || (mode !== 'after' && flip)) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); +} + +function normalPath(ctx, points, spanGaps, options) { + const steppedLine = options.steppedLine; + const lineMethod = steppedLine ? steppedLineTo : bezierCurveTo; let move = true; let index, currentVM, previousVM; for (index = 0; index < points.length; ++index) { - currentVM = points[index]._view; + currentVM = points[index]; if (currentVM.skip) { move = move || !spanGaps; @@ -68,7 +92,7 @@ function normalPath(ctx, points, spanGaps, vm) { if (move) { ctx.moveTo(currentVM.x, currentVM.y); move = false; - } else if (vm.tension || steppedLine) { + } else if (options.tension || steppedLine) { lineMethod(ctx, previousVM, currentVM, false, steppedLine); } else { ctx.lineTo(currentVM.x, currentVM.y); @@ -91,7 +115,7 @@ function fastPath(ctx, points, spanGaps) { let index, vm, truncX, x, y, prevX, minY, maxY, lastY; for (index = 0; index < points.length; ++index) { - vm = points[index]._view; + vm = points[index]; // If point is skipped, we either move to next (not skipped) point // or line to it if spanGaps is true. `move` can already be true. @@ -135,8 +159,64 @@ function fastPath(ctx, points, spanGaps) { } } -function useFastPath(vm) { - return vm.tension === 0 && !vm.steppedLine && !vm.fill && !vm.borderDash.length; +function useFastPath(options) { + return options.tension === 0 && !options.steppedLine && !options.fill && !options.borderDash.length; +} + +function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); +} + +function capBezierPoints(points, area) { + var i, ilen, model; + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]; + if (isPointInArea(model, area)) { + if (i > 0 && isPointInArea(points[i - 1], area)) { + model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); + model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); + } + if (i < points.length - 1 && isPointInArea(points[i + 1], area)) { + model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); + model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); + } + } + } +} + +function updateBezierControlPoints(points, options, area, loop) { + var i, ilen, point, controlPoints; + + // Only consider points that are drawn in case the spanGaps option is used + if (options.spanGaps) { + points = points.filter(function(pt) { + return !pt.skip; + }); + } + + if (options.cubicInterpolationMode === 'monotone') { + helpers.curve.splineCurveMonotone(points); + } else { + let prev = loop ? points[points.length - 1] : points[0]; + for (i = 0, ilen = points.length; i < ilen; ++i) { + point = points[i]; + controlPoints = helpers.curve.splineCurve( + prev, + point, + points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], + options.tension + ); + point.controlPointPreviousX = controlPoints.previous.x; + point.controlPointPreviousY = controlPoints.previous.y; + point.controlPointNextX = controlPoints.next.x; + point.controlPointNextY = controlPoints.next.y; + prev = point; + } + } + + if (options.capBezierPoints) { + capBezierPoints(points, area); + } } class Line extends Element { @@ -145,10 +225,21 @@ class Line extends Element { super(props); } - draw(ctx) { + updateControlPoints(chartArea) { const me = this; - const vm = me._view; - const spanGaps = vm.spanGaps; + if (me._controlPointsUpdated) { + return; + } + const options = me.options; + if (options.tension && !options.steppedLine) { + updateBezierControlPoints(me._children, options, chartArea, me._loop); + } + } + + drawPath(ctx, area) { + const me = this; + const options = me.options; + const spanGaps = options.spanGaps; let closePath = me._loop; let points = me._children; @@ -161,19 +252,30 @@ class Line extends Element { closePath = points.closePath; } - ctx.save(); + if (useFastPath(options)) { + fastPath(ctx, points, spanGaps); + } else { + me.updateControlPoints(area); + normalPath(ctx, points, spanGaps, options); + } - setStyle(ctx, vm); + return closePath; + } - ctx.beginPath(); + draw(ctx, area) { + const me = this; - if (useFastPath(vm)) { - fastPath(ctx, points, spanGaps); - } else { - normalPath(ctx, points, spanGaps, vm); + if (!me._children.length) { + return; } - if (closePath) { + ctx.save(); + + setStyle(ctx, me.options); + + ctx.beginPath(); + + if (me.drawPath(ctx, area)) { ctx.closePath(); } diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 8de393121..be92d51fb 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -29,62 +29,55 @@ class Point extends Element { } inRange(mouseX, mouseY) { - const vm = this._view; - return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; + const options = this.options; + return ((Math.pow(mouseX - this.x, 2) + Math.pow(mouseY - this.y, 2)) < Math.pow(options.hitRadius + options.radius, 2)); } inXRange(mouseX) { - const vm = this._view; - return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; + const options = this.options; + return (Math.abs(mouseX - this.x) < options.radius + options.hitRadius); } inYRange(mouseY) { - const vm = this._view; - return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; + const options = this.options; + return (Math.abs(mouseY - this.y) < options.radius + options.hitRadius); } getCenterPoint() { - const vm = this._view; - return { - x: vm.x, - y: vm.y - }; + return {x: this.x, y: this.y}; } size() { - const vm = this._view; - const radius = vm.radius || 0; - const borderWidth = vm.borderWidth || 0; + const options = this.options || {}; + const radius = options.radius || 0; + const borderWidth = radius && options.borderWidth || 0; return (radius + borderWidth) * 2; } tooltipPosition() { - const vm = this._view; + const options = this.options; return { - x: vm.x, - y: vm.y, - padding: vm.radius + vm.borderWidth + x: this.x, + y: this.y, + padding: options.radius + options.borderWidth }; } draw(ctx, chartArea) { - const vm = this._view; - const pointStyle = vm.pointStyle; - const rotation = vm.rotation; - const radius = vm.radius; - const x = vm.x; - const y = vm.y; - - if (vm.skip || radius <= 0) { + const me = this; + const options = me.options; + const radius = options.radius; + + if (me.skip || radius <= 0) { return; } // Clipping for Points. - if (chartArea === undefined || helpers.canvas._isPointInArea(vm, chartArea)) { - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - ctx.fillStyle = vm.backgroundColor; - helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); + if (chartArea === undefined || helpers.canvas._isPointInArea(me, chartArea)) { + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.fillStyle = options.backgroundColor; + helpers.canvas.drawPoint(ctx, options.pointStyle, radius, me.x, me.y, options.rotation); } } } diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index f77512e3a..29cf411d2 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -17,31 +17,27 @@ defaults._set('global', { } }); -function isVertical(vm) { - return vm && vm.width !== undefined; -} - /** * Helper function to get the bounds of the bar regardless of the orientation * @param bar {Chart.Element.Rectangle} the bar * @return {Bounds} bounds of the bar * @private */ -function getBarBounds(vm) { +function getBarBounds(bar) { var x1, x2, y1, y2, half; - if (isVertical(vm)) { - half = vm.width / 2; - x1 = vm.x - half; - x2 = vm.x + half; - y1 = Math.min(vm.y, vm.base); - y2 = Math.max(vm.y, vm.base); + if (bar.horizontal) { + half = bar.height / 2; + x1 = Math.min(bar.x, bar.base); + x2 = Math.max(bar.x, bar.base); + y1 = bar.y - half; + y2 = bar.y + half; } else { - half = vm.height / 2; - x1 = Math.min(vm.x, vm.base); - x2 = Math.max(vm.x, vm.base); - y1 = vm.y - half; - y2 = vm.y + half; + half = bar.width / 2; + x1 = bar.x - half; + x2 = bar.x + half; + y1 = Math.min(bar.y, bar.base); + y2 = Math.max(bar.y, bar.base); } return { @@ -56,19 +52,19 @@ function swap(orig, v1, v2) { return orig === v1 ? v2 : orig === v2 ? v1 : orig; } -function parseBorderSkipped(vm) { - var edge = vm.borderSkipped; +function parseBorderSkipped(bar) { + var edge = bar.options.borderSkipped; var res = {}; if (!edge) { return res; } - if (vm.horizontal) { - if (vm.base > vm.x) { + if (bar.horizontal) { + if (bar.base > bar.x) { edge = swap(edge, 'left', 'right'); } - } else if (vm.base < vm.y) { + } else if (bar.base < bar.y) { edge = swap(edge, 'bottom', 'top'); } @@ -76,9 +72,9 @@ function parseBorderSkipped(vm) { return res; } -function parseBorderWidth(vm, maxW, maxH) { - var value = vm.borderWidth; - var skip = parseBorderSkipped(vm); +function parseBorderWidth(bar, maxW, maxH) { + var value = bar.options.borderWidth; + var skip = parseBorderSkipped(bar); var t, r, b, l; if (helpers.isObject(value)) { @@ -98,11 +94,11 @@ function parseBorderWidth(vm, maxW, maxH) { }; } -function boundingRects(vm) { - var bounds = getBarBounds(vm); +function boundingRects(bar) { + var bounds = getBarBounds(bar); var width = bounds.right - bounds.left; var height = bounds.bottom - bounds.top; - var border = parseBorderWidth(vm, width / 2, height / 2); + var border = parseBorderWidth(bar, width / 2, height / 2); return { outer: { @@ -120,10 +116,10 @@ function boundingRects(vm) { }; } -function inRange(vm, x, y) { +function inRange(bar, x, y) { var skipX = x === null; var skipY = y === null; - var bounds = !vm || (skipX && skipY) ? false : getBarBounds(vm); + var bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar); return bounds && (skipX || x >= bounds.left && x <= bounds.right) @@ -137,12 +133,12 @@ class Rectangle extends Element { } draw(ctx) { - var vm = this._view; - var rects = boundingRects(vm); + var options = this.options; + var rects = boundingRects(this); var outer = rects.outer; var inner = rects.inner; - ctx.fillStyle = vm.backgroundColor; + ctx.fillStyle = options.backgroundColor; ctx.fillRect(outer.x, outer.y, outer.w, outer.h); if (outer.w === inner.w && outer.h === inner.h) { @@ -153,43 +149,36 @@ class Rectangle extends Element { ctx.beginPath(); ctx.rect(outer.x, outer.y, outer.w, outer.h); ctx.clip(); - ctx.fillStyle = vm.borderColor; + ctx.fillStyle = options.borderColor; ctx.rect(inner.x, inner.y, inner.w, inner.h); ctx.fill('evenodd'); ctx.restore(); } inRange(mouseX, mouseY) { - return inRange(this._view, mouseX, mouseY); + return inRange(this, mouseX, mouseY); } inXRange(mouseX) { - return inRange(this._view, mouseX, null); + return inRange(this, mouseX, null); } inYRange(mouseY) { - return inRange(this._view, null, mouseY); + return inRange(this, null, mouseY); } getCenterPoint() { - var vm = this._view; - var x, y; - if (isVertical(vm)) { - x = vm.x; - y = (vm.y + vm.base) / 2; - } else { - x = (vm.x + vm.base) / 2; - y = vm.y; - } - - return {x: x, y: y}; + const {x, y, base, horizontal} = this; + return { + x: horizontal ? (x + base) / 2 : x, + y: horizontal ? y : (y + base) / 2 + }; } tooltipPosition() { - var vm = this._view; return { - x: vm.x, - y: vm.y + x: this.x, + y: this.y }; } } diff --git a/src/helpers/helpers.curve.js b/src/helpers/helpers.curve.js index 292e27740..d4bf75823 100644 --- a/src/helpers/helpers.curve.js +++ b/src/helpers/helpers.curve.js @@ -45,7 +45,7 @@ export function splineCurveMonotone(points) { var pointsWithTangents = (points || []).map(function(point) { return { - model: point._model, + model: point, deltaK: 0, mK: 0 }; diff --git a/src/index.js b/src/index.js index 92ed94caa..a3101d0f8 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ var Chart = require('./core/core.controller'); Chart.helpers = require('./helpers/index'); Chart._adapters = require('./core/core.adapters'); Chart.Animation = require('./core/core.animation'); +Chart.Animator = require('./core/core.animator'); Chart.animationService = require('./core/core.animations'); Chart.controllers = require('./controllers/index'); Chart.DatasetController = require('./core/core.datasetController'); diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index b2487e35b..2ddda44de 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -28,7 +28,7 @@ var mappers = { var length = points.length || 0; return !length ? null : function(point, i) { - return (i < length && points[i]._view) || null; + return (i < length && points[i]) || null; }; }, @@ -55,7 +55,7 @@ var mappers = { // @todo if (fill[0] === '#') function decodeFill(el, index, count) { - var model = el._model || {}; + var model = el.options || {}; var fillOption = model.fill; var fill = fillOption && typeof fillOption.target !== 'undefined' ? fillOption.target : fillOption; var target; @@ -105,7 +105,7 @@ function decodeFill(el, index, count) { } function computeLinearBoundary(source) { - var model = source.el._model || {}; + var model = source.el || {}; var scale = source.scale || {}; var fill = source.fill; var target = null; @@ -352,11 +352,11 @@ function clipAndFill(ctx, clippingPointsSets, fillingPointsSets, color, stepped, function doFill(ctx, points, mapper, colors, el, area) { const count = points.length; - const view = el._view; + const options = el.options; const loop = el._loop; - const span = view.spanGaps; - const stepped = view.steppedLine; - const tension = view.tension; + const span = options.spanGaps; + const stepped = options.steppedLine; + const tension = options.tension; let curve0 = []; let curve1 = []; let len0 = 0; @@ -369,8 +369,8 @@ function doFill(ctx, points, mapper, colors, el, area) { for (i = 0, ilen = count; i < ilen; ++i) { index = i % count; - p0 = points[index]._view; - p1 = mapper(p0, index, view); + p0 = points[index]; + p1 = mapper(p0, index); d0 = isDrawable(p0); d1 = isDrawable(p1); @@ -423,7 +423,7 @@ module.exports = { el = meta.dataset; source = null; - if (el && el._model && el instanceof elements.Line) { + if (el && el.options && el instanceof elements.Line) { source = { visible: chart.isDatasetVisible(i), fill: decodeFill(el, i, count), @@ -450,9 +450,19 @@ module.exports = { }, beforeDatasetsDraw: function(chart) { - var metasets = chart._getSortedVisibleDatasetMetas(); - var ctx = chart.ctx; - var meta, i, el, view, points, mapper, color, colors, fillOption; + const metasets = chart._getSortedVisibleDatasetMetas(); + const area = chart.chartArea; + const ctx = chart.ctx; + var meta, i, el, options, points, mapper, color, colors, fillOption; + + for (i = metasets.length - 1; i >= 0; --i) { + meta = metasets[i].$filler; + + if (!meta || !meta.visible) { + continue; + } + meta.el.updateControlPoints(area); + } for (i = metasets.length - 1; i >= 0; --i) { meta = metasets[i].$filler; @@ -462,11 +472,11 @@ module.exports = { } el = meta.el; - view = el._view; + options = el.options; points = el._children || []; mapper = meta.mapper; - fillOption = meta.el._model.fill; - color = view.backgroundColor || defaults.global.defaultColor; + fillOption = options.fill; + color = options.backgroundColor || defaults.global.defaultColor; colors = {above: color, below: color}; if (fillOption && typeof fillOption === 'object') { @@ -474,8 +484,8 @@ module.exports = { colors.below = fillOption.below || color; } if (mapper && points.length) { - helpers.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, colors, el, chart.chartArea); + helpers.canvas.clipArea(ctx, area); + doFill(ctx, points, mapper, colors, el, area); helpers.canvas.unclipArea(ctx); } } diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 5913809e3..12f71afd9 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -58,7 +58,7 @@ defaults._set('global', { return { text: datasets[meta.index].label, fillStyle: style.backgroundColor, - hidden: !chart.isDatasetVisible(meta.index), + hidden: !meta.visible, lineCap: style.borderCapStyle, lineDash: style.borderDash, lineDashOffset: style.borderDashOffset, diff --git a/test/fixtures/controller.line/clip/default-y-max.png b/test/fixtures/controller.line/clip/default-y-max.png index 651e1a7d3c913fe501852cbb602fa78834e2d358..d5cbf2bfe11bb0b7d0bcedcb0f1becc84beb9879 100644 GIT binary patch literal 14351 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 B@E!mF literal 14590 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 diff --git a/test/fixtures/controller.line/clip/default-y.png b/test/fixtures/controller.line/clip/default-y.png index 923cf75aa8731fede7eb4634742f047d5db075f5..bd5d96c5eaac8120a1d7f19b074bf9679abb1c89 100644 GIT binary patch literal 14217 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�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 JtCX!H{tp^I9Sr~g literal 14464 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 item.element); @@ -88,8 +88,8 @@ describe('Core.Interaction', function() { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event - x: point._model.x, - y: point._model.y, + x: point.x, + y: point.y, }; var elements = Chart.Interaction.modes.index(chart, evt, {intersect: true}).map(item => item.element); @@ -223,8 +223,8 @@ describe('Core.Interaction', function() { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event - x: point._model.x, - y: point._model.y + x: point.x, + y: point.y }; var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: true}); @@ -365,8 +365,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: (meta0.data[1]._view.y + meta1.data[1]._view.y) / 2 + x: meta0.data[1].x, + y: (meta0.data[1].y + meta1.data[1].y) / 2 }; var evt = { @@ -391,8 +391,8 @@ describe('Core.Interaction', function() { // At 'Point 2', 10 var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[0]._view.y + x: meta0.data[1].x, + y: meta0.data[0].y }; var evt = { @@ -415,8 +415,8 @@ describe('Core.Interaction', function() { // Haflway between 'Point 1' and 'Point 2', y=10 var pt = { - x: (meta0.data[0]._view.x + meta0.data[1]._view.x) / 2, - y: meta0.data[0]._view.y + x: (meta0.data[0].x + meta0.data[1].x) / 2, + y: meta0.data[0].y }; var evt = { @@ -440,8 +440,8 @@ describe('Core.Interaction', function() { // 'Point 1', y = 30 var pt = { - x: meta0.data[0]._view.x, - y: meta0.data[2]._view.y + x: meta0.data[0].x, + y: meta0.data[2].y }; var evt = { @@ -464,8 +464,8 @@ describe('Core.Interaction', function() { // 'Point 1', y = 40 var pt = { - x: meta0.data[0]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[0].x, + y: meta0.data[1].y }; var evt = { @@ -514,8 +514,8 @@ describe('Core.Interaction', function() { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event - x: point._view.x + 15, - y: point._view.y + x: point.x + 15, + y: point.y }; // Nothing intersects so find nothing @@ -526,8 +526,8 @@ describe('Core.Interaction', function() { type: 'click', chart: chart, native: true, - x: point._view.x, - y: point._view.y + x: point.x, + y: point.y }; elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([point]); @@ -547,8 +547,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { @@ -577,8 +577,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { @@ -626,8 +626,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { @@ -660,8 +660,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { @@ -718,8 +718,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { @@ -752,8 +752,8 @@ describe('Core.Interaction', function() { // Halfway between 2 mid points var pt = { - x: meta0.data[1]._view.x, - y: meta0.data[1]._view.y + x: meta0.data[1].x, + y: meta0.data[1].y }; var evt = { diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index f0ad7441e..4b8da46e4 100644 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -69,7 +69,7 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, + clientX: rect.left + point.x, clientY: 0 }); @@ -80,46 +80,55 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'left', - yAlign: 'center', + expect(tooltip.options.xPadding).toEqual(6); + expect(tooltip.options.yPadding).toEqual(6); + expect(tooltip.xAlign).toEqual('left'); + expect(tooltip.yAlign).toEqual('center'); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Body bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', + bodyFontFamily: globalDefaults.defaultFontFamily, + bodyFontStyle: globalDefaults.defaultFontStyle, + bodyAlign: 'left', bodyFontSize: globalDefaults.defaultFontSize, bodySpacing: 2, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Title titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', + titleFontFamily: globalDefaults.defaultFontFamily, + titleFontStyle: 'bold', titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', + titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Footer footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', + footerFontFamily: globalDefaults.defaultFontFamily, + footerFontStyle: 'bold', footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', + footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Appearance caretSize: 5, + caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: true + })); + + expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, - legendColorBackground: '#fff', - displayColors: true, // Text title: ['Point 2'], @@ -135,7 +144,6 @@ describe('Core.Tooltip', function() { }], afterBody: [], footer: [], - caretPadding: 2, labelColors: [{ borderColor: globalDefaults.defaultColor, backgroundColor: globalDefaults.defaultColor @@ -145,8 +153,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(267); - expect(tooltip._view.y).toBeCloseToPixel(155); + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(155); }); it('Should only display if intersecting if intersect is set', function() { @@ -185,7 +193,7 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, + clientX: rect.left + point.x, clientY: 0 }); @@ -194,46 +202,9 @@ describe('Core.Tooltip', function() { // Check and see if tooltip was displayed var tooltip = chart.tooltip; - var globalDefaults = Chart.defaults.global; - - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - - // Body - bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', - bodyFontSize: globalDefaults.defaultFontSize, - bodySpacing: 2, - - // Title - titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', - titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', - titleSpacing: 2, - titleMarginBottom: 6, - - // Footer - footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', - footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', - footerSpacing: 2, - footerMarginTop: 6, - // Appearance - caretSize: 5, - cornerRadius: 6, - backgroundColor: 'rgba(0,0,0,0.8)', + expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 0, - legendColorBackground: '#fff', - displayColors: true, })); }); }); @@ -274,8 +245,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + clientX: rect.left + point.x, + clientY: rect.top + point.y }); // Manually trigger rather than having an async test @@ -285,46 +256,55 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'left', - yAlign: 'center', + expect(tooltip.options.xPadding).toEqual(6); + expect(tooltip.options.yPadding).toEqual(6); + expect(tooltip.xAlign).toEqual('left'); + expect(tooltip.yAlign).toEqual('center'); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Body bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', + bodyFontFamily: globalDefaults.defaultFontFamily, + bodyFontStyle: globalDefaults.defaultFontStyle, + bodyAlign: 'left', bodyFontSize: globalDefaults.defaultFontSize, bodySpacing: 2, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Title titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', + titleFontFamily: globalDefaults.defaultFontFamily, + titleFontStyle: 'bold', titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', + titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Footer footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', + footerFontFamily: globalDefaults.defaultFontFamily, + footerFontStyle: 'bold', footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', + footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Appearance caretSize: 5, + caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: true + })); + + expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, - legendColorBackground: '#fff', - displayColors: true, // Text title: ['Point 2'], @@ -336,7 +316,6 @@ describe('Core.Tooltip', function() { }], afterBody: [], footer: [], - caretPadding: 2, labelTextColors: ['#fff'], labelColors: [{ borderColor: globalDefaults.defaultColor, @@ -344,8 +323,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(267); - expect(tooltip._view.y).toBeCloseToPixel(312); + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(312); }); it('Should display information from user callbacks', function() { @@ -421,8 +400,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + clientX: rect.left + point.x, + clientY: rect.top + point.y }); // Manually trigger rather than having an async test @@ -432,45 +411,54 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'center', - yAlign: 'top', + expect(tooltip.options.xPadding).toEqual(6); + expect(tooltip.options.yPadding).toEqual(6); + expect(tooltip.xAlign).toEqual('center'); + expect(tooltip.yAlign).toEqual('top'); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Body bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', + bodyFontFamily: globalDefaults.defaultFontFamily, + bodyFontStyle: globalDefaults.defaultFontStyle, + bodyAlign: 'left', bodyFontSize: globalDefaults.defaultFontSize, bodySpacing: 2, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Title titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', + titleFontFamily: globalDefaults.defaultFontFamily, + titleFontStyle: 'bold', titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', + titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Footer footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', + footerFontFamily: globalDefaults.defaultFontFamily, + footerFontStyle: 'bold', footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', + footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Appearance caretSize: 5, + caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + })); + + expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, - legendColorBackground: '#fff', // Text title: ['beforeTitle', 'title', 'afterTitle'], @@ -486,7 +474,6 @@ describe('Core.Tooltip', function() { }], afterBody: ['afterBody'], footer: ['beforeFooter', 'footer', 'afterFooter'], - caretPadding: 2, labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ borderColor: globalDefaults.defaultColor, @@ -497,8 +484,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(214); - expect(tooltip._view.y).toBeCloseToPixel(190); + expect(tooltip.x).toBeCloseToPixel(214); + expect(tooltip.y).toBeCloseToPixel(190); }); it('Should allow sorting items', function() { @@ -539,8 +526,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point0._model.x, - clientY: rect.top + point0._model.y + clientX: rect.left + point0.x, + clientY: rect.top + point0.y }); // Manually trigger rather than having an async test @@ -550,7 +537,7 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ + expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', @@ -578,8 +565,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(267); - expect(tooltip._view.y).toBeCloseToPixel(155); + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(155); }); it('Should allow reversing items', function() { @@ -618,8 +605,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point0._model.x, - clientY: rect.top + point0._model.y + clientX: rect.left + point0.x, + clientY: rect.top + point0.y }); // Manually trigger rather than having an async test @@ -629,7 +616,7 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ + expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', @@ -657,8 +644,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(267); - expect(tooltip._view.y).toBeCloseToPixel(155); + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(155); }); it('Should follow dataset order', function() { @@ -698,8 +685,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point0._model.x, - clientY: rect.top + point0._model.y + clientX: rect.left + point0.x, + clientY: rect.top + point0.y }); // Manually trigger rather than having an async test @@ -709,7 +696,7 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ + expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', @@ -737,8 +724,8 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(267); - expect(tooltip._view.y).toBeCloseToPixel(155); + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(155); }); it('should filter items from the tooltip using the callback', function() { @@ -781,8 +768,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point0._model.x, - clientY: rect.top + point0._model.y + clientX: rect.left + point0.x, + clientY: rect.top + point0.y }); // Manually trigger rather than having an async test @@ -792,7 +779,7 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ + expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', @@ -850,8 +837,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point0._model.x, - clientY: rect.top + point0._model.y + clientX: rect.left + point0.x, + clientY: rect.top + point0.y }); // Manually trigger rather than having an async test @@ -860,7 +847,7 @@ describe('Core.Tooltip', function() { // Check and see if tooltip was displayed var tooltip = chart.tooltip; - expect(tooltip._model).toEqual(jasmine.objectContaining({ + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Positioning caretPadding: 10, })); @@ -901,11 +888,11 @@ describe('Core.Tooltip', function() { // Check and see if tooltip was displayed var tooltip = chart.tooltip; - expect(tooltip._view instanceof Object).toBe(true); - expect(tooltip._view.dataPoints instanceof Array).toBe(true); - expect(tooltip._view.dataPoints.length).toBe(1); + expect(tooltip instanceof Object).toBe(true); + expect(tooltip.dataPoints instanceof Array).toBe(true); + expect(tooltip.dataPoints.length).toBe(1); - var tooltipItem = tooltip._view.dataPoints[0]; + var tooltipItem = tooltip.dataPoints[0]; expect(tooltipItem.index).toBe(pointIndex); expect(tooltipItem.datasetIndex).toBe(datasetIndex); @@ -957,8 +944,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: false, cancelable: true, - clientX: rect.left + firstPoint._model.x, - clientY: rect.top + firstPoint._model.y + clientX: rect.left + firstPoint.x, + clientY: rect.top + firstPoint.y }); var tooltip = chart.tooltip; @@ -1022,8 +1009,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + clientX: rect.left + point.x, + clientY: rect.top + point.y }); // Manually trigger rather than having an async test @@ -1063,6 +1050,9 @@ describe('Core.Tooltip', function() { animation: { // without this slice center point is calculated wrong animateRotate: false + }, + tooltips: { + animation: false } } }); @@ -1091,14 +1081,15 @@ describe('Core.Tooltip', function() { chart.update(); node.dispatchEvent(mouseOutEvent); node.dispatchEvent(mouseMoveEvent); - var model = chart.tooltip._model; - expect(model.x).toBeGreaterThanOrEqual(0); - if (model.width <= chart.width) { - expect(model.x + model.width).toBeLessThanOrEqual(chart.width); + var tooltip = chart.tooltip; + expect(tooltip.dataPoints.length).toBe(1); + expect(tooltip.x).toBeGreaterThanOrEqual(0); + if (tooltip.width <= chart.width) { + expect(tooltip.x + tooltip.width).toBeLessThanOrEqual(chart.width); } - expect(model.caretX).toBeCloseToPixel(tooltipPosition.x); + expect(tooltip.caretX).toBeCloseToPixel(tooltipPosition.x); // if tooltip is longer than chart area then all tests done - if (model.width > chart.width) { + if (tooltip.width > chart.width) { break; } } @@ -1176,8 +1167,8 @@ describe('Core.Tooltip', function() { view: window, bubbles: true, cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + clientX: rect.left + point.x, + clientY: rect.top + point.y }); // Manually trigger rather than having an async test @@ -1187,45 +1178,54 @@ describe('Core.Tooltip', function() { var tooltip = chart.tooltip; var globalDefaults = Chart.defaults.global; - expect(tooltip._view).toEqual(jasmine.objectContaining({ - // Positioning - xPadding: 6, - yPadding: 6, - xAlign: 'center', - yAlign: 'top', + expect(tooltip.options.xPadding).toEqual(6); + expect(tooltip.options.yPadding).toEqual(6); + expect(tooltip.xAlign).toEqual('center'); + expect(tooltip.yAlign).toEqual('top'); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Body bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: 'left', + bodyFontFamily: globalDefaults.defaultFontFamily, + bodyFontStyle: globalDefaults.defaultFontStyle, + bodyAlign: 'left', bodyFontSize: globalDefaults.defaultFontSize, bodySpacing: 2, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Title titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', + titleFontFamily: globalDefaults.defaultFontFamily, + titleFontStyle: 'bold', titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: 'left', + titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Footer footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', + footerFontFamily: globalDefaults.defaultFontFamily, + footerFontStyle: 'bold', footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: 'left', + footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, + })); + expect(tooltip.options).toEqual(jasmine.objectContaining({ // Appearance caretSize: 5, + caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + })); + + expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, - legendColorBackground: '#fff', // Text title: ['beforeTitle', 'newline', 'title', 'newline', 'afterTitle', 'newline'], @@ -1241,7 +1241,6 @@ describe('Core.Tooltip', function() { }], afterBody: ['afterBody', 'newline'], footer: ['beforeFooter', 'newline', 'footer', 'newline', 'afterFooter', 'newline'], - caretPadding: 2, labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ borderColor: globalDefaults.defaultColor, @@ -1262,45 +1261,51 @@ describe('Core.Tooltip', function() { y: 100, width: 100, height: 100, - xPadding: 5, - yPadding: 5, xAlign: 'left', yAlign: 'top', - // Body - bodyFontColor: '#fff', - _bodyFontFamily: globalDefaults.defaultFontFamily, - _bodyFontStyle: globalDefaults.defaultFontStyle, - _bodyAlign: body, - bodyFontSize: globalDefaults.defaultFontSize, - bodySpacing: 2, - - // Title - titleFontColor: '#fff', - _titleFontFamily: globalDefaults.defaultFontFamily, - _titleFontStyle: 'bold', - titleFontSize: globalDefaults.defaultFontSize, - _titleAlign: title, - titleSpacing: 2, - titleMarginBottom: 6, - - // Footer - footerFontColor: '#fff', - _footerFontFamily: globalDefaults.defaultFontFamily, - _footerFontStyle: 'bold', - footerFontSize: globalDefaults.defaultFontSize, - _footerAlign: footer, - footerSpacing: 2, - footerMarginTop: 6, + options: { + xPadding: 5, + yPadding: 5, + + // Body + bodyFontColor: '#fff', + bodyFontFamily: globalDefaults.defaultFontFamily, + bodyFontStyle: globalDefaults.defaultFontStyle, + bodyAlign: body, + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + titleFontFamily: globalDefaults.defaultFontFamily, + titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + titleAlign: title, + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + footerFontFamily: globalDefaults.defaultFontFamily, + footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + footerAlign: footer, + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + caretPadding: 2, + borderColor: '#aaa', + borderWidth: 1, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: false - // Appearance - caretSize: 5, - cornerRadius: 6, - borderColor: '#aaa', - borderWidth: 1, - backgroundColor: 'rgba(0,0,0,0.8)', + }, opacity: 1, - legendColorBackground: '#fff', // Text title: ['title'], @@ -1312,7 +1317,6 @@ describe('Core.Tooltip', function() { }], afterBody: [], footer: ['footer'], - caretPadding: 2, labelTextColors: ['#fff'], labelColors: [{ borderColor: 'rgb(255, 0, 0)', @@ -1348,16 +1352,19 @@ describe('Core.Tooltip', function() { var mockContext = window.createMockContext(); var tooltip = new Chart.Tooltip({ - _options: globalDefaults.tooltips, _chart: { - ctx: mockContext, + options: { + tooltips: { + animation: false, + } + } } }); it('Should go left', function() { mockContext.resetCalls(); - tooltip._view = makeView('left', 'left', 'left'); - tooltip.draw(); + Chart.helpers.merge(tooltip, makeView('left', 'left', 'left')); + tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['left']}, @@ -1376,8 +1383,8 @@ describe('Core.Tooltip', function() { it('Should go right', function() { mockContext.resetCalls(); - tooltip._view = makeView('right', 'right', 'right'); - tooltip.draw(); + Chart.helpers.merge(tooltip, makeView('right', 'right', 'right')); + tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, @@ -1396,8 +1403,8 @@ describe('Core.Tooltip', function() { it('Should center', function() { mockContext.resetCalls(); - tooltip._view = makeView('center', 'center', 'center'); - tooltip.draw(); + Chart.helpers.merge(tooltip, makeView('center', 'center', 'center')); + tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['center']}, @@ -1416,8 +1423,8 @@ describe('Core.Tooltip', function() { it('Should allow mixed', function() { mockContext.resetCalls(); - tooltip._view = makeView('right', 'center', 'left'); - tooltip.draw(); + Chart.helpers.merge(tooltip, makeView('right', 'center', 'left')); + tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, diff --git a/test/specs/element.arc.tests.js b/test/specs/element.arc.tests.js index 41c720d9b..bc47a2752 100644 --- a/test/specs/element.arc.tests.js +++ b/test/specs/element.arc.tests.js @@ -13,20 +13,15 @@ describe('Arc element tests', function() { }); it ('should determine if in range', function() { + // Mock out the arc as if the controller put it there var arc = new Chart.elements.Arc({ - _datasetIndex: 2, - _index: 1 - }); - - // Mock out the view as if the controller put it there - arc._view = { startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 5, outerRadius: 10, - }; + }); expect(arc.inRange(2, 2)).toBe(false); expect(arc.inRange(7, 0)).toBe(true); @@ -36,20 +31,15 @@ describe('Arc element tests', function() { }); it ('should get the tooltip position', function() { + // Mock out the arc as if the controller put it there var arc = new Chart.elements.Arc({ - _datasetIndex: 2, - _index: 1 - }); - - // Mock out the view as if the controller put it there - arc._view = { startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: Math.sqrt(2), - }; + }); var pos = arc.tooltipPosition(); expect(pos.x).toBeCloseTo(0.5); @@ -57,20 +47,15 @@ describe('Arc element tests', function() { }); it ('should get the center', function() { + // Mock out the arc as if the controller put it there var arc = new Chart.elements.Arc({ - _datasetIndex: 2, - _index: 1 - }); - - // Mock out the view as if the controller put it there - arc._view = { startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: Math.sqrt(2), - }; + }); var center = arc.getCenterPoint(); expect(center.x).toBeCloseTo(0.5, 6); diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 199998dd0..6d14fe8ce 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -13,21 +13,15 @@ describe('Chart.elements.Point', function() { }); it ('Should correctly identify as in range', function() { + // Mock out the point as if we were made by the controller var point = new Chart.elements.Point({ - _datasetIndex: 2, - _index: 1 - }); - - // Safely handles if these are called before the viewmodel is instantiated - expect(point.inRange(5)).toBe(false); - - // Attach a view object as if we were the controller - point._view = { - radius: 2, - hitRadius: 3, + options: { + radius: 2, + hitRadius: 3, + }, x: 10, y: 15 - }; + }); expect(point.inRange(10, 15)).toBe(true); expect(point.inRange(10, 10)).toBe(false); @@ -36,18 +30,15 @@ describe('Chart.elements.Point', function() { }); it ('should get the correct tooltip position', function() { + // Mock out the point as if we were made by the controller var point = new Chart.elements.Point({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - point._view = { - radius: 2, - borderWidth: 6, + options: { + radius: 2, + borderWidth: 6, + }, x: 10, y: 15 - }; + }); expect(point.tooltipPosition()).toEqual({ x: 10, @@ -57,34 +48,31 @@ describe('Chart.elements.Point', function() { }); it('should get the correct center point', function() { + // Mock out the point as if we were made by the controller var point = new Chart.elements.Point({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - point._view = { - radius: 2, + options: { + radius: 2, + }, x: 10, y: 10 - }; + }); expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); it ('should not draw if skipped', function() { var mockContext = window.createMockContext(); - var point = new Chart.elements.Point(); - // Attach a view object as if we were the controller - point._view = { - radius: 2, - hitRadius: 3, + // Mock out the point as if we were made by the controller + var point = new Chart.elements.Point({ + options: { + radius: 2, + hitRadius: 3, + }, x: 10, y: 15, - ctx: mockContext, skip: true - }; + }); point.draw(mockContext); diff --git a/test/specs/element.rectangle.tests.js b/test/specs/element.rectangle.tests.js index 686eea932..e85d7f202 100644 --- a/test/specs/element.rectangle.tests.js +++ b/test/specs/element.rectangle.tests.js @@ -14,20 +14,11 @@ describe('Rectangle element tests', function() { it('Should correctly identify as in range', function() { var rectangle = new Chart.elements.Rectangle({ - _datasetIndex: 2, - _index: 1 - }); - - // Safely handles if these are called before the viewmodel is instantiated - expect(rectangle.inRange(5)).toBe(false); - - // Attach a view object as if we were the controller - rectangle._view = { base: 0, width: 4, x: 10, y: 15 - }; + }); expect(rectangle.inRange(10, 15)).toBe(true); expect(rectangle.inRange(10, 10)).toBe(true); @@ -36,17 +27,11 @@ describe('Rectangle element tests', function() { // Test when the y is below the base (negative bar) var negativeRectangle = new Chart.elements.Rectangle({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - negativeRectangle._view = { base: 0, width: 4, x: 10, y: -15 - }; + }); expect(negativeRectangle.inRange(10, -16)).toBe(false); expect(negativeRectangle.inRange(10, 1)).toBe(false); @@ -55,17 +40,11 @@ describe('Rectangle element tests', function() { it('should get the correct tooltip position', function() { var rectangle = new Chart.elements.Rectangle({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - rectangle._view = { base: 0, width: 4, x: 10, y: 15 - }; + }); expect(rectangle.tooltipPosition()).toEqual({ x: 10, @@ -74,17 +53,11 @@ describe('Rectangle element tests', function() { // Test when the y is below the base (negative bar) var negativeRectangle = new Chart.elements.Rectangle({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - negativeRectangle._view = { base: -10, width: 4, x: 10, y: -15 - }; + }); expect(negativeRectangle.tooltipPosition()).toEqual({ x: 10, @@ -94,17 +67,11 @@ describe('Rectangle element tests', function() { it('should get the center', function() { var rectangle = new Chart.elements.Rectangle({ - _datasetIndex: 2, - _index: 1 - }); - - // Attach a view object as if we were the controller - rectangle._view = { base: 0, width: 4, x: 10, y: 15 - }; + }); expect(rectangle.getCenterPoint()).toEqual({x: 10, y: 7.5}); }); diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index e1188458a..4da3d4ae1 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -22,8 +22,8 @@ describe('Default Configs', function() { chart.tooltip.update(); // Title is always blank - expect(chart.tooltip._model.title).toEqual([]); - expect(chart.tooltip._model.body).toEqual([{ + expect(chart.tooltip.title).toEqual([]); + expect(chart.tooltip.body).toEqual([{ before: [], lines: ['My dataset: (10, 12, 5)'], after: [] @@ -50,8 +50,8 @@ describe('Default Configs', function() { chart.tooltip.update(); // Title is always blank - expect(chart.tooltip._model.title).toEqual([]); - expect(chart.tooltip._model.body).toEqual([{ + expect(chart.tooltip.title).toEqual([]); + expect(chart.tooltip.body).toEqual([{ before: [], lines: ['label2: 20'], after: [] @@ -76,8 +76,8 @@ describe('Default Configs', function() { chart.tooltip.update(); // Title is always blank - expect(chart.tooltip._model.title).toEqual([]); - expect(chart.tooltip._model.body).toEqual([{ + expect(chart.tooltip.title).toEqual([]); + expect(chart.tooltip.body).toEqual([{ before: [], lines: [ 'row1: 20', @@ -196,8 +196,8 @@ describe('Default Configs', function() { chart.tooltip.update(); // Title is always blank - expect(chart.tooltip._model.title).toEqual([]); - expect(chart.tooltip._model.body).toEqual([{ + expect(chart.tooltip.title).toEqual([]); + expect(chart.tooltip.body).toEqual([{ before: [], lines: ['label2: 20'], after: [] diff --git a/test/specs/helpers.curve.tests.js b/test/specs/helpers.curve.tests.js index fea8adccc..d0f4e7115 100644 --- a/test/specs/helpers.curve.tests.js +++ b/test/specs/helpers.curve.tests.js @@ -49,163 +49,135 @@ describe('Curve helper tests', function() { it('should spline curves with monotone cubic interpolation', function() { var dataPoints = [ - {_model: {x: 0, y: 0, skip: false}}, - {_model: {x: 3, y: 6, skip: false}}, - {_model: {x: 9, y: 6, skip: false}}, - {_model: {x: 12, y: 60, skip: false}}, - {_model: {x: 15, y: 60, skip: false}}, - {_model: {x: 18, y: 120, skip: false}}, - {_model: {x: null, y: null, skip: true}}, - {_model: {x: 21, y: 180, skip: false}}, - {_model: {x: 24, y: 120, skip: false}}, - {_model: {x: 27, y: 125, skip: false}}, - {_model: {x: 30, y: 105, skip: false}}, - {_model: {x: 33, y: 110, skip: false}}, - {_model: {x: 33, y: 110, skip: false}}, - {_model: {x: 36, y: 170, skip: false}} + {x: 0, y: 0, skip: false}, + {x: 3, y: 6, skip: false}, + {x: 9, y: 6, skip: false}, + {x: 12, y: 60, skip: false}, + {x: 15, y: 60, skip: false}, + {x: 18, y: 120, skip: false}, + {x: null, y: null, skip: true}, + {x: 21, y: 180, skip: false}, + {x: 24, y: 120, skip: false}, + {x: 27, y: 125, skip: false}, + {x: 30, y: 105, skip: false}, + {x: 33, y: 110, skip: false}, + {x: 33, y: 110, skip: false}, + {x: 36, y: 170, skip: false} ]; helpers.splineCurveMonotone(dataPoints); expect(dataPoints).toEqual([{ - _model: { - x: 0, - y: 0, - skip: false, - controlPointNextX: 1, - controlPointNextY: 2 - } + x: 0, + y: 0, + skip: false, + controlPointNextX: 1, + controlPointNextY: 2 }, { - _model: { - x: 3, - y: 6, - skip: false, - controlPointPreviousX: 2, - controlPointPreviousY: 6, - controlPointNextX: 5, - controlPointNextY: 6 - } + x: 3, + y: 6, + skip: false, + controlPointPreviousX: 2, + controlPointPreviousY: 6, + controlPointNextX: 5, + controlPointNextY: 6 }, { - _model: { - x: 9, - y: 6, - skip: false, - controlPointPreviousX: 7, - controlPointPreviousY: 6, - controlPointNextX: 10, - controlPointNextY: 6 - } + x: 9, + y: 6, + skip: false, + controlPointPreviousX: 7, + controlPointPreviousY: 6, + controlPointNextX: 10, + controlPointNextY: 6 }, { - _model: { - x: 12, - y: 60, - skip: false, - controlPointPreviousX: 11, - controlPointPreviousY: 60, - controlPointNextX: 13, - controlPointNextY: 60 - } + x: 12, + y: 60, + skip: false, + controlPointPreviousX: 11, + controlPointPreviousY: 60, + controlPointNextX: 13, + controlPointNextY: 60 }, { - _model: { - x: 15, - y: 60, - skip: false, - controlPointPreviousX: 14, - controlPointPreviousY: 60, - controlPointNextX: 16, - controlPointNextY: 60 - } + x: 15, + y: 60, + skip: false, + controlPointPreviousX: 14, + controlPointPreviousY: 60, + controlPointNextX: 16, + controlPointNextY: 60 }, { - _model: { - x: 18, - y: 120, - skip: false, - controlPointPreviousX: 17, - controlPointPreviousY: 100 - } + x: 18, + y: 120, + skip: false, + controlPointPreviousX: 17, + controlPointPreviousY: 100 }, { - _model: { - x: null, - y: null, - skip: true - } + x: null, + y: null, + skip: true }, { - _model: { - x: 21, - y: 180, - skip: false, - controlPointNextX: 22, - controlPointNextY: 160 - } + x: 21, + y: 180, + skip: false, + controlPointNextX: 22, + controlPointNextY: 160 }, { - _model: { - x: 24, - y: 120, - skip: false, - controlPointPreviousX: 23, - controlPointPreviousY: 120, - controlPointNextX: 25, - controlPointNextY: 120 - } + x: 24, + y: 120, + skip: false, + controlPointPreviousX: 23, + controlPointPreviousY: 120, + controlPointNextX: 25, + controlPointNextY: 120 }, { - _model: { - x: 27, - y: 125, - skip: false, - controlPointPreviousX: 26, - controlPointPreviousY: 125, - controlPointNextX: 28, - controlPointNextY: 125 - } + x: 27, + y: 125, + skip: false, + controlPointPreviousX: 26, + controlPointPreviousY: 125, + controlPointNextX: 28, + controlPointNextY: 125 }, { - _model: { - x: 30, - y: 105, - skip: false, - controlPointPreviousX: 29, - controlPointPreviousY: 105, - controlPointNextX: 31, - controlPointNextY: 105 - } + x: 30, + y: 105, + skip: false, + controlPointPreviousX: 29, + controlPointPreviousY: 105, + controlPointNextX: 31, + controlPointNextY: 105 }, { - _model: { - x: 33, - y: 110, - skip: false, - controlPointPreviousX: 32, - controlPointPreviousY: 110, - controlPointNextX: 33, - controlPointNextY: 110 - } + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 32, + controlPointPreviousY: 110, + controlPointNextX: 33, + controlPointNextY: 110 }, { - _model: { - x: 33, - y: 110, - skip: false, - controlPointPreviousX: 33, - controlPointPreviousY: 110, - controlPointNextX: 34, - controlPointNextY: 110 - } + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 33, + controlPointPreviousY: 110, + controlPointNextX: 34, + controlPointNextY: 110 }, { - _model: { - x: 36, - y: 170, - skip: false, - controlPointPreviousX: 35, - controlPointPreviousY: 150 - } + x: 36, + y: 170, + skip: false, + controlPointPreviousX: 35, + controlPointPreviousY: 150 }]); }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 880a72c65..671e3af2c 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -149,7 +149,7 @@ describe('Legend block tests', function() { datasetIndex: 1 }, { text: 'dataset3', - fillStyle: 'green', + fillStyle: 'rgba(0,0,0,0.1)', hidden: false, lineCap: 'butt', lineDash: [], @@ -198,7 +198,7 @@ describe('Legend block tests', function() { expect(chart.legend.legendItems).toEqual([{ text: 'dataset3', - fillStyle: 'green', + fillStyle: 'rgba(0,0,0,0.1)', hidden: false, lineCap: 'butt', lineDash: [], diff --git a/test/utils.js b/test/utils.js index b082a2f03..e2d3e7a28 100644 --- a/test/utils.js +++ b/test/utils.js @@ -113,8 +113,6 @@ function _resolveElementPoint(el) { point = el.getCenterPoint(); } else if (el.x !== undefined && el.y !== undefined) { point = el; - } else if (el._model && el._model.x !== undefined && el._model.y !== undefined) { - point = el._model; } } return point; -- 2.47.2