From 72c8b074b9a60aa861197306949dce3715aa921b Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sat, 16 Nov 2019 19:06:59 +0200 Subject: [PATCH 1/5] Rewrite animation logic --- .eslintrc.yml | 1 + docs/configuration/tooltip.md | 2 +- docs/developers/api.md | 2 +- docs/developers/charts.md | 4 +- docs/getting-started/v3-migration.md | 6 +- src/controllers/controller.bar.js | 53 +- src/controllers/controller.bubble.js | 108 ++- src/controllers/controller.doughnut.js | 94 +-- src/controllers/controller.line.js | 148 +--- src/controllers/controller.polarArea.js | 105 +-- src/controllers/controller.radar.js | 115 +-- src/core/core.animation.js | 94 ++- src/core/core.animations.js | 213 +++-- src/core/core.animator.js | 197 +++++ src/core/core.controller.js | 241 ++---- src/core/core.datasetController.js | 253 ++++-- src/core/core.element.js | 104 +-- src/core/core.interaction.js | 6 +- src/core/core.plugins.js | 17 +- src/core/core.tooltip.js | 771 +++++++++--------- 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 | 60 +- 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 - 49 files changed, 2266 insertions(+), 2439 deletions(-) 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 ace7ae7a470..2a65bd04291 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/tooltip.md b/docs/configuration/tooltip.md index 26b1d6a7103..6e086a16716 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 e248db3a572..7c7b8682444 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -175,5 +175,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 2e2e5c231c9..c96350373db 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/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index 9fa7610fac0..c2f854a90de 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -89,10 +89,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` diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 7b00ea00f89..42eec8086dd 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', '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 1cf947b24ae..21cacfb7848 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 ad5fa86ea8e..954c2ec792c 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 34d421f9859..918aa646b2f 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,70 +66,61 @@ module.exports = DatasetController.extend({ rotation: 'pointRotation' }, - update: function(reset) { + update: function(mode) { const me = this; const meta = me._cachedMeta; const line = meta.dataset; - const points = meta.data || []; + const points = meta.data || (meta.data = []); 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(); - - line.pivot(); - } + if (showLine && mode !== 'resize') { - // 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 c31663492d1..57bf3a83130 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 c52f871de59..9f3ae118e58 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 ce47f6d7359..1e97684da1c 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 5e7222a779c..f3c879550a8 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -1,121 +1,154 @@ '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'] + }, 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; - }); + /** + * @private + * @todo if new options are $shared, target.options should be replaced with those new share 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) { + 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 + **/ + 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); + 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 00000000000..5178c1c8a4d --- /dev/null +++ b/src/core/core.animator.js @@ -0,0 +1,197 @@ +'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(anims, type, args) { + const callbacks = anims._listeners[type] || []; + callbacks.forEach(fn => fn(args || [])); + } + + /** + * @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 date = Date.now(); + const charts = this._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.fps) { + drawFPS(chart, items.length, date, this._lastDate); + } + } + + if (!items.length) { + anims.running = false; + this._notify(chart, '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: []} + }; + 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; + 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, 'complete'); + } +} + +const instance = new Animator(); + +module.exports = instance; diff --git a/src/core/core.controller.js b/src/core/core.controller.js index f13433399b4..95f4c65023e 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) { @@ -114,11 +111,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) { @@ -142,8 +135,6 @@ function updateConfig(chart) { chart.ensureScalesHaveIDs(); chart.buildOrUpdateScales(); - // Tooltip - chart.tooltip._options = newOptions.tooltips; chart.tooltip.initialize(); } @@ -212,10 +203,20 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { return; } + Animator.listen(me, 'complete', me._onAnimationsComplete); + me.initialize(); me.update(); }, + _onAnimationsComplete: function() { + const me = this; + const animationOptions = me.options.animation; + + plugins.notify(me, 'afterRender'); + helpers.callback(animationOptions && animationOptions.onComplete, [], me); + }, + /** * @private */ @@ -248,8 +249,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { }, stop: function() { - // Stops any current animation loop occurring - animations.cancelAnimation(this); + Animator.stop(this); return this; }, @@ -288,9 +288,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { } me.stop(); - me.update({ - duration: options.responsiveAnimationDuration - }); + me.update('resize'); } }, @@ -454,11 +452,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,7 +469,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { } // In case the entire data object changed - me.tooltip._data = me.data; + // me.tooltip._data = me.data; // Make sure dataset controllers are updated and new controllers are reset var newControllers = me.buildOrUpdateControllers(); @@ -484,40 +482,34 @@ 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(); + me.updateDatasets(mode); // Need to reset tooltip in case it is displayed with elements that are removed // after update. - me.tooltip.initialize(); + // me.tooltip.initialize(); // Last active contains items that were previously hovered. - me.lastActive = []; + // me.lastActive = []; // 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; }, /** @@ -556,7 +548,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) { @@ -564,7 +556,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'); @@ -575,91 +567,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; } @@ -671,41 +624,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'); }, /** @@ -739,20 +667,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'); }, /** @@ -760,7 +688,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; @@ -769,7 +697,6 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { var args = { meta: meta, index: meta.index, - easingValue: easingValue }; if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { @@ -783,7 +710,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); @@ -795,19 +722,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]); }, @@ -924,12 +850,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}); }, /** @@ -1019,48 +940,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; }, @@ -1085,12 +980,12 @@ 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 // Need to call with native event here to not break backwards compatibility - helpers.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); + // helpers.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); if (e.type === 'mouseup' || e.type === 'click') { if (options.onClick && helpers.canvas._isPointInArea(e, me.chartArea)) { @@ -1099,8 +994,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 7ce9c0d66aa..fbabf43344b 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,108 +853,172 @@ 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); + values.$shared = true; + cached[mode] = values; } return values; }, - removeHoverStyle: function(element) { - helpers.merge(element._model, element.$previousStyle || {}); - delete element.$previousStyle; - }, + /** + * @private + */ + _resolveAnimations: function(index, mode) { + const me = this; + const chart = me.chart; + const cached = me._cachedAnimations; + const active = mode === 'active'; - setHoverStyle: function(element, datasetIndex, index) { - const dataset = this.chart.data.datasets[datasetIndex]; - const model = element._model; - const getHoverColor = helpers.getHoverColor; + mode = mode || 'default'; - element.$previousStyle = { - backgroundColor: model.backgroundColor, - borderColor: model.borderColor, - borderWidth: model.borderWidth - }; + 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); + } - 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); + return animations; }, /** * @private */ - _removeDatasetHoverStyle: function() { - const element = this._cachedMeta.dataset; + _getSharedOptions: function(mode, element, options) { + if (mode !== 'reset' && options && options.$shared && + element && element.options && element.options.$shared) { + return {target: element.options, options}; + } + }, - if (element) { - this.removeHoverStyle(element); + /** + * @private + */ + _includeOptions: function(mode, sharedOptions) { + return mode !== 'resize' && !sharedOptions; + }, + + /** + * @private + */ + _updateElement: function(element, index, properties, mode) { + if (mode === 'reset') { + helpers.extend(element, properties); + } else { + this._resolveAnimations(index, mode).update(element, properties); } }, /** * @private */ - _setDatasetHoverStyle: function() { + _updateSharedOptions: function(sharedOptions, mode) { + if (sharedOptions) { + this._resolveAnimations(undefined, mode).update(sharedOptions.target, sharedOptions.options); + } + }, + + /** + * @private + */ + _setStyle(element, index, active) { + this._resolveAnimations(index, active && 'active').update(element, {options: this.getStyle(index, active)}); + }, + + removeHoverStyle: function(element, datasetIndex, index) { + this._setStyle(element, index, false); + }, + + setHoverStyle: function(element, datasetIndex, index) { + this._setStyle(element, index, true); + }, + + /** + * @private + */ + _removeDatasetHoverStyle: 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); + this._setStyle(element, undefined, false); + }, - keys = Object.keys(hoverOptions); - for (i = 0, ilen = keys.length; i < ilen; ++i) { - key = keys[i]; - prev[key] = model[key]; - model[key] = hoverOptions[key]; + /** + * @private + */ + _setDatasetHoverStyle: function() { + const me = this; + const element = me._cachedMeta.dataset; + + if (!element) { + return; } - element.$previousStyle = prev; + this._setStyle(element, undefined, true); }, /** @@ -986,7 +1061,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 159e4721cd0..fb2e442c37f 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 f1975cd1e38..2224d72429e 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 cf44af0ace3..1d68c20bda1 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 73122f8df50..8caa62fa773 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,43 @@ 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); + // me.update(); } - 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 +505,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 +532,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 +553,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 +668,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 +795,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 +809,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 +877,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 +967,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 +975,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 +996,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 000c98bc311..6d1ec447fcf 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 533b93fcaab..67bd08e1e34 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 8de39312187..be92d51fbd1 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 f77512e3af2..29cf411d252 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 292e27740e6..d4bf75823bc 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 c8a88d7d41a..9088b65bf02 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 b2487e35b01..2ddda44de7b 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 8bceb9f5312..ee68a1514cb 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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 literal 14590 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 Uur&n6+5qrCN&Q}tqD9F62MnQ4N&o-= 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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 literal 14464 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 RaIz4%qk3PZ__p=a{{we6wm<*? diff --git a/test/fixtures/core.tooltip/opacity.js b/test/fixtures/core.tooltip/opacity.js index fc63d3740ae..12652cc90df 100644 --- a/test/fixtures/core.tooltip/opacity.js +++ b/test/fixtures/core.tooltip/opacity.js @@ -82,14 +82,13 @@ module.exports = { event = { type: 'mousemove', target: canvas, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y + clientX: rect.left + point.x, + clientY: rect.top + point.y }; chart.handleEvent(event); chart.tooltip.handleEvent(event); - chart.tooltip.transition(1); - chart.tooltip._view.opacity = j / 10; - chart.tooltip.draw(); + chart.tooltip.opacity = j / 10; + chart.tooltip.draw(chart.ctx); } } } diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js index e6d199452f5..0ec56a7a9db 100644 --- a/test/specs/controller.bar.tests.js +++ b/test/specs/controller.bar.tests.js @@ -728,11 +728,11 @@ describe('Chart.controllers.bar', function() { {x: 89, y: 512}, {x: 217, y: 0} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model.base).toBeCloseToPixel(1024); - expect(meta.data[i]._model.width).toBeCloseToPixel(46); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].base).toBeCloseToPixel(1024); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'red', borderSkipped: 'top', borderColor: 'blue', @@ -785,10 +785,10 @@ describe('Chart.controllers.bar', function() { var bar1 = meta.data[0]; var bar2 = meta.data[1]; - expect(bar1._model.x).toBeCloseToPixel(179); - expect(bar1._model.y).toBeCloseToPixel(114); - expect(bar2._model.x).toBeCloseToPixel(435); - expect(bar2._model.y).toBeCloseToPixel(0); + expect(bar1.x).toBeCloseToPixel(179); + expect(bar1.y).toBeCloseToPixel(114); + expect(bar2.x).toBeCloseToPixel(435); + expect(bar2.y).toBeCloseToPixel(0); }); it('should update elements when the scales are stacked', function() { @@ -829,10 +829,10 @@ describe('Chart.controllers.bar', function() { {b: 293, w: 92 / 2, x: 295, y: 146}, {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -843,10 +843,10 @@ describe('Chart.controllers.bar', function() { {b: 146, w: 92 / 2, x: 345, y: 146}, {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -890,10 +890,10 @@ describe('Chart.controllers.bar', function() { {b: 1024, w: 92 / 2, x: 294, y: 922}, {b: 1024, w: 92 / 2, x: 422.5, y: 0} ].forEach(function(values, i) { - expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -904,10 +904,10 @@ describe('Chart.controllers.bar', function() { {b: 922, w: 92 / 2, x: 345, y: 0}, {b: 0, w: 92 / 2, x: 473.5, y: 0} ].forEach(function(values, i) { - expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -949,10 +949,10 @@ describe('Chart.controllers.bar', function() { {b: 293, w: 92, x: 320, y: 146}, {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -963,10 +963,10 @@ describe('Chart.controllers.bar', function() { {b: 293, w: 92, x: 320, y: 293}, {b: 293, w: 92, x: 448, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -1008,10 +1008,10 @@ describe('Chart.controllers.bar', function() { {b: 293, w: 92 / 2, x: 295, y: 146}, {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -1022,10 +1022,10 @@ describe('Chart.controllers.bar', function() { {b: 146, w: 92 / 2, x: 345, y: 146}, {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -1069,10 +1069,10 @@ describe('Chart.controllers.bar', function() { {b: 293, w: 92, x: 320, y: 146}, {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta = chart.getDatasetMeta(1); @@ -1083,10 +1083,10 @@ describe('Chart.controllers.bar', function() { {b: 146, w: 92, x: 320, y: 146}, {b: 439, w: 92, x: 448, y: 497} ].forEach(function(values, i) { - expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta.data[i]._model.width).toBeCloseToPixel(values.w); - expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta.data[i].base).toBeCloseToPixel(values.b); + expect(meta.data[i].width).toBeCloseToPixel(values.w); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -1126,10 +1126,10 @@ describe('Chart.controllers.bar', function() { {x: 89, y: 256}, {x: 217, y: 0} ].forEach(function(values, i) { - expect(meta.data[i]._model.base).toBeCloseToPixel(512); - expect(meta.data[i]._model.width).toBeCloseToPixel(46); - expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta.data[i].base).toBeCloseToPixel(512); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -1172,10 +1172,10 @@ describe('Chart.controllers.bar', function() { {b: 384, x: 89, y: 256}, {b: 256, x: 217, y: 0} ].forEach(function(values, i) { - expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); - expect(meta.data[i]._model.width).toBeCloseToPixel(46); - expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta.data[i].base).toBeCloseToPixel(values.b); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -1235,29 +1235,31 @@ describe('Chart.controllers.bar', function() { var bar = meta.data[0]; meta.controller.setHoverStyle(bar, 1, 0); - expect(bar._model.backgroundColor).toBe('rgb(230, 0, 0)'); - expect(bar._model.borderColor).toBe('rgb(0, 0, 230)'); - expect(bar._model.borderWidth).toBe(2); + expect(bar.options.backgroundColor).toBe('rgb(230, 0, 0)'); + expect(bar.options.borderColor).toBe('rgb(0, 0, 230)'); + expect(bar.options.borderWidth).toBe(2); // Set a dataset style chart.data.datasets[1].hoverBackgroundColor = 'rgb(128, 128, 128)'; chart.data.datasets[1].hoverBorderColor = 'rgb(0, 0, 0)'; chart.data.datasets[1].hoverBorderWidth = 5; + chart.update(); meta.controller.setHoverStyle(bar, 1, 0); - expect(bar._model.backgroundColor).toBe('rgb(128, 128, 128)'); - expect(bar._model.borderColor).toBe('rgb(0, 0, 0)'); - expect(bar._model.borderWidth).toBe(5); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(0, 0, 0)'); + expect(bar.options.borderWidth).toBe(5); // Should work with array styles so that we can set per bar chart.data.datasets[1].hoverBackgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; chart.data.datasets[1].hoverBorderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; chart.data.datasets[1].hoverBorderWidth = [2.5, 5]; + chart.update(); meta.controller.setHoverStyle(bar, 1, 0); - expect(bar._model.backgroundColor).toBe('rgb(255, 255, 255)'); - expect(bar._model.borderColor).toBe('rgb(9, 9, 9)'); - expect(bar._model.borderWidth).toBe(2.5); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); }); it('should remove a hover style from a bar', function() { @@ -1293,17 +1295,17 @@ describe('Chart.controllers.bar', function() { chart.options.elements.rectangle.borderWidth = 3.14; chart.update(); - expect(bar._model.backgroundColor).toBe('rgb(128, 128, 128)'); - expect(bar._model.borderColor).toBe('rgb(15, 15, 15)'); - expect(bar._model.borderWidth).toBe(3.14); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); + expect(bar.options.borderWidth).toBe(3.14); meta.controller.setHoverStyle(bar, 1, 0); - expect(bar._model.backgroundColor).toBe(helpers.getHoverColor('rgb(128, 128, 128)')); - expect(bar._model.borderColor).toBe(helpers.getHoverColor('rgb(15, 15, 15)')); - expect(bar._model.borderWidth).toBe(3.14); + expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(128, 128, 128)')); + expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(15, 15, 15)')); + expect(bar.options.borderWidth).toBe(3.14); meta.controller.removeHoverStyle(bar); - expect(bar._model.backgroundColor).toBe('rgb(128, 128, 128)'); - expect(bar._model.borderColor).toBe('rgb(15, 15, 15)'); - expect(bar._model.borderWidth).toBe(3.14); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); + expect(bar.options.borderWidth).toBe(3.14); // Should work with array styles so that we can set per bar chart.data.datasets[1].backgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; @@ -1311,17 +1313,17 @@ describe('Chart.controllers.bar', function() { chart.data.datasets[1].borderWidth = [2.5, 5]; chart.update(); - expect(bar._model.backgroundColor).toBe('rgb(255, 255, 255)'); - expect(bar._model.borderColor).toBe('rgb(9, 9, 9)'); - expect(bar._model.borderWidth).toBe(2.5); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); meta.controller.setHoverStyle(bar, 1, 0); - expect(bar._model.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 255, 255)')); - expect(bar._model.borderColor).toBe(helpers.getHoverColor('rgb(9, 9, 9)')); - expect(bar._model.borderWidth).toBe(2.5); + expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 255, 255)')); + expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(9, 9, 9)')); + expect(bar.options.borderWidth).toBe(2.5); meta.controller.removeHoverStyle(bar); - expect(bar._model.backgroundColor).toBe('rgb(255, 255, 255)'); - expect(bar._model.borderColor).toBe('rgb(9, 9, 9)'); - expect(bar._model.borderWidth).toBe(2.5); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); }); describe('Bar width', function() { @@ -1351,7 +1353,7 @@ describe('Chart.controllers.bar', function() { for (var i = 0; i < chart.data.datasets.length; i++) { var bars = chart.getDatasetMeta(i).data; for (var j = xScale.min; j <= xScale.max; j++) { - totalBarWidth += bars[j]._model.width; + totalBarWidth += bars[j].width; } if (stacked) { break; @@ -1425,7 +1427,7 @@ describe('Chart.controllers.bar', function() { for (var i = 0; i < chart.data.datasets.length; i++) { var bars = chart.getDatasetMeta(i).data; for (var j = yScale.min; j <= yScale.max; j++) { - totalBarHeight += bars[j]._model.height; + totalBarHeight += bars[j].height; } if (stacked) { break; @@ -1525,8 +1527,8 @@ describe('Chart.controllers.bar', function() { for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { meta = chart.getDatasetMeta(i); - expect(meta.data[0]._model.width).toBeCloseToPixel(expected); - expect(meta.data[1]._model.width).toBeCloseToPixel(expected); + expect(meta.data[0].width).toBeCloseToPixel(expected); + expect(meta.data[1].width).toBeCloseToPixel(expected); } }); @@ -1540,8 +1542,8 @@ describe('Chart.controllers.bar', function() { for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { meta = chart.getDatasetMeta(i); - expect(meta.data[0]._model.width).toBeCloseToPixel(10); - expect(meta.data[1]._model.width).toBeCloseToPixel(10); + expect(meta.data[0].width).toBeCloseToPixel(10); + expect(meta.data[1].width).toBeCloseToPixel(10); } }); }); @@ -1562,8 +1564,8 @@ describe('Chart.controllers.bar', function() { var data = chart.getDatasetMeta(0).data; - expect(data[0]._model.base - minBarLength).toEqual(data[0]._model.y); - expect(data[1]._model.base + minBarLength).toEqual(data[1]._model.y); + expect(data[0].base - minBarLength).toEqual(data[0].y); + expect(data[1].base + minBarLength).toEqual(data[1].y); }); it('minBarLength settings should be used on X axis on horizontalBar chart', function() { @@ -1580,7 +1582,7 @@ describe('Chart.controllers.bar', function() { var data = chart.getDatasetMeta(0).data; - expect(data[0]._model.base + minBarLength).toEqual(data[0]._model.x); - expect(data[1]._model.base - minBarLength).toEqual(data[1]._model.x); + expect(data[0].base + minBarLength).toEqual(data[0].x); + expect(data[1].base - minBarLength).toEqual(data[1].x); }); }); diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js index 9de15b1b812..8f4211423e3 100644 --- a/test/specs/controller.bubble.tests.js +++ b/test/specs/controller.bubble.tests.js @@ -138,15 +138,14 @@ describe('Chart.controllers.bubble', function() { {r: 2, x: 341, y: 486}, {r: 1, x: 512, y: 0} ].forEach(function(expected, i) { - expect(meta.data[i]._model.radius).toBe(expected.r); - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.global.defaultColor, borderColor: Chart.defaults.global.defaultColor, borderWidth: 1, hitRadius: 1, - skip: false + radius: expected.r })); }); @@ -162,12 +161,11 @@ describe('Chart.controllers.bubble', function() { chart.update(); for (var i = 0; i < 4; ++i) { - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(98, 98, 98)', borderColor: 'rgb(8, 8, 8)', borderWidth: 0.55, - hitRadius: 3.3, - skip: false + hitRadius: 3.3 })); } }); @@ -295,16 +293,16 @@ describe('Chart.controllers.bubble', function() { var point = chart.getDatasetMeta(0).data[0]; jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); - expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(20 + 4); + expect(point.options.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point.options.borderColor).toBe('rgb(22, 89, 156)'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(20 + 4); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(20); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); }); it ('should handle hover styles defined via dataset properties', function() { @@ -321,16 +319,16 @@ describe('Chart.controllers.bubble', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(20 + 4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(20 + 4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(20); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); }); it ('should handle hover styles defined via element options', function() { @@ -347,16 +345,16 @@ describe('Chart.controllers.bubble', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(20 + 4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(20 + 4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(20); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); }); }); }); diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 328d5dab85e..a1eaf71c859 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -78,6 +78,7 @@ describe('Chart.controllers.doughnut', function() { legend: false, title: false, animation: { + duration: 0, animateRotate: true, animateScale: false }, @@ -106,14 +107,14 @@ describe('Chart.controllers.doughnut', function() { {c: 0}, {c: 0} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(256); - expect(meta.data[i]._model.y).toBeCloseToPixel(256); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(256); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(192); - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ - startAngle: Math.PI * -0.5, - endAngle: Math.PI * -0.5, + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(256); + expect(meta.data[i].outerRadius).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).toBeCloseToPixel(192); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseToPixel(Math.PI * -0.5); + expect(meta.data[i].endAngle).toBeCloseToPixel(Math.PI * -0.5); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 @@ -128,14 +129,14 @@ describe('Chart.controllers.doughnut', function() { {c: 0, s: 2.2689280275, e: 2.2689280275}, {c: 2.4434609527, s: 2.2689280275, e: 4.7123889803} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(256); - expect(meta.data[i]._model.y).toBeCloseToPixel(256); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(256); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(192); - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); - expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); - expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(256); + expect(meta.data[i].outerRadius).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).toBeCloseToPixel(192); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 @@ -200,13 +201,13 @@ describe('Chart.controllers.doughnut', function() { {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(512); - expect(meta.data[i]._model.y).toBeCloseToPixel(512); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(512); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(384); - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); - expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); - expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); + expect(meta.data[i].x).toBeCloseToPixel(512); + expect(meta.data[i].y).toBeCloseToPixel(512); + expect(meta.data[i].outerRadius).toBeCloseToPixel(512); + expect(meta.data[i].innerRadius).toBeCloseToPixel(384); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); }); }); @@ -244,9 +245,9 @@ describe('Chart.controllers.doughnut', function() { {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} ].forEach(function(expected, i) { - expect(meta.data[i]._model.circumference).toBeCloseTo(expected.c, 8); - expect(meta.data[i]._model.startAngle).toBeCloseTo(expected.s, 8); - expect(meta.data[i]._model.endAngle).toBeCloseTo(expected.e, 8); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); }); }); @@ -351,14 +352,14 @@ describe('Chart.controllers.doughnut', function() { var arc = chart.getDatasetMeta(0).data[0]; jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(49, 135, 221)'); - expect(arc._model.borderColor).toBe('rgb(22, 89, 156)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(arc.options.borderColor).toBe('rgb(22, 89, 156)'); + expect(arc.options.borderWidth).toBe(2); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via dataset properties', function() { @@ -374,14 +375,14 @@ describe('Chart.controllers.doughnut', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(arc._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(arc._model.borderWidth).toBe(8.4); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via element options', function() { @@ -397,14 +398,14 @@ describe('Chart.controllers.doughnut', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(arc._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(arc._model.borderWidth).toBe(8.4); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); }); }); diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js index 61248398dd9..4b0e87851a0 100644 --- a/test/specs/controller.line.tests.js +++ b/test/specs/controller.line.tests.js @@ -203,9 +203,9 @@ describe('Chart.controllers.line', function() { {x: 0, y: 512}, {x: 171, y: 0} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'red', borderColor: 'blue', })); @@ -248,7 +248,7 @@ describe('Chart.controllers.line', function() { var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; - expect(point._model.x).toBeCloseToPixel(0); + expect(point.x).toBeCloseToPixel(0); // 2 points chart.data.labels = ['One', 'Two']; @@ -257,8 +257,8 @@ describe('Chart.controllers.line', function() { var points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(0); - expect(points[1]._model.x).toBeCloseToPixel(512); + expect(points[0].x).toBeCloseToPixel(0); + expect(points[1].x).toBeCloseToPixel(512); // 3 points chart.data.labels = ['One', 'Two', 'Three']; @@ -267,9 +267,9 @@ describe('Chart.controllers.line', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(0); - expect(points[1]._model.x).toBeCloseToPixel(256); - expect(points[2]._model.x).toBeCloseToPixel(512); + expect(points[0].x).toBeCloseToPixel(0); + expect(points[1].x).toBeCloseToPixel(256); + expect(points[2].x).toBeCloseToPixel(512); // 4 points chart.data.labels = ['One', 'Two', 'Three', 'Four']; @@ -278,10 +278,10 @@ describe('Chart.controllers.line', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(0); - expect(points[1]._model.x).toBeCloseToPixel(171); - expect(points[2]._model.x).toBeCloseToPixel(340); - expect(points[3]._model.x).toBeCloseToPixel(512); + expect(points[0].x).toBeCloseToPixel(0); + expect(points[1].x).toBeCloseToPixel(171); + expect(points[2].x).toBeCloseToPixel(340); + expect(points[3].x).toBeCloseToPixel(512); }); it('should update elements when the y scale is stacked', function() { @@ -320,8 +320,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -332,8 +332,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -383,8 +383,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -395,8 +395,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -461,8 +461,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -473,8 +473,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -515,8 +515,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 439} ].forEach(function(values, i) { - expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); @@ -527,8 +527,8 @@ describe('Chart.controllers.line', function() { {x: 341, y: 146}, {x: 512, y: 497} ].forEach(function(values, i) { - expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); - expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); @@ -552,9 +552,9 @@ describe('Chart.controllers.line', function() { var meta = chart.getDatasetMeta(0); - expect(meta.dataset._model.backgroundColor).toBe('rgb(98, 98, 98)'); - expect(meta.dataset._model.borderColor).toBe('rgb(8, 8, 8)'); - expect(meta.dataset._model.borderWidth).toBe(0.55); + expect(meta.dataset.options.backgroundColor).toBe('rgb(98, 98, 98)'); + expect(meta.dataset.options.borderColor).toBe('rgb(8, 8, 8)'); + expect(meta.dataset.options.borderWidth).toBe(0.55); }); describe('dataset global defaults', function() { @@ -595,19 +595,19 @@ describe('Chart.controllers.line', function() { } }); - var model = chart.getDatasetMeta(0).dataset._model; + var options = chart.getDatasetMeta(0).dataset.options; - expect(model.spanGaps).toBe(true); - expect(model.tension).toBe(0.231); - expect(model.backgroundColor).toBe('#add'); - expect(model.borderWidth).toBe('#daa'); - expect(model.borderColor).toBe('#dad'); - expect(model.borderCapStyle).toBe('round'); - expect(model.borderDash).toEqual([0]); - expect(model.borderDashOffset).toBe(0.871); - expect(model.borderJoinStyle).toBe('miter'); - expect(model.fill).toBe('start'); - expect(model.cubicInterpolationMode).toBe('monotone'); + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should be overriden by user-supplied values', function() { @@ -639,14 +639,14 @@ describe('Chart.controllers.line', function() { } }); - var model = chart.getDatasetMeta(0).dataset._model; + var options = chart.getDatasetMeta(0).dataset.options; // dataset-level option overrides global default - expect(model.spanGaps).toBe(true); + expect(options.spanGaps).toBe(true); // chart-level default overrides global default - expect(model.tension).toBe(0.345); + expect(options.tension).toBe(0.345); // dataset-level option overrides chart-level default - expect(model.backgroundColor).toBe('#dad'); + expect(options.backgroundColor).toBe('#dad'); }); }); @@ -679,19 +679,19 @@ describe('Chart.controllers.line', function() { } }); - var model = chart.getDatasetMeta(0).dataset._model; + var options = chart.getDatasetMeta(0).dataset.options; - expect(model.spanGaps).toBe(true); - expect(model.tension).toBe(0.231); - expect(model.backgroundColor).toBe('#add'); - expect(model.borderWidth).toBe('#daa'); - expect(model.borderColor).toBe('#dad'); - expect(model.borderCapStyle).toBe('round'); - expect(model.borderDash).toEqual([0]); - expect(model.borderDashOffset).toBe(0.871); - expect(model.borderJoinStyle).toBe('miter'); - expect(model.fill).toBe('start'); - expect(model.cubicInterpolationMode).toBe('monotone'); + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should obey the dataset options', function() { @@ -717,19 +717,19 @@ describe('Chart.controllers.line', function() { } }); - var model = chart.getDatasetMeta(0).dataset._model; + var options = chart.getDatasetMeta(0).dataset.options; - expect(model.spanGaps).toBe(true); - expect(model.tension).toBe(0.231); - expect(model.backgroundColor).toBe('#add'); - expect(model.borderWidth).toBe('#daa'); - expect(model.borderColor).toBe('#dad'); - expect(model.borderCapStyle).toBe('round'); - expect(model.borderDash).toEqual([0]); - expect(model.borderDashOffset).toBe(0.871); - expect(model.borderJoinStyle).toBe('miter'); - expect(model.fill).toBe('start'); - expect(model.cubicInterpolationMode).toBe('monotone'); + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should handle number of data point changes in update', function() { @@ -790,16 +790,16 @@ describe('Chart.controllers.line', function() { var point = chart.getDatasetMeta(0).data[0]; jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); - expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(4); + expect(point.options.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point.options.borderColor).toBe('rgb(22, 89, 156)'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(4); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); it ('should handle hover styles defined via dataset properties', function() { @@ -816,16 +816,16 @@ describe('Chart.controllers.line', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); it ('should handle hover styles defined via element options', function() { @@ -842,16 +842,16 @@ describe('Chart.controllers.line', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); it ('should handle dataset hover styles defined via dataset properties', function() { @@ -872,14 +872,14 @@ describe('Chart.controllers.line', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(dataset._model.backgroundColor).toBe('#000'); - expect(dataset._model.borderColor).toBe('#111'); - expect(dataset._model.borderWidth).toBe(12); + expect(dataset.options.backgroundColor).toBe('#000'); + expect(dataset.options.borderColor).toBe('#111'); + expect(dataset.options.borderWidth).toBe(12); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(dataset._model.backgroundColor).toBe('#AAA'); - expect(dataset._model.borderColor).toBe('#BBB'); - expect(dataset._model.borderWidth).toBe(6); + expect(dataset.options.backgroundColor).toBe('#AAA'); + expect(dataset.options.borderColor).toBe('#BBB'); + expect(dataset.options.borderWidth).toBe(6); }); }); @@ -899,7 +899,7 @@ describe('Chart.controllers.line', function() { var meta = chart.getDatasetMeta(0); var point = meta.data[0]; - expect(point._model.borderWidth).toBe(0); + expect(point.options.borderWidth).toBe(0); }); it('should allow an array as the point border width setting', function() { @@ -916,9 +916,9 @@ describe('Chart.controllers.line', function() { }); var meta = chart.getDatasetMeta(0); - expect(meta.data[0]._model.borderWidth).toBe(1); - expect(meta.data[1]._model.borderWidth).toBe(2); - expect(meta.data[2]._model.borderWidth).toBe(3); - expect(meta.data[3]._model.borderWidth).toBe(4); + expect(meta.data[0].options.borderWidth).toBe(1); + expect(meta.data[1].options.borderWidth).toBe(2); + expect(meta.data[2].options.borderWidth).toBe(3); + expect(meta.data[3].options.borderWidth).toBe(4); }); }); diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js index f4b41075689..f7ea6e6a5bf 100644 --- a/test/specs/controller.polarArea.tests.js +++ b/test/specs/controller.polarArea.tests.js @@ -108,13 +108,13 @@ describe('Chart.controllers.polarArea', function() { {o: 51, s: 0.5 * Math.PI, e: Math.PI}, {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(256); - expect(meta.data[i]._model.y).toBeCloseToPixel(259); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); - expect(meta.data[i]._model.startAngle).toBe(expected.s); - expect(meta.data[i]._model.endAngle).toBe(expected.e); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(259); + expect(meta.data[i].innerRadius).toBeCloseToPixel(0); + expect(meta.data[i].outerRadius).toBeCloseToPixel(expected.o); + expect(meta.data[i].startAngle).toBe(expected.s); + expect(meta.data[i].endAngle).toBe(expected.e); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 @@ -129,17 +129,17 @@ describe('Chart.controllers.polarArea', function() { chart.update(); for (var i = 0; i < 4; ++i) { - expect(meta.data[i]._model.backgroundColor).toBe('rgb(128, 129, 130)'); - expect(meta.data[i]._model.borderColor).toBe('rgb(56, 57, 58)'); - expect(meta.data[i]._model.borderWidth).toBe(1.123); + expect(meta.data[i].options.backgroundColor).toBe('rgb(128, 129, 130)'); + expect(meta.data[i].options.borderColor).toBe('rgb(56, 57, 58)'); + expect(meta.data[i].options.borderWidth).toBe(1.123); } chart.update(); - expect(meta.data[0]._model.x).toBeCloseToPixel(256); - expect(meta.data[0]._model.y).toBeCloseToPixel(259); - expect(meta.data[0]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(177); + expect(meta.data[0].x).toBeCloseToPixel(256); + expect(meta.data[0].y).toBeCloseToPixel(259); + expect(meta.data[0].innerRadius).toBeCloseToPixel(0); + expect(meta.data[0].outerRadius).toBeCloseToPixel(177); }); it('should update elements with start angle from options', function() { @@ -176,13 +176,13 @@ describe('Chart.controllers.polarArea', function() { {o: 51, s: Math.PI, e: 1.5 * Math.PI}, {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(256); - expect(meta.data[i]._model.y).toBeCloseToPixel(259); - expect(meta.data[i]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[i]._model.outerRadius).toBeCloseToPixel(expected.o); - expect(meta.data[i]._model.startAngle).toBe(expected.s); - expect(meta.data[i]._model.endAngle).toBe(expected.e); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(259); + expect(meta.data[i].innerRadius).toBeCloseToPixel(0); + expect(meta.data[i].outerRadius).toBeCloseToPixel(expected.o); + expect(meta.data[i].startAngle).toBe(expected.s); + expect(meta.data[i].endAngle).toBe(expected.e); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 @@ -265,14 +265,14 @@ describe('Chart.controllers.polarArea', function() { var arc = chart.getDatasetMeta(0).data[0]; jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(49, 135, 221)'); - expect(arc._model.borderColor).toBe('rgb(22, 89, 156)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(arc.options.borderColor).toBe('rgb(22, 89, 156)'); + expect(arc.options.borderWidth).toBe(2); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via dataset properties', function() { @@ -288,14 +288,14 @@ describe('Chart.controllers.polarArea', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(arc._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(arc._model.borderWidth).toBe(8.4); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via element options', function() { @@ -311,14 +311,14 @@ describe('Chart.controllers.polarArea', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', arc); - expect(arc._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(arc._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(arc._model.borderWidth).toBe(8.4); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); jasmine.triggerMouseEvent(chart, 'mouseout', arc); - expect(arc._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(arc._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(arc._model.borderWidth).toBe(2); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); }); }); }); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index c93bc1b3501..c3031cefab1 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -118,7 +118,7 @@ describe('Chart.controllers.radar', function() { meta.controller.reset(); // reset first // Line element - expect(meta.dataset._model).toEqual(jasmine.objectContaining({ + expect(meta.dataset.options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderCapStyle: 'round', borderColor: 'rgb(0, 255, 0)', @@ -136,20 +136,19 @@ describe('Chart.controllers.radar', function() { {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, {x: 256, y: 260, cppx: 256, cppy: 260, cpnx: 256, cpny: 260}, ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model.controlPointPreviousX).toBeCloseToPixel(expected.cppx); - expect(meta.data[i]._model.controlPointPreviousY).toBeCloseToPixel(expected.cppy); - expect(meta.data[i]._model.controlPointNextX).toBeCloseToPixel(expected.cpnx); - expect(meta.data[i]._model.controlPointNextY).toBeCloseToPixel(expected.cpny); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].controlPointPreviousX).toBeCloseToPixel(expected.cppx); + expect(meta.data[i].controlPointPreviousY).toBeCloseToPixel(expected.cppy); + expect(meta.data[i].controlPointNextX).toBeCloseToPixel(expected.cpnx); + expect(meta.data[i].controlPointNextY).toBeCloseToPixel(expected.cpny); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.global.defaultColor, borderWidth: 1, borderColor: Chart.defaults.global.defaultColor, hitRadius: 1, radius: 3, pointStyle: 'circle', - skip: false, })); }); @@ -162,20 +161,19 @@ describe('Chart.controllers.radar', function() { {x: 256, y: 260, cppx: 277, cppy: 260, cpnx: 250, cpny: 260}, {x: 200, y: 260, cppx: 200, cppy: 264, cpnx: 200, cpny: 250}, ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model.controlPointPreviousX).toBeCloseToPixel(expected.cppx); - expect(meta.data[i]._model.controlPointPreviousY).toBeCloseToPixel(expected.cppy); - expect(meta.data[i]._model.controlPointNextX).toBeCloseToPixel(expected.cpnx); - expect(meta.data[i]._model.controlPointNextY).toBeCloseToPixel(expected.cpny); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].controlPointPreviousX).toBeCloseToPixel(expected.cppx); + expect(meta.data[i].controlPointPreviousY).toBeCloseToPixel(expected.cppy); + expect(meta.data[i].controlPointNextX).toBeCloseToPixel(expected.cpnx); + expect(meta.data[i].controlPointNextY).toBeCloseToPixel(expected.cpny); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.global.defaultColor, borderWidth: 1, borderColor: Chart.defaults.global.defaultColor, hitRadius: 1, radius: 3, pointStyle: 'circle', - skip: false, })); }); @@ -199,7 +197,7 @@ describe('Chart.controllers.radar', function() { meta.controller._update(); - expect(meta.dataset._model).toEqual(jasmine.objectContaining({ + expect(meta.dataset.options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(98, 98, 98)', borderCapStyle: 'butt', borderColor: 'rgb(8, 8, 8)', @@ -218,16 +216,15 @@ describe('Chart.controllers.radar', function() { {x: 256, y: 260}, {x: 200, y: 260}, ].forEach(function(expected, i) { - expect(meta.data[i]._model.x).toBeCloseToPixel(expected.x); - expect(meta.data[i]._model.y).toBeCloseToPixel(expected.y); - expect(meta.data[i]._model).toEqual(jasmine.objectContaining({ + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(128, 129, 130)', borderWidth: 1.123, borderColor: 'rgb(56, 57, 58)', hitRadius: 3.3, radius: 22, - pointStyle: 'circle', - skip: false, + pointStyle: 'circle' })); }); }); @@ -260,16 +257,16 @@ describe('Chart.controllers.radar', function() { var point = chart.getDatasetMeta(0).data[0]; jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(49, 135, 221)'); - expect(point._model.borderColor).toBe('rgb(22, 89, 156)'); - expect(point._model.borderWidth).toBe(1); - expect(point._model.radius).toBe(4); + expect(point.options.backgroundColor).toBe('rgb(49, 135, 221)'); + expect(point.options.borderColor).toBe('rgb(22, 89, 156)'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(4); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); it ('should handle hover styles defined via dataset properties', function() { @@ -286,16 +283,16 @@ describe('Chart.controllers.radar', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); it ('should handle hover styles defined via element options', function() { @@ -312,16 +309,16 @@ describe('Chart.controllers.radar', function() { chart.update(); jasmine.triggerMouseEvent(chart, 'mousemove', point); - expect(point._model.backgroundColor).toBe('rgb(200, 100, 150)'); - expect(point._model.borderColor).toBe('rgb(150, 50, 100)'); - expect(point._model.borderWidth).toBe(8.4); - expect(point._model.radius).toBe(4.2); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); jasmine.triggerMouseEvent(chart, 'mouseout', point); - expect(point._model.backgroundColor).toBe('rgb(100, 150, 200)'); - expect(point._model.borderColor).toBe('rgb(50, 100, 150)'); - expect(point._model.borderWidth).toBe(2); - expect(point._model.radius).toBe(3); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); }); }); @@ -339,7 +336,7 @@ describe('Chart.controllers.radar', function() { var meta = chart.getDatasetMeta(0); var point = meta.data[0]; - expect(point._model.borderWidth).toBe(0); + expect(point.options.borderWidth).toBe(0); }); it('should use the pointRadius setting over the radius setting', function() { @@ -360,8 +357,8 @@ describe('Chart.controllers.radar', function() { var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); - expect(meta0.data[0]._model.radius).toBe(10); - expect(meta1.data[0]._model.radius).toBe(20); + expect(meta0.data[0].options.radius).toBe(10); + expect(meta1.data[0].options.radius).toBe(20); }); it('should return id for value scale', function() { diff --git a/test/specs/controller.scatter.test.js b/test/specs/controller.scatter.tests.js similarity index 92% rename from test/specs/controller.scatter.test.js rename to test/specs/controller.scatter.tests.js index a2744b9e437..45f4bf08c2f 100644 --- a/test/specs/controller.scatter.test.js +++ b/test/specs/controller.scatter.tests.js @@ -21,8 +21,8 @@ describe('Chart.controllers.scatter', function() { jasmine.triggerMouseEvent(chart, 'mousemove', point); // Title should be empty - expect(chart.tooltip._view.title.length).toBe(0); - expect(chart.tooltip._view.body[0].lines).toEqual(['(10, 15)']); + expect(chart.tooltip.title.length).toBe(0); + expect(chart.tooltip.body[0].lines).toEqual(['(10, 15)']); }); describe('showLines option', function() { diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index c0528ccf985..430be6e1455 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -950,18 +950,18 @@ describe('Chart', function() { // Verify that points are at their initial correct location, // then we will reset and see that they moved - expect(meta.data[0]._model.y).toBeCloseToPixel(333); - expect(meta.data[1]._model.y).toBeCloseToPixel(183); - expect(meta.data[2]._model.y).toBeCloseToPixel(32); - expect(meta.data[3]._model.y).toBeCloseToPixel(482); + expect(meta.data[0].y).toBeCloseToPixel(333); + expect(meta.data[1].y).toBeCloseToPixel(183); + expect(meta.data[2].y).toBeCloseToPixel(32); + expect(meta.data[3].y).toBeCloseToPixel(482); chart.reset(); // For a line chart, the animation state is the bottom - expect(meta.data[0]._model.y).toBeCloseToPixel(482); - expect(meta.data[1]._model.y).toBeCloseToPixel(482); - expect(meta.data[2]._model.y).toBeCloseToPixel(482); - expect(meta.data[3]._model.y).toBeCloseToPixel(482); + expect(meta.data[0].y).toBeCloseToPixel(482); + expect(meta.data[1].y).toBeCloseToPixel(482); + expect(meta.data[2].y).toBeCloseToPixel(482); + expect(meta.data[3].y).toBeCloseToPixel(482); }); }); @@ -1106,7 +1106,7 @@ describe('Chart', function() { chart.options.tooltips = newTooltipConfig; chart.update(); - expect(chart.tooltip._options).toEqual(jasmine.objectContaining(newTooltipConfig)); + expect(chart.tooltip.options).toEqual(jasmine.objectContaining(newTooltipConfig)); }); it ('should update the tooltip on update', function() { @@ -1283,46 +1283,4 @@ describe('Chart', function() { ]); }); }); - - describe('controller.update', function() { - beforeEach(function() { - this.chart = acquireChart({ - type: 'doughnut', - options: { - animation: { - easing: 'linear', - duration: 500 - } - } - }); - - this.addAnimationSpy = spyOn(Chart.animationService, 'addAnimation'); - }); - - it('should add an animation with the default options', function() { - this.chart.update(); - - expect(this.addAnimationSpy).toHaveBeenCalledWith( - this.chart, - jasmine.objectContaining({easing: 'linear'}), - 500, - undefined - ); - }); - - it('should add an animation with the provided options', function() { - this.chart.update({ - duration: 800, - easing: 'easeOutBounce', - lazy: false, - }); - - expect(this.addAnimationSpy).toHaveBeenCalledWith( - this.chart, - jasmine.objectContaining({easing: 'easeOutBounce'}), - 800, - false - ); - }); - }); }); diff --git a/test/specs/core.element.tests.js b/test/specs/core.element.tests.js deleted file mode 100644 index 8b72a0ccc5a..00000000000 --- a/test/specs/core.element.tests.js +++ /dev/null @@ -1,51 +0,0 @@ -// Test the core element functionality -describe('Core element tests', function() { - it ('should transition model properties', function() { - var element = new Chart.Element({ - _model: { - numberProp: 0, - numberProp2: 100, - _underscoreProp: 0, - stringProp: 'abc', - objectProp: { - myObject: true - }, - colorProp: 'rgb(0, 0, 0)' - } - }); - - // First transition clones model into view - element.transition(0.25); - - expect(element._view).toEqual(element._model); - expect(element._view).not.toBe(element._model); - expect(element._view.objectProp).toBe(element._model.objectProp); // not cloned - - element._model.numberProp = 100; - element._model.numberProp2 = 250; - element._model._underscoreProp = 200; - element._model.stringProp = 'def'; - element._model.newStringProp = 'newString'; - element._model.colorProp = 'rgb(255, 255, 0)'; - - element.transition(0.25); - - expect(element._view).toEqual({ - numberProp: 25, - numberProp2: 137.5, - _underscoreProp: 0, // underscore props are not transition to a new value - stringProp: 'def', - newStringProp: 'newString', - objectProp: { - myObject: true - }, - colorProp: 'rgb(64, 64, 0)', - }); - - // Final transition clones model into view - element.transition(1); - - expect(element._view).toEqual(element._model); - expect(element._view).not.toBe(element._model); - }); -}); diff --git a/test/specs/core.interaction.tests.js b/test/specs/core.interaction.tests.js index 86eabf44e85..8eae2e9d780 100644 --- a/test/specs/core.interaction.tests.js +++ b/test/specs/core.interaction.tests.js @@ -33,8 +33,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.point(chart, evt, {}).map(item => 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 f0ad7441e03..4b8da46e454 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 41c720d9b87..bc47a2752e3 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 199998dd039..6d14fe8ce9b 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 686eea9324b..e85d7f2027c 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 e1188458a91..4da3d4ae18b 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 fea8adcccec..d0f4e7115f6 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 880a72c6532..671e3af2c48 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 b082a2f03ca..e2d3e7a2877 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; From eb7c2f35a1fc6f900a8aa65db739ec5900c4c005 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Thu, 19 Dec 2019 08:55:43 +0200 Subject: [PATCH 2/5] Review update 1 --- docs/configuration/animations.md | 41 ++++--- docs/getting-started/v3-migration.md | 7 +- samples/animations/delay.html | 122 +++++++++++++++++++ samples/animations/drop.html | 175 +++++++++++++++++++++++++++ samples/samples.js | 12 ++ src/controllers/controller.bar.js | 2 +- src/core/core.animations.js | 16 ++- src/core/core.animator.js | 30 +++-- src/core/core.controller.js | 35 +++--- src/core/core.datasetController.js | 29 +++-- 10 files changed, 407 insertions(+), 62 deletions(-) create mode 100644 samples/animations/delay.html create mode 100644 samples/animations/drop.html diff --git a/docs/configuration/animations.md b/docs/configuration/animations.md index 40dd8ec3ff7..e410c4522a8 100644 --- a/docs/configuration/animations.md +++ b/docs/configuration/animations.md @@ -11,11 +11,31 @@ 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) | `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 +72,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/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index c2f854a90de..daa140c2500 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -49,6 +49,10 @@ 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. + ## Developer migration ### Removed @@ -105,7 +109,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` @@ -126,10 +129,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 00000000000..38f24c8bbf1 --- /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 00000000000..c4db7ae9b87 --- /dev/null +++ b/samples/animations/drop.html @@ -0,0 +1,175 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/samples/samples.js b/samples/samples.js index d1cae567280..1c22f0234a7 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,15 @@ title: 'Radar Chart', path: 'scriptable/radar.html' }] + }, { + title: 'Animations', + items: [{ + title: 'Delay', + path: 'animations/delay.html' + }, { + title: 'Drop', + path: 'animations/drop.html' + }] }, { title: 'Advanced', items: [{ diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 42eec8086dd..7755cd14ff7 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -34,7 +34,7 @@ defaults._set('global', { animation: { numbers: { type: 'number', - properties: ['x', 'y', 'width', 'height'] + properties: ['x', 'y', 'base', 'width', 'height'] } } } diff --git a/src/core/core.animations.js b/src/core/core.animations.js index f3c879550a8..86b022af9a5 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -17,7 +17,11 @@ defaults._set('global', { }, numbers: { type: 'number', - properties: ['x', 'y', 'borderWidth', 'radius'] + properties: ['x', 'y', 'borderWidth', 'radius', 'tension'] + }, + colors: { + type: 'color', + properties: ['borderColor', 'backgroundColor'] }, onProgress: helpers.noop, onComplete: helpers.noop @@ -66,8 +70,11 @@ class Animations { } /** + * 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 share options after all animations have completed + * @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; @@ -79,6 +86,8 @@ class Animations { 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); @@ -132,12 +141,15 @@ class Animations { * 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; } diff --git a/src/core/core.animator.js b/src/core/core.animator.js index 5178c1c8a4d..39223968442 100644 --- a/src/core/core.animator.js +++ b/src/core/core.animator.js @@ -26,9 +26,15 @@ class Animator { /** * @private */ - _notify(anims, type, args) { - const callbacks = anims._listeners[type] || []; - callbacks.forEach(fn => fn(args || [])); + _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 + })); } /** @@ -56,8 +62,9 @@ class Animator { * @private */ _update() { + const me = this; const date = Date.now(); - const charts = this._charts; + const charts = me._charts; let remaining = 0; for (let [chart, anims] of charts) { @@ -86,13 +93,15 @@ class Animator { if (draw) { chart.draw(); if (chart.options.animation.fps) { - drawFPS(chart, items.length, date, this._lastDate); + drawFPS(chart, items.length, date, me._lastDate); } } + me._notify(chart, anims, date, 'progress'); + if (!items.length) { anims.running = false; - this._notify(chart, 'complete'); + me._notify(chart, anims, date, 'complete'); } remaining += items.length; @@ -112,7 +121,10 @@ class Animator { anims = { running: false, items: [], - listeners: {complete: []} + listeners: { + complete: [], + progress: [] + } }; charts.set(chart, anims); } @@ -158,6 +170,8 @@ class Animator { return; } anims.running = true; + anims.start = Date.now(); + anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); this._refresh(); } @@ -188,7 +202,7 @@ class Animator { items[i].cancel(); } anims.items = []; - this._notify(chart, 'complete'); + this._notify(chart, anims, Date.now(), 'complete'); } } diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 95f4c65023e..03564844003 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -151,6 +151,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; @@ -203,20 +217,13 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { return; } - Animator.listen(me, 'complete', me._onAnimationsComplete); + Animator.listen(me, 'complete', onAnimationsComplete); + Animator.listen(me, 'progress', onAnimationProgress); me.initialize(); me.update(); }, - _onAnimationsComplete: function() { - const me = this; - const animationOptions = me.options.animation; - - plugins.notify(me, 'afterRender'); - helpers.callback(animationOptions && animationOptions.onComplete, [], me); - }, - /** * @private */ @@ -468,9 +475,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(); @@ -490,13 +494,6 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { me.updateDatasets(mode); - // 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 = []; - // Do this before render so that any plugins that need final scale updates can use it plugins.notify(me, 'afterUpdate'); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index fbabf43344b..b1d8005bfb4 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -943,16 +943,18 @@ helpers.extend(DatasetController.prototype, { }, /** + * Utility for checking if the options are shared and should be animated separately. * @private */ _getSharedOptions: function(mode, element, options) { if (mode !== 'reset' && options && options.$shared && - element && element.options && element.options.$shared) { + element && element.options && element.options.$shared) { return {target: element.options, options}; } }, /** + * Utility for determining if `options` should be included in the updated properties * @private */ _includeOptions: function(mode, sharedOptions) { @@ -960,6 +962,7 @@ helpers.extend(DatasetController.prototype, { }, /** + * Utility for updating a element with new properties, using animations when appropriate. * @private */ _updateElement: function(element, index, properties, mode) { @@ -971,6 +974,7 @@ helpers.extend(DatasetController.prototype, { }, /** + * Utility to animate the shared options, that are potentially affecting multiple elements. * @private */ _updateSharedOptions: function(sharedOptions, mode) { @@ -982,16 +986,16 @@ helpers.extend(DatasetController.prototype, { /** * @private */ - _setStyle(element, index, active) { - this._resolveAnimations(index, active && 'active').update(element, {options: this.getStyle(index, active)}); + _setStyle(element, index, mode, active) { + this._resolveAnimations(index, mode).update(element, {options: this.getStyle(index, active)}); }, removeHoverStyle: function(element, datasetIndex, index) { - this._setStyle(element, index, false); + this._setStyle(element, index, 'active', false); }, setHoverStyle: function(element, datasetIndex, index) { - this._setStyle(element, index, true); + this._setStyle(element, index, 'active', true); }, /** @@ -1000,25 +1004,20 @@ helpers.extend(DatasetController.prototype, { _removeDatasetHoverStyle: function() { const element = this._cachedMeta.dataset; - if (!element) { - return; + if (element) { + this._setStyle(element, undefined, 'active', false); } - - this._setStyle(element, undefined, false); }, /** * @private */ _setDatasetHoverStyle: function() { - const me = this; - const element = me._cachedMeta.dataset; + const element = this._cachedMeta.dataset; - if (!element) { - return; + if (element) { + this._setStyle(element, undefined, 'active', true); } - - this._setStyle(element, undefined, true); }, /** From 64444102bd454780c42d4caef03799a1a0fbb3e9 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sun, 22 Dec 2019 19:23:33 +0200 Subject: [PATCH 3/5] Review update 2 --- docs/developers/api.md | 32 +---- 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 | 3 + samples/animations/loop.html | 181 +++++++++++++++++++++++++++ samples/charts/bubble.html | 3 - samples/samples.js | 3 + src/core/core.datasetController.js | 8 +- src/core/core.tooltip.js | 1 - test/specs/core.controller.tests.js | 7 -- 12 files changed, 197 insertions(+), 53 deletions(-) create mode 100644 samples/animations/loop.html diff --git a/docs/developers/api.md b/docs/developers/api.md index 7c7b8682444..77aed8233be 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -17,32 +17,22 @@ 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'`. Example: ```javascript -myChart.update({ - duration: 800, - easing: 'easeOutBounce' -}); +myChart.update(); ``` See [Updating Charts](updates.md) for more details. @@ -55,25 +45,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 diff --git a/docs/developers/updates.md b/docs/developers/updates.md index e50b26004a5..8b7da7b0ed6 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 9b5ad6d3271..be5ec8e239d 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 8c0dc11c78e..1d970a6f9bb 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 319709a5a16..81569476bb0 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 daa140c2500..d3baf1a3422 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -53,6 +53,9 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released 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 diff --git a/samples/animations/loop.html b/samples/animations/loop.html new file mode 100644 index 00000000000..15d743a85d3 --- /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 b604a5ac12e..19cae60bb7c 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 1c22f0234a7..d95a3fe7e4f 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -220,6 +220,9 @@ }, { title: 'Drop', path: 'animations/drop.html' + }, { + title: 'Loop', + path: 'animations/loop.html' }] }, { title: 'Advanced', diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index b1d8005bfb4..c215e6610de 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -908,12 +908,10 @@ helpers.extend(DatasetController.prototype, { /** * @private */ - _resolveAnimations: function(index, mode) { + _resolveAnimations: function(index, mode, active) { const me = this; const chart = me.chart; const cached = me._cachedAnimations; - const active = mode === 'active'; - mode = mode || 'default'; if (cached[mode]) { @@ -966,7 +964,7 @@ helpers.extend(DatasetController.prototype, { * @private */ _updateElement: function(element, index, properties, mode) { - if (mode === 'reset') { + if (mode === 'reset' || mode === 'none') { helpers.extend(element, properties); } else { this._resolveAnimations(index, mode).update(element, properties); @@ -987,7 +985,7 @@ helpers.extend(DatasetController.prototype, { * @private */ _setStyle(element, index, mode, active) { - this._resolveAnimations(index, mode).update(element, {options: this.getStyle(index, active)}); + this._resolveAnimations(index, mode, active).update(element, {options: this.getStyle(index, active)}); }, removeHoverStyle: function(element, datasetIndex, index) { diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 8caa62fa773..eac403f9029 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -462,7 +462,6 @@ class Tooltip extends Element { initialize() { const me = this; me.options = resolveOptions(me._chart.options.tooltips); - // me.update(); } /** diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js index 430be6e1455..cc707f26bd3 100644 --- a/test/specs/core.controller.tests.js +++ b/test/specs/core.controller.tests.js @@ -66,7 +66,6 @@ describe('Chart', function() { var callback = function() {}; var defaults = Chart.defaults; - defaults.global.responsiveAnimationDuration = 42; defaults.global.hover.onHover = callback; defaults.line.spanGaps = true; defaults.line.hover.mode = 'x-axis'; @@ -79,11 +78,9 @@ describe('Chart', function() { expect(options.defaultFontSize).toBe(defaults.global.defaultFontSize); expect(options.showLines).toBe(defaults.line.showLines); expect(options.spanGaps).toBe(true); - expect(options.responsiveAnimationDuration).toBe(42); expect(options.hover.onHover).toBe(callback); expect(options.hover.mode).toBe('x-axis'); - defaults.global.responsiveAnimationDuration = 0; defaults.global.hover.onHover = null; defaults.line.spanGaps = false; defaults.line.hover.mode = 'index'; @@ -93,7 +90,6 @@ describe('Chart', function() { var callback = function() {}; var defaults = Chart.defaults; - defaults.global.responsiveAnimationDuration = 42; defaults.global.hover.onHover = callback; defaults.line.hover.mode = 'x-axis'; defaults.line.spanGaps = true; @@ -101,7 +97,6 @@ describe('Chart', function() { var chart = acquireChart({ type: 'line', options: { - responsiveAnimationDuration: 4242, spanGaps: false, hover: { mode: 'dataset', @@ -113,13 +108,11 @@ describe('Chart', function() { }); var options = chart.options; - expect(options.responsiveAnimationDuration).toBe(4242); expect(options.showLines).toBe(defaults.global.showLines); expect(options.spanGaps).toBe(false); expect(options.hover.mode).toBe('dataset'); expect(options.title.position).toBe('bottom'); - defaults.global.responsiveAnimationDuration = 0; defaults.global.hover.onHover = null; defaults.line.hover.mode = 'index'; defaults.line.spanGaps = false; From 1858a6d4f44376038d4e8f609359d9c1a9ece9db Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 24 Dec 2019 10:48:32 +0200 Subject: [PATCH 4/5] Review update 3 --- docs/configuration/animations.md | 1 + src/controllers/controller.line.js | 2 +- src/core/core.animator.js | 2 +- src/core/core.controller.js | 2 +- src/core/core.datasetController.js | 13 +++++++++---- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/configuration/animations.md b/docs/configuration/animations.md index e410c4522a8..8131941d7e7 100644 --- a/docs/configuration/animations.md +++ b/docs/configuration/animations.md @@ -10,6 +10,7 @@ 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 when all animations are completed. [more...](#animation-callbacks) | `delay` | `number` | `undefined` | Delay before starting the animations. diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 918aa646b2f..5277d9cb021 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -70,7 +70,7 @@ module.exports = DatasetController.extend({ const me = this; const meta = me._cachedMeta; const line = meta.dataset; - const points = meta.data || (meta.data = []); + const points = meta.data || []; const options = me.chart.options; const config = me._config; const showLine = me._showLine = valueOrDefault(config.showLine, options.showLines); diff --git a/src/core/core.animator.js b/src/core/core.animator.js index 39223968442..29422d5ba85 100644 --- a/src/core/core.animator.js +++ b/src/core/core.animator.js @@ -92,7 +92,7 @@ class Animator { if (draw) { chart.draw(); - if (chart.options.animation.fps) { + if (chart.options.animation.debug) { drawFPS(chart, items.length, date, me._lastDate); } } diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 03564844003..d9e9894a231 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -982,7 +982,7 @@ helpers.extend(Chart.prototype, /** @lends Chart */ { // Invoke onHover hook // Need to call with native event here to not break backwards compatibility - // helpers.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); + helpers.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); if (e.type === 'mouseup' || e.type === 'click') { if (options.onClick && helpers.canvas._isPointInArea(e, me.chartArea)) { diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index c215e6610de..31fec16cc13 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -898,7 +898,13 @@ helpers.extend(DatasetController.prototype, { } if (info.cacheable) { + // `$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; } @@ -944,10 +950,9 @@ helpers.extend(DatasetController.prototype, { * Utility for checking if the options are shared and should be animated separately. * @private */ - _getSharedOptions: function(mode, element, options) { - if (mode !== 'reset' && options && options.$shared && - element && element.options && element.options.$shared) { - return {target: element.options, options}; + _getSharedOptions: function(mode, el, options) { + if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) { + return {target: el.options, options}; } }, From 30d47598b8b1f608abf33bb6c5ab884a8ab1b09e Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Fri, 27 Dec 2019 09:56:53 +0200 Subject: [PATCH 5/5] Add 'none' to api.md --- docs/developers/api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developers/api.md b/docs/developers/api.md index 77aed8233be..01b10801c34 100644 --- a/docs/developers/api.md +++ b/docs/developers/api.md @@ -28,9 +28,10 @@ myLineChart.update(); // Calling update now animates the position of March from > **Note:** replacing the data reference (e.g. `myLineChart.data = {datasets: [...]}` only works starting **version 2.6**. Prior that, replacing the entire data object could be achieved with the following workaround: `myLineChart.config.data = {datasets: [...]}`. -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'`. +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(); ```