Skip to content

Commit 57d2d85

Browse files
authored
Merge pull request #6947 from plotly/res4755-hover-subplots
Add `layout.subplots` to enable (x|y) hover effects across multiple cartesian and splom suplots sharing one axis
2 parents d20b8e2 + d9aea4b commit 57d2d85

File tree

7 files changed

+531
-23
lines changed

7 files changed

+531
-23
lines changed

Diff for: draftlogs/6947_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `layout.hoversubplots` to enable hover effects across multiple cartesian suplots sharing one axis [[#6947](https://github.com/plotly/plotly.js/pull/6947)]

Diff for: src/components/fx/hover.js

+46-14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var isNumeric = require('fast-isnumeric');
55
var tinycolor = require('tinycolor2');
66

77
var Lib = require('../../lib');
8+
var pushUnique = Lib.pushUnique;
89
var strTranslate = Lib.strTranslate;
910
var strRotate = Lib.strRotate;
1011
var Events = require('../../lib/events');
@@ -257,13 +258,40 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
257258
// use those instead of finding overlayed plots
258259
var subplots = Array.isArray(subplot) ? subplot : [subplot];
259260

261+
var spId;
262+
260263
var fullLayout = gd._fullLayout;
264+
var hoversubplots = fullLayout.hoversubplots;
261265
var plots = fullLayout._plots || [];
262266
var plotinfo = plots[subplot];
263267
var hasCartesian = fullLayout._has('cartesian');
264268

269+
var hovermode = evt.hovermode || fullLayout.hovermode;
270+
var hovermodeHasX = (hovermode || '').charAt(0) === 'x';
271+
var hovermodeHasY = (hovermode || '').charAt(0) === 'y';
272+
273+
if(hasCartesian && (hovermodeHasX || hovermodeHasY) && hoversubplots === 'axis') {
274+
var subplotsLength = subplots.length;
275+
for(var p = 0; p < subplotsLength; p++) {
276+
spId = subplots[p];
277+
if(plots[spId]) {
278+
// 'cartesian' case
279+
280+
var subplotsWith = (
281+
Axes.getFromId(gd, spId, hovermodeHasX ? 'x' : 'y')
282+
)._subplotsWith;
283+
284+
if(subplotsWith && subplotsWith.length) {
285+
for(var q = 0; q < subplotsWith.length; q++) {
286+
pushUnique(subplots, subplotsWith[q]);
287+
}
288+
}
289+
}
290+
}
291+
}
292+
265293
// list of all overlaid subplots to look at
266-
if(plotinfo) {
294+
if(plotinfo && hoversubplots !== 'single') {
267295
var overlayedSubplots = plotinfo.overlays.map(function(pi) {
268296
return pi.id;
269297
});
@@ -277,7 +305,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
277305
var supportsCompare = false;
278306

279307
for(var i = 0; i < len; i++) {
280-
var spId = subplots[i];
308+
spId = subplots[i];
281309

282310
if(plots[spId]) {
283311
// 'cartesian' case
@@ -295,8 +323,6 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
295323
}
296324
}
297325

298-
var hovermode = evt.hovermode || fullLayout.hovermode;
299-
300326
if(hovermode && !supportsCompare) hovermode = 'closest';
301327

302328
if(['x', 'y', 'closest', 'x unified', 'y unified'].indexOf(hovermode) === -1 || !gd.calcdata ||
@@ -441,6 +467,12 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
441467
// the rest of this function from running and failing
442468
if(['carpet', 'contourcarpet'].indexOf(trace._module.name) !== -1) continue;
443469

470+
// within one trace mode can sometimes be overridden
471+
_mode = hovermode;
472+
if(helpers.isUnifiedHover(_mode)) {
473+
_mode = _mode.charAt(0);
474+
}
475+
444476
if(trace.type === 'splom') {
445477
// splom traces do not generate overlay subplots,
446478
// it is safe to assume here splom traces correspond to the 0th subplot
@@ -451,12 +483,6 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
451483
subploti = subplots.indexOf(subplotId);
452484
}
453485

454-
// within one trace mode can sometimes be overridden
455-
_mode = hovermode;
456-
if(helpers.isUnifiedHover(_mode)) {
457-
_mode = _mode.charAt(0);
458-
}
459-
460486
// container for new point, also used to pass info into module.hoverPoints
461487
pointData = {
462488
// trace properties
@@ -508,8 +534,6 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
508534
pointData.scene = fullLayout._splomScenes[trace.uid];
509535
}
510536

511-
closedataPreviousLength = hoverData.length;
512-
513537
// for a highlighting array, figure out what
514538
// we're searching for with this element
515539
if(_mode === 'array') {
@@ -536,12 +560,18 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
536560
yval = yvalArray[subploti];
537561
}
538562

563+
closedataPreviousLength = hoverData.length;
564+
539565
// Now if there is range to look in, find the points to hover.
540566
if(hoverdistance !== 0) {
541567
if(trace._module && trace._module.hoverPoints) {
542568
var newPoints = trace._module.hoverPoints(pointData, xval, yval, _mode, {
543569
finiteRange: true,
544-
hoverLayer: fullLayout._hoverlayer
570+
hoverLayer: fullLayout._hoverlayer,
571+
572+
// options for splom when hovering on same axis
573+
hoversubplots: hoversubplots,
574+
gd: gd
545575
});
546576

547577
if(newPoints) {
@@ -662,7 +692,9 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
662692
gd._spikepoints = newspikepoints;
663693

664694
var sortHoverData = function() {
665-
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
695+
if(hoversubplots !== 'axis') {
696+
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
697+
}
666698

667699
// move period positioned points and box/bar-like traces to the end of the list
668700
hoverData = orderRangePoints(hoverData, hovermode);

Diff for: src/components/fx/hovermode_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ module.exports = function handleHoverModeDefaults(layoutIn, layoutOut) {
1212
}
1313

1414
coerce('clickmode');
15+
coerce('hoversubplots');
1516
return coerce('hovermode');
1617
};

Diff for: src/components/fx/layout_attributes.js

+13
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ module.exports = {
7878
'If false, hover interactions are disabled.'
7979
].join(' ')
8080
},
81+
hoversubplots: {
82+
valType: 'enumerated',
83+
values: ['single', 'overlaying', 'axis'],
84+
dflt: 'overlaying',
85+
editType: 'none',
86+
description: [
87+
'Determines expansion of hover effects to other subplots',
88+
'If *single* just the axis pair of the primary point is included without overlaying subplots.',
89+
'If *overlaying* all subplots using the main axis and occupying the same space are included.',
90+
'If *axis*, also include stacked subplots using the same axis',
91+
'when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.',
92+
].join(' ')
93+
},
8194
hoverdistance: {
8295
valType: 'integer',
8396
min: -1,

Diff for: src/traces/splom/hover.js

+77-9
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,64 @@
22

33
var helpers = require('./helpers');
44
var calcHover = require('../scattergl/hover').calcHover;
5+
var getFromId = require('../../plots/cartesian/axes').getFromId;
6+
var extendFlat = require('../../lib/extend').extendFlat;
57

6-
function hoverPoints(pointData, xval, yval) {
8+
function hoverPoints(pointData, xval, yval, hovermode, opts) {
9+
if(!opts) opts = {};
10+
11+
var hovermodeHasX = (hovermode || '').charAt(0) === 'x';
12+
var hovermodeHasY = (hovermode || '').charAt(0) === 'y';
13+
14+
var xpx = pointData.xa.c2p(xval);
15+
var ypx = pointData.ya.c2p(yval);
16+
17+
var points = _hoverPoints(pointData, xpx, ypx);
18+
19+
if((hovermodeHasX || hovermodeHasY) && opts.hoversubplots === 'axis') {
20+
var _xpx = points[0]._xpx;
21+
var _ypx = points[0]._ypx;
22+
23+
if(
24+
(hovermodeHasX && _xpx !== undefined) ||
25+
(hovermodeHasY && _ypx !== undefined)
26+
) {
27+
var subplotsWith = (
28+
hovermodeHasX ?
29+
pointData.xa :
30+
pointData.ya
31+
)._subplotsWith;
32+
33+
var gd = opts.gd;
34+
35+
var _pointData = extendFlat({}, pointData);
36+
37+
for(var i = 0; i < subplotsWith.length; i++) {
38+
var spId = subplotsWith[i];
39+
40+
if(hovermodeHasY) {
41+
_pointData.xa = getFromId(gd, spId, 'x');
42+
} else { // hovermodeHasX
43+
_pointData.ya = getFromId(gd, spId, 'y');
44+
}
45+
46+
var newPoints = _hoverPoints(_pointData, _xpx, _ypx, hovermodeHasX, hovermodeHasY);
47+
48+
points = points.concat(newPoints);
49+
}
50+
}
51+
}
52+
53+
return points;
54+
}
55+
56+
function _hoverPoints(pointData, xpx, ypx, hoversubplotsX, hoversubplotsY) {
757
var cd = pointData.cd;
858
var trace = cd[0].trace;
959
var scene = pointData.scene;
1060
var cdata = scene.matrixOptions.cdata;
1161
var xa = pointData.xa;
1262
var ya = pointData.ya;
13-
var xpx = xa.c2p(xval);
14-
var ypx = ya.c2p(yval);
1563
var maxDistance = pointData.distance;
1664

1765
var xi = helpers.getDimIndex(trace, xa);
@@ -21,19 +69,36 @@ function hoverPoints(pointData, xval, yval) {
2169
var x = cdata[xi];
2270
var y = cdata[yi];
2371

24-
var id, dxy;
72+
var id, dxy, _xpx, _ypx;
2573
var minDist = maxDistance;
2674

2775
for(var i = 0; i < x.length; i++) {
76+
if((hoversubplotsX || hoversubplotsY) && i !== pointData.index) continue;
77+
2878
var ptx = x[i];
2979
var pty = y[i];
30-
var dx = xa.c2p(ptx) - xpx;
31-
var dy = ya.c2p(pty) - ypx;
32-
var dist = Math.sqrt(dx * dx + dy * dy);
80+
var thisXpx = xa.c2p(ptx);
81+
var thisYpx = ya.c2p(pty);
82+
83+
var dx = thisXpx - xpx;
84+
var dy = thisYpx - ypx;
85+
var dist = 0;
86+
87+
var pick = false;
88+
if(hoversubplotsX) {
89+
if(dx === 0) pick = true;
90+
} else if(hoversubplotsY) {
91+
if(dy === 0) pick = true;
92+
} else {
93+
dist = Math.sqrt(dx * dx + dy * dy);
94+
if(dist < minDist) pick = true;
95+
}
3396

34-
if(dist < minDist) {
97+
if(pick) {
3598
minDist = dxy = dist;
3699
id = i;
100+
_xpx = thisXpx;
101+
_ypx = thisYpx;
37102
}
38103
}
39104

@@ -43,7 +108,10 @@ function hoverPoints(pointData, xval, yval) {
43108

44109
if(id === undefined) return [pointData];
45110

46-
return [calcHover(pointData, x, y, trace)];
111+
var out = calcHover(pointData, x, y, trace);
112+
out._xpx = _xpx;
113+
out._ypx = _ypx;
114+
return [out];
47115
}
48116

49117
module.exports = {

0 commit comments

Comments
 (0)