Skip to content

Commit 3eb4738

Browse files
committed
Using SVGElement.isPointInfFill instead of custom polygons for hover tests in scatter plots.
However, SVGElement does not allow for an easy way to determine the positioning of the hover label, so the polygons are still in use for that.
1 parent be658dd commit 3eb4738

File tree

3 files changed

+106
-47
lines changed

3 files changed

+106
-47
lines changed

Diff for: src/traces/scatter/hover.js

+95-44
Original file line numberDiff line numberDiff line change
@@ -119,64 +119,115 @@ 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 && trace._polygons.length > 0) {
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 yminAll = Infinity;
155+
var ymaxAll = -Infinity;
156+
var yPos;
133157

134158
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?
159+
var polygon = polygons[i];
160+
// This is not going to work right for curved or jagged edges, it will
161+
// act as though they're straight.
162+
yminAll = Math.min(yminAll, polygon.ymin);
163+
ymaxAll = Math.max(ymaxAll, polygon.ymax);
139164
if(polygon.contains(pt)) {
140-
inside = !inside;
141-
// TODO: need better than just the overall bounding box
142165
polygonsIn.push(polygon);
143166
ymin = Math.min(ymin, polygon.ymin);
144167
ymax = Math.max(ymax, polygon.ymax);
145168
}
146169
}
147170

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-
}
171+
// The above found no polygon that contains the cursor, but we know that
172+
// the cursor must be inside the fill as determined by the SVGElement
173+
// (so we are probably close to a curved/jagged edge...). In this case
174+
// as a crude approximation, simply consider all polygons for determination
175+
// of the hover label position.
176+
// TODO: This might cause some jumpiness of the label close to edges...
177+
if(polygonsIn.length === 0) {
178+
polygonsIn = polygons;
179+
ymin = yminAll;
180+
ymax = ymaxAll;
181+
}
182+
183+
// constrain ymin/max to the visible plot, so the label goes
184+
// at the middle of the piece you can see
185+
ymin = Math.max(ymin, 0);
186+
ymax = Math.min(ymax, ya._length);
187+
188+
yPos = (ymin + ymax) / 2;
189+
190+
// find the overall left-most and right-most points of the
191+
// polygon(s) we're inside at their combined vertical midpoint.
192+
// This is where we will draw the hover label.
193+
// Note that this might not be the vertical midpoint of the
194+
// whole trace, if it's disjoint.
195+
var j, pts, xAtYPos, x0, x1, y0, y1;
196+
for(i = 0; i < polygonsIn.length; i++) {
197+
pts = polygonsIn[i].pts;
198+
for(j = 1; j < pts.length; j++) {
199+
y0 = pts[j - 1][1];
200+
y1 = pts[j][1];
201+
if((y0 > yPos) !== (y1 >= yPos)) {
202+
x0 = pts[j - 1][0];
203+
x1 = pts[j][0];
204+
if(y1 - y0) {
205+
xAtYPos = x0 + (x1 - x0) * (yPos - y0) / (y1 - y0);
206+
xmin = Math.min(xmin, xAtYPos);
207+
xmax = Math.max(xmax, xAtYPos);
173208
}
174209
}
175210
}
211+
}
212+
213+
// constrain xmin/max to the visible plot now too
214+
xmin = Math.max(xmin, 0);
215+
xmax = Math.min(xmax, xa._length);
216+
217+
return {
218+
x0: xmin,
219+
x1: xmax,
220+
y0: yPos,
221+
y1: yPos,
222+
};
223+
}
176224

177-
// constrain xmin/max to the visible plot now too
178-
xmin = Math.max(xmin, 0);
179-
xmax = Math.min(xmax, xa._length);
225+
// even if hoveron is 'fills', only use it if we have a fill element too
226+
if(hoveron.indexOf('fills') !== -1 && trace._fillElement) {
227+
var inside = isHoverPointInFillElement(trace._fillElement) && !isHoverPointInFillElement(trace._fillExclusionElement);
228+
229+
if(inside) {
230+
var hoverLabelCoords = getHoverLabelPosition(trace._polygons);
180231

181232
// get only fill or line color for the hover color
182233
var color = Color.defaultLine;
@@ -189,10 +240,10 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
189240
// never let a 2D override 1D type as closest point
190241
// also: no spikeDistance, it's not allowed for fills
191242
distance: pointData.maxHoverDistance,
192-
x0: xmin,
193-
x1: xmax,
194-
y0: yAvg,
195-
y1: yAvg,
243+
x0: hoverLabelCoords.x0,
244+
x1: hoverLabelCoords.x1,
245+
y0: hoverLabelCoords.y0,
246+
y1: hoverLabelCoords.y1,
196247
color: color,
197248
hovertemplate: false
198249
});

Diff for: src/traces/scatter/plot.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,14 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
159159
var prevPolygons = [];
160160
var prevtrace = trace._prevtrace;
161161
var prevFillsegments = null;
162+
var prevFillElement = null;
162163

163164
if(prevtrace) {
164165
prevRevpath = prevtrace._prevRevpath || '';
165166
tonext = prevtrace._nextFill;
166167
prevPolygons = prevtrace._ownPolygons;
167168
prevFillsegments = prevtrace._fillsegments;
169+
prevFillElement = prevtrace._fillElement;
168170
}
169171

170172
var thispath;
@@ -257,6 +259,10 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
257259
curpoints.push.apply(curpoints, pts);
258260
}
259261
}
262+
263+
trace._fillElement = null;
264+
trace._fillExclusionElement = prevFillElement;
265+
260266
trace._fillsegments = fillsegments.slice(0, fillsegmentCount);
261267
fillsegments = trace._fillsegments;
262268

@@ -398,6 +404,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
398404
}
399405
}
400406
trace._polygons = thisPolygons;
407+
trace._fillElement = ownFillEl3;
401408
} else if(tonext) {
402409
if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevRevpath) {
403410
// fill to next: full trace path, plus the previous path reversed
@@ -409,7 +416,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
409416
transition(tonext).attr('d', fullpath + 'Z' + prevRevpath + 'Z')
410417
.call(Drawing.singleFillStyle, gd);
411418

412-
// and simply emit hover polygons for each segment
419+
// and simply emit hover polygons for each segment
413420
thisPolygons = makeSelfPolygons();
414421

415422
// we add the polygons of the previous trace which causes hover
@@ -431,6 +438,7 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
431438
// so must not include previous trace polygons for hover detection.
432439
trace._polygons = thisPolygons;
433440
}
441+
trace._fillElement = tonext;
434442
} else {
435443
clearFill(tonext);
436444
}

Diff for: test/jasmine/tests/scatter_test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1576,15 +1576,15 @@ describe('scatter hoverFills', function() {
15761576
var traceOffset = 0;
15771577

15781578
var testPoints = [ // all the following points should be in fill region of corresponding tozeroy traces 0-4
1579-
[[1.5, 1.24], [1.5, 1.06]], // single point has "fill" along line to zero
1579+
[], // single point has no "fill" when using SVG element containment tests
15801580
[[0.1, 0.9], [0.1, 0.8], [1.5, 0.9], [1.5, 1.04], [2, 0.8], [2, 1.09], [3, 0.8]],
15811581
[[0.1, 0.75], [0.1, 0.61], [1.01, 0.501], [1.5, 0.8], [1.5, 0.55], [2, 0.74], [2, 0.55], [3, 0.74], [3, 0.51]],
15821582
[[0.1, 0.599], [0.1, 0.5], [0.1, 0.3], [0.99, 0.59], [1, 0.49], [1, 0.36], [1.5, 0.26], [2, 0.49], [2, 0.16], [3, 0.49], [3, 0.26]],
15831583
[[0.1, 0.25], [0.1, 0.1], [1, 0.34], [1.5, 0.24], [2, 0.14], [3, 0.24], [3, 0.1]],
15841584
];
15851585

15861586
var outsidePoints = [ // all these should not result in a hover detection, for any trace
1587-
[1, 1.1], [2, 1.14],
1587+
[1, 1.1], [2, 1.14], [1.5, 1.24], [1.5, 1.06]
15881588
];
15891589

15901590
Plotly.newPlot(gd, mock).then(function() {

0 commit comments

Comments
 (0)