Skip to content

Commit

Permalink
Merge pull request #6947 from plotly/res4755-hover-subplots
Browse files Browse the repository at this point in the history
Add `layout.subplots` to enable (x|y) hover effects across multiple cartesian and splom suplots sharing one axis
  • Loading branch information
archmoj authored Apr 9, 2024
2 parents d20b8e2 + d9aea4b commit 57d2d85
Show file tree
Hide file tree
Showing 7 changed files with 531 additions and 23 deletions.
1 change: 1 addition & 0 deletions draftlogs/6947_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `layout.hoversubplots` to enable hover effects across multiple cartesian suplots sharing one axis [[#6947](https://github.com/plotly/plotly.js/pull/6947)]
60 changes: 46 additions & 14 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var isNumeric = require('fast-isnumeric');
var tinycolor = require('tinycolor2');

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

var spId;

var fullLayout = gd._fullLayout;
var hoversubplots = fullLayout.hoversubplots;
var plots = fullLayout._plots || [];
var plotinfo = plots[subplot];
var hasCartesian = fullLayout._has('cartesian');

var hovermode = evt.hovermode || fullLayout.hovermode;
var hovermodeHasX = (hovermode || '').charAt(0) === 'x';
var hovermodeHasY = (hovermode || '').charAt(0) === 'y';

if(hasCartesian && (hovermodeHasX || hovermodeHasY) && hoversubplots === 'axis') {
var subplotsLength = subplots.length;
for(var p = 0; p < subplotsLength; p++) {
spId = subplots[p];
if(plots[spId]) {
// 'cartesian' case

var subplotsWith = (
Axes.getFromId(gd, spId, hovermodeHasX ? 'x' : 'y')
)._subplotsWith;

if(subplotsWith && subplotsWith.length) {
for(var q = 0; q < subplotsWith.length; q++) {
pushUnique(subplots, subplotsWith[q]);
}
}
}
}
}

// list of all overlaid subplots to look at
if(plotinfo) {
if(plotinfo && hoversubplots !== 'single') {
var overlayedSubplots = plotinfo.overlays.map(function(pi) {
return pi.id;
});
Expand All @@ -277,7 +305,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
var supportsCompare = false;

for(var i = 0; i < len; i++) {
var spId = subplots[i];
spId = subplots[i];

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

var hovermode = evt.hovermode || fullLayout.hovermode;

if(hovermode && !supportsCompare) hovermode = 'closest';

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

// within one trace mode can sometimes be overridden
_mode = hovermode;
if(helpers.isUnifiedHover(_mode)) {
_mode = _mode.charAt(0);
}

if(trace.type === 'splom') {
// splom traces do not generate overlay subplots,
// it is safe to assume here splom traces correspond to the 0th subplot
Expand All @@ -451,12 +483,6 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
subploti = subplots.indexOf(subplotId);
}

// within one trace mode can sometimes be overridden
_mode = hovermode;
if(helpers.isUnifiedHover(_mode)) {
_mode = _mode.charAt(0);
}

// container for new point, also used to pass info into module.hoverPoints
pointData = {
// trace properties
Expand Down Expand Up @@ -508,8 +534,6 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
pointData.scene = fullLayout._splomScenes[trace.uid];
}

closedataPreviousLength = hoverData.length;

// for a highlighting array, figure out what
// we're searching for with this element
if(_mode === 'array') {
Expand All @@ -536,12 +560,18 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
yval = yvalArray[subploti];
}

closedataPreviousLength = hoverData.length;

// Now if there is range to look in, find the points to hover.
if(hoverdistance !== 0) {
if(trace._module && trace._module.hoverPoints) {
var newPoints = trace._module.hoverPoints(pointData, xval, yval, _mode, {
finiteRange: true,
hoverLayer: fullLayout._hoverlayer
hoverLayer: fullLayout._hoverlayer,

// options for splom when hovering on same axis
hoversubplots: hoversubplots,
gd: gd
});

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

var sortHoverData = function() {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
if(hoversubplots !== 'axis') {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
}

// move period positioned points and box/bar-like traces to the end of the list
hoverData = orderRangePoints(hoverData, hovermode);
Expand Down
1 change: 1 addition & 0 deletions src/components/fx/hovermode_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ module.exports = function handleHoverModeDefaults(layoutIn, layoutOut) {
}

coerce('clickmode');
coerce('hoversubplots');
return coerce('hovermode');
};
13 changes: 13 additions & 0 deletions src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ module.exports = {
'If false, hover interactions are disabled.'
].join(' ')
},
hoversubplots: {
valType: 'enumerated',
values: ['single', 'overlaying', 'axis'],
dflt: 'overlaying',
editType: 'none',
description: [
'Determines expansion of hover effects to other subplots',
'If *single* just the axis pair of the primary point is included without overlaying subplots.',
'If *overlaying* all subplots using the main axis and occupying the same space are included.',
'If *axis*, also include stacked subplots using the same axis',
'when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.',
].join(' ')
},
hoverdistance: {
valType: 'integer',
min: -1,
Expand Down
86 changes: 77 additions & 9 deletions src/traces/splom/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,64 @@

var helpers = require('./helpers');
var calcHover = require('../scattergl/hover').calcHover;
var getFromId = require('../../plots/cartesian/axes').getFromId;
var extendFlat = require('../../lib/extend').extendFlat;

function hoverPoints(pointData, xval, yval) {
function hoverPoints(pointData, xval, yval, hovermode, opts) {
if(!opts) opts = {};

var hovermodeHasX = (hovermode || '').charAt(0) === 'x';
var hovermodeHasY = (hovermode || '').charAt(0) === 'y';

var xpx = pointData.xa.c2p(xval);
var ypx = pointData.ya.c2p(yval);

var points = _hoverPoints(pointData, xpx, ypx);

if((hovermodeHasX || hovermodeHasY) && opts.hoversubplots === 'axis') {
var _xpx = points[0]._xpx;
var _ypx = points[0]._ypx;

if(
(hovermodeHasX && _xpx !== undefined) ||
(hovermodeHasY && _ypx !== undefined)
) {
var subplotsWith = (
hovermodeHasX ?
pointData.xa :
pointData.ya
)._subplotsWith;

var gd = opts.gd;

var _pointData = extendFlat({}, pointData);

for(var i = 0; i < subplotsWith.length; i++) {
var spId = subplotsWith[i];

if(hovermodeHasY) {
_pointData.xa = getFromId(gd, spId, 'x');
} else { // hovermodeHasX
_pointData.ya = getFromId(gd, spId, 'y');
}

var newPoints = _hoverPoints(_pointData, _xpx, _ypx, hovermodeHasX, hovermodeHasY);

points = points.concat(newPoints);
}
}
}

return points;
}

function _hoverPoints(pointData, xpx, ypx, hoversubplotsX, hoversubplotsY) {
var cd = pointData.cd;
var trace = cd[0].trace;
var scene = pointData.scene;
var cdata = scene.matrixOptions.cdata;
var xa = pointData.xa;
var ya = pointData.ya;
var xpx = xa.c2p(xval);
var ypx = ya.c2p(yval);
var maxDistance = pointData.distance;

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

var id, dxy;
var id, dxy, _xpx, _ypx;
var minDist = maxDistance;

for(var i = 0; i < x.length; i++) {
if((hoversubplotsX || hoversubplotsY) && i !== pointData.index) continue;

var ptx = x[i];
var pty = y[i];
var dx = xa.c2p(ptx) - xpx;
var dy = ya.c2p(pty) - ypx;
var dist = Math.sqrt(dx * dx + dy * dy);
var thisXpx = xa.c2p(ptx);
var thisYpx = ya.c2p(pty);

var dx = thisXpx - xpx;
var dy = thisYpx - ypx;
var dist = 0;

var pick = false;
if(hoversubplotsX) {
if(dx === 0) pick = true;
} else if(hoversubplotsY) {
if(dy === 0) pick = true;
} else {
dist = Math.sqrt(dx * dx + dy * dy);
if(dist < minDist) pick = true;
}

if(dist < minDist) {
if(pick) {
minDist = dxy = dist;
id = i;
_xpx = thisXpx;
_ypx = thisYpx;
}
}

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

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

return [calcHover(pointData, x, y, trace)];
var out = calcHover(pointData, x, y, trace);
out._xpx = _xpx;
out._ypx = _ypx;
return [out];
}

module.exports = {
Expand Down
Loading

0 comments on commit 57d2d85

Please sign in to comment.