Skip to content

Commit 589e642

Browse files
authored
Merge pull request #7964 from plotly/hover-click-anywhere-c2d-2
Return actual data values (rather than calcdata values) for `xvals` / `yvals` in `hoveranywhere` and `clickanywhere` events
2 parents 499a8b2 + b4ad58d commit 589e642

5 files changed

Lines changed: 138 additions & 17 deletions

File tree

draftlogs/7964_change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- **Breaking**: Return actual data values (rather than calcdata values) for `xvals` / `yvals` in `hoveranywhere` and `clickanywhere` events. Date and category axes will now return strings rather than numeric values. Linear and log axis values remain unchanged. [[#7964](https://github.com/plotly/plotly.js/pull/7964)]

src/components/fx/click.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
var Registry = require('../../registry');
4+
var helpers = require('./helpers');
45
var hover = require('./hover').hover;
56

67
module.exports = function click(gd, evt, subplot) {
@@ -21,8 +22,8 @@ module.exports = function click(gd, evt, subplot) {
2122
// get coordinate values from latest hover call, if available
2223
clickData.xaxes ??= gd._hoverXAxes;
2324
clickData.yaxes ??= gd._hoverYAxes;
24-
clickData.xvals ??= gd._hoverXVals;
25-
clickData.yvals ??= gd._hoverYVals;
25+
clickData.xvals ??= gd._hoverXVals && helpers.c2dApply(gd._hoverXAxes, gd._hoverXVals);
26+
clickData.yvals ??= gd._hoverYVals && helpers.c2dApply(gd._hoverYAxes, gd._hoverYVals);
2627

2728
gd.emit('plotly_click', clickData);
2829
}

src/components/fx/helpers.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ exports.p2c = function (axArray, v) {
4444
return out;
4545
};
4646

47+
/*
48+
* Given an array of calcdata values and an array of axes corresponding to each value,
49+
* convert the calcdata values to data values by calling the `c2d` method of each axis.
50+
*
51+
* This function is intended to be used in constructing hover and click events,
52+
* for converting x/y values from calc space to data space. axArray and valArray are arrays
53+
* rather than single values because in the case of stacked subplots, there may be multiple axes
54+
* (and therefore multiple data values) corresponding to a single hover or click event.
55+
*
56+
* For linear and log axes, this conversion has no effect beyond validating the inputs.
57+
* However, for some axes types, the converted values may be of a different type than the
58+
* inputs:
59+
* - For category axes, `c2d` converts calcdata values (numeric) into category labels (strings)
60+
* - For date axes, `c2d` converts calcdata values (numeric values in ms) into date strings
61+
*
62+
* For axes which don't define `c2d` (e.g. geo, map), the inputs are passed through untouched.
63+
*
64+
* @param {Array} axArray : axes corresponding to each value in valArray
65+
* @param {Array} valArray : calcdata values
66+
* @return {Array} : data values, computed by calling `ax.c2d` (if defined) on each input value
67+
*/
68+
exports.c2dApply = function (axArray, valArray) {
69+
if(axArray.length !== valArray.length) {
70+
Lib.warn('c2dApply: axArray and valArray must be the same length');
71+
}
72+
var out = new Array(valArray.length);
73+
for (var i = 0; i < valArray.length; i++) {
74+
var ax = axArray && axArray[i];
75+
out[i] = ax && ax.c2d ? ax.c2d(valArray[i]) : valArray[i];
76+
}
77+
return out;
78+
};
79+
4780
exports.getDistanceFunction = function (mode, dx, dy, dxy) {
4881
if (mode === 'closest') return dxy || exports.quadrature(dx, dy);
4982
return mode.charAt(0) === 'x' ? dx : dy;

src/components/fx/hover.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,8 +976,8 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
976976
points: points,
977977
xaxes: xaArray,
978978
yaxes: yaArray,
979-
xvals: xvalArray,
980-
yvals: yvalArray
979+
xvals: helpers.c2dApply(xaArray, xvalArray),
980+
yvals: helpers.c2dApply(yaArray, yvalArray)
981981
});
982982
}
983983
}

test/jasmine/tests/hover_click_anywhere_test.js

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ var createGraphDiv = require('../assets/create_graph_div');
66
var destroyGraphDiv = require('../assets/destroy_graph_div');
77
var click = require('../assets/click');
88

9-
function makePlot(gd, layoutExtras = {}, configExtras) {
9+
function makePlot(gd, traceExtras = {}, layoutExtras = {}, configExtras) {
1010
return Plotly.newPlot(
1111
gd,
1212
[
13-
{
14-
x: [1, 2, 3],
15-
y: [1, 3, 2],
16-
type: 'scatter',
17-
mode: 'markers'
18-
}
13+
Lib.extendFlat(
14+
{
15+
x: [1, 2, 3],
16+
y: [1, 3, 2],
17+
type: 'scatter',
18+
mode: 'markers'
19+
},
20+
traceExtras
21+
)
1922
],
2023
Lib.extendFlat(
2124
{
@@ -32,6 +35,21 @@ function makePlot(gd, layoutExtras = {}, configExtras) {
3235
);
3336
}
3437

38+
// local midnight, as in https://github.com/plotly/plotly.js/issues/7816
39+
var dayStart = new Date(2026, 4, 31);
40+
var dayNoon = new Date(2026, 4, 31, 12);
41+
var dayEnd = new Date(2026, 5, 1);
42+
43+
// the 300px-wide plot area spans exactly one day, so 0px is local midnight
44+
// and 150px is local noon, in any timezone
45+
function makeDatePlot(gd, traceExtras, layoutExtras) {
46+
return makePlot(
47+
gd,
48+
Lib.extendFlat({ x: [dayStart, dayNoon], y: [1, 3] }, traceExtras),
49+
Lib.extendFlat({ xaxis: { type: 'date', range: [dayStart, dayEnd] } }, layoutExtras)
50+
);
51+
}
52+
3553
describe('hoveranywhere', () => {
3654
'use strict';
3755

@@ -58,7 +76,7 @@ describe('hoveranywhere', () => {
5876
it('emits plotly_hover with coordinate data on empty space', (done) => {
5977
var hoverData;
6078

61-
makePlot(gd, { hoveranywhere: true })
79+
makePlot(gd, {}, { hoveranywhere: true })
6280
.then(() => {
6381
gd.on('plotly_hover', (d) => (hoverData = d));
6482

@@ -94,7 +112,7 @@ describe('hoveranywhere', () => {
94112
it('still returns normal point data on traces', (done) => {
95113
var hoverData;
96114

97-
makePlot(gd, { hoveranywhere: true })
115+
makePlot(gd, {}, { hoveranywhere: true })
98116
.then(() => {
99117
gd.on('plotly_hover', (d) => (hoverData = d));
100118

@@ -132,7 +150,7 @@ describe('hoveranywhere', () => {
132150
it('respects hovermode:false', (done) => {
133151
var hoverData;
134152

135-
makePlot(gd, { hoveranywhere: true, hovermode: false })
153+
makePlot(gd, {}, { hoveranywhere: true, hovermode: false })
136154
.then(() => {
137155
gd.on('plotly_hover', (d) => (hoverData = d));
138156
_hover(250, 50);
@@ -144,7 +162,7 @@ describe('hoveranywhere', () => {
144162
it('emits plotly_hover over an editable shape', (done) => {
145163
let hoverData;
146164

147-
makePlot(gd, {
165+
makePlot(gd, {}, {
148166
hoveranywhere: true,
149167
shapes: [
150168
{
@@ -192,6 +210,7 @@ describe('hoveranywhere', () => {
192210

193211
makePlot(
194212
gd,
213+
{},
195214
{
196215
hoveranywhere: true,
197216
shapes: [
@@ -231,6 +250,55 @@ describe('hoveranywhere', () => {
231250
})
232251
.then(done, done.fail);
233252
});
253+
254+
it('reports date axis positions as date strings', (done) => {
255+
var hoverData;
256+
257+
makeDatePlot(gd, {}, { hoveranywhere: true })
258+
.then(() => {
259+
gd.on('plotly_hover', (d) => (hoverData = d));
260+
261+
_hover(0, 60);
262+
expect(hoverData.points).toEqual([]);
263+
expect(hoverData.xvals[0]).toBe('2026-05-31');
264+
expect(hoverData.yvals[0]).toBeCloseTo(10 - 60 / 30, 2);
265+
266+
_hover(150, 60);
267+
expect(hoverData.xvals[0]).toBe('2026-05-31 12:00');
268+
269+
// the point at (dayStart, 1) reports that same value
270+
_hover(0, gd._fullLayout.yaxis.c2p(1));
271+
expect(hoverData.points[0].x).toBe('2026-05-31');
272+
expect(hoverData.xvals[0]).toBe('2026-05-31');
273+
})
274+
.then(done, done.fail);
275+
});
276+
277+
it('reports category names and log axis data values', (done) => {
278+
var hoverData;
279+
280+
makePlot(
281+
gd,
282+
{ x: ['a', 'b', 'c'], y: [10, 20, 30] },
283+
{ xaxis: { type: 'category' }, yaxis: { type: 'log', range: [1, 3] }, hoveranywhere: true }
284+
)
285+
.then(() => {
286+
gd.on('plotly_hover', (d) => (hoverData = d));
287+
288+
var xa = gd._fullLayout.xaxis;
289+
290+
// empty space above the middle category, halfway up 10 -> 1000
291+
_hover(xa.c2p(1), 150);
292+
expect(hoverData.points).toEqual([]);
293+
expect(hoverData.xvals[0]).toBe('b');
294+
expect(hoverData.yvals[0]).toBeCloseTo(100, 6);
295+
296+
_hover(xa.c2p(1), gd._fullLayout.yaxis.c2p(20));
297+
expect(hoverData.points[0].x).toBe('b');
298+
expect(hoverData.xvals[0]).toBe('b');
299+
})
300+
.then(done, done.fail);
301+
});
234302
});
235303

236304
describe('clickanywhere', () => {
@@ -244,7 +312,7 @@ describe('clickanywhere', () => {
244312
it('emits plotly_click with empty points on empty space', (done) => {
245313
var clickData;
246314

247-
makePlot(gd, { clickanywhere: true })
315+
makePlot(gd, {}, { clickanywhere: true })
248316
.then(() => {
249317
gd.on('plotly_click', (d) => (clickData = d));
250318

@@ -285,7 +353,7 @@ describe('clickanywhere', () => {
285353
it('emits plotly_click over an editable shape', (done) => {
286354
let clickData;
287355

288-
makePlot(gd, {
356+
makePlot(gd, {}, {
289357
clickanywhere: true,
290358
shapes: [
291359
{
@@ -329,6 +397,7 @@ describe('clickanywhere', () => {
329397

330398
makePlot(
331399
gd,
400+
{},
332401
{
333402
clickanywhere: true,
334403
shapes: [
@@ -367,4 +436,21 @@ describe('clickanywhere', () => {
367436
})
368437
.then(done, done.fail);
369438
});
439+
it('reports date axis positions as date strings', (done) => {
440+
var clickData;
441+
442+
makeDatePlot(gd, {}, { clickanywhere: true })
443+
.then(() => {
444+
gd.on('plotly_click', (d) => (clickData = d));
445+
446+
var bb = gd.getBoundingClientRect();
447+
var s = gd._fullLayout._size;
448+
click(bb.left + s.l, bb.top + s.t + 60);
449+
450+
expect(clickData.points).toEqual([]);
451+
expect(clickData.xvals[0]).toBe('2026-05-31');
452+
expect(clickData.yvals[0]).toBeCloseTo(10 - 60 / 30, 2);
453+
})
454+
.then(done, done.fail);
455+
});
370456
});

0 commit comments

Comments
 (0)