Skip to content

Commit 86fb8a9

Browse files
authored
Merge pull request #6865 from lumip/scatter_hoveron_fill_fix
Improved hover detection for scatter plot fill `tonext*`
2 parents 259abab + af0af9c commit 86fb8a9

File tree

4 files changed

+391
-62
lines changed

4 files changed

+391
-62
lines changed

draftlogs/6865_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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!

src/traces/scatter/hover.js

Lines changed: 98 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -119,64 +119,118 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
119119
}
120120
}
121121

122-
// even if hoveron is 'fills', only use it if we have polygons too
123-
if(hoveron.indexOf('fills') !== -1 && trace._polygons) {
124-
var polygons = trace._polygons;
122+
function isHoverPointInFillElement(el) {
123+
// Uses SVGElement.isPointInFill to accurately determine wether
124+
// the hover point / cursor is contained in the fill, taking
125+
// curved or jagged edges into account, which the Polygon-based
126+
// approach does not.
127+
if(!el) {
128+
return false;
129+
}
130+
var svgElement = el.node();
131+
try {
132+
var domPoint = new DOMPoint(pt[0], pt[1]);
133+
return svgElement.isPointInFill(domPoint);
134+
} catch(TypeError) {
135+
var svgPoint = svgElement.ownerSVGElement.createSVGPoint();
136+
svgPoint.x = pt[0];
137+
svgPoint.y = pt[1];
138+
return svgElement.isPointInFill(svgPoint);
139+
}
140+
}
141+
142+
function getHoverLabelPosition(polygons) {
143+
// Uses Polygon s to determine the left- and right-most x-coordinates
144+
// of the subshape of the fill that contains the hover point / cursor.
145+
// Doing this with the SVGElement directly is quite tricky, so this falls
146+
// back to the existing relatively simple code, accepting some small inaccuracies
147+
// of label positioning for curved/jagged edges.
148+
var i;
125149
var polygonsIn = [];
126-
var inside = false;
127150
var xmin = Infinity;
128151
var xmax = -Infinity;
129152
var ymin = Infinity;
130153
var ymax = -Infinity;
131-
132-
var i, j, polygon, pts, xCross, x0, x1, y0, y1;
154+
var yPos;
133155

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

148-
if(inside) {
149-
// constrain ymin/max to the visible plot, so the label goes
150-
// at the middle of the piece you can see
151-
ymin = Math.max(ymin, 0);
152-
ymax = Math.min(ymax, ya._length);
153-
154-
// find the overall left-most and right-most points of the
155-
// polygon(s) we're inside at their combined vertical midpoint.
156-
// This is where we will draw the hover label.
157-
// Note that this might not be the vertical midpoint of the
158-
// whole trace, if it's disjoint.
159-
var yAvg = (ymin + ymax) / 2;
160-
for(i = 0; i < polygonsIn.length; i++) {
161-
pts = polygonsIn[i].pts;
162-
for(j = 1; j < pts.length; j++) {
163-
y0 = pts[j - 1][1];
164-
y1 = pts[j][1];
165-
if((y0 > yAvg) !== (y1 >= yAvg)) {
166-
x0 = pts[j - 1][0];
167-
x1 = pts[j][0];
168-
if(y1 - y0) {
169-
xCross = x0 + (x1 - x0) * (yAvg - y0) / (y1 - y0);
170-
xmin = Math.min(xmin, xCross);
171-
xmax = Math.max(xmax, xCross);
172-
}
167+
// The above found no polygon that contains the cursor, but we know that
168+
// the cursor must be inside the fill as determined by the SVGElement
169+
// (so we are probably close to a curved/jagged edge...).
170+
if(polygonsIn.length === 0) {
171+
return null;
172+
}
173+
174+
// constrain ymin/max to the visible plot, so the label goes
175+
// at the middle of the piece you can see
176+
ymin = Math.max(ymin, 0);
177+
ymax = Math.min(ymax, ya._length);
178+
179+
yPos = (ymin + ymax) / 2;
180+
181+
// find the overall left-most and right-most points of the
182+
// polygon(s) we're inside at their combined vertical midpoint.
183+
// This is where we will draw the hover label.
184+
// Note that this might not be the vertical midpoint of the
185+
// whole trace, if it's disjoint.
186+
var j, pts, xAtYPos, x0, x1, y0, y1;
187+
for(i = 0; i < polygonsIn.length; i++) {
188+
pts = polygonsIn[i].pts;
189+
for(j = 1; j < pts.length; j++) {
190+
y0 = pts[j - 1][1];
191+
y1 = pts[j][1];
192+
if((y0 > yPos) !== (y1 >= yPos)) {
193+
x0 = pts[j - 1][0];
194+
x1 = pts[j][0];
195+
if(y1 - y0) {
196+
xAtYPos = x0 + (x1 - x0) * (yPos - y0) / (y1 - y0);
197+
xmin = Math.min(xmin, xAtYPos);
198+
xmax = Math.max(xmax, xAtYPos);
173199
}
174200
}
175201
}
202+
}
203+
204+
// constrain xmin/max to the visible plot now too
205+
xmin = Math.max(xmin, 0);
206+
xmax = Math.min(xmax, xa._length);
207+
208+
return {
209+
x0: xmin,
210+
x1: xmax,
211+
y0: yPos,
212+
y1: yPos,
213+
};
214+
}
176215

177-
// constrain xmin/max to the visible plot now too
178-
xmin = Math.max(xmin, 0);
179-
xmax = Math.min(xmax, xa._length);
216+
// even if hoveron is 'fills', only use it if we have a fill element too
217+
if(hoveron.indexOf('fills') !== -1 && trace._fillElement) {
218+
var inside = isHoverPointInFillElement(trace._fillElement) && !isHoverPointInFillElement(trace._fillExclusionElement);
219+
220+
if(inside) {
221+
var hoverLabelCoords = getHoverLabelPosition(trace._polygons);
222+
223+
// getHoverLabelPosition may return null if the cursor / hover point is not contained
224+
// in any of the trace's polygons, which can happen close to curved edges. in that
225+
// case we fall back to displaying the hover label at the cursor position.
226+
if(hoverLabelCoords === null) {
227+
hoverLabelCoords = {
228+
x0: pt[0],
229+
x1: pt[0],
230+
y0: pt[1],
231+
y1: pt[1]
232+
};
233+
}
180234

181235
// get only fill or line color for the hover color
182236
var color = Color.defaultLine;
@@ -189,10 +243,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
189243
// never let a 2D override 1D type as closest point
190244
// also: no spikeDistance, it's not allowed for fills
191245
distance: pointData.maxHoverDistance,
192-
x0: xmin,
193-
x1: xmax,
194-
y0: yAvg,
195-
y1: yAvg,
246+
x0: hoverLabelCoords.x0,
247+
x1: hoverLabelCoords.x1,
248+
y0: hoverLabelCoords.y0,
249+
y1: hoverLabelCoords.y1,
196250
color: color,
197251
hovertemplate: false
198252
});

0 commit comments

Comments
 (0)