Skip to content

Commit 2fcdb7c

Browse files
committed
Merge branch 'master' into persistent-point-selection
2 parents 10a2fa9 + 2656167 commit 2fcdb7c

File tree

5 files changed

+253
-43
lines changed

5 files changed

+253
-43
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"ndarray-fill": "^1.0.2",
9494
"ndarray-homography": "^1.0.0",
9595
"ndarray-ops": "^1.2.2",
96+
"polybooljs": "^1.2.0",
9697
"regl": "^1.3.0",
9798
"right-now": "^1.0.0",
9899
"robust-orientation": "^1.1.3",

src/lib/polygon.js

+61-3
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ var polygon = module.exports = {};
3131
* returns boolean: is pt inside the polygon (including on its edges)
3232
*/
3333
polygon.tester = function tester(ptsIn) {
34+
if(Array.isArray(ptsIn[0][0])) return polygon.multitester(ptsIn);
35+
3436
var pts = ptsIn.slice(),
3537
xmin = pts[0][0],
3638
xmax = xmin,
3739
ymin = pts[0][1],
38-
ymax = ymin;
40+
ymax = ymin,
41+
i;
3942

4043
pts.push(pts[0]);
41-
for(var i = 1; i < pts.length; i++) {
44+
for(i = 1; i < pts.length; i++) {
4245
xmin = Math.min(xmin, pts[i][0]);
4346
xmax = Math.max(xmax, pts[i][0]);
4447
ymin = Math.min(ymin, pts[i][1]);
@@ -149,14 +152,69 @@ polygon.tester = function tester(ptsIn) {
149152
return crossings % 2 === 1;
150153
}
151154

155+
// detect if poly is degenerate
156+
var degenerate = true;
157+
var lastPt = pts[0];
158+
for(i = 1; i < pts.length; i++) {
159+
if(lastPt[0] !== pts[i][0] || lastPt[1] !== pts[i][1]) {
160+
degenerate = false;
161+
break;
162+
}
163+
}
164+
152165
return {
153166
xmin: xmin,
154167
xmax: xmax,
155168
ymin: ymin,
156169
ymax: ymax,
157170
pts: pts,
158171
contains: isRect ? rectContains : contains,
159-
isRect: isRect
172+
isRect: isRect,
173+
degenerate: degenerate
174+
};
175+
};
176+
177+
/**
178+
* Test multiple polygons
179+
*/
180+
polygon.multitester = function multitester(list) {
181+
var testers = [],
182+
xmin = list[0][0][0],
183+
xmax = xmin,
184+
ymin = list[0][0][1],
185+
ymax = ymin;
186+
187+
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);
195+
}
196+
197+
function contains(pt, arg) {
198+
var yes = false;
199+
for(var i = 0; i < testers.length; i++) {
200+
if(testers[i].contains(pt, arg)) {
201+
// if contained by subtract polygon - exclude the point
202+
yes = testers[i].subtract === false;
203+
}
204+
}
205+
206+
return yes;
207+
}
208+
209+
return {
210+
xmin: xmin,
211+
xmax: xmax,
212+
ymin: ymin,
213+
ymax: ymax,
214+
pts: [],
215+
contains: contains,
216+
isRect: false,
217+
degenerate: false
160218
};
161219
};
162220

src/plots/cartesian/dragbox.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
140140
// to pan (or to zoom if it already is pan) on shift
141141
if(e.shiftKey) {
142142
if(dragModeNow === 'pan') dragModeNow = 'zoom';
143-
else dragModeNow = 'pan';
143+
else if(!isSelectOrLasso(dragModeNow)) dragModeNow = 'pan';
144+
}
145+
else if(e.ctrlKey) {
146+
dragModeNow = 'pan';
144147
}
145148
}
146149
// all other draggers just pan
@@ -526,6 +529,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
526529
}
527530

528531
updateSubplots([x0, y0, pw - dx, ph - dy]);
532+
529533
ticksAndAnnotations(yActive, xActive);
530534
}
531535

@@ -924,9 +928,7 @@ function removeZoombox(gd) {
924928
}
925929

926930
function isSelectOrLasso(dragmode) {
927-
var modes = ['lasso', 'select'];
928-
929-
return modes.indexOf(dragmode) !== -1;
931+
return dragmode === 'lasso' || dragmode === 'select';
930932
}
931933

932934
function xCorners(box, y0) {

src/plots/cartesian/select.js

+105-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
'use strict';
1111

12+
var polybool = require('polybooljs');
1213
var polygon = require('../../lib/polygon');
1314
var throttle = require('../../lib/throttle');
1415
var color = require('../../components/color');
@@ -19,6 +20,7 @@ var constants = require('./constants');
1920

2021
var filteredPolygon = polygon.filter;
2122
var polygonTester = polygon.tester;
23+
var multipolygonTester = polygon.multitester;
2224
var MINSELECT = constants.MINSELECT;
2325

2426
function getAxId(ax) { return ax._id; }
@@ -41,10 +43,24 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
4143
xAxisIds = dragOptions.xaxes.map(getAxId),
4244
yAxisIds = dragOptions.yaxes.map(getAxId),
4345
allAxes = dragOptions.xaxes.concat(dragOptions.yaxes),
44-
pts;
46+
filterPoly, testPoly, mergedPolygons, currentPolygon,
47+
subtract = e.altKey;
48+
49+
50+
// take over selection polygons from prev mode, if any
51+
if((e.shiftKey || e.altKey) && (plotinfo.selection && plotinfo.selection.polygons) && !dragOptions.polygons) {
52+
dragOptions.polygons = plotinfo.selection.polygons;
53+
dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
54+
}
55+
// create new polygons, if shift mode
56+
else if((!e.shiftKey && !e.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) {
57+
plotinfo.selection = {};
58+
plotinfo.selection.polygons = dragOptions.polygons = [];
59+
plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = [];
60+
}
4561

4662
if(mode === 'lasso') {
47-
pts = filteredPolygon([[x0, y0]], constants.BENDPX);
63+
filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
4864
}
4965

5066
var outlines = zoomLayer.selectAll('path.select-outline').data([1, 2]);
@@ -132,20 +148,18 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
132148
}
133149
};
134150
} else {
135-
fillRangeItems = function(eventData, poly, pts) {
151+
fillRangeItems = function(eventData, currentPolygon, filterPoly) {
136152
var dataPts = eventData.lassoPoints = {};
137153

138154
for(i = 0; i < allAxes.length; i++) {
139155
var ax = allAxes[i];
140-
dataPts[ax._id] = pts.filtered.map(axValue(ax));
156+
dataPts[ax._id] = filterPoly.filtered.map(axValue(ax));
141157
}
142158
};
143159
}
144160
}
145161

146162
dragOptions.moveFn = function(dx0, dy0) {
147-
var poly;
148-
149163
x1 = Math.max(0, Math.min(pw, dx0 + x0));
150164
y1 = Math.max(0, Math.min(ph, dy0 + y0));
151165

@@ -155,46 +169,79 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
155169
if(mode === 'select') {
156170
if(dy < Math.min(dx * 0.6, MINSELECT)) {
157171
// horizontal motion: make a vertical box
158-
poly = polygonTester([[x0, 0], [x0, ph], [x1, ph], [x1, 0]]);
172+
currentPolygon = [[x0, 0], [x0, ph], [x1, ph], [x1, 0]];
173+
currentPolygon.xmin = Math.min(x0, x1);
174+
currentPolygon.xmax = Math.max(x0, x1);
175+
currentPolygon.ymin = Math.min(0, ph);
176+
currentPolygon.ymax = Math.max(0, ph);
159177
// extras to guide users in keeping a straight selection
160-
corners.attr('d', 'M' + poly.xmin + ',' + (y0 - MINSELECT) +
178+
corners.attr('d', 'M' + currentPolygon.xmin + ',' + (y0 - MINSELECT) +
161179
'h-4v' + (2 * MINSELECT) + 'h4Z' +
162-
'M' + (poly.xmax - 1) + ',' + (y0 - MINSELECT) +
180+
'M' + (currentPolygon.xmax - 1) + ',' + (y0 - MINSELECT) +
163181
'h4v' + (2 * MINSELECT) + 'h-4Z');
164182

165183
}
166184
else if(dx < Math.min(dy * 0.6, MINSELECT)) {
167185
// vertical motion: make a horizontal box
168-
poly = polygonTester([[0, y0], [0, y1], [pw, y1], [pw, y0]]);
169-
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + poly.ymin +
186+
currentPolygon = [[0, y0], [0, y1], [pw, y1], [pw, y0]];
187+
currentPolygon.xmin = Math.min(0, pw);
188+
currentPolygon.xmax = Math.max(0, pw);
189+
currentPolygon.ymin = Math.min(y0, y1);
190+
currentPolygon.ymax = Math.max(y0, y1);
191+
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + currentPolygon.ymin +
170192
'v-4h' + (2 * MINSELECT) + 'v4Z' +
171-
'M' + (x0 - MINSELECT) + ',' + (poly.ymax - 1) +
193+
'M' + (x0 - MINSELECT) + ',' + (currentPolygon.ymax - 1) +
172194
'v4h' + (2 * MINSELECT) + 'v-4Z');
173195
}
174196
else {
175197
// diagonal motion
176-
poly = polygonTester([[x0, y0], [x0, y1], [x1, y1], [x1, y0]]);
198+
currentPolygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]];
199+
currentPolygon.xmin = Math.min(x0, x1);
200+
currentPolygon.xmax = Math.max(x0, x1);
201+
currentPolygon.ymin = Math.min(y0, y1);
202+
currentPolygon.ymax = Math.max(y0, y1);
177203
corners.attr('d', 'M0,0Z');
178204
}
179-
outlines.attr('d', 'M' + poly.xmin + ',' + poly.ymin +
180-
'H' + (poly.xmax - 1) + 'V' + (poly.ymax - 1) +
181-
'H' + poly.xmin + 'Z');
182205
}
183206
else if(mode === 'lasso') {
184-
pts.addPt([x1, y1]);
185-
poly = polygonTester(pts.filtered);
186-
outlines.attr('d', 'M' + pts.filtered.join('L') + 'Z');
207+
filterPoly.addPt([x1, y1]);
208+
currentPolygon = filterPoly.filtered;
187209
}
188210

211+
// create outline & tester
212+
if(dragOptions.polygons && dragOptions.polygons.length) {
213+
mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract);
214+
currentPolygon.subtract = subtract;
215+
testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon]));
216+
}
217+
else {
218+
mergedPolygons = [currentPolygon];
219+
testPoly = polygonTester(currentPolygon);
220+
}
221+
222+
// draw selection
223+
var paths = [];
224+
for(i = 0; i < mergedPolygons.length; i++) {
225+
var ppts = mergedPolygons[i];
226+
paths.push(ppts.join('L') + 'L' + ppts[0]);
227+
}
228+
outlines.attr('d', 'M' + paths.join('M') + 'Z');
229+
189230
throttle.throttle(
190231
throttleID,
191232
constants.SELECTDELAY,
192233
function() {
193234
selection = [];
235+
236+
var traceSelections = [], traceSelection;
194237
for(i = 0; i < searchTraces.length; i++) {
195238
searchInfo = searchTraces[i];
239+
240+
traceSelection = searchInfo.selectPoints(searchInfo, testPoly);
241+
traceSelections.push(traceSelection);
242+
196243
var thisSelection = fillSelectionItem(
197-
searchInfo.selectPoints(searchInfo, poly), searchInfo
244+
traceSelection, searchInfo
198245
);
199246
if(selection.length) {
200247
for(var j = 0; j < thisSelection.length; j++) {
@@ -206,14 +253,15 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
206253

207254
eventData = {points: selection};
208255
updateSelectedState(gd, searchTraces, eventData);
209-
fillRangeItems(eventData, poly, pts);
256+
fillRangeItems(eventData, currentPolygon, filterPoly);
210257
dragOptions.gd.emit('plotly_selecting', eventData);
211258
}
212259
);
213260
};
214261

215262
dragOptions.doneFn = function(dragged, numclicks) {
216263
corners.remove();
264+
217265
throttle.done(throttleID).then(function() {
218266
throttle.clear(throttleID);
219267

@@ -231,6 +279,16 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
231279
else {
232280
dragOptions.gd.emit('plotly_selected', eventData);
233281
}
282+
283+
if(currentPolygon && dragOptions.polygons) {
284+
// save last polygons
285+
currentPolygon.subtract = subtract;
286+
dragOptions.polygons.push(currentPolygon);
287+
288+
// we have to keep reference to arrays container
289+
dragOptions.mergedPolygons.length = 0;
290+
[].push.apply(dragOptions.mergedPolygons, mergedPolygons);
291+
}
234292
});
235293
};
236294
};
@@ -269,6 +327,32 @@ function updateSelectedState(gd, searchTraces, eventData) {
269327
}
270328
}
271329

330+
function mergePolygons(list, poly, subtract) {
331+
var res;
332+
333+
if(subtract) {
334+
res = polybool.difference({
335+
regions: list,
336+
inverted: false
337+
}, {
338+
regions: [poly],
339+
inverted: false
340+
});
341+
342+
return res.regions;
343+
}
344+
345+
res = polybool.union({
346+
regions: list,
347+
inverted: false
348+
}, {
349+
regions: [poly],
350+
inverted: false
351+
});
352+
353+
return res.regions;
354+
}
355+
272356
function fillSelectionItem(selection, searchInfo) {
273357
if(Array.isArray(selection)) {
274358
var trace = searchInfo.cd[0].trace;

0 commit comments

Comments
 (0)