Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7964_change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Return actual data values (rather than calcdata values) for `xvals` / `yvals` in `hoveranywhere` and `clickanywhere` events [[#7964](https://github.com/plotly/plotly.js/pull/7964)]
5 changes: 3 additions & 2 deletions src/components/fx/click.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var Registry = require('../../registry');
var helpers = require('./helpers');
var hover = require('./hover').hover;

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

gd.emit('plotly_click', clickData);
}
Expand Down
33 changes: 33 additions & 0 deletions src/components/fx/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,39 @@ exports.p2c = function (axArray, v) {
return out;
};

/*
* Given an array of calcdata values and an array of axes corresponding to each value,
* convert the calcdata values to data values by calling the `c2d` method of each axis.
*
* This function is intended to be used in constructing hover and click events,
* for converting x/y values from calc space to data space. axArray and valArray are arrays
* rather than single values because in the case of stacked subplots, there may be multiple axes
* (and therefore multiple data values) corresponding to a single hover or click event.
*
* For linear and log axes, this conversion has no effect beyond validating the inputs.
* However, for some axes types, the converted values may be of a different type than the
* inputs:
* - For category axes, `c2d` converts calcdata values (numeric) into category labels (strings)
* - For date axes, `c2d` converts calcdata values (numeric values in ms) into date strings
*
* For axes which don't define `c2d` (e.g. geo, map), the inputs are passed through untouched.
*
* @param {Array} axArray : axes corresponding to each value in valArray
* @param {Array} valArray : calcdata values
* @return {Array} : data values, computed by calling `ax.c2d` (if defined) on each input value
*/
exports.c2dApply = function (axArray, valArray) {
if(axArray.length !== valArray.length) {
Lib.warn('c2dApply: axArray and valArray must be the same length');
}
var out = new Array(valArray.length);
for (var i = 0; i < valArray.length; i++) {
var ax = axArray && axArray[i];
out[i] = ax && ax.c2d ? ax.c2d(valArray[i]) : valArray[i];
}
return out;
};

exports.getDistanceFunction = function (mode, dx, dy, dxy) {
if (mode === 'closest') return dxy || exports.quadrature(dx, dy);
return mode.charAt(0) === 'x' ? dx : dy;
Expand Down
4 changes: 2 additions & 2 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,8 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
points: points,
xaxes: xaArray,
yaxes: yaArray,
xvals: xvalArray,
yvals: yvalArray
xvals: helpers.c2dApply(xaArray, xvalArray),
yvals: helpers.c2dApply(yaArray, yvalArray)
});
}
}
Expand Down
99 changes: 92 additions & 7 deletions test/jasmine/tests/hover_click_anywhere_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var click = require('../assets/click');

function makePlot(gd, layoutExtras = {}, configExtras) {
function makePlot(gd, layoutExtras = {}, traceExtras = {}, configExtras) {
return Plotly.newPlot(
gd,
[
{
x: [1, 2, 3],
y: [1, 3, 2],
type: 'scatter',
mode: 'markers'
}
Lib.extendFlat(
{
x: [1, 2, 3],
y: [1, 3, 2],
type: 'scatter',
mode: 'markers'
},
traceExtras
)
],
Lib.extendFlat(
{
Expand All @@ -32,6 +35,20 @@ function makePlot(gd, layoutExtras = {}, configExtras) {
);
}

// local midnight, as in https://github.com/plotly/plotly.js/issues/7816
var dayStart = new Date(2026, 4, 31);
var dayNoon = new Date(2026, 4, 31, 12);
var dayEnd = new Date(2026, 5, 1);

// the 300px-wide plot area spans exactly one day, so 0px is local midnight
// and 150px is local noon, in any timezone
function makeDatePlot(gd, layoutExtras) {
return makePlot(gd, Lib.extendFlat({ xaxis: { type: 'date', range: [dayStart, dayEnd] } }, layoutExtras), {
x: [dayStart, dayNoon],
y: [1, 3]
});
}

describe('hoveranywhere', () => {
'use strict';

Expand Down Expand Up @@ -205,6 +222,7 @@ describe('hoveranywhere', () => {
}
]
},
{},
{ edits: { shapePosition: true } }
)
.then(() => {
Expand All @@ -231,6 +249,55 @@ describe('hoveranywhere', () => {
})
.then(done, done.fail);
});

it('reports date axis positions as date strings', (done) => {
var hoverData;

makeDatePlot(gd, { hoveranywhere: true })
.then(() => {
gd.on('plotly_hover', (d) => (hoverData = d));

_hover(0, 60);
expect(hoverData.points).toEqual([]);
expect(hoverData.xvals[0]).toBe('2026-05-31');
expect(hoverData.yvals[0]).toBeCloseTo(10 - 60 / 30, 2);

_hover(150, 60);
expect(hoverData.xvals[0]).toBe('2026-05-31 12:00');

// the point at (dayStart, 1) reports that same value
_hover(0, gd._fullLayout.yaxis.c2p(1));
expect(hoverData.points[0].x).toBe('2026-05-31');
expect(hoverData.xvals[0]).toBe('2026-05-31');
})
.then(done, done.fail);
});

it('reports category names and log axis data values', (done) => {
var hoverData;

makePlot(
gd,
{ xaxis: { type: 'category' }, yaxis: { type: 'log', range: [1, 3] }, hoveranywhere: true },
{ x: ['a', 'b', 'c'], y: [10, 20, 30] }
)
.then(() => {
gd.on('plotly_hover', (d) => (hoverData = d));

var xa = gd._fullLayout.xaxis;

// empty space above the middle category, halfway up 10 -> 1000
_hover(xa.c2p(1), 150);
expect(hoverData.points).toEqual([]);
expect(hoverData.xvals[0]).toBe('b');
expect(hoverData.yvals[0]).toBeCloseTo(100, 6);

_hover(xa.c2p(1), gd._fullLayout.yaxis.c2p(20));
expect(hoverData.points[0].x).toBe('b');
expect(hoverData.xvals[0]).toBe('b');
})
.then(done, done.fail);
});
});

describe('clickanywhere', () => {
Expand Down Expand Up @@ -342,6 +409,7 @@ describe('clickanywhere', () => {
}
]
},
{},
{ edits: { shapePosition: true } }
)
.then(() => {
Expand All @@ -367,4 +435,21 @@ describe('clickanywhere', () => {
})
.then(done, done.fail);
});
it('reports date axis positions as date strings', (done) => {
var clickData;

makeDatePlot(gd, { clickanywhere: true })
.then(() => {
gd.on('plotly_click', (d) => (clickData = d));

var bb = gd.getBoundingClientRect();
var s = gd._fullLayout._size;
click(bb.left + s.l, bb.top + s.t + 60);

expect(clickData.points).toEqual([]);
expect(clickData.xvals[0]).toBe('2026-05-31');
expect(clickData.yvals[0]).toBeCloseTo(10 - 60 / 30, 2);
})
.then(done, done.fail);
});
});