Skip to content

Commit be658dd

Browse files
committed
Fix for hover behavior on scatter fills tonextx/y.
Previous code would not correctly construct the polygons used for detection whether the cursor is within the fill region of a trace. Some of the previously created tests fail with this as the hover points in question cannot currently be correctly detected using the detection polygons.
1 parent 4628741 commit be658dd

File tree

2 files changed

+115
-19
lines changed

2 files changed

+115
-19
lines changed

src/traces/scatter/hover.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
120120
}
121121

122122
// even if hoveron is 'fills', only use it if we have polygons too
123-
if(hoveron.indexOf('fills') !== -1 && trace._polygons) {
123+
if(hoveron.indexOf('fills') !== -1 && trace._polygons && trace._polygons.length > 0) {
124124
var polygons = trace._polygons;
125125
var polygonsIn = [];
126126
var inside = false;

src/traces/scatter/plot.js

+114-18
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,28 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
143143
var ownFillDir = trace.fill.charAt(trace.fill.length - 1);
144144
if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = '';
145145

146+
var fillAxisIndex, fillAxisZero;
147+
if(ownFillDir === 'y') {
148+
fillAxisIndex = 1;
149+
fillAxisZero = ya.c2p(0, true);
150+
} else if(ownFillDir === 'x') {
151+
fillAxisIndex = 0;
152+
fillAxisZero = xa.c2p(0, true);
153+
}
154+
146155
// store node for tweaking by selectPoints
147156
cdscatter[0][plotinfo.isRangePlot ? 'nodeRangePlot3' : 'node3'] = tr;
148157

149158
var prevRevpath = '';
150159
var prevPolygons = [];
151160
var prevtrace = trace._prevtrace;
161+
var prevFillsegments = null;
152162

153163
if(prevtrace) {
154164
prevRevpath = prevtrace._prevRevpath || '';
155165
tonext = prevtrace._nextFill;
156-
prevPolygons = prevtrace._polygons;
166+
prevPolygons = prevtrace._ownPolygons;
167+
prevFillsegments = prevtrace._fillsegments;
157168
}
158169

159170
var thispath;
@@ -166,7 +177,15 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
166177
// functions for converting a point array to a path
167178
var pathfn, revpathbase, revpathfn;
168179
// variables used before and after the data join
169-
var pt0, lastSegment, pt1, thisPolygons;
180+
var pt0, lastSegment, pt1;
181+
182+
// thisPolygons always contains only the polygons of this trace only
183+
// whereas trace._polygons may be extended to include those of the previous
184+
// trace as well for exclusion during hover detection
185+
var thisPolygons = [];
186+
trace._polygons = [];
187+
188+
var fillsegments = [];
170189

171190
// initialize line join data / method
172191
var segments = [];
@@ -220,35 +239,51 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
220239
});
221240

222241
// since we already have the pixel segments here, use them to make
223-
// polygons for hover on fill
242+
// polygons for hover on fill; we first merge segments where the fill
243+
// is connected into "fillsegments"; the actual polygon construction
244+
// is deferred to later to distinguish between self and tonext/tozero fills.
224245
// TODO: can we skip this if hoveron!=fills? That would mean we
225246
// need to redraw when you change hoveron...
226-
thisPolygons = trace._polygons = new Array(segments.length);
247+
fillsegments = new Array(segments.length);
248+
var fillsegmentCount = 0;
227249
for(i = 0; i < segments.length; i++) {
228-
trace._polygons[i] = polygonTester(segments[i]);
250+
var curpoints;
251+
var pts = segments[i];
252+
if(!curpoints || !ownFillDir) {
253+
curpoints = pts.slice();
254+
fillsegments[fillsegmentCount] = curpoints;
255+
fillsegmentCount++;
256+
} else {
257+
curpoints.push.apply(curpoints, pts);
258+
}
229259
}
260+
trace._fillsegments = fillsegments.slice(0, fillsegmentCount);
261+
fillsegments = trace._fillsegments;
230262

231263
if(segments.length) {
232-
pt0 = segments[0][0];
264+
pt0 = segments[0][0].slice();
233265
lastSegment = segments[segments.length - 1];
234-
pt1 = lastSegment[lastSegment.length - 1];
266+
pt1 = lastSegment[lastSegment.length - 1].slice();
235267
}
236268

237269
makeUpdate = function(isEnter) {
238270
return function(pts) {
239271
thispath = pathfn(pts);
240-
thisrevpath = revpathfn(pts);
272+
thisrevpath = revpathfn(pts); // side-effect: reverses input
273+
// calculate SVG path over all segments for fills
241274
if(!fullpath) {
242275
fullpath = thispath;
243276
revpath = thisrevpath;
244277
} else if(ownFillDir) {
278+
// for fills with fill direction: ignore gaps
245279
fullpath += 'L' + thispath.substr(1);
246280
revpath = thisrevpath + ('L' + revpath.substr(1));
247281
} else {
248282
fullpath += 'Z' + thispath;
249283
revpath = thisrevpath + 'Z' + revpath;
250284
}
251285

286+
// actual lines get drawn here, with gaps between segments if requested
252287
if(subTypes.hasLines(trace)) {
253288
var el = d3.select(this);
254289

@@ -290,16 +325,58 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
290325
transition(selection).attr('d', 'M0,0Z');
291326
}
292327

328+
// helper functions to create polygons for hoveron fill detection
329+
var makeSelfPolygons = function() {
330+
var polygons = new Array(fillsegments.length);
331+
for(i = 0; i < fillsegments.length; i++) {
332+
polygons[i] = polygonTester(fillsegments[i]);
333+
}
334+
return polygons;
335+
};
336+
337+
var makePolygonsToPrevious = function(prevFillsegments) {
338+
var polygons, i;
339+
if(!prevFillsegments || prevFillsegments.length === 0) {
340+
// if there are no fill segments of a previous trace, stretch the
341+
// polygon to the relevant axis
342+
polygons = new Array(fillsegments.length);
343+
for(i = 0; i < fillsegments.length; i++) {
344+
var pt0 = fillsegments[i][0].slice();
345+
var pt1 = fillsegments[i][fillsegments[i].length - 1].slice();
346+
347+
pt0[fillAxisIndex] = pt1[fillAxisIndex] = fillAxisZero;
348+
349+
var zeropoints = [pt1, pt0];
350+
var polypoints = zeropoints.concat(fillsegments[i]);
351+
polygons[i] = polygonTester(polypoints);
352+
}
353+
} else {
354+
// if there are more than one previous fill segment, the
355+
// way that fills work is to "self" fill all but the last segments
356+
// of the previous and then fill from the new trace to the last
357+
// segment of the previous.
358+
polygons = new Array(prevFillsegments.length - 1 + fillsegments.length);
359+
for(i = 0; i < prevFillsegments.length - 1; i++) {
360+
polygons[i] = polygonTester(prevFillsegments[i]);
361+
}
362+
363+
var reversedPrevFillsegment = prevFillsegments[prevFillsegments.length - 1].slice();
364+
reversedPrevFillsegment.reverse();
365+
366+
for(i = 0; i < fillsegments.length; i++) {
367+
polygons[prevFillsegments.length - 1 + i] = polygonTester(fillsegments[i].concat(reversedPrevFillsegment));
368+
}
369+
}
370+
return polygons;
371+
};
372+
373+
// draw fills and create hover detection polygons
293374
if(segments.length) {
294375
if(ownFillEl3) {
295376
ownFillEl3.datum(cdscatter);
296-
if(pt0 && pt1) {
377+
if(pt0 && pt1) { // TODO(2023-12-10): this is always true if segments is not empty (?)
297378
if(ownFillDir) {
298-
if(ownFillDir === 'y') {
299-
pt0[1] = pt1[1] = ya.c2p(0, true);
300-
} else if(ownFillDir === 'x') {
301-
pt0[0] = pt1[0] = xa.c2p(0, true);
302-
}
379+
pt0[fillAxisIndex] = pt1[fillAxisIndex] = fillAxisZero;
303380

304381
// fill to zero: full trace path, plus extension of
305382
// the endpoints to the appropriate axis
@@ -308,12 +385,19 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
308385
// animations get a little crazy if the number of points changes.
309386
transition(ownFillEl3).attr('d', 'M' + pt1 + 'L' + pt0 + 'L' + fullpath.substr(1))
310387
.call(Drawing.singleFillStyle, gd);
388+
389+
// create hover polygons that extend to the axis as well.
390+
thisPolygons = makePolygonsToPrevious(null); // polygon to axis
311391
} else {
312392
// fill to self: just join the path to itself
313393
transition(ownFillEl3).attr('d', fullpath + 'Z')
314394
.call(Drawing.singleFillStyle, gd);
395+
396+
// and simply emit hover polygons for each segment
397+
thisPolygons = makeSelfPolygons();
315398
}
316399
}
400+
trace._polygons = thisPolygons;
317401
} else if(tonext) {
318402
if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevRevpath) {
319403
// fill to next: full trace path, plus the previous path reversed
@@ -324,6 +408,13 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
324408
// inside the other, but then that is a strange usage.
325409
transition(tonext).attr('d', fullpath + 'Z' + prevRevpath + 'Z')
326410
.call(Drawing.singleFillStyle, gd);
411+
412+
// and simply emit hover polygons for each segment
413+
thisPolygons = makeSelfPolygons();
414+
415+
// we add the polygons of the previous trace which causes hover
416+
// detection to ignore points contained in them.
417+
trace._polygons = thisPolygons.concat(prevPolygons); // this does not modify thisPolygons, on purpose
327418
} else {
328419
// tonextx/y: for now just connect endpoints with lines. This is
329420
// the correct behavior if the endpoints are at the same value of
@@ -332,20 +423,25 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition
332423
// existing curve or off the end of it
333424
transition(tonext).attr('d', fullpath + 'L' + prevRevpath.substr(1) + 'Z')
334425
.call(Drawing.singleFillStyle, gd);
426+
427+
// create hover polygons that extend to the previous trace.
428+
thisPolygons = makePolygonsToPrevious(prevFillsegments);
429+
430+
// in this case our polygons do not cover that of previous traces,
431+
// so must not include previous trace polygons for hover detection.
432+
trace._polygons = thisPolygons;
335433
}
336-
trace._polygons = trace._polygons.concat(prevPolygons);
337434
} else {
338435
clearFill(tonext);
339-
trace._polygons = null;
340436
}
341437
}
342438
trace._prevRevpath = revpath;
343-
trace._prevPolygons = thisPolygons;
344439
} else {
345440
if(ownFillEl3) clearFill(ownFillEl3);
346441
else if(tonext) clearFill(tonext);
347-
trace._polygons = trace._prevRevpath = trace._prevPolygons = null;
442+
trace._prevRevpath = null;
348443
}
444+
trace._ownPolygons = thisPolygons;
349445

350446

351447
function visFilter(d) {

0 commit comments

Comments
 (0)