-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Improvements to hoveranywhere / clickanywhere feature
#7966
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4.0
Are you sure you want to change the base?
Changes from all commits
8e3d3a1
c50e9d0
a606c3a
a43a4b6
c1c4662
1e34379
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - 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. Also, 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)] | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,23 @@ unhover.wrapped = function(gd, evt, subplot) { | |||||
| throttle.clear(gd._fullLayout._uid + hoverConstants.HOVERID); | ||||||
| } | ||||||
|
|
||||||
| var oldhoverdata = gd._hoverdata; | ||||||
|
|
||||||
| 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(gd._hoverAnywhereActive) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we update the guard to also check if
Suggested change
|
||||||
| gd._hoverAnywhereActive = false; | ||||||
|
|
||||||
| if(evt && evt.target && !oldhoverdata) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| gd.emit('plotly_unhover', { | ||||||
| event: evt, | ||||||
| points: [] | ||||||
| }); | ||||||
|
Comment on lines
+30
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This event doesn't check to see if |
||||||
| } | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -483,6 +483,8 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { | |
| gd._hoverYVals = yvalArray; | ||
| gd._hoverXAxes = xaArray; | ||
| gd._hoverYAxes = yaArray; | ||
| gd._hoverPointerX = evt.pointerX; | ||
| gd._hoverPointerY = evt.pointerY; | ||
|
Comment on lines
+486
to
+487
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these need to be moved out of this block to ensure that they're available without |
||
| } | ||
|
|
||
| // the pixel distance to beat as a matching point | ||
|
|
@@ -818,6 +820,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; | ||
| } | ||
|
|
@@ -977,7 +984,11 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { | |
| xaxes: xaArray, | ||
| yaxes: yaArray, | ||
| xvals: xvalArray, | ||
| yvals: yvalArray | ||
| yvals: 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 | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ var Lib = require('../../../src/lib'); | |||||
| var createGraphDiv = require('../assets/create_graph_div'); | ||||||
| var destroyGraphDiv = require('../assets/destroy_graph_div'); | ||||||
| var click = require('../assets/click'); | ||||||
| var mouseEvent = require('../assets/mouse_event'); | ||||||
|
|
||||||
| function makePlot(gd, layoutExtras = {}, configExtras) { | ||||||
| return Plotly.newPlot( | ||||||
|
|
@@ -55,6 +56,15 @@ describe('hoveranywhere', () => { | |||||
| Lib.clearThrottle(); | ||||||
| } | ||||||
|
|
||||||
| // leave the plot area, as the maindrag sees it | ||||||
| function _leavePlotArea() { | ||||||
| var bb = gd.getBoundingClientRect(); | ||||||
| mouseEvent('mouseout', bb.left - 50, bb.top - 50, { | ||||||
| element: gd.querySelector('.nsewdrag') | ||||||
| }); | ||||||
| Lib.clearThrottle(); | ||||||
| } | ||||||
|
|
||||||
| it('emits plotly_hover with coordinate data on empty space', (done) => { | ||||||
| var hoverData; | ||||||
|
|
||||||
|
|
@@ -75,6 +85,8 @@ describe('hoveranywhere', () => { | |||||
| expect(hoverData.yvals.length).toBe(1); | ||||||
| expect(hoverData.xvals[0]).toBeCloseTo(250 / 30, 2); | ||||||
| expect(hoverData.yvals[0]).toBeCloseTo(10 - 50 / 30, 2); | ||||||
| expect(hoverData.xPixel).toBeCloseTo(300, 1); // hover x-position (250) + left margin (50) | ||||||
| expect(hoverData.yPixel).toBeCloseTo(100, 1); // hover y-position (50) + top margin (50) | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
@@ -129,6 +141,27 @@ describe('hoveranywhere', () => { | |||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('reports cursor position in top-level xPixel/yPixel, and point position in point-level xPixel/yPixel', (done) => { | ||||||
| var hoverData; | ||||||
|
|
||||||
| makePlot(gd, { hoveranywhere: true }) | ||||||
| .then(() => { | ||||||
| gd.on('plotly_hover', (d) => (hoverData = d)); | ||||||
|
|
||||||
| // hover near, but not exactly on, the point (2, 3), which is at px (60, 210) | ||||||
| _hover(65, 205); | ||||||
|
|
||||||
| expect(hoverData.points.length).toBe(1); | ||||||
| // top-level: cursor position | ||||||
| expect(hoverData.xPixel).toBeCloseTo(115, 1); // hover x-position (65) + left margin (50) | ||||||
| expect(hoverData.yPixel).toBeCloseTo(255, 1); // hover y-position (205) + top margin (50) | ||||||
| // point-level: position of the point itself | ||||||
| expect(hoverData.points[0].xPixel).toBeCloseTo(110, 1); // point x-position in plot area (60) + left margin (50) | ||||||
| expect(hoverData.points[0].yPixel).toBeCloseTo(260, 1); // point y-position in plot area (210) + top margin (50) | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('respects hovermode:false', (done) => { | ||||||
| var hoverData; | ||||||
|
|
||||||
|
|
@@ -141,6 +174,102 @@ describe('hoveranywhere', () => { | |||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('emits plotly_unhover when the cursor leaves the plot area after hovering empty space', (done) => { | ||||||
| var events = []; | ||||||
| var unhoverData; | ||||||
|
|
||||||
| makePlot(gd, { hoveranywhere: true }) | ||||||
| .then(() => { | ||||||
| gd.on('plotly_hover', () => events.push('hover')); | ||||||
| gd.on('plotly_unhover', (d) => { | ||||||
| events.push('unhover'); | ||||||
| unhoverData = d; | ||||||
| }); | ||||||
|
|
||||||
| _hover(250, 50); | ||||||
| expect(events).toEqual(['hover']); | ||||||
|
|
||||||
| _leavePlotArea(); | ||||||
|
|
||||||
| expect(events).toEqual(['hover', 'unhover']); | ||||||
| expect(unhoverData.points).toEqual([]); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('emits only one unhover per departure from the plot area', (done) => { | ||||||
| var events = []; | ||||||
|
|
||||||
| makePlot(gd, { hoveranywhere: true }) | ||||||
| .then(() => { | ||||||
| gd.on('plotly_unhover', () => events.push('unhover')); | ||||||
|
|
||||||
| _hover(250, 50); | ||||||
| _leavePlotArea(); | ||||||
| _leavePlotArea(); | ||||||
|
|
||||||
| expect(events).toEqual(['unhover']); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('does not emit unhover while moving within empty space', (done) => { | ||||||
| var events = []; | ||||||
|
|
||||||
| makePlot(gd, { hoveranywhere: true }) | ||||||
| .then(() => { | ||||||
| gd.on('plotly_hover', () => events.push('hover')); | ||||||
| gd.on('plotly_unhover', () => events.push('unhover')); | ||||||
|
|
||||||
| _hover(250, 50); | ||||||
| _hover(255, 55); | ||||||
| _hover(260, 60); | ||||||
|
|
||||||
| expect(events).toEqual(['hover', 'hover', 'hover']); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('emits unhover with point data, not empty points, when leaving from a point', (done) => { | ||||||
| var events = []; | ||||||
| var unhoverData; | ||||||
|
|
||||||
| makePlot(gd, { hoveranywhere: true }) | ||||||
| .then(() => { | ||||||
| gd.on('plotly_unhover', (d) => { | ||||||
| events.push('unhover'); | ||||||
| unhoverData = d; | ||||||
| }); | ||||||
|
|
||||||
| // hover empty space, then the point (2, 3), then leave | ||||||
| _hover(250, 50); | ||||||
| _hover(60, 210); | ||||||
| _leavePlotArea(); | ||||||
|
|
||||||
| expect(events).toEqual(['unhover']); | ||||||
| expect(unhoverData.points.length).toBe(1); | ||||||
| expect(unhoverData.points[0].x).toBe(2); | ||||||
| expect(unhoverData.points[0].y).toBe(3); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('does not emit unhover on leaving empty space when hoveranywhere is false', (done) => { | ||||||
| var events = []; | ||||||
|
|
||||||
| makePlot(gd) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be good to make this explicit:
Suggested change
|
||||||
| .then(() => { | ||||||
| gd.on('plotly_hover', () => events.push('hover')); | ||||||
| gd.on('plotly_unhover', () => events.push('unhover')); | ||||||
|
|
||||||
| _hover(250, 50); | ||||||
| _leavePlotArea(); | ||||||
|
|
||||||
| expect(events).toEqual([]); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
| it('emits plotly_hover over an editable shape', (done) => { | ||||||
| let hoverData; | ||||||
|
|
||||||
|
|
@@ -170,11 +299,13 @@ describe('hoveranywhere', () => { | |||||
| const bb = gd.getBoundingClientRect(); | ||||||
| const s = gd._fullLayout._size; | ||||||
| // center of shape at data (7.5, 7.5) = plot-area px (225, 75) | ||||||
| const mouseX = bb.left + s.l + 225; | ||||||
| const mouseY = bb.top + s.t + 75; | ||||||
| shapePath.dispatchEvent( | ||||||
| new MouseEvent('mousemove', { | ||||||
| bubbles: true, | ||||||
| clientX: bb.left + s.l + 225, | ||||||
| clientY: bb.top + s.t + 75 | ||||||
| clientX: mouseX, | ||||||
| clientY: mouseY | ||||||
| }) | ||||||
| ); | ||||||
| Lib.clearThrottle(); | ||||||
|
|
@@ -183,6 +314,10 @@ describe('hoveranywhere', () => { | |||||
| expect(hoverData.points).toEqual([]); | ||||||
| expect(hoverData.xvals[0]).toBeCloseTo(7.5, 1); | ||||||
| expect(hoverData.yvals[0]).toBeCloseTo(7.5, 1); | ||||||
| // mouseX and mouseY are relative to the full page, so subtract the bounding box | ||||||
| // to get pixel coordinates relative to the graph div, which should match hoverData.xPixel/yPixel | ||||||
| expect(hoverData.xPixel).toBeCloseTo(mouseX - bb.left, 1); | ||||||
| expect(hoverData.yPixel).toBeCloseTo(mouseY - bb.top, 1); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
@@ -248,9 +383,12 @@ describe('clickanywhere', () => { | |||||
| .then(() => { | ||||||
| gd.on('plotly_click', (d) => (clickData = d)); | ||||||
|
|
||||||
| var bb = gd.getBoundingClientRect(); | ||||||
| var s = gd._fullLayout._size; | ||||||
| click(bb.left + s.l + 250, bb.top + s.t + 50); | ||||||
| const bb = gd.getBoundingClientRect(); | ||||||
| const s = gd._fullLayout._size; | ||||||
| const clickX = bb.left + s.l + 250; | ||||||
| const clickY = bb.top + s.t + 50; | ||||||
|
|
||||||
| click(clickX, clickY); | ||||||
|
|
||||||
| expect(clickData).toBeDefined(); | ||||||
| expect(clickData.points).toEqual([]); | ||||||
|
|
@@ -262,6 +400,10 @@ describe('clickanywhere', () => { | |||||
| expect(clickData.xvals[0]).toBeCloseTo(250 / 30, 2); | ||||||
| // click at 50px into 300px plot area, yrange [0,10]: 10 - 50/300*10 = 8.33 | ||||||
| expect(clickData.yvals[0]).toBeCloseTo(10 - 50 / 30, 2); | ||||||
| // click pixels: clickX and clickY are relative to full page, so subtract the graph div bounding box | ||||||
| // to get pixel coordinates relative to the graph div, which should match clickData.xPixel/yPixel | ||||||
| expect(clickData.xPixel).toBeCloseTo(clickX - bb.left, 1); | ||||||
| expect(clickData.yPixel).toBeCloseTo(clickY - bb.top, 1); | ||||||
| }) | ||||||
| .then(done, done.fail); | ||||||
| }); | ||||||
|
|
||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.