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
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.

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 c6afbc6

}
}
};


// 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
3 changes: 3 additions & 0 deletions src/types/core/events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export interface PlotMouseEvent {
points: PlotDatum[];
/** The original DOM mouse event. */
event: MouseEvent;
/** x and y pixel position of the mouse */
xPixel: number;
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