Skip to content

Enable Hover and Click Events Anywhere#7707

Open
alexshoe wants to merge 14 commits intoplotly:masterfrom
alexshoe:hover-click-events-anywhere
Open

Enable Hover and Click Events Anywhere#7707
alexshoe wants to merge 14 commits intoplotly:masterfrom
alexshoe:hover-click-events-anywhere

Conversation

@alexshoe
Copy link
Contributor

@alexshoe alexshoe commented Feb 12, 2026

Description:

Added 2 new boolean attributes to the layour object: hoveranywhere and clickanywhere, which respectively allow plotly_hover and plotly_click events to be received anywhere in the plot area and not just over traces.

When hovering/clicking on empty space and with no nearby trace, the events will fire with an empty points array but includes xvals, yvals, xaxes, and yaxes so you still get cursor coordinates in data space. When hovering/clicking over a trace, the event behaves as before with full point data.

Example:

layout: {
  hoveranywhere: true, // enables hover events anywhere, even on empty plotting area
  clickanywhere: true // same for click events
}

See this codepen for an interactive demo of this feature

Screenshot 2026-02-13 at 1 19 35 PM

New API:

hoveranywhere

  • Type: boolean
  • Default: false
  • When true, plotly_hover events fire for any cursor position within the plot area, not just over traces. Events on empty space include an empty points array plus xvals and yvals with cursor coordinates in data space.

clickanywhere

  • Type: boolean
  • Default: false
  • When true, plotly_click events fire for any click position within the plot area, not just over traces. Events on empty space include an empty points array plus xvals and yvals with click coordinates in data space.

@alexshoe alexshoe requested a review from emilykl February 13, 2026 18:20
@alexshoe alexshoe linked an issue Feb 13, 2026 that may be closed by this pull request
@alexshoe alexshoe marked this pull request as ready for review February 13, 2026 18:20
@alexcjohnson
Copy link
Collaborator

@alexshoe this looks great! Really nice codepen, does a great job showing the feature and how to use it 🎉 But the pseudo-spikeline is a little off. There are actually two ways you might want this to behave, and both have some issues today.

(1) Follow the cursor even if there's a data point to hover on. That's mostly what happens in the codepen as written right now, except that when you DO get a data point, we stop emitting hover events until you get a different data point. This means that the pseudo-spikeline gets stuck at the first mouse position where plotlyjs picked a particular data point, until you get to a position where it picks a different data point. Seems to me when we have hover anywhere - and maybe also when we have spike lines snapping to the cursor? - this means we need to emit a hover event, and update the hoverdata so the click event will get the proper coordinates, on every mouse move even if the hovered data point exists and has not changed.

(2) Stick to the data point when you're hovered on one, or follow the cursor when not near a data point. For this flavor I think we have all the events we need, and if you make the pseudo-spikeline out of a shape, I think we have all the information we need in the event. But if you make the pseudo-spikeline out of a raw DOM element, d.event.offsetY isn't correct when you have a data point, you need the actual pixel position of the data point. Personally I could probably get this using things like fullLayout.yaxis.d2p and fullLayout.yaxis._offset but we probably don't want our users to have to use our internals like this; better would be to publish the mouse location of the actual hover point as part of the event data.

@alexshoe
Copy link
Contributor Author

@alexcjohnson Thanks for the comment and for going in depth in those two use cases. Do you think we should enable both of these modes with another attribute or maybehoveranwhere allows you to pick a mode where data points are non-sticky?

For point two, by "mouse location of the actual hover point", do you mean the pixel position of the mouse or the data point that we are close to? I think if we published the latter, we would be able to easily modify my CodePen such that the spikeline follows the cursor when not near a data point, then snaps to the exact position of the nearest data point, when close enough.

For point 1, there's a function in hover.js called hoverChanged. When the mouse moves but the closest data point hasn't changed, hoverChanged returns false and hover event emission is skipped. So we can just set it to NOT skip emission if hoveranywhere is true.

Curious to know what you think

@alexcjohnson
Copy link
Collaborator

Do you think we should enable both of these modes with another attribute or maybehoveranwhere allows you to pick a mode where data points are non-sticky?

I don't see that we need any new attributes here, just more events and more data in the existing events.

For point two, by "mouse location of the actual hover point", do you mean the pixel position of the mouse or the data point that we are close to? I think if we published the latter, we would be able to easily modify my CodePen such that the spikeline follows the cursor when not near a data point, then snaps to the exact position of the nearest data point, when close enough.

Yes, the latter. Specifically I guess the position where we would draw the spikeline, which can be different from the position of the data point or the position of the hover box in certain cases, like grouped bars where the data point and hover box are drawn to the bar, wherever it's aligned, while the spikeline is drawn to the data value.

For point 1, there's a function in hover.js called hoverChanged. When the mouse moves but the closest data point hasn't changed, hoverChanged returns false and hover event emission is skipped. So we can just set it to NOT skip emission if hoveranywhere is true.

That seems likely to give the behavior we want!

@alexshoe
Copy link
Contributor Author

@alexcjohnson thanks for the clarification. I've made both changes so that:

  1. Hover events ALWAYS fire if hoveranywhere is on, even if near a data point
  2. Expose x- and. y-pixel values data points

I updated this codepen to reflect these changes too. You'll notice the spikeline now follows the cursor at all times, regardless of proximity to data points. There is also a new hover label that does snap to nearby datapoints, and shares the same color as the trace it references.

@alexshoe alexshoe requested a review from camdecoster February 27, 2026 17:24
@alexcjohnson
Copy link
Collaborator

Excellent, thanks @alexshoe! The updated code and the new behavior in the codepen both look good to me now.

codeCraft-Ritik

This comment was marked as spam.

@alexshoe
Copy link
Contributor Author

alexshoe commented Mar 6, 2026

@camdecoster and @emilykl, whenever you get a free moment, would you mind taking a look at this PR? Thank you so much!

@emilykl
Copy link
Contributor

emilykl commented Mar 10, 2026

@alexcjohnson It occurs to me that perhaps these properties (hoveranywhere and clickanywhere) should be part of config rather than layout -- do you have thoughts on which location is more appropriate?

y1: y1 + gTop
};

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?

}
}

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

Comment on lines +791 to +800
if (fullLayout.hoveranywhere && !noHoverEvent && eventTarget) {
gd.emit('plotly_hover', {
event: evt,
points: [],
xaxes: xaArray,
yaxes: yaArray,
xvals: xvalArray,
yvals: yvalArray
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be more maintanable to have this gd.emit('plotly_hover', ...) (for the hoveranywhere case) closer to the gd.emit('plotly_hover', ...) for the normal case (which I believe is on line 950-957). To ensure that the same keys are included in both objects, etc.

I realize we do want to keep the early return on line 801.

Maybe you could add a emitHover() function akin to emitClick(), and then call it in both cases (both here and on line 950).

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.

@emilykl
Copy link
Contributor

emilykl commented Mar 10, 2026

@alexshoe Can you document somewhere what xPixel and yPixel mean? I assume they're the pixel position of the hover/click, but relative to what? The upper left corner of the plot div?

Comment on lines +53 to +57
expect(hoverData).toBeDefined();
expect(hoverData.points).toEqual([]);
expect(hoverData.xvals.length).toBe(1);
expect(hoverData.yvals.length).toBe(1);
expect(typeof hoverData.xvals[0]).toBe('number');
Copy link
Contributor

Choose a reason for hiding this comment

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

Should verify exact values of all hoverData keys here, for thoroughness. No reason not to. Same goes for all of these tests.

beforeEach(function() { gd = createGraphDiv(); });
afterEach(destroyGraphDiv);

function _hover(xPixel, yPixel) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not have a _hover() utility function elsewhere in the tests we can use here?

.then(done, done.fail);
});

it('does not fire on empty space by default', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('does not fire on empty space by default', function(done) {
it('does not emit plotly_click event on empty space when clickanywhere is false', function(done) {

.then(done, done.fail);
});

it('does not fire on empty space by default', function(done) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('does not fire on empty space by default', function(done) {
it('does not emit plotly_hover event on empty space when hoveranywhere is false', function(done) {

@alexcjohnson
Copy link
Collaborator

@alexcjohnson It occurs to me that perhaps these properties (hoveranywhere and clickanywhere) should be part of config rather than layout -- do you have thoughts on which location is more appropriate?

Oh that's a good point - the original concept of config vs layout is anything that's not portable, ie it depends on the context in which you're displaying the plot, belongs in config. And these clearly aren't portable, because they depend on event handlers being attached to the plot. I'm not sure if this distinction is meaningful or intuitive to users (or as important as it what many years ago when we created it and Chart Studio, with its sharing and separate view, edit, and embed views, was the main consumer of these plots), and we've broken that convention with good reason when the context-dependent attribute is scoped to a particular component like one shape, subplot, or trace - certainly don't want to implement a parallel object structure just for config!

All that to say I don't feel strongly about it, but if you think these would be easier to find in config I can get on board.

gd.on('plotly_click', function(d) { clickData = d; });

var s = gd._fullLayout._size;
click(s.l + 250, s.t + 50);
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this clicking outside the plot div?

@emilykl
Copy link
Contributor

emilykl commented Mar 10, 2026

@alexshoe I've finished my first pass review! All looks good, left some fairly minor comments.

Ideally this feature should add (almost) zero additional overhead when hoveranywhere and clickanywhere are false. Do you know whether that's the case? I'd have to do another pass through to confirm but maybe you've thought about this already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Enable click and hover events anywhere

4 participants