]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Issue 4991 (#7084)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 25 Feb 2020 19:35:32 +0000 (21:35 +0200)
committerGitHub <noreply@github.com>
Tue, 25 Feb 2020 19:35:32 +0000 (14:35 -0500)
* Fix remaining handleEvent issues

* Reduce lines

* Update tooltip always on replay

* Address issues

* Fix test

* More tooltip fixing

* Extend comment

src/core/core.controller.js
src/core/core.element.js
src/core/core.interaction.js
src/core/core.plugins.js
src/elements/element.arc.js
src/elements/element.point.js
src/elements/element.rectangle.js
src/plugins/plugin.tooltip.js
test/specs/element.arc.tests.js
test/specs/element.point.tests.js
test/specs/global.defaults.tests.js

index 03483718488b02b534180d4d4dbbe64634e3fdf6..5f8ef2f814a10721109d7d7990cbbaca415f671c 100644 (file)
@@ -212,7 +212,7 @@ export default class Chart {
                this.chartArea = undefined;
                this.data = undefined;
                this.active = undefined;
-               this.lastActive = undefined;
+               this.lastActive = [];
                this._lastEvent = undefined;
                /** @type {{resize?: function}} */
                this._listeners = {};
@@ -581,7 +581,7 @@ export default class Chart {
 
                // Replay last event from before update
                if (me._lastEvent) {
-                       me._eventHandler(me._lastEvent);
+                       me._eventHandler(me._lastEvent, true);
                }
 
                me.render();
@@ -808,10 +808,10 @@ export default class Chart {
                return Interaction.modes.index(this, e, {intersect: false});
        }
 
-       getElementsAtEventForMode(e, mode, options) {
+       getElementsAtEventForMode(e, mode, options, useFinalPosition) {
                const method = Interaction.modes[mode];
                if (typeof method === 'function') {
-                       return method(this, e, options);
+                       return method(this, e, options, useFinalPosition);
                }
 
                return [];
@@ -1021,16 +1021,16 @@ export default class Chart {
        /**
         * @private
         */
-       _eventHandler(e) {
+       _eventHandler(e, replay) {
                const me = this;
 
-               if (plugins.notify(me, 'beforeEvent', [e]) === false) {
+               if (plugins.notify(me, 'beforeEvent', [e, replay]) === false) {
                        return;
                }
 
-               me._handleEvent(e);
+               me._handleEvent(e, replay);
 
-               plugins.notify(me, 'afterEvent', [e]);
+               plugins.notify(me, 'afterEvent', [e, replay]);
 
                me.render();
 
@@ -1040,23 +1040,38 @@ export default class Chart {
        /**
         * Handle an event
         * @param {IEvent} e the event to handle
+        * @param {boolean} [replay] - true if the event was replayed by `update`
         * @return {boolean} true if the chart needs to re-render
         * @private
         */
-       _handleEvent(e) {
+       _handleEvent(e, replay) {
                const me = this;
-               const options = me.options || {};
+               const options = me.options;
                const hoverOptions = options.hover;
-               let changed = false;
 
-               me.lastActive = me.lastActive || [];
+               // If the event is replayed from `update`, we should evaluate with the final positions.
+               //
+               // The `replay`:
+               // It's the last event (excluding click) that has occured before `update`.
+               // So mouse has not moved. It's also over the chart, because there is a `replay`.
+               //
+               // The why:
+               // If animations are active, the elements haven't moved yet compared to state before update.
+               // But if they will, we are activating the elements that would be active, if this check
+               // was done after the animations have completed. => "final positions".
+               // If there is no animations, the "final" and "current" positions are equal.
+               // This is done so we do not have to evaluate the active elements each animation frame
+               // - it would be expensive.
+               const useFinalPosition = replay;
+
+               let changed = false;
 
                // Find Active Elements for hover and tooltips
                if (e.type === 'mouseout') {
                        me.active = [];
                        me._lastEvent = null;
                } else {
-                       me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
+                       me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition);
                        me._lastEvent = e.type === 'click' ? me._lastEvent : e;
                }
 
@@ -1072,7 +1087,7 @@ export default class Chart {
                }
 
                changed = !helpers._elementsEqual(me.active, me.lastActive);
-               if (changed) {
+               if (changed || replay) {
                        me._updateHoverStyles();
                }
 
index 54f4d77e978c7255649de3cb12f4e52b4b70919a..db4ebaa639bf3c3959827a2480238848d757f8ee 100644 (file)
@@ -5,27 +5,48 @@ export default class Element {
 
        static extend = inherits;
 
-       /**
-        * @param {object} [cfg] optional configuration
-        */
        constructor(cfg) {
                this.x = undefined;
                this.y = undefined;
-               this.hidden = undefined;
+               this.hidden = false;
+               this.active = false;
+               this.options = undefined;
+               this.$animations = undefined;
 
                if (cfg) {
                        Object.assign(this, cfg);
                }
        }
 
-       tooltipPosition() {
-               return {
-                       x: this.x,
-                       y: this.y
-               };
+       /**
+        * @param {boolean} [useFinalPosition]
+        */
+       tooltipPosition(useFinalPosition) {
+               const {x, y} = this.getProps(['x', 'y'], useFinalPosition);
+               return {x, y};
        }
 
        hasValue() {
                return isNumber(this.x) && isNumber(this.y);
        }
+
+       /**
+        * Gets the current or final value of each prop. Can return extra properties (whole object).
+        * @param {string[]} props - properties to get
+        * @param {boolean} [final] - get the final value (animation target)
+        * @return {object}
+        */
+       getProps(props, final) {
+               const me = this;
+               const anims = this.$animations;
+               if (!final || !anims) {
+                       // let's not create an object, if not needed
+                       return me;
+               }
+               const ret = {};
+               props.forEach(prop => {
+                       ret[prop] = anims[prop] && anims[prop].active ? anims[prop]._to : me[prop];
+               });
+               return ret;
+       }
 }
index b296db031fedcec628bfbec7edf61abfc861dbbe..6980111663e27dc6b88439f2ee8bc059f4469ed3 100644 (file)
@@ -5,7 +5,8 @@ import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection';
 /**
  * @typedef { import("./core.controller").default } Chart
  * @typedef { import("../platform/platform.base").IEvent } IEvent
- * @typedef {{axis?:'x'|'y'|'xy', intersect:boolean}} IInteractionOptions
+ * @typedef {{axis?: string, intersect?: boolean}} InteractionOptions
+ * @typedef {{datasetIndex: number, index: number, element: import("../core/core.element").default}} InteractionItem
  */
 
 /**
@@ -121,9 +122,10 @@ function getDistanceMetricForAxis(axis) {
  * @param {Chart} chart - the chart
  * @param {object} position - the point to be nearest to
  * @param {string} axis - the axis mode. x|y|xy
- * @return {object[]} the nearest items
+ * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
+ * @return {InteractionItem[]} the nearest items
  */
-function getIntersectItems(chart, position, axis) {
+function getIntersectItems(chart, position, axis, useFinalPosition) {
        const items = [];
 
        if (!_isPointInArea(position, chart.chartArea)) {
@@ -131,7 +133,7 @@ function getIntersectItems(chart, position, axis) {
        }
 
        const evaluationFunc = function(element, datasetIndex, index) {
-               if (element.inRange(position.x, position.y)) {
+               if (element.inRange(position.x, position.y, useFinalPosition)) {
                        items.push({element, datasetIndex, index});
                }
        };
@@ -146,9 +148,10 @@ function getIntersectItems(chart, position, axis) {
  * @param {object} position - the point to be nearest to
  * @param {string} axis - the axes along which to measure distance
  * @param {boolean} [intersect] - if true, only consider items that intersect the position
- * @return {object[]} the nearest items
+ * @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
+ * @return {InteractionItem[]} the nearest items
  */
-function getNearestItems(chart, position, axis, intersect) {
+function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
        const distanceMetric = getDistanceMetricForAxis(axis);
        let minDistance = Number.POSITIVE_INFINITY;
        let items = [];
@@ -158,11 +161,11 @@ function getNearestItems(chart, position, axis, intersect) {
        }
 
        const evaluationFunc = function(element, datasetIndex, index) {
-               if (intersect && !element.inRange(position.x, position.y)) {
+               if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) {
                        return;
                }
 
-               const center = element.getCenterPoint();
+               const center = element.getCenterPoint(useFinalPosition);
                const distance = distanceMetric(position, center);
                if (distance < minDistance) {
                        items = [{element, datasetIndex, index}];
@@ -191,14 +194,17 @@ export default {
                 * @since v2.4.0
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use during interaction
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               index(chart, e, options) {
+               index(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        // Default axis for index mode is 'x' to match old behaviour
                        const axis = options.axis || 'x';
-                       const items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
+                       const items = options.intersect
+                               ? getIntersectItems(chart, position, axis, useFinalPosition)
+                               : getNearestItems(chart, position, axis, false, useFinalPosition);
                        const elements = [];
 
                        if (!items.length) {
@@ -224,13 +230,16 @@ export default {
                 * @function Chart.Interaction.modes.dataset
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use during interaction
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               dataset(chart, e, options) {
+               dataset(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        const axis = options.axis || 'xy';
-                       let items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
+                       let items = options.intersect
+                               ? getIntersectItems(chart, position, axis, useFinalPosition) :
+                               getNearestItems(chart, position, axis, false, useFinalPosition);
 
                        if (items.length > 0) {
                                const datasetIndex = items[0].datasetIndex;
@@ -250,13 +259,14 @@ export default {
                 * @function Chart.Interaction.modes.intersect
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               point(chart, e, options) {
+               point(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        const axis = options.axis || 'xy';
-                       return getIntersectItems(chart, position, axis);
+                       return getIntersectItems(chart, position, axis, useFinalPosition);
                },
 
                /**
@@ -264,13 +274,14 @@ export default {
                 * @function Chart.Interaction.modes.intersect
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               nearest(chart, e, options) {
+               nearest(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        const axis = options.axis || 'xy';
-                       return getNearestItems(chart, position, axis, options.intersect);
+                       return getNearestItems(chart, position, axis, options.intersect, useFinalPosition);
                },
 
                /**
@@ -278,20 +289,21 @@ export default {
                 * @function Chart.Interaction.modes.x
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               x(chart, e, options) {
+               x(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        const items = [];
                        let intersectsItem = false;
 
                        evaluateAllVisibleItems(chart, (element, datasetIndex, index) => {
-                               if (element.inXRange(position.x)) {
+                               if (element.inXRange(position.x, useFinalPosition)) {
                                        items.push({element, datasetIndex, index});
                                }
 
-                               if (element.inRange(position.x, position.y)) {
+                               if (element.inRange(position.x, position.y, useFinalPosition)) {
                                        intersectsItem = true;
                                }
                        });
@@ -309,20 +321,21 @@ export default {
                 * @function Chart.Interaction.modes.y
                 * @param {Chart} chart - the chart we are returning items from
                 * @param {Event} e - the event we are find things at
-                * @param {IInteractionOptions} options - options to use
-                * @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
+                * @param {InteractionOptions} options - options to use
+                * @param {boolean} [useFinalPosition] - use final element position (animation target)
+                * @return {InteractionItem[]} - items that are found
                 */
-               y(chart, e, options) {
+               y(chart, e, options, useFinalPosition) {
                        const position = getRelativePosition(e, chart);
                        const items = [];
                        let intersectsItem = false;
 
                        evaluateAllVisibleItems(chart, (element, datasetIndex, index) => {
-                               if (element.inYRange(position.y)) {
+                               if (element.inYRange(position.y, useFinalPosition)) {
                                        items.push({element, datasetIndex, index});
                                }
 
-                               if (element.inRange(position.x, position.y)) {
+                               if (element.inRange(position.x, position.y, useFinalPosition)) {
                                        intersectsItem = true;
                                }
                        });
index b38e9db66ee1da4705347797e2914286b4894499..adad43d8c6315f14971b9898ae107d677814c177 100644 (file)
@@ -365,6 +365,7 @@ export default new PluginService();
  * @param {Chart} chart - The chart instance.
  * @param {IEvent} event - The event object.
  * @param {object} options - The plugin options.
+ * @param {boolean} replay - True if this event is replayed from `Chart.update`
  */
 /**
  * @method IPlugin#afterEvent
@@ -373,6 +374,7 @@ export default new PluginService();
  * @param {Chart} chart - The chart instance.
  * @param {IEvent} event - The event object.
  * @param {object} options - The plugin options.
+ * @param {boolean} replay - True if this event is replayed from `Chart.update`
  */
 /**
  * @method IPlugin#resize
index 8c972b5216db986cf6d881d6c3ea38b2321f9aa4..88afb7757b05301edb7b159ad6ab53973425da77 100644 (file)
@@ -106,38 +106,49 @@ export default class Arc extends Element {
        /**
         * @param {number} chartX
         * @param {number} chartY
+        * @param {boolean} [useFinalPosition]
         */
-       inRange(chartX, chartY) {
-               const me = this;
-
-               const {angle, distance} = getAngleFromPoint(me, {x: chartX, y: chartY});
-
-               // Check if within the range of the open/close angle
-               const betweenAngles = _angleBetween(angle, me.startAngle, me.endAngle);
-               const withinRadius = (distance >= me.innerRadius && distance <= me.outerRadius);
+       inRange(chartX, chartY, useFinalPosition) {
+               const point = this.getProps(['x', 'y'], useFinalPosition);
+               const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY});
+               const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([
+                       'startAngle',
+                       'endAngle',
+                       'innerRadius',
+                       'outerRadius',
+                       'circumference'
+               ], useFinalPosition);
+               const betweenAngles = circumference >= TAU || _angleBetween(angle, startAngle, endAngle);
+               const withinRadius = (distance >= innerRadius && distance <= outerRadius);
 
                return (betweenAngles && withinRadius);
        }
 
-       getCenterPoint() {
-               const me = this;
-               const halfAngle = (me.startAngle + me.endAngle) / 2;
-               const halfRadius = (me.innerRadius + me.outerRadius) / 2;
+       /**
+        * @param {boolean} [useFinalPosition]
+        */
+       getCenterPoint(useFinalPosition) {
+               const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([
+                       'x',
+                       'y',
+                       'startAngle',
+                       'endAngle',
+                       'innerRadius',
+                       'outerRadius'
+               ], useFinalPosition);
+               const halfAngle = (startAngle + endAngle) / 2;
+               const halfRadius = (innerRadius + outerRadius) / 2;
                return {
-                       x: me.x + Math.cos(halfAngle) * halfRadius,
-                       y: me.y + Math.sin(halfAngle) * halfRadius
+                       x: x + Math.cos(halfAngle) * halfRadius,
+                       y: y + Math.sin(halfAngle) * halfRadius
                };
        }
 
-       tooltipPosition() {
-               const me = this;
-               const centreAngle = me.startAngle + ((me.endAngle - me.startAngle) / 2);
-               const rangeFromCentre = (me.outerRadius - me.innerRadius) / 2 + me.innerRadius;
-
-               return {
-                       x: me.x + (Math.cos(centreAngle) * rangeFromCentre),
-                       y: me.y + (Math.sin(centreAngle) * rangeFromCentre)
-               };
+       /**
+        * @param {boolean} [useFinalPosition]
+        */
+       tooltipPosition(useFinalPosition) {
+               return this.getCenterPoint(useFinalPosition);
        }
 
        draw(ctx) {
index 6fc760f067a1cc57761860d5c4cffe14b93e28cd..8ca3bcbc03d326fadc06afaf0cb81854b3bb5a83 100644 (file)
@@ -34,23 +34,28 @@ export default class Point extends Element {
                }
        }
 
-       inRange(mouseX, mouseY) {
+       inRange(mouseX, mouseY, useFinalPosition) {
                const options = this.options;
-               return ((Math.pow(mouseX - this.x, 2) + Math.pow(mouseY - this.y, 2)) < Math.pow(options.hitRadius + options.radius, 2));
+               const {x, y} = this.getProps(['x', 'y'], useFinalPosition);
+               return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2));
        }
 
-       inXRange(mouseX) {
+       inXRange(mouseX, useFinalPosition) {
                const options = this.options;
-               return (Math.abs(mouseX - this.x) < options.radius + options.hitRadius);
+               const {x} = this.getProps(['x'], useFinalPosition);
+
+               return (Math.abs(mouseX - x) < options.radius + options.hitRadius);
        }
 
-       inYRange(mouseY) {
+       inYRange(mouseY, useFinalPosition) {
                const options = this.options;
-               return (Math.abs(mouseY - this.y) < options.radius + options.hitRadius);
+               const {y} = this.getProps(['x'], useFinalPosition);
+               return (Math.abs(mouseY - y) < options.radius + options.hitRadius);
        }
 
-       getCenterPoint() {
-               return {x: this.x, y: this.y};
+       getCenterPoint(useFinalPosition) {
+               const {x, y} = this.getProps(['x', 'y'], useFinalPosition);
+               return {x, y};
        }
 
        size() {
@@ -60,15 +65,6 @@ export default class Point extends Element {
                return (radius + borderWidth) * 2;
        }
 
-       tooltipPosition() {
-               const options = this.options;
-               return {
-                       x: this.x,
-                       y: this.y,
-                       padding: options.radius + options.borderWidth
-               };
-       }
-
        draw(ctx, chartArea) {
                const me = this;
                const options = me.options;
index 57a933c2669baf71ad7f6408dcf72e855ac8df4a..e58a02a054cd82622014d6c64e52f8d07af8da4d 100644 (file)
@@ -15,33 +15,31 @@ defaults.set('elements', {
 
 /**
  * Helper function to get the bounds of the bar regardless of the orientation
- * @param bar {Rectangle} the bar
+ * @param {Rectangle} bar the bar
+ * @param {boolean} [useFinalPosition]
  * @return {object} bounds of the bar
  * @private
  */
-function getBarBounds(bar) {
-       let x1, x2, y1, y2, half;
+function getBarBounds(bar, useFinalPosition) {
+       const {x, y, base, width, height} = bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition);
+
+       let left, right, top, bottom, half;
 
        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;
+               half = height / 2;
+               left = Math.min(x, base);
+               right = Math.max(x, base);
+               top = y - half;
+               bottom = y + half;
        } else {
-               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);
+               half = width / 2;
+               left = x - half;
+               right = x + half;
+               top = Math.min(y, base);
+               bottom = Math.max(y, base);
        }
 
-       return {
-               left: x1,
-               top: y1,
-               right: x2,
-               bottom: y2
-       };
+       return {left, top, right, bottom};
 }
 
 function swap(orig, v1, v2) {
@@ -116,10 +114,10 @@ function boundingRects(bar) {
        };
 }
 
-function inRange(bar, x, y) {
+function inRange(bar, x, y, useFinalPosition) {
        const skipX = x === null;
        const skipY = y === null;
-       const bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar);
+       const bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar, useFinalPosition);
 
        return bounds
                && (skipX || x >= bounds.left && x <= bounds.right)
@@ -165,33 +163,26 @@ export default class Rectangle extends Element {
                ctx.restore();
        }
 
-       inRange(mouseX, mouseY) {
-               return inRange(this, mouseX, mouseY);
+       inRange(mouseX, mouseY, useFinalPosition) {
+               return inRange(this, mouseX, mouseY, useFinalPosition);
        }
 
-       inXRange(mouseX) {
-               return inRange(this, mouseX, null);
+       inXRange(mouseX, useFinalPosition) {
+               return inRange(this, mouseX, null, useFinalPosition);
        }
 
-       inYRange(mouseY) {
-               return inRange(this, null, mouseY);
+       inYRange(mouseY, useFinalPosition) {
+               return inRange(this, null, mouseY, useFinalPosition);
        }
 
-       getCenterPoint() {
-               const {x, y, base, horizontal} = this;
+       getCenterPoint(useFinalPosition) {
+               const {x, y, base, horizontal} = this.getProps(['x', 'y', 'base', 'horizontal', useFinalPosition]);
                return {
                        x: horizontal ? (x + base) / 2 : x,
                        y: horizontal ? y : (y + base) / 2
                };
        }
 
-       tooltipPosition() {
-               return {
-                       x: this.x,
-                       y: this.y
-               };
-       }
-
        getRange(axis) {
                return axis === 'x' ? this.width / 2 : this.height / 2;
        }
index b92b7967152f97790d671061df8ca516989ceea1..fdcecf84a3e5ffa8c939b9bdc0be69d63c165d63 100644 (file)
@@ -478,6 +478,7 @@ export class Tooltip extends Element {
                this.height = undefined;
                this.width = undefined;
                this.caretX = undefined;
+               this.caretY = undefined;
                this.labelColors = undefined;
                this.labelTextColors = undefined;
 
@@ -916,15 +917,22 @@ export class Tooltip extends Element {
                const anims = me.$animations;
                const animX = anims && anims.x;
                const animY = anims && anims.y;
-               if (animX && animX.active() || animY && animY.active()) {
+               if (animX || animY) {
                        const position = positioners[options.position].call(me, me._active, me._eventPosition);
                        if (!position) {
                                return;
                        }
+                       const size = me._size = getTooltipSize(me);
                        const positionAndSize = Object.assign({}, 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.xAlign = alignment.xAlign;
+                               me.yAlign = alignment.yAlign;
+                               me.width = size.width;
+                               me.height = size.height;
+                               me.caretX = position.x;
+                               me.caretY = position.y;
                                me._resolveAnimations().update(me, point);
                        }
                }
@@ -985,9 +993,10 @@ export class Tooltip extends Element {
        /**
         * Handle an event
         * @param {IEvent} e - The event to handle
+        * @param {boolean} [replay] - This is a replayed event (from update)
         * @returns {boolean} true if the tooltip changed
         */
-       handleEvent(e) {
+       handleEvent(e, replay) {
                const me = this;
                const options = me.options;
                const lastActive = me._active || [];
@@ -996,14 +1005,14 @@ export class Tooltip extends Element {
 
                // Find Active Elements for tooltips
                if (e.type !== 'mouseout') {
-                       active = me._chart.getElementsAtEventForMode(e, options.mode, options);
+                       active = me._chart.getElementsAtEventForMode(e, options.mode, options, replay);
                        if (options.reverse) {
                                active.reverse();
                        }
                }
 
                // Remember Last Actives
-               changed = !helpers._elementsEqual(active, lastActive);
+               changed = replay || !helpers._elementsEqual(active, lastActive);
 
                // Only handle target event on tooltip change
                if (changed) {
@@ -1068,9 +1077,11 @@ export default {
                plugins.notify(chart, 'afterTooltipDraw', [args]);
        },
 
-       afterEvent(chart, e) {
+       afterEvent(chart, e, replay) {
                if (chart.tooltip) {
-                       chart.tooltip.handleEvent(e);
+                       // If the event is replayed from `update`, we should evaluate with the final positions.
+                       const useFinalPosition = replay;
+                       chart.tooltip.handleEvent(e, useFinalPosition);
                }
        }
 };
index 22e7d421a00f6f9452315b8e824dc6454b57396a..78b626a88a7b8c5a1e24872a45c930fc16c7e723 100644 (file)
@@ -19,6 +19,21 @@ describe('Arc element tests', function() {
                expect(arc.inRange(-1.0 * Math.sqrt(7), Math.sqrt(7))).toBe(false);
        });
 
+       it ('should determine if in range, when full circle', function() {
+               // Mock out the arc as if the controller put it there
+               var arc = new Chart.elements.Arc({
+                       startAngle: -Math.PI,
+                       endAngle: Math.PI * 1.5,
+                       x: 0,
+                       y: 0,
+                       innerRadius: 0,
+                       outerRadius: 10,
+                       circumference: Math.PI * 2
+               });
+
+               expect(arc.inRange(7, 7)).toBe(true);
+       });
+
        it ('should get the tooltip position', function() {
                // Mock out the arc as if the controller put it there
                var arc = new Chart.elements.Arc({
index 077d2f0252ff70ae8fa3bef390adc53548490942..4de4d6eb24fc50bad548d90aadaa5f617fae895c 100644 (file)
@@ -31,8 +31,7 @@ describe('Chart.elements.Point', function() {
 
                expect(point.tooltipPosition()).toEqual({
                        x: 10,
-                       y: 15,
-                       padding: 8
+                       y: 15
                });
        });
 
index c59a8ab56ba8060b019186ec7ff821dfe17cb16a..bf9e589d1a21636a83d2fb5792d5a5199b04073b 100644 (file)
@@ -107,14 +107,14 @@ describe('Default Configs', function() {
                        var expected = [{
                                text: 'label1',
                                fillStyle: 'red',
-                               hidden: undefined,
+                               hidden: false,
                                index: 0,
                                strokeStyle: '#000',
                                lineWidth: 2
                        }, {
                                text: 'label2',
                                fillStyle: 'green',
-                               hidden: undefined,
+                               hidden: false,
                                index: 1,
                                strokeStyle: '#000',
                                lineWidth: 2
@@ -205,14 +205,14 @@ describe('Default Configs', function() {
                        var expected = [{
                                text: 'label1',
                                fillStyle: 'red',
-                               hidden: undefined,
+                               hidden: false,
                                index: 0,
                                strokeStyle: '#000',
                                lineWidth: 2
                        }, {
                                text: 'label2',
                                fillStyle: 'green',
-                               hidden: undefined,
+                               hidden: false,
                                index: 1,
                                strokeStyle: '#000',
                                lineWidth: 2