Skip to content

Commit

Permalink
Merge pull request #6865 from lumip/scatter_hoveron_fill_fix
Browse files Browse the repository at this point in the history
Improved hover detection for scatter plot fill `tonext*`
  • Loading branch information
archmoj authored Jan 30, 2024
2 parents 259abab + af0af9c commit 86fb8a9
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 62 deletions.
1 change: 1 addition & 0 deletions draftlogs/6865_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Improved hover detection for for scatter plot fill tonext* [[#6865](https://github.com/plotly/plotly.js/pull/6865)], with thanks to @lumip for the contribution!
142 changes: 98 additions & 44 deletions src/traces/scatter/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,64 +119,118 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
}
}

// even if hoveron is 'fills', only use it if we have polygons too
if(hoveron.indexOf('fills') !== -1 && trace._polygons) {
var polygons = trace._polygons;
function isHoverPointInFillElement(el) {
// Uses SVGElement.isPointInFill to accurately determine wether
// the hover point / cursor is contained in the fill, taking
// curved or jagged edges into account, which the Polygon-based
// approach does not.
if(!el) {
return false;
}
var svgElement = el.node();
try {
var domPoint = new DOMPoint(pt[0], pt[1]);
return svgElement.isPointInFill(domPoint);
} catch(TypeError) {
var svgPoint = svgElement.ownerSVGElement.createSVGPoint();
svgPoint.x = pt[0];
svgPoint.y = pt[1];
return svgElement.isPointInFill(svgPoint);
}
}

function getHoverLabelPosition(polygons) {
// Uses Polygon s to determine the left- and right-most x-coordinates
// of the subshape of the fill that contains the hover point / cursor.
// Doing this with the SVGElement directly is quite tricky, so this falls
// back to the existing relatively simple code, accepting some small inaccuracies
// of label positioning for curved/jagged edges.
var i;
var polygonsIn = [];
var inside = false;
var xmin = Infinity;
var xmax = -Infinity;
var ymin = Infinity;
var ymax = -Infinity;

var i, j, polygon, pts, xCross, x0, x1, y0, y1;
var yPos;

for(i = 0; i < polygons.length; i++) {
polygon = polygons[i];
// TODO: this is not going to work right for curved edges, it will
// act as though they're straight. That's probably going to need
// the elements themselves to capture the events. Worth it?
var polygon = polygons[i];
// This is not going to work right for curved or jagged edges, it will
// act as though they're straight.
if(polygon.contains(pt)) {
inside = !inside;
// TODO: need better than just the overall bounding box
polygonsIn.push(polygon);
ymin = Math.min(ymin, polygon.ymin);
ymax = Math.max(ymax, polygon.ymax);
}
}

if(inside) {
// constrain ymin/max to the visible plot, so the label goes
// at the middle of the piece you can see
ymin = Math.max(ymin, 0);
ymax = Math.min(ymax, ya._length);

// find the overall left-most and right-most points of the
// polygon(s) we're inside at their combined vertical midpoint.
// This is where we will draw the hover label.
// Note that this might not be the vertical midpoint of the
// whole trace, if it's disjoint.
var yAvg = (ymin + ymax) / 2;
for(i = 0; i < polygonsIn.length; i++) {
pts = polygonsIn[i].pts;
for(j = 1; j < pts.length; j++) {
y0 = pts[j - 1][1];
y1 = pts[j][1];
if((y0 > yAvg) !== (y1 >= yAvg)) {
x0 = pts[j - 1][0];
x1 = pts[j][0];
if(y1 - y0) {
xCross = x0 + (x1 - x0) * (yAvg - y0) / (y1 - y0);
xmin = Math.min(xmin, xCross);
xmax = Math.max(xmax, xCross);
}
// The above found no polygon that contains the cursor, but we know that
// the cursor must be inside the fill as determined by the SVGElement
// (so we are probably close to a curved/jagged edge...).
if(polygonsIn.length === 0) {
return null;
}

// constrain ymin/max to the visible plot, so the label goes
// at the middle of the piece you can see
ymin = Math.max(ymin, 0);
ymax = Math.min(ymax, ya._length);

yPos = (ymin + ymax) / 2;

// find the overall left-most and right-most points of the
// polygon(s) we're inside at their combined vertical midpoint.
// This is where we will draw the hover label.
// Note that this might not be the vertical midpoint of the
// whole trace, if it's disjoint.
var j, pts, xAtYPos, x0, x1, y0, y1;
for(i = 0; i < polygonsIn.length; i++) {
pts = polygonsIn[i].pts;
for(j = 1; j < pts.length; j++) {
y0 = pts[j - 1][1];
y1 = pts[j][1];
if((y0 > yPos) !== (y1 >= yPos)) {
x0 = pts[j - 1][0];
x1 = pts[j][0];
if(y1 - y0) {
xAtYPos = x0 + (x1 - x0) * (yPos - y0) / (y1 - y0);
xmin = Math.min(xmin, xAtYPos);
xmax = Math.max(xmax, xAtYPos);
}
}
}
}

// constrain xmin/max to the visible plot now too
xmin = Math.max(xmin, 0);
xmax = Math.min(xmax, xa._length);

return {
x0: xmin,
x1: xmax,
y0: yPos,
y1: yPos,
};
}

// constrain xmin/max to the visible plot now too
xmin = Math.max(xmin, 0);
xmax = Math.min(xmax, xa._length);
// even if hoveron is 'fills', only use it if we have a fill element too
if(hoveron.indexOf('fills') !== -1 && trace._fillElement) {
var inside = isHoverPointInFillElement(trace._fillElement) && !isHoverPointInFillElement(trace._fillExclusionElement);

if(inside) {
var hoverLabelCoords = getHoverLabelPosition(trace._polygons);

// getHoverLabelPosition may return null if the cursor / hover point is not contained
// in any of the trace's polygons, which can happen close to curved edges. in that
// case we fall back to displaying the hover label at the cursor position.
if(hoverLabelCoords === null) {
hoverLabelCoords = {
x0: pt[0],
x1: pt[0],
y0: pt[1],
y1: pt[1]
};
}

// get only fill or line color for the hover color
var color = Color.defaultLine;
Expand All @@ -189,10 +243,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
// never let a 2D override 1D type as closest point
// also: no spikeDistance, it's not allowed for fills
distance: pointData.maxHoverDistance,
x0: xmin,
x1: xmax,
y0: yAvg,
y1: yAvg,
x0: hoverLabelCoords.x0,
x1: hoverLabelCoords.x1,
y0: hoverLabelCoords.y0,
y1: hoverLabelCoords.y1,
color: color,
hovertemplate: false
});
Expand Down
Loading

0 comments on commit 86fb8a9

Please sign in to comment.