Skip to content

Commit b800e55

Browse files
committed
Add multiselect
1 parent f49b18e commit b800e55

File tree

4 files changed

+156
-31
lines changed

4 files changed

+156
-31
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+
"poly-bool": "^1.0.0",
9697
"regl": "^1.3.0",
9798
"right-now": "^1.0.0",
9899
"robust-orientation": "^1.1.3",

src/lib/polygon.js

+55-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,63 @@ 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+
testers.push(tester);
190+
xmin = Math.min(xmin, tester.xmin);
191+
xmax = Math.max(xmax, tester.xmax);
192+
ymin = Math.min(ymin, tester.ymin);
193+
ymax = Math.max(ymax, tester.ymax);
194+
}
195+
196+
function contains(pt, arg) {
197+
for(var i = 0; i < testers.length; i++) {
198+
if(testers[i].contains(pt, arg)) return true;
199+
}
200+
return false;
201+
}
202+
203+
return {
204+
xmin: xmin,
205+
xmax: xmax,
206+
ymin: ymin,
207+
ymax: ymax,
208+
pts: [],
209+
contains: contains,
210+
isRect: false,
211+
degenerate: false
160212
};
161213
};
162214

src/plots/cartesian/dragbox.js

+49-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
@@ -168,13 +171,31 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
168171
else if(isSelectOrLasso(dragModeNow)) {
169172
dragOptions.xaxes = xa;
170173
dragOptions.yaxes = ya;
174+
175+
// take over selection polygons from prev mode, if any
176+
if(e.shiftKey && plotinfo.selection.polygons && !dragOptions.polygons) {
177+
dragOptions.polygons = plotinfo.selection.polygons;
178+
dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
179+
}
180+
// create new polygons, if shift mode
181+
else if(!e.shiftKey || (e.shiftKey && !plotinfo.selection.polygons)) {
182+
plotinfo.selection = {};
183+
plotinfo.selection.polygons = dragOptions.polygons = [];
184+
dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons = [];
185+
}
186+
171187
prepSelect(e, startX, startY, dragOptions, dragModeNow);
172188
}
173189
}
174190
};
175191

176192
dragElement.init(dragOptions);
177193

194+
// FIXME: this hack highlights selection once we enter select/lasso mode
195+
if(isSelectOrLasso(gd._fullLayout.dragmode) && plotinfo.selection) {
196+
showSelect(zoomlayer, dragOptions);
197+
}
198+
178199
var x0,
179200
y0,
180201
box,
@@ -526,6 +547,9 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
526547
}
527548

528549
updateSubplots([x0, y0, pw - dx, ph - dy]);
550+
551+
if(plotinfo.ondrag) plotinfo.ondrag.call([x0, y0, pw - dx, ph - dy]);
552+
529553
ticksAndAnnotations(yActive, xActive);
530554
}
531555

@@ -902,6 +926,29 @@ function clearSelect(zoomlayer) {
902926
zoomlayer.selectAll('.select-outline').remove();
903927
}
904928

929+
function showSelect(zoomlayer, dragOptions) {
930+
var outlines = zoomlayer.selectAll('path.select-outline').data([1, 2]),
931+
plotinfo = dragOptions.plotinfo,
932+
xaxis = plotinfo.xaxis,
933+
yaxis = plotinfo.yaxis,
934+
selection = plotinfo.selection,
935+
polygons = selection.mergedPolygons,
936+
xs = xaxis._offset,
937+
ys = yaxis._offset,
938+
paths = [];
939+
940+
for(var i = 0; i < polygons.length; i++) {
941+
var ppts = polygons[i];
942+
paths.push(ppts.join('L') + 'L' + ppts[0]);
943+
}
944+
945+
outlines.enter()
946+
.append('path')
947+
.attr('class', function(d) { return 'select-outline select-outline-' + d; })
948+
.attr('transform', 'translate(' + xs + ', ' + ys + ')')
949+
.attr('d', 'M' + paths.join('M') + 'Z');
950+
}
951+
905952
function updateZoombox(zb, corners, box, path0, dimmed, lum) {
906953
zb.attr('d',
907954
path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) +
@@ -924,9 +971,7 @@ function removeZoombox(gd) {
924971
}
925972

926973
function isSelectOrLasso(dragmode) {
927-
var modes = ['lasso', 'select'];
928-
929-
return modes.indexOf(dragmode) !== -1;
974+
return dragmode === 'lasso' || dragmode === 'select';
930975
}
931976

932977
function xCorners(box, y0) {

src/plots/cartesian/select.js

+51-24
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
'use strict';
1111

12+
var polybool = require('poly-bool');
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; }
@@ -39,10 +41,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
3941
xAxisIds = dragOptions.xaxes.map(getAxId),
4042
yAxisIds = dragOptions.yaxes.map(getAxId),
4143
allAxes = dragOptions.xaxes.concat(dragOptions.yaxes),
42-
pts;
44+
filterPoly, testPoly, mergedPolygons, currentPolygon;
4345

4446
if(mode === 'lasso') {
45-
pts = filteredPolygon([[x0, y0]], constants.BENDPX);
47+
filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
4648
}
4749

4850
var outlines = zoomLayer.selectAll('path.select-outline').data([1, 2]);
@@ -115,34 +117,33 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
115117
fillRangeItems = plotinfo.fillRangeItems;
116118
} else {
117119
if(mode === 'select') {
118-
fillRangeItems = function(eventData, poly) {
120+
fillRangeItems = function(eventData, currentPolygon) {
119121
var ranges = eventData.range = {};
120122

121123
for(i = 0; i < allAxes.length; i++) {
122124
var ax = allAxes[i];
123125
var axLetter = ax._id.charAt(0);
126+
var x = axLetter === 'x';
124127

125128
ranges[ax._id] = [
126-
ax.p2d(poly[axLetter + 'min']),
127-
ax.p2d(poly[axLetter + 'max'])
129+
ax.p2d(currentPolygon[0][x ? 0 : 1]),
130+
ax.p2d(currentPolygon[2][x ? 0 : 1])
128131
].sort(ascending);
129132
}
130133
};
131134
} else {
132-
fillRangeItems = function(eventData, poly, pts) {
135+
fillRangeItems = function(eventData, currentPolygon, filterPoly) {
133136
var dataPts = eventData.lassoPoints = {};
134137

135138
for(i = 0; i < allAxes.length; i++) {
136139
var ax = allAxes[i];
137-
dataPts[ax._id] = pts.filtered.map(axValue(ax));
140+
dataPts[ax._id] = filterPoly.filtered.map(axValue(ax));
138141
}
139142
};
140143
}
141144
}
142145

143146
dragOptions.moveFn = function(dx0, dy0) {
144-
var poly;
145-
146147
x1 = Math.max(0, Math.min(pw, dx0 + x0));
147148
y1 = Math.max(0, Math.min(ph, dy0 + y0));
148149

@@ -152,46 +153,66 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
152153
if(mode === 'select') {
153154
if(dy < Math.min(dx * 0.6, MINSELECT)) {
154155
// horizontal motion: make a vertical box
155-
poly = polygonTester([[x0, 0], [x0, ph], [x1, ph], [x1, 0]]);
156+
currentPolygon = [[x0, 0], [x0, ph], [x1, ph], [x1, 0]];
156157
// extras to guide users in keeping a straight selection
157-
corners.attr('d', 'M' + poly.xmin + ',' + (y0 - MINSELECT) +
158+
corners.attr('d', 'M' + Math.min(x0, x1) + ',' + (y0 - MINSELECT) +
158159
'h-4v' + (2 * MINSELECT) + 'h4Z' +
159-
'M' + (poly.xmax - 1) + ',' + (y0 - MINSELECT) +
160+
'M' + (Math.max(x0, x1) - 1) + ',' + (y0 - MINSELECT) +
160161
'h4v' + (2 * MINSELECT) + 'h-4Z');
161162

162163
}
163164
else if(dx < Math.min(dy * 0.6, MINSELECT)) {
164165
// vertical motion: make a horizontal box
165-
poly = polygonTester([[0, y0], [0, y1], [pw, y1], [pw, y0]]);
166-
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + poly.ymin +
166+
currentPolygon = [[0, y0], [0, y1], [pw, y1], [pw, y0]];
167+
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + Math.min(y0, y1) +
167168
'v-4h' + (2 * MINSELECT) + 'v4Z' +
168-
'M' + (x0 - MINSELECT) + ',' + (poly.ymax - 1) +
169+
'M' + (x0 - MINSELECT) + ',' + (Math.max(y0, y1) - 1) +
169170
'v4h' + (2 * MINSELECT) + 'v-4Z');
170171
}
171172
else {
172173
// diagonal motion
173-
poly = polygonTester([[x0, y0], [x0, y1], [x1, y1], [x1, y0]]);
174+
currentPolygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]];
174175
corners.attr('d', 'M0,0Z');
175176
}
176-
outlines.attr('d', 'M' + poly.xmin + ',' + poly.ymin +
177-
'H' + (poly.xmax - 1) + 'V' + (poly.ymax - 1) +
178-
'H' + poly.xmin + 'Z');
179177
}
180178
else if(mode === 'lasso') {
181-
pts.addPt([x1, y1]);
182-
poly = polygonTester(pts.filtered);
183-
outlines.attr('d', 'M' + pts.filtered.join('L') + 'Z');
179+
filterPoly.addPt([x1, y1]);
180+
currentPolygon = filterPoly.filtered;
181+
}
182+
183+
// create outline & tester
184+
if(dragOptions.polygons.length) {
185+
mergedPolygons = polybool(dragOptions.mergedPolygons, [currentPolygon], 'or');
186+
testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon]));
187+
}
188+
else {
189+
mergedPolygons = [currentPolygon];
190+
testPoly = polygonTester(currentPolygon);
191+
}
192+
193+
// draw selection
194+
var paths = [];
195+
for(i = 0; i < mergedPolygons.length; i++) {
196+
var ppts = mergedPolygons[i];
197+
paths.push(ppts.join('L') + 'L' + ppts[0]);
184198
}
199+
outlines.attr('d', 'M' + paths.join('M') + 'Z');
185200

186201
throttle.throttle(
187202
throttleID,
188203
constants.SELECTDELAY,
189204
function() {
190205
selection = [];
206+
207+
var traceSelections = [], traceSelection;
191208
for(i = 0; i < searchTraces.length; i++) {
192209
searchInfo = searchTraces[i];
210+
211+
traceSelection = searchInfo.selectPoints(searchInfo, testPoly);
212+
traceSelections.push(traceSelection);
213+
193214
var thisSelection = fillSelectionItem(
194-
searchInfo.selectPoints(searchInfo, poly), searchInfo
215+
traceSelection, searchInfo
195216
);
196217
if(selection.length) {
197218
for(var j = 0; j < thisSelection.length; j++) {
@@ -202,7 +223,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
202223
}
203224

204225
eventData = {points: selection};
205-
fillRangeItems(eventData, poly, pts);
226+
fillRangeItems(eventData, currentPolygon, filterPoly);
206227
dragOptions.gd.emit('plotly_selecting', eventData);
207228
}
208229
);
@@ -226,6 +247,12 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
226247
else {
227248
dragOptions.gd.emit('plotly_selected', eventData);
228249
}
250+
251+
// save last polygons
252+
dragOptions.polygons.push(currentPolygon);
253+
254+
// we have to keep reference to arrays, therefore just replace items
255+
dragOptions.mergedPolygons.splice.apply(dragOptions.mergedPolygons, [0, dragOptions.mergedPolygons.length].concat(mergedPolygons));
229256
});
230257
};
231258
};

0 commit comments

Comments
 (0)