Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable multiple selection for box/lasso tool #1853

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"ndarray-fill": "^1.0.2",
"ndarray-homography": "^1.0.0",
"ndarray-ops": "^1.2.2",
"poly-bool": "^1.0.0",
"regl": "^1.3.0",
"right-now": "^1.0.0",
"robust-orientation": "^1.1.3",
Expand Down
39 changes: 39 additions & 0 deletions src/lib/polygon.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var polygon = module.exports = {};
* returns boolean: is pt inside the polygon (including on its edges)
*/
polygon.tester = function tester(ptsIn) {
if(Array.isArray(ptsIn[0][0])) return polygon.multitester(ptsIn);

var pts = ptsIn.slice(),
xmin = pts[0][0],
xmax = xmin,
Expand Down Expand Up @@ -160,6 +162,43 @@ polygon.tester = function tester(ptsIn) {
};
};

/**
* Test multiple polygons
*/
polygon.multitester = function multitester(list) {
var testers = [],
xmin = list[0][0][0],
xmax = xmin,
ymin = list[0][0][1],
ymax = ymin;

for(var i = 0; i < list.length; i++) {
var tester = polygon.tester(list[i]);
testers.push(tester);
xmin = Math.min(xmin, tester.xmin);
xmax = Math.max(xmax, tester.xmax);
ymin = Math.min(ymin, tester.ymin);
ymax = Math.max(ymax, tester.ymax);
}

function contains(pt, arg) {
for(var i = 0; i < testers.length; i++) {
if(testers[i].contains(pt, arg)) return true;
}
return false;
}

return {
xmin: xmin,
xmax: xmax,
ymin: ymin,
ymax: ymax,
pts: [],
contains: contains,
isRect: false
};
};

/**
* Test if a segment of a points array is bent or straight
*
Expand Down
48 changes: 44 additions & 4 deletions src/plots/cartesian/dragbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
// to pan (or to zoom if it already is pan) on shift
if(e.shiftKey) {
if(dragModeNow === 'pan') dragModeNow = 'zoom';
else dragModeNow = 'pan';
else if(!isSelectOrLasso(dragModeNow)) dragModeNow = 'pan';
}
else if(e.ctrlKey) {
dragModeNow = 'pan';
}
}
// all other draggers just pan
Expand Down Expand Up @@ -168,13 +171,29 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) {
else if(isSelectOrLasso(dragModeNow)) {
dragOptions.xaxes = xa;
dragOptions.yaxes = ya;
// take over selection polygons from prev mode, if any
if(e.shiftKey && plotinfo.selection.polygons && !dragOptions.polygons) {
dragOptions.polygons = plotinfo.selection.polygons;
dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons;
}
// create new polygons, if shift mode
else if(!e.shiftKey || (e.shiftKey && !plotinfo.selection.polygons)) {
plotinfo.selection = {};
plotinfo.selection.polygons = dragOptions.polygons = [];
dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons = [];
}
prepSelect(e, startX, startY, dragOptions, dragModeNow);
}
}
};

dragElement.init(dragOptions);

// FIXME: this hack highlights selection once we enter select/lasso mode
if(isSelectOrLasso(gd._fullLayout.dragmode) && plotinfo.selection) {
showSelect(zoomlayer, dragOptions);
}

var x0,
y0,
box,
Expand Down Expand Up @@ -899,6 +918,29 @@ function clearSelect(zoomlayer) {
zoomlayer.selectAll('.select-outline').remove();
}

function showSelect(zoomlayer, dragOptions) {
var outlines = zoomlayer.selectAll('path.select-outline').data([1, 2]),
plotinfo = dragOptions.plotinfo,
xaxis = plotinfo.xaxis,
yaxis = plotinfo.yaxis,
selection = plotinfo.selection,
polygons = selection.mergedPolygons,
xs = xaxis._offset,
ys = yaxis._offset,
paths = [];

for(var i = 0; i < polygons.length; i++) {
var ppts = polygons[i];
paths.push(ppts.join('L') + 'L' + ppts[0]);
}

outlines.enter()
.append('path')
.attr('class', function(d) { return 'select-outline select-outline-' + d; })
.attr('transform', 'translate(' + xs + ', ' + ys + ')')
.attr('d', 'M' + paths.join('M') + 'Z');
}

function updateZoombox(zb, corners, box, path0, dimmed, lum) {
zb.attr('d',
path0 + 'M' + (box.l) + ',' + (box.t) + 'v' + (box.h) +
Expand All @@ -921,9 +963,7 @@ function removeZoombox(gd) {
}

function isSelectOrLasso(dragmode) {
var modes = ['lasso', 'select'];

return modes.indexOf(dragmode) !== -1;
return dragmode === 'lasso' || dragmode === 'select';
}

function xCorners(box, y0) {
Expand Down
77 changes: 51 additions & 26 deletions src/plots/cartesian/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

'use strict';

var polybool = require('poly-bool');
var polygon = require('../../lib/polygon');
var color = require('../../components/color');
var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue;
Expand All @@ -18,12 +19,16 @@ var constants = require('./constants');

var filteredPolygon = polygon.filter;
var polygonTester = polygon.tester;
var multipolygonTester = polygon.multitester;
var MINSELECT = constants.MINSELECT;

function getAxId(ax) { return ax._id; }

module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
var plot = dragOptions.gd._fullLayout._zoomlayer,

module.exports = prepSelect;

function prepSelect(e, startX, startY, dragOptions, mode) {
var zoomlayer = dragOptions.gd._fullLayout._zoomlayer,
dragBBox = dragOptions.element.getBoundingClientRect(),
xs = dragOptions.plotinfo.xaxis._offset,
ys = dragOptions.plotinfo.yaxis._offset,
Expand All @@ -37,21 +42,21 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
xAxisIds = dragOptions.xaxes.map(getAxId),
yAxisIds = dragOptions.yaxes.map(getAxId),
allAxes = dragOptions.xaxes.concat(dragOptions.yaxes),
pts;
filterPoly, testPoly, mergedPolygons, currentPolygon;

if(mode === 'lasso') {
pts = filteredPolygon([[x0, y0]], constants.BENDPX);
filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX);
}

var outlines = plot.selectAll('path.select-outline').data([1, 2]);
var outlines = zoomlayer.selectAll('path.select-outline').data([1, 2]);

outlines.enter()
.append('path')
.attr('class', function(d) { return 'select-outline select-outline-' + d; })
.attr('transform', 'translate(' + xs + ', ' + ys + ')')
.attr('d', path0 + 'Z');

var corners = plot.append('path')
var corners = zoomlayer.append('path')
.attr('class', 'zoombox-corners')
.style({
fill: color.background,
Expand Down Expand Up @@ -107,8 +112,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
function ascending(a, b) { return a - b; }

dragOptions.moveFn = function(dx0, dy0) {
var poly,
ax;
var ax, i;
x1 = Math.max(0, Math.min(pw, dx0 + x0));
y1 = Math.max(0, Math.min(ph, dy0 + y0));

Expand All @@ -118,42 +122,57 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
if(mode === 'select') {
if(dy < Math.min(dx * 0.6, MINSELECT)) {
// horizontal motion: make a vertical box
poly = polygonTester([[x0, 0], [x0, ph], [x1, ph], [x1, 0]]);
currentPolygon = [[x0, 0], [x0, ph], [x1, ph], [x1, 0]];
// extras to guide users in keeping a straight selection
corners.attr('d', 'M' + poly.xmin + ',' + (y0 - MINSELECT) +
corners.attr('d', 'M' + Math.min(x0, x1) + ',' + (y0 - MINSELECT) +
'h-4v' + (2 * MINSELECT) + 'h4Z' +
'M' + (poly.xmax - 1) + ',' + (y0 - MINSELECT) +
'M' + (Math.max(x0, x1) - 1) + ',' + (y0 - MINSELECT) +
'h4v' + (2 * MINSELECT) + 'h-4Z');

}
else if(dx < Math.min(dy * 0.6, MINSELECT)) {
// vertical motion: make a horizontal box
poly = polygonTester([[0, y0], [0, y1], [pw, y1], [pw, y0]]);
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + poly.ymin +
currentPolygon = [[0, y0], [0, y1], [pw, y1], [pw, y0]];
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + Math.min(y0, y1) +
'v-4h' + (2 * MINSELECT) + 'v4Z' +
'M' + (x0 - MINSELECT) + ',' + (poly.ymax - 1) +
'M' + (x0 - MINSELECT) + ',' + (Math.max(y0, y1) - 1) +
'v4h' + (2 * MINSELECT) + 'v-4Z');
}
else {
// diagonal motion
poly = polygonTester([[x0, y0], [x0, y1], [x1, y1], [x1, y0]]);
currentPolygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]];
corners.attr('d', 'M0,0Z');
}
outlines.attr('d', 'M' + poly.xmin + ',' + poly.ymin +
'H' + (poly.xmax - 1) + 'V' + (poly.ymax - 1) +
'H' + poly.xmin + 'Z');
}
else if(mode === 'lasso') {
pts.addPt([x1, y1]);
poly = polygonTester(pts.filtered);
outlines.attr('d', 'M' + pts.filtered.join('L') + 'Z');
filterPoly.addPt([x1, y1]);
currentPolygon = filterPoly.filtered;
}

// create outline & tester
if(dragOptions.polygons.length) {
mergedPolygons = polybool(dragOptions.mergedPolygons, [currentPolygon], 'or');
testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon]));
}
else {
mergedPolygons = [currentPolygon];
testPoly = polygonTester(currentPolygon);
}

// draw selection
var paths = [];
for(i = 0; i < mergedPolygons.length; i++) {
var ppts = mergedPolygons[i];
paths.push(ppts.join('L') + 'L' + ppts[0]);
}
outlines.attr('d', 'M' + paths.join('M') + 'Z');

// select points
selection = [];
for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
[].push.apply(selection, fillSelectionItem(
searchInfo.selectPoints(searchInfo, poly), searchInfo
searchInfo.selectPoints(searchInfo, testPoly), searchInfo
));
}

Expand All @@ -167,16 +186,16 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
ax = allAxes[i];
axLetter = ax._id.charAt(0);
ranges[ax._id] = [
ax.p2d(poly[axLetter + 'min']),
ax.p2d(poly[axLetter + 'max'])].sort(ascending);
ax.p2d(testPoly[axLetter + 'min']),
ax.p2d(testPoly[axLetter + 'max'])].sort(ascending);
}
}
else {
var dataPts = eventData.lassoPoints = {};

for(i = 0; i < allAxes.length; i++) {
ax = allAxes[i];
dataPts[ax._id] = pts.filtered.map(axValue(ax));
dataPts[ax._id] = filterPoly.filtered.map(axValue(ax));
}
}
dragOptions.gd.emit('plotly_selecting', eventData);
Expand All @@ -197,8 +216,14 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
else {
dragOptions.gd.emit('plotly_selected', eventData);
}

// save last polygons
dragOptions.polygons.push(currentPolygon);

// we have to keep reference to arrays, therefore just replace items
dragOptions.mergedPolygons.splice.apply(dragOptions.mergedPolygons, [0, dragOptions.mergedPolygons.length].concat(mergedPolygons));
};
};
}

function fillSelectionItem(selection, searchInfo) {
if(Array.isArray(selection)) {
Expand Down