Skip to content

Commit 5e489f1

Browse files
authored
Issue 4991 (#7084)
* Fix remaining handleEvent issues * Reduce lines * Update tooltip always on replay * Address issues * Fix test * More tooltip fixing * Extend comment
1 parent a9ae64f commit 5e489f1

File tree

11 files changed

+219
-145
lines changed

11 files changed

+219
-145
lines changed

src/core/core.controller.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export default class Chart {
212212
this.chartArea = undefined;
213213
this.data = undefined;
214214
this.active = undefined;
215-
this.lastActive = undefined;
215+
this.lastActive = [];
216216
this._lastEvent = undefined;
217217
/** @type {{resize?: function}} */
218218
this._listeners = {};
@@ -581,7 +581,7 @@ export default class Chart {
581581

582582
// Replay last event from before update
583583
if (me._lastEvent) {
584-
me._eventHandler(me._lastEvent);
584+
me._eventHandler(me._lastEvent, true);
585585
}
586586

587587
me.render();
@@ -808,10 +808,10 @@ export default class Chart {
808808
return Interaction.modes.index(this, e, {intersect: false});
809809
}
810810

811-
getElementsAtEventForMode(e, mode, options) {
811+
getElementsAtEventForMode(e, mode, options, useFinalPosition) {
812812
const method = Interaction.modes[mode];
813813
if (typeof method === 'function') {
814-
return method(this, e, options);
814+
return method(this, e, options, useFinalPosition);
815815
}
816816

817817
return [];
@@ -1021,16 +1021,16 @@ export default class Chart {
10211021
/**
10221022
* @private
10231023
*/
1024-
_eventHandler(e) {
1024+
_eventHandler(e, replay) {
10251025
const me = this;
10261026

1027-
if (plugins.notify(me, 'beforeEvent', [e]) === false) {
1027+
if (plugins.notify(me, 'beforeEvent', [e, replay]) === false) {
10281028
return;
10291029
}
10301030

1031-
me._handleEvent(e);
1031+
me._handleEvent(e, replay);
10321032

1033-
plugins.notify(me, 'afterEvent', [e]);
1033+
plugins.notify(me, 'afterEvent', [e, replay]);
10341034

10351035
me.render();
10361036

@@ -1040,23 +1040,38 @@ export default class Chart {
10401040
/**
10411041
* Handle an event
10421042
* @param {IEvent} e the event to handle
1043+
* @param {boolean} [replay] - true if the event was replayed by `update`
10431044
* @return {boolean} true if the chart needs to re-render
10441045
* @private
10451046
*/
1046-
_handleEvent(e) {
1047+
_handleEvent(e, replay) {
10471048
const me = this;
1048-
const options = me.options || {};
1049+
const options = me.options;
10491050
const hoverOptions = options.hover;
1050-
let changed = false;
10511051

1052-
me.lastActive = me.lastActive || [];
1052+
// If the event is replayed from `update`, we should evaluate with the final positions.
1053+
//
1054+
// The `replay`:
1055+
// It's the last event (excluding click) that has occured before `update`.
1056+
// So mouse has not moved. It's also over the chart, because there is a `replay`.
1057+
//
1058+
// The why:
1059+
// If animations are active, the elements haven't moved yet compared to state before update.
1060+
// But if they will, we are activating the elements that would be active, if this check
1061+
// was done after the animations have completed. => "final positions".
1062+
// If there is no animations, the "final" and "current" positions are equal.
1063+
// This is done so we do not have to evaluate the active elements each animation frame
1064+
// - it would be expensive.
1065+
const useFinalPosition = replay;
1066+
1067+
let changed = false;
10531068

10541069
// Find Active Elements for hover and tooltips
10551070
if (e.type === 'mouseout') {
10561071
me.active = [];
10571072
me._lastEvent = null;
10581073
} else {
1059-
me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
1074+
me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition);
10601075
me._lastEvent = e.type === 'click' ? me._lastEvent : e;
10611076
}
10621077

@@ -1072,7 +1087,7 @@ export default class Chart {
10721087
}
10731088

10741089
changed = !helpers._elementsEqual(me.active, me.lastActive);
1075-
if (changed) {
1090+
if (changed || replay) {
10761091
me._updateHoverStyles();
10771092
}
10781093

src/core/core.element.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,48 @@ export default class Element {
55

66
static extend = inherits;
77

8-
/**
9-
* @param {object} [cfg] optional configuration
10-
*/
118
constructor(cfg) {
129
this.x = undefined;
1310
this.y = undefined;
14-
this.hidden = undefined;
11+
this.hidden = false;
12+
this.active = false;
13+
this.options = undefined;
14+
this.$animations = undefined;
1515

1616
if (cfg) {
1717
Object.assign(this, cfg);
1818
}
1919
}
2020

21-
tooltipPosition() {
22-
return {
23-
x: this.x,
24-
y: this.y
25-
};
21+
/**
22+
* @param {boolean} [useFinalPosition]
23+
*/
24+
tooltipPosition(useFinalPosition) {
25+
const {x, y} = this.getProps(['x', 'y'], useFinalPosition);
26+
return {x, y};
2627
}
2728

2829
hasValue() {
2930
return isNumber(this.x) && isNumber(this.y);
3031
}
32+
33+
/**
34+
* Gets the current or final value of each prop. Can return extra properties (whole object).
35+
* @param {string[]} props - properties to get
36+
* @param {boolean} [final] - get the final value (animation target)
37+
* @return {object}
38+
*/
39+
getProps(props, final) {
40+
const me = this;
41+
const anims = this.$animations;
42+
if (!final || !anims) {
43+
// let's not create an object, if not needed
44+
return me;
45+
}
46+
const ret = {};
47+
props.forEach(prop => {
48+
ret[prop] = anims[prop] && anims[prop].active ? anims[prop]._to : me[prop];
49+
});
50+
return ret;
51+
}
3152
}

src/core/core.interaction.js

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection';
55
/**
66
* @typedef { import("./core.controller").default } Chart
77
* @typedef { import("../platform/platform.base").IEvent } IEvent
8-
* @typedef {{axis?:'x'|'y'|'xy', intersect:boolean}} IInteractionOptions
8+
* @typedef {{axis?: string, intersect?: boolean}} InteractionOptions
9+
* @typedef {{datasetIndex: number, index: number, element: import("../core/core.element").default}} InteractionItem
910
*/
1011

1112
/**
@@ -121,17 +122,18 @@ function getDistanceMetricForAxis(axis) {
121122
* @param {Chart} chart - the chart
122123
* @param {object} position - the point to be nearest to
123124
* @param {string} axis - the axis mode. x|y|xy
124-
* @return {object[]} the nearest items
125+
* @param {boolean} [useFinalPosition] - use the element's animation target instead of current position
126+
* @return {InteractionItem[]} the nearest items
125127
*/
126-
function getIntersectItems(chart, position, axis) {
128+
function getIntersectItems(chart, position, axis, useFinalPosition) {
127129
const items = [];
128130

129131
if (!_isPointInArea(position, chart.chartArea)) {
130132
return items;
131133
}
132134

133135
const evaluationFunc = function(element, datasetIndex, index) {
134-
if (element.inRange(position.x, position.y)) {
136+
if (element.inRange(position.x, position.y, useFinalPosition)) {
135137
items.push({element, datasetIndex, index});
136138
}
137139
};
@@ -146,9 +148,10 @@ function getIntersectItems(chart, position, axis) {
146148
* @param {object} position - the point to be nearest to
147149
* @param {string} axis - the axes along which to measure distance
148150
* @param {boolean} [intersect] - if true, only consider items that intersect the position
149-
* @return {object[]} the nearest items
151+
* @param {boolean} [useFinalPosition] - use the elements animation target instead of current position
152+
* @return {InteractionItem[]} the nearest items
150153
*/
151-
function getNearestItems(chart, position, axis, intersect) {
154+
function getNearestItems(chart, position, axis, intersect, useFinalPosition) {
152155
const distanceMetric = getDistanceMetricForAxis(axis);
153156
let minDistance = Number.POSITIVE_INFINITY;
154157
let items = [];
@@ -158,11 +161,11 @@ function getNearestItems(chart, position, axis, intersect) {
158161
}
159162

160163
const evaluationFunc = function(element, datasetIndex, index) {
161-
if (intersect && !element.inRange(position.x, position.y)) {
164+
if (intersect && !element.inRange(position.x, position.y, useFinalPosition)) {
162165
return;
163166
}
164167

165-
const center = element.getCenterPoint();
168+
const center = element.getCenterPoint(useFinalPosition);
166169
const distance = distanceMetric(position, center);
167170
if (distance < minDistance) {
168171
items = [{element, datasetIndex, index}];
@@ -191,14 +194,17 @@ export default {
191194
* @since v2.4.0
192195
* @param {Chart} chart - the chart we are returning items from
193196
* @param {Event} e - the event we are find things at
194-
* @param {IInteractionOptions} options - options to use during interaction
195-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
197+
* @param {InteractionOptions} options - options to use
198+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
199+
* @return {InteractionItem[]} - items that are found
196200
*/
197-
index(chart, e, options) {
201+
index(chart, e, options, useFinalPosition) {
198202
const position = getRelativePosition(e, chart);
199203
// Default axis for index mode is 'x' to match old behaviour
200204
const axis = options.axis || 'x';
201-
const items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
205+
const items = options.intersect
206+
? getIntersectItems(chart, position, axis, useFinalPosition)
207+
: getNearestItems(chart, position, axis, false, useFinalPosition);
202208
const elements = [];
203209

204210
if (!items.length) {
@@ -224,13 +230,16 @@ export default {
224230
* @function Chart.Interaction.modes.dataset
225231
* @param {Chart} chart - the chart we are returning items from
226232
* @param {Event} e - the event we are find things at
227-
* @param {IInteractionOptions} options - options to use during interaction
228-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
233+
* @param {InteractionOptions} options - options to use
234+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
235+
* @return {InteractionItem[]} - items that are found
229236
*/
230-
dataset(chart, e, options) {
237+
dataset(chart, e, options, useFinalPosition) {
231238
const position = getRelativePosition(e, chart);
232239
const axis = options.axis || 'xy';
233-
let items = options.intersect ? getIntersectItems(chart, position, axis) : getNearestItems(chart, position, axis);
240+
let items = options.intersect
241+
? getIntersectItems(chart, position, axis, useFinalPosition) :
242+
getNearestItems(chart, position, axis, false, useFinalPosition);
234243

235244
if (items.length > 0) {
236245
const datasetIndex = items[0].datasetIndex;
@@ -250,48 +259,51 @@ export default {
250259
* @function Chart.Interaction.modes.intersect
251260
* @param {Chart} chart - the chart we are returning items from
252261
* @param {Event} e - the event we are find things at
253-
* @param {IInteractionOptions} options - options to use
254-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
262+
* @param {InteractionOptions} options - options to use
263+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
264+
* @return {InteractionItem[]} - items that are found
255265
*/
256-
point(chart, e, options) {
266+
point(chart, e, options, useFinalPosition) {
257267
const position = getRelativePosition(e, chart);
258268
const axis = options.axis || 'xy';
259-
return getIntersectItems(chart, position, axis);
269+
return getIntersectItems(chart, position, axis, useFinalPosition);
260270
},
261271

262272
/**
263273
* nearest mode returns the element closest to the point
264274
* @function Chart.Interaction.modes.intersect
265275
* @param {Chart} chart - the chart we are returning items from
266276
* @param {Event} e - the event we are find things at
267-
* @param {IInteractionOptions} options - options to use
268-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
277+
* @param {InteractionOptions} options - options to use
278+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
279+
* @return {InteractionItem[]} - items that are found
269280
*/
270-
nearest(chart, e, options) {
281+
nearest(chart, e, options, useFinalPosition) {
271282
const position = getRelativePosition(e, chart);
272283
const axis = options.axis || 'xy';
273-
return getNearestItems(chart, position, axis, options.intersect);
284+
return getNearestItems(chart, position, axis, options.intersect, useFinalPosition);
274285
},
275286

276287
/**
277288
* x mode returns the elements that hit-test at the current x coordinate
278289
* @function Chart.Interaction.modes.x
279290
* @param {Chart} chart - the chart we are returning items from
280291
* @param {Event} e - the event we are find things at
281-
* @param {IInteractionOptions} options - options to use
282-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
292+
* @param {InteractionOptions} options - options to use
293+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
294+
* @return {InteractionItem[]} - items that are found
283295
*/
284-
x(chart, e, options) {
296+
x(chart, e, options, useFinalPosition) {
285297
const position = getRelativePosition(e, chart);
286298
const items = [];
287299
let intersectsItem = false;
288300

289301
evaluateAllVisibleItems(chart, (element, datasetIndex, index) => {
290-
if (element.inXRange(position.x)) {
302+
if (element.inXRange(position.x, useFinalPosition)) {
291303
items.push({element, datasetIndex, index});
292304
}
293305

294-
if (element.inRange(position.x, position.y)) {
306+
if (element.inRange(position.x, position.y, useFinalPosition)) {
295307
intersectsItem = true;
296308
}
297309
});
@@ -309,20 +321,21 @@ export default {
309321
* @function Chart.Interaction.modes.y
310322
* @param {Chart} chart - the chart we are returning items from
311323
* @param {Event} e - the event we are find things at
312-
* @param {IInteractionOptions} options - options to use
313-
* @return {Object[]} Array of elements that are under the point. If none are found, an empty array is returned
324+
* @param {InteractionOptions} options - options to use
325+
* @param {boolean} [useFinalPosition] - use final element position (animation target)
326+
* @return {InteractionItem[]} - items that are found
314327
*/
315-
y(chart, e, options) {
328+
y(chart, e, options, useFinalPosition) {
316329
const position = getRelativePosition(e, chart);
317330
const items = [];
318331
let intersectsItem = false;
319332

320333
evaluateAllVisibleItems(chart, (element, datasetIndex, index) => {
321-
if (element.inYRange(position.y)) {
334+
if (element.inYRange(position.y, useFinalPosition)) {
322335
items.push({element, datasetIndex, index});
323336
}
324337

325-
if (element.inRange(position.x, position.y)) {
338+
if (element.inRange(position.x, position.y, useFinalPosition)) {
326339
intersectsItem = true;
327340
}
328341
});

src/core/core.plugins.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ export default new PluginService();
365365
* @param {Chart} chart - The chart instance.
366366
* @param {IEvent} event - The event object.
367367
* @param {object} options - The plugin options.
368+
* @param {boolean} replay - True if this event is replayed from `Chart.update`
368369
*/
369370
/**
370371
* @method IPlugin#afterEvent
@@ -373,6 +374,7 @@ export default new PluginService();
373374
* @param {Chart} chart - The chart instance.
374375
* @param {IEvent} event - The event object.
375376
* @param {object} options - The plugin options.
377+
* @param {boolean} replay - True if this event is replayed from `Chart.update`
376378
*/
377379
/**
378380
* @method IPlugin#resize

0 commit comments

Comments
 (0)