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
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,33 +91,33 @@ jobs:
- uses: ./.github/actions/setup-workspace
- uses: ./.github/actions/setup-chrome

- name: Run hover_label test in UTC timezone
- name: Run hover test in UTC timezone
uses: ./.github/actions/run-xvfb
env:
TZ: 'UTC'
with:
run: npm run test-jasmine hover_label
run: npm run test-jasmine hover

- name: Run hover_label test in Europe/Berlin timezone
- name: Run hover test in Europe/Berlin timezone
uses: ./.github/actions/run-xvfb
env:
TZ: 'Europe/Berlin'
with:
run: npm run test-jasmine hover_label
run: npm run test-jasmine hover

- name: Run hover_label test in Asia/Tokyo timezone
- name: Run hover test in Asia/Tokyo timezone
uses: ./.github/actions/run-xvfb
env:
TZ: 'Asia/Tokyo'
with:
run: npm run test-jasmine hover_label
run: npm run test-jasmine hover

- name: Run hover_label test in America/Toronto timezone
- name: Run hover test in America/Toronto timezone
uses: ./.github/actions/run-xvfb
env:
TZ: 'America/Toronto'
with:
run: npm run test-jasmine hover_label
run: npm run test-jasmine hover

no-gl-jasmine:
needs: install-and-cibuild
Expand Down
2 changes: 2 additions & 0 deletions draftlogs/7966_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add top-level `xPixel` and `yPixel` keys to hover and click event data, corresponding to the pixel position of the cursor relative to the top-left corner of the graph div [[#7966](https://github.com/plotly/plotly.js/pull/7966)]
- When `hoveranywhere` is enabled, emit a `plotly_unhover` event when the cursor leaves the plot area [[#7966](https://github.com/plotly/plotly.js/pull/7966)]
24 changes: 22 additions & 2 deletions src/components/dragelement/unhover.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,38 @@ unhover.wrapped = function(gd, evt, subplot) {
throttle.clear(gd._fullLayout._uid + hoverConstants.HOVERID);
}

unhover.raw(gd, evt, subplot);
const oldhoverdata = gd._hoverdata;

const shouldEmitUnhover = unhover.raw(gd, evt, subplot);

// Special handling for `hoveranywhere`, to ensure we emit exactly one unhover event
// when the cursor leaves the plot area.
// gd._hoverAnywhereActive is set in fx/hover.js when we emit an empty-space hover event.
if(shouldEmitUnhover && gd._hoverAnywhereActive) {
gd._hoverAnywhereActive = false;

// Make sure hoveranywhere is still enabled
if(gd._fullLayout?.hoveranywhere && evt?.target && !oldhoverdata) {
gd.emit('plotly_unhover', {
event: evt,
points: []
});
Comment on lines +31 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This event doesn't check to see if plotly_beforehover is false, but it probably should to preserve that behavior. The current check happens inside raw. You could save that result on gd (or somewhere else) and use it in the conditional check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, I'll update the logic.

@emilykl emilykl Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b068fe4

}
}
};


// remove hover effects on mouse out, and emit unhover event
// returns false if unhover was skipped due to the plotly_beforehover handler returning false;
// returns true otherwise
unhover.raw = function raw(gd, evt) {
var fullLayout = gd._fullLayout;
var oldhoverdata = gd._hoverdata;

if(!evt) evt = {};
if(evt.target && !gd._dragged &&
Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) {
return;
return false;
}

fullLayout._hoverlayer.selectAll('g').remove();
Expand All @@ -42,4 +61,5 @@ unhover.raw = function raw(gd, evt) {
points: oldhoverdata
});
}
return true;
};
2 changes: 2 additions & 0 deletions src/components/fx/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = function click(gd, evt, subplot) {
clickData.yaxes ??= gd._hoverYAxes;
clickData.xvals ??= gd._hoverXVals && helpers.c2dApply(gd._hoverXAxes, gd._hoverXVals);
clickData.yvals ??= gd._hoverYVals && helpers.c2dApply(gd._hoverYAxes, gd._hoverYVals);
clickData.xPixel ??= gd._hoverPointerX;
clickData.yPixel ??= gd._hoverPointerY;

gd.emit('plotly_click', clickData);
}
Expand Down
16 changes: 15 additions & 1 deletion src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
if ('yval' in evt) yvalArray = helpers.flat(subplots, evt.yval);
else yvalArray = helpers.p2c(yaArray, ypx);

// Save pointer position to gd so that it can be included in all hover/click event data,
// even when hoveranywhere and clickanywhere are not enabled
gd._hoverPointerX = evt.pointerX;
gd._hoverPointerY = evt.pointerY;

if (!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) {
Lib.warn('Fx.hover failed', evt, gd);
return dragElement.unhoverRaw(gd, evt);
Expand Down Expand Up @@ -818,6 +823,11 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
gd._hoverdata = [];
}
emitHover([]);

// Set a flag to note that an empty-space hover event is being emitted,
// so that we know to emit an unhover event when the mouse leaves the plot area.
// See dragelement/unhover.js.
gd._hoverAnywhereActive = true;
}
return result;
}
Expand Down Expand Up @@ -977,7 +987,11 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
xaxes: xaArray,
yaxes: yaArray,
xvals: helpers.c2dApply(xaArray, xvalArray),
yvals: helpers.c2dApply(yaArray, yvalArray)
yvals: helpers.c2dApply(yaArray, yvalArray),
// Note: top-level xPixel/yPixel correspond to the pixel position of the cursor.
// Inside `points` array, points[i].xPixel/yPixel correspond to the pixel position of the point itself.
xPixel: evt.pointerX,
yPixel: evt.pointerY
});
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ module.exports = {
'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.'
'but will include `xvals` and `yvals` with cursor coordinates in data space,',
'and `xPixel` and `yPixel` with cursor coordinates in pixels,',
'relative to the top-left corner of the graph div.',
'A `plotly_unhover` event fires when the cursor leaves the plot area.'
].join(' ')
},
clickanywhere: {
Expand All @@ -123,7 +126,9 @@ module.exports = {
'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.'
'but will include `xvals` and `yvals` with click coordinates in data space,',
'and `xPixel` and `yPixel` with click coordinates in pixels,',
'relative to the top-left corner of the graph div.'
].join(' ')
},
hoverdistance: {
Expand Down
8 changes: 8 additions & 0 deletions src/types/core/events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,14 @@ export interface PlotDatum {
x: Datum;
/** Axis the point's x is plotted against. */
xaxis: LayoutAxis;
/** Point center x position in pixels from the graph div's top-left corner */
xPixel?: number;
/** y coordinate of the point. */
y: Datum;
/** Axis the point's y is plotted against. */
yaxis: LayoutAxis;
/** Point center y position in pixels from the graph div's top-left corner */
yPixel?: number;
/** Resolved hover/display text for the point. */
text: string;
}
Expand Down Expand Up @@ -104,6 +108,10 @@ export interface PlotMouseEvent {
points: PlotDatum[];
/** The original DOM mouse event. */
event: MouseEvent;
/** Cursor x position in pixels from the graph div's top-left corner */
xPixel?: number;
/** Cursor y position in pixels from the graph div's top-left corner */
yPixel?: number;
}

/** Payload for `plotly_hover` — augments `PlotMouseEvent` with axis values. */
Expand Down
4 changes: 2 additions & 2 deletions src/types/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15945,7 +15945,7 @@ export interface Layout {
*/
calendar?: Calendar;
/**
* 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.
* 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, and `xPixel` and `yPixel` with click coordinates in pixels, relative to the top-left corner of the graph div.
* @default false
*/
clickanywhere?: boolean;
Expand Down Expand Up @@ -16058,7 +16058,7 @@ export interface Layout {
*/
height?: number;
/**
* 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.
* 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, and `xPixel` and `yPixel` with cursor coordinates in pixels, relative to the top-left corner of the graph div. A `plotly_unhover` event fires when the cursor leaves the plot area.
* @default false
*/
hoveranywhere?: boolean;
Expand Down
23 changes: 20 additions & 3 deletions test/jasmine/tests/click_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ describe('Test click interactions:', function() {
expect(contextPassthroughs).toBe(0);
});

function checkData() {
expect(Object.keys(futureData).sort()).toEqual([
'event', 'points', 'xaxes', 'yaxes', 'xvals', 'yvals', 'xPixel', 'yPixel'
].sort());

expect(futureData.event).not.toBe(null);
checkPointData();
// xvals, yvals, xaxes, and yaxes should all be undefined since clickanywhere is not enabled
expect(futureData.xvals).toBe(undefined);
expect(futureData.yvals).toBe(undefined);
expect(futureData.xaxes).toBe(undefined);
expect(futureData.yaxes).toBe(undefined);
// However, xPixel and yPixel should be defined and match the click position
expect(futureData.xPixel).toEqual(pointPos[0]);
expect(futureData.yPixel).toEqual(pointPos[1]);
}

function checkPointData() {
expect(futureData.points.length).toEqual(1);
expect(clickPassthroughs).toBe(2);
Expand All @@ -131,14 +148,14 @@ describe('Test click interactions:', function() {
expect(evt.clientY).toEqual(pointPos[1]);
}

it('should contain the correct fields', function() {
it('should contain the correct fields with the correct values', function() {
click(pointPos[0], pointPos[1]);
checkPointData();
checkData();
});

it('should work with a sloppy click (shift < minDrag before mouseup)', function() {
click(pointPos[0], pointPos[1], {slop: [4, 4]});
checkPointData();
checkData();
});

it('works with fixedrange axes', function(done) {
Expand Down
Loading
Loading