Skip to content
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

Add layout.subplots to enable (x|y) hover effects across multiple cartesian and splom suplots sharing one axis #6947

Merged
merged 17 commits into from
Apr 9, 2024
Merged
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