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

Improved hover detection for scatter plot fill tonext* #6865

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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