Skip to content

Commit 2fbaaaa

Browse files
committed
Tracer-bullet impl of fast click-to-select [1852]
Reason: to be reviewable from a perf standpoint.
1 parent def6aa5 commit 2fbaaaa

File tree

5 files changed

+199
-21
lines changed

5 files changed

+199
-21
lines changed

Diff for: src/lib/polygon.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -179,25 +179,29 @@ polygon.tester = function tester(ptsIn) {
179179
*/
180180
polygon.multitester = function multitester(list) {
181181
var testers = [],
182-
xmin = list[0][0][0],
182+
xmin = list[0].contains ? 0 : list[0][0][0],
183183
xmax = xmin,
184-
ymin = list[0][0][1],
184+
ymin = list[0].contains ? 0 : list[0][0][1],
185185
ymax = ymin;
186186

187187
for(var i = 0; i < list.length; i++) {
188-
var tester = polygon.tester(list[i]);
189-
tester.subtract = list[i].subtract;
190-
testers.push(tester);
191-
xmin = Math.min(xmin, tester.xmin);
192-
xmax = Math.max(xmax, tester.xmax);
193-
ymin = Math.min(ymin, tester.ymin);
194-
ymax = Math.max(ymax, tester.ymax);
188+
if(list[i].contains) {
189+
testers.push(list[i]);
190+
} else {
191+
var tester = polygon.tester(list[i]);
192+
tester.subtract = list[i].subtract;
193+
testers.push(tester);
194+
xmin = Math.min(xmin, tester.xmin);
195+
xmax = Math.max(xmax, tester.xmax);
196+
ymin = Math.min(ymin, tester.ymin);
197+
ymax = Math.max(ymax, tester.ymax);
198+
}
195199
}
196200

197-
function contains(pt, arg) {
201+
function contains(pt, arg, index, expandedTraceIndex) {
198202
var yes = false;
199203
for(var i = 0; i < testers.length; i++) {
200-
if(testers[i].contains(pt, arg)) {
204+
if(testers[i].contains(pt, arg, index, expandedTraceIndex)) {
201205
// if contained by subtract polygon - exclude the point
202206
yes = testers[i].subtract === false;
203207
}

Diff for: src/plots/cartesian/select.js

+181-7
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
264264
}
265265

266266
// draw selection
267-
var paths = [];
268-
for(i = 0; i < mergedPolygons.length; i++) {
269-
var ppts = mergedPolygons[i];
270-
paths.push(ppts.join('L') + 'L' + ppts[0]);
271-
}
272-
outlines
273-
.attr('d', 'M' + paths.join('M') + 'Z');
267+
drawSelection(mergedPolygons, outlines);
268+
274269

275270
throttle.throttle(
276271
throttleID,
@@ -320,6 +315,65 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
320315
gd.emit('plotly_deselect', null);
321316
}
322317
else {
318+
319+
320+
321+
var hoverData = gd._hoverdata;
322+
var selection = [];
323+
var traceSelection;
324+
var thisTracesSelection;
325+
var pointSelected;
326+
var subtract;
327+
328+
if(isHoverDataSet(hoverData)) {
329+
var clickedPtInfo = extractClickedPtInfo(hoverData, searchTraces);
330+
331+
// TODO perf: call potentially costly operation (see impl comment) only when needed
332+
pointSelected = isPointSelected(clickedPtInfo.searchInfo.cd[0].trace,
333+
clickedPtInfo.pointNumber);
334+
335+
if(pointSelected && isOnlyOnePointSelected(searchTraces)) {
336+
// TODO DRY see doubleClick handling above
337+
outlines.remove();
338+
for(i = 0; i < searchTraces.length; i++) {
339+
searchInfo = searchTraces[i];
340+
searchInfo._module.selectPoints(searchInfo, false);
341+
}
342+
343+
updateSelectedState(gd, searchTraces);
344+
gd.emit('plotly_deselect', null);
345+
} else {
346+
subtract = evt.shiftKey && pointSelected;
347+
currentPolygon = createPtNumTester(clickedPtInfo.pointNumber,
348+
clickedPtInfo.searchInfo.cd[0].trace._expandedIndex, subtract);
349+
350+
var concatenatedPolygons = dragOptions.polygons.concat([currentPolygon]);
351+
testPoly = multipolygonTester(concatenatedPolygons);
352+
353+
for(i = 0; i < searchTraces.length; i++) {
354+
traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], testPoly);
355+
thisTracesSelection = fillSelectionItem(traceSelection, searchTraces[i]);
356+
357+
if(selection.length) {
358+
for(var j = 0; j < thisTracesSelection.length; j++) {
359+
selection.push(thisTracesSelection[j]);
360+
}
361+
}
362+
else selection = thisTracesSelection;
363+
}
364+
365+
eventData = {points: selection};
366+
updateSelectedState(gd, searchTraces, eventData);
367+
368+
if(currentPolygon && dragOptions.polygons) {
369+
dragOptions.polygons.push(currentPolygon);
370+
}
371+
}
372+
373+
}
374+
375+
drawSelection(dragOptions.mergedPolygons, outlines);
376+
323377
// TODO: remove in v2 - this was probably never intended to work as it does,
324378
// but in case anyone depends on it we don't want to break it now.
325379
gd.emit('plotly_selected', undefined);
@@ -349,6 +403,126 @@ function prepSelect(e, startX, startY, dragOptions, mode) {
349403
};
350404
}
351405

406+
function drawSelection(polygons, outlines) {
407+
var paths = [];
408+
var i;
409+
var d;
410+
411+
for(i = 0; i < polygons.length; i++) {
412+
var ppts = polygons[i];
413+
paths.push(ppts.join('L') + 'L' + ppts[0]);
414+
}
415+
416+
d = polygons.length > 0 ?
417+
'M' + paths.join('M') + 'Z' :
418+
''; // TODO empty d attribute works in Chrome, but is it valid / can we rely on it?
419+
outlines.attr('d', d);
420+
}
421+
422+
function isHoverDataSet(hoverData) {
423+
return hoverData &&
424+
Array.isArray(hoverData) &&
425+
hoverData[0].hoverOnBox !== true;
426+
}
427+
428+
function extractClickedPtInfo(hoverData, searchTraces) {
429+
var hoverDatum = hoverData[0];
430+
var pointNumber = -1;
431+
var pointNumbers = [];
432+
var searchInfo;
433+
var i;
434+
435+
for(i = 0; i < searchTraces.length; i++) {
436+
searchInfo = searchTraces[i];
437+
if(hoverDatum.fullData._expandedIndex === searchInfo.cd[0].trace._expandedIndex) {
438+
439+
// Special case for box (and violin)
440+
if(hoverDatum.hoverOnBox === true) {
441+
break;
442+
}
443+
444+
// TODO hoverDatum not having a pointNumber but a binNumber seems to be an oddity of histogram only
445+
// Not deleting .pointNumber in histogram/event_data.js would simplify code here and in addition
446+
// would not break the hover event structure
447+
// documented at https://plot.ly/javascript/hover-events/
448+
if(hoverDatum.pointNumber !== undefined) {
449+
pointNumber = hoverDatum.pointNumber;
450+
} else if(hoverDatum.binNumber !== undefined) {
451+
pointNumber = hoverDatum.binNumber;
452+
pointNumbers = hoverDatum.pointNumbers;
453+
}
454+
455+
break;
456+
}
457+
}
458+
459+
return {
460+
pointNumber: pointNumber,
461+
pointNumbers: pointNumbers,
462+
searchInfo: searchInfo
463+
};
464+
}
465+
466+
// TODO What about passing a searchInfo instead of wantedExpandedTraceIndex?
467+
function createPtNumTester(wantedPointNumber, wantedExpandedTraceIndex, subtract) {
468+
return {
469+
xmin: 0,
470+
xmax: 0,
471+
ymin: 0,
472+
ymax: 0,
473+
pts: [],
474+
// TODO Consider making signature of contains more lean
475+
contains: function(pt, omitFirstEdge, pointNumber, expandedTraceIndex) {
476+
return expandedTraceIndex === wantedExpandedTraceIndex && pointNumber === wantedPointNumber;
477+
},
478+
isRect: false,
479+
degenerate: false,
480+
subtract: subtract
481+
};
482+
}
483+
484+
function isPointSelected(trace, pointNumber) {
485+
// TODO improve perf
486+
// Primarily we need this function to determine if a click adds or subtracts from a selection.
487+
//
488+
// IME best user experience would be
489+
// - that Shift+Click an unselected points adds to selection
490+
// - and Shift+Click a selected point subtracts from selection.
491+
//
492+
// Several options:
493+
// 1. Avoid problem at all by binding subtract-selection-by-click operation to Shift+Alt-Click.
494+
// Slightly less intuitive. A lot of programs deselect an already selected element when you
495+
// Shift+Click it.
496+
// 2. Delegate decision to the traces module through an additional
497+
// isSelected(searchInfo, pointNumber) function. Traces like scatter or bar have
498+
// a selected flag attached to each calcData element, thus access to that information
499+
// would be fast. However, scattergl only maintains selectBatch and unselectBatch arrays.
500+
// So simply searching through those arrays in scattegl would be slow. Just imagine
501+
// a user selecting all data points with one lasso polygon. So scattergl would require some
502+
// work.
503+
return trace.selectedpoints ? trace.selectedpoints.indexOf(pointNumber) > -1 : false;
504+
}
505+
506+
function isOnlyOnePointSelected(searchTraces) {
507+
var len = 0;
508+
var searchInfo;
509+
var trace;
510+
var i;
511+
512+
for(i = 0; i < searchTraces.length; i++) {
513+
searchInfo = searchTraces[i];
514+
trace = searchInfo.cd[0].trace;
515+
if(trace.selectedpoints) {
516+
if(trace.selectedpoints.length > 1) return false;
517+
518+
len += trace.selectedpoints.length;
519+
if(len > 1) return false;
520+
}
521+
}
522+
523+
return len === 1;
524+
}
525+
352526
function updateSelectedState(gd, searchTraces, eventData) {
353527
var i, j, searchInfo, trace;
354528

Diff for: src/traces/bar/select.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
2424
for(i = 0; i < cd.length; i++) {
2525
var di = cd[i];
2626

27-
if(polygon.contains(di.ct)) {
27+
if(polygon.contains(di.ct, false, i, searchInfo.cd[0].trace._expandedIndex)) {
2828
selection.push({
2929
pointNumber: i,
3030
x: xa.c2d(di.x),

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module.exports = function selectPoints(searchInfo, polygon) {
3636
x = xa.c2p(di.x);
3737
y = ya.c2p(di.y);
3838

39-
if(polygon.contains([x, y])) {
39+
if(polygon.contains([x, y], false, i, searchInfo.cd[0].trace._expandedIndex)) {
4040
selection.push({
4141
pointNumber: i,
4242
x: xa.c2d(di.x),

Diff for: src/traces/scattergl/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ function selectPoints(searchInfo, polygon) {
840840
if(polygon !== false && !polygon.degenerate) {
841841
els = [], unels = [];
842842
for(i = 0; i < stash.count; i++) {
843-
if(polygon.contains([stash.xpx[i], stash.ypx[i]])) {
843+
if(polygon.contains([stash.xpx[i], stash.ypx[i]], false, i, searchInfo.cd[0].trace._expandedIndex)) {
844844
els.push(i);
845845
selection.push({
846846
pointNumber: i,

0 commit comments

Comments
 (0)