Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5ab032d
Add hoveranywhere and clickanywhere attributes to emit hover/click ev…
alexshoe Feb 12, 2026
8d4562a
Add jasmine tests for `hoveranywhere` and `clickanywhere`r
alexshoe Feb 12, 2026
a92246d
Update schema
alexshoe Feb 12, 2026
1eb8d6e
Merge remote-tracking branch 'origin/master' into hover-click-events-…
alexshoe Feb 12, 2026
fb4ccbe
Merge remote-tracking branch 'origin/master' into hover-click-events-…
alexshoe Feb 18, 2026
a981ca5
Merge remote-tracking branch 'origin/master' into hover-click-events-…
alexshoe Feb 20, 2026
2ba5ddb
Expose pixel coordinates of hover event data and fire hover events ev…
alexshoe Feb 26, 2026
19ac09c
Merge remote-tracking branch 'origin/master' into hover-click-events-…
alexshoe Feb 26, 2026
5ba0f25
Add new attributes to map test
alexshoe Feb 26, 2026
78305ac
Add xPixel and yPixel to tests
alexshoe Feb 27, 2026
7868ae9
AddxPixel and yPixel to more tests
alexshoe Feb 27, 2026
dae94d4
Add xPixel and yPixel to remaining tests
alexshoe Feb 27, 2026
7dfecf6
Remove xPixel and yPixel from funnelarea and pie tests, as they are n…
alexshoe Feb 27, 2026
e93310f
Update map_custom-style baseline
alexshoe Feb 27, 2026
ed0a5a8
Update test/jasmine/tests/hover_click_anywhere_test.js
alexshoe Mar 12, 2026
a974535
Update test/jasmine/tests/hover_click_anywhere_test.js
alexshoe Mar 12, 2026
e7934c9
Add xPixel/yPixel documentation, and add emitHover helper
alexshoe Mar 12, 2026
2df7c4b
Verify `hoverData` key values in hover/click anywhere tests
alexshoe Mar 12, 2026
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
16 changes: 14 additions & 2 deletions src/components/fx/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var hover = require('./hover').hover;

module.exports = function click(gd, evt, subplot) {
var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata);
var fullLayout = gd._fullLayout;

// fallback to fail-safe in case the plot type's hover method doesn't pass the subplot.
// Ternary, for example, didn't, but it was caught because tested.
Expand All @@ -14,9 +15,20 @@ module.exports = function click(gd, evt, subplot) {
hover(gd, evt, subplot, true);
}

function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, event: evt}); }
function emitClick() {
var clickData = {points: gd._hoverdata, event: evt};

// 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;

if(gd._hoverdata && evt && evt.target) {
gd.emit('plotly_click', clickData);
}

if((gd._hoverdata || fullLayout.clickanywhere) && evt && evt.target) {
if(!gd._hoverdata) gd._hoverdata = [];
if(annotationsDone && annotationsDone.then) {
annotationsDone.then(emitClick);
} else emitClick();
Expand Down
53 changes: 43 additions & 10 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ exports.loneHover = function loneHover(hoverItems, opts) {
y1: y1 + gTop
};

// xPixel/yPixel are pixel coordinates of the hover point's center,
// relative to the top-left corner of the graph div
eventData.xPixel = (_x0 + _x1) / 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexshoe what's the purpose of these lines?

Copy link
Contributor Author

@alexshoe alexshoe Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we track hover points as bounding boxes with x0, x1, y0, y1
So xPixel and yPixel are calculated centers of those bounding boxes for convenience

and all these are pixel values with the top-left corner of the graph div as the origin. I'll add a comment to document this

eventData.yPixel = (_y0 + _y1) / 2;

if (opts.inOut_bbox) {
opts.inOut_bbox.push(eventData.bbox);
}
Expand Down Expand Up @@ -473,6 +478,14 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
}
}

// Save coordinate values so clickanywhere can be used without hoveranywhere
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only do this step if clickanywhere is enabled? Does it matter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't functionally matter but I can change it just to be more explicit + make it more intuitive

if (fullLayout.clickanywhere) {
gd._hoverXVals = xvalArray;
gd._hoverYVals = yvalArray;
gd._hoverXAxes = xaArray;
gd._hoverYAxes = yaArray;
}

// the pixel distance to beat as a matching point
// in 'x' or 'y' mode this resets for each trace
var distance = Infinity;
Expand Down Expand Up @@ -778,6 +791,18 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
createSpikelines(gd, spikePoints, spikelineOpts);
}
}

if (fullLayout.hoveranywhere && !noHoverEvent && eventTarget) {
var oldHoverData = gd._hoverdata;
if (oldHoverData && oldHoverData.length) {
gd.emit('plotly_unhover', {
event: evt,
points: oldHoverData
});
gd._hoverdata = [];
}
emitHover([]);
}
return result;
}

Expand Down Expand Up @@ -877,6 +902,9 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
y0: y0 + gTop,
y1: y1 + gTop
};

eventData.xPixel = (_x0 + _x1) / 2;
eventData.yPixel = (_y0 + _y1) / 2;
}

pt.eventData = [eventData];
Expand Down Expand Up @@ -914,23 +942,28 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
}

// don't emit events if called manually
if (!eventTarget || noHoverEvent || !hoverChanged(gd, evt, oldhoverdata)) return;
var _hoverChanged = hoverChanged(gd, evt, oldhoverdata);
if (!eventTarget || noHoverEvent || (!_hoverChanged && !fullLayout.hoveranywhere)) return;

if (oldhoverdata) {
if (oldhoverdata && _hoverChanged) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that when hoveranywhere is enabled, we emit plotly_unhover any time the hover data changes? That doesn't seem like the right behavior to me. Let me know if I'm misunderstanding.

gd.emit('plotly_unhover', {
event: evt,
points: oldhoverdata
});
}

gd.emit('plotly_hover', {
event: evt,
points: gd._hoverdata,
xaxes: xaArray,
yaxes: yaArray,
xvals: xvalArray,
yvals: yvalArray
});
emitHover(gd._hoverdata);

function emitHover(points) {
gd.emit('plotly_hover', {
event: evt,
points: points,
xaxes: xaArray,
yaxes: yaArray,
xvals: xvalArray,
yvals: yvalArray
});
}
}

function hoverDataKey(d) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/fx/hovermode_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ module.exports = function handleHoverModeDefaults(layoutIn, layoutOut) {

coerce('clickmode');
coerce('hoversubplots');
coerce('hoveranywhere');
coerce('clickanywhere');
return coerce('hovermode');
};
22 changes: 22 additions & 0 deletions src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ module.exports = {
'when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.',
].join(' ')
},
hoveranywhere: {
valType: 'boolean',
dflt: false,
editType: 'none',
description: [
'If true, `plotly_hover` events will fire for any cursor position',
'within the plot area, not just over traces.',
'When the cursor is not over a trace, the event will have an empty `points` array',
'but will include `xvals` and `yvals` with cursor coordinates in data space.'
].join(' ')
},
clickanywhere: {
valType: 'boolean',
dflt: false,
editType: 'none',
description: [
'If true, `plotly_click` events will fire for any click position',
'within the plot area, not just over traces.',
'When clicking where there is no trace data, the event will have an empty `points` array',
'but will include `xvals` and `yvals` with click coordinates in data space.'
].join(' ')
},
hoverdistance: {
valType: 'integer',
min: -1,
Expand Down
Binary file modified test/image/baselines/map_custom-style.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions test/jasmine/tests/click_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down Expand Up @@ -225,7 +225,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down Expand Up @@ -315,7 +315,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down Expand Up @@ -349,7 +349,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down Expand Up @@ -387,7 +387,7 @@ describe('Test click interactions:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
'bbox',
'x', 'y', 'xaxis', 'yaxis'
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
].sort());
expect(pt.curveNumber).toEqual(0);
expect(pt.pointNumber).toEqual(11);
Expand Down
18 changes: 9 additions & 9 deletions test/jasmine/tests/geo_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat', 'location', 'marker.size'
'lon', 'lat', 'location', 'marker.size', 'xPixel', 'yPixel'
].sort());
expect(cnt).toEqual(1);
});
Expand Down Expand Up @@ -947,7 +947,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat', 'location', 'marker.size'
'lon', 'lat', 'location', 'marker.size', 'xPixel', 'yPixel'
].sort());
});

Expand Down Expand Up @@ -979,7 +979,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat', 'location', 'marker.size'
'lon', 'lat', 'location', 'marker.size', 'xPixel', 'yPixel'
].sort());
});

Expand Down Expand Up @@ -1008,7 +1008,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'location', 'z', 'ct'
'location', 'z', 'ct', 'xPixel', 'yPixel'
].sort());
});

Expand Down Expand Up @@ -1036,7 +1036,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'location', 'z', 'ct'
'location', 'z', 'ct', 'xPixel', 'yPixel'
].sort());
});

Expand Down Expand Up @@ -1068,7 +1068,7 @@ describe('Test geo interactions', function() {
it('should contain the correct fields', function() {
expect(Object.keys(ptData).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'location', 'z', 'ct'
'location', 'z', 'ct', 'xPixel', 'yPixel'
].sort());
});

Expand Down Expand Up @@ -1792,7 +1792,7 @@ describe('Test event property of interactions on a geo plot:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat',
'location', 'text', 'marker.size'
'location', 'text', 'marker.size', 'xPixel', 'yPixel'
].sort());

expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
Expand Down Expand Up @@ -1896,7 +1896,7 @@ describe('Test event property of interactions on a geo plot:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat',
'location', 'text', 'marker.size'
'location', 'text', 'marker.size', 'xPixel', 'yPixel'
].sort());

expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
Expand Down Expand Up @@ -1937,7 +1937,7 @@ describe('Test event property of interactions on a geo plot:', function() {
expect(Object.keys(pt).sort()).toEqual([
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
'lon', 'lat',
'location', 'text', 'marker.size'
'location', 'text', 'marker.size', 'xPixel', 'yPixel'
].sort());

expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
Expand Down
Loading