Skip to content

Commit 36baa98

Browse files
authored
Merge pull request #7707 from alexshoe/hover-click-events-anywhere
Enable Hover and Click Events Anywhere
2 parents 928ad92 + 9a823c0 commit 36baa98

File tree

13 files changed

+292
-36
lines changed

13 files changed

+292
-36
lines changed

draftlogs/7707_add.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `hoveranywhere` and `clickanywhere` layout attributes to emit hover and click events anywhere in the plot area, not just over traces [[#7707](https://github.com/plotly/plotly.js/pull/7707)]

src/components/fx/click.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var hover = require('./hover').hover;
55

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

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

17-
function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, event: evt}); }
18+
function emitClick() {
19+
var clickData = {points: gd._hoverdata, event: evt};
20+
21+
// get coordinate values from latest hover call, if available
22+
clickData.xaxes ??= gd._hoverXAxes;
23+
clickData.yaxes ??= gd._hoverYAxes;
24+
clickData.xvals ??= gd._hoverXVals;
25+
clickData.yvals ??= gd._hoverYVals;
1826

19-
if(gd._hoverdata && evt && evt.target) {
27+
gd.emit('plotly_click', clickData);
28+
}
29+
30+
if((gd._hoverdata || fullLayout.clickanywhere) && evt && evt.target) {
31+
if(!gd._hoverdata) gd._hoverdata = [];
2032
if(annotationsDone && annotationsDone.then) {
2133
annotationsDone.then(emitClick);
2234
} else emitClick();

src/components/fx/hover.js

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ exports.loneHover = function loneHover(hoverItems, opts) {
167167
y1: y1 + gTop
168168
};
169169

170+
// xPixel/yPixel are pixel coordinates of the hover point's center,
171+
// relative to the top-left corner of the graph div
172+
eventData.xPixel = (_x0 + _x1) / 2;
173+
eventData.yPixel = (_y0 + _y1) / 2;
174+
170175
if (opts.inOut_bbox) {
171176
opts.inOut_bbox.push(eventData.bbox);
172177
}
@@ -473,6 +478,14 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
473478
}
474479
}
475480

481+
// Save coordinate values so clickanywhere can be used without hoveranywhere
482+
if (fullLayout.clickanywhere) {
483+
gd._hoverXVals = xvalArray;
484+
gd._hoverYVals = yvalArray;
485+
gd._hoverXAxes = xaArray;
486+
gd._hoverYAxes = yaArray;
487+
}
488+
476489
// the pixel distance to beat as a matching point
477490
// in 'x' or 'y' mode this resets for each trace
478491
var distance = Infinity;
@@ -778,6 +791,18 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
778791
createSpikelines(gd, spikePoints, spikelineOpts);
779792
}
780793
}
794+
795+
if (fullLayout.hoveranywhere && !noHoverEvent && eventTarget) {
796+
var oldHoverData = gd._hoverdata;
797+
if (oldHoverData && oldHoverData.length) {
798+
gd.emit('plotly_unhover', {
799+
event: evt,
800+
points: oldHoverData
801+
});
802+
gd._hoverdata = [];
803+
}
804+
emitHover([]);
805+
}
781806
return result;
782807
}
783808

@@ -877,6 +902,9 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
877902
y0: y0 + gTop,
878903
y1: y1 + gTop
879904
};
905+
906+
eventData.xPixel = (_x0 + _x1) / 2;
907+
eventData.yPixel = (_y0 + _y1) / 2;
880908
}
881909

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

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

919-
if (oldhoverdata) {
948+
if (oldhoverdata && _hoverChanged) {
920949
gd.emit('plotly_unhover', {
921950
event: evt,
922951
points: oldhoverdata
923952
});
924953
}
925954

926-
gd.emit('plotly_hover', {
927-
event: evt,
928-
points: gd._hoverdata,
929-
xaxes: xaArray,
930-
yaxes: yaArray,
931-
xvals: xvalArray,
932-
yvals: yvalArray
933-
});
955+
emitHover(gd._hoverdata);
956+
957+
function emitHover(points) {
958+
gd.emit('plotly_hover', {
959+
event: evt,
960+
points: points,
961+
xaxes: xaArray,
962+
yaxes: yaArray,
963+
xvals: xvalArray,
964+
yvals: yvalArray
965+
});
966+
}
934967
}
935968

936969
function hoverDataKey(d) {

src/components/fx/hovermode_defaults.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ module.exports = function handleHoverModeDefaults(layoutIn, layoutOut) {
1313

1414
coerce('clickmode');
1515
coerce('hoversubplots');
16+
coerce('hoveranywhere');
17+
coerce('clickanywhere');
1618
return coerce('hovermode');
1719
};

src/components/fx/layout_attributes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ module.exports = {
9191
'when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.',
9292
].join(' ')
9393
},
94+
hoveranywhere: {
95+
valType: 'boolean',
96+
dflt: false,
97+
editType: 'none',
98+
description: [
99+
'If true, `plotly_hover` events will fire for any cursor position',
100+
'within the plot area, not just over traces.',
101+
'When the cursor is not over a trace, the event will have an empty `points` array',
102+
'but will include `xvals` and `yvals` with cursor coordinates in data space.'
103+
].join(' ')
104+
},
105+
clickanywhere: {
106+
valType: 'boolean',
107+
dflt: false,
108+
editType: 'none',
109+
description: [
110+
'If true, `plotly_click` events will fire for any click position',
111+
'within the plot area, not just over traces.',
112+
'When clicking where there is no trace data, the event will have an empty `points` array',
113+
'but will include `xvals` and `yvals` with click coordinates in data space.'
114+
].join(' ')
115+
},
94116
hoverdistance: {
95117
valType: 'integer',
96118
min: -1,
5.46 KB
Loading

test/jasmine/tests/click_test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ describe('Test click interactions:', function() {
119119
expect(Object.keys(pt).sort()).toEqual([
120120
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
121121
'bbox',
122-
'x', 'y', 'xaxis', 'yaxis'
122+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
123123
].sort());
124124
expect(pt.curveNumber).toEqual(0);
125125
expect(pt.pointNumber).toEqual(11);
@@ -153,7 +153,7 @@ describe('Test click interactions:', function() {
153153
expect(Object.keys(pt).sort()).toEqual([
154154
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
155155
'bbox',
156-
'x', 'y', 'xaxis', 'yaxis'
156+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
157157
].sort());
158158
expect(pt.curveNumber).toEqual(0);
159159
expect(pt.pointNumber).toEqual(11);
@@ -225,7 +225,7 @@ describe('Test click interactions:', function() {
225225
expect(Object.keys(pt).sort()).toEqual([
226226
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
227227
'bbox',
228-
'x', 'y', 'xaxis', 'yaxis'
228+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
229229
].sort());
230230
expect(pt.curveNumber).toEqual(0);
231231
expect(pt.pointNumber).toEqual(11);
@@ -315,7 +315,7 @@ describe('Test click interactions:', function() {
315315
expect(Object.keys(pt).sort()).toEqual([
316316
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
317317
'bbox',
318-
'x', 'y', 'xaxis', 'yaxis'
318+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
319319
].sort());
320320
expect(pt.curveNumber).toEqual(0);
321321
expect(pt.pointNumber).toEqual(11);
@@ -349,7 +349,7 @@ describe('Test click interactions:', function() {
349349
expect(Object.keys(pt).sort()).toEqual([
350350
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
351351
'bbox',
352-
'x', 'y', 'xaxis', 'yaxis'
352+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
353353
].sort());
354354
expect(pt.curveNumber).toEqual(0);
355355
expect(pt.pointNumber).toEqual(11);
@@ -387,7 +387,7 @@ describe('Test click interactions:', function() {
387387
expect(Object.keys(pt).sort()).toEqual([
388388
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex',
389389
'bbox',
390-
'x', 'y', 'xaxis', 'yaxis'
390+
'x', 'y', 'xaxis', 'yaxis', 'xPixel', 'yPixel'
391391
].sort());
392392
expect(pt.curveNumber).toEqual(0);
393393
expect(pt.pointNumber).toEqual(11);

test/jasmine/tests/geo_test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ describe('Test geo interactions', function() {
882882
it('should contain the correct fields', function() {
883883
expect(Object.keys(ptData).sort()).toEqual([
884884
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
885-
'lon', 'lat', 'location', 'marker.size'
885+
'lon', 'lat', 'location', 'marker.size', 'xPixel', 'yPixel'
886886
].sort());
887887
expect(cnt).toEqual(1);
888888
});
@@ -947,7 +947,7 @@ describe('Test geo interactions', function() {
947947
it('should contain the correct fields', function() {
948948
expect(Object.keys(ptData).sort()).toEqual([
949949
'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'bbox',
950-
'lon', 'lat', 'location', 'marker.size'
950+
'lon', 'lat', 'location', 'marker.size', 'xPixel', 'yPixel'
951951
].sort());
952952
});
953953

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

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

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

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

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

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

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

19431943
expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');

0 commit comments

Comments
 (0)