-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Add multiselect #2140
Add multiselect #2140
Changes from 5 commits
b800e55
05b0dca
40fbca9
473d34d
02db56f
e8c18f0
ca4aaae
39a4dc4
ab92fd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -168,13 +171,31 @@ 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, | ||
|
@@ -526,6 +547,9 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { | |
} | ||
|
||
updateSubplots([x0, y0, pw - dx, ph - dy]); | ||
|
||
if(plotinfo.ondrag) plotinfo.ondrag.call([x0, y0, pw - dx, ph - dy]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
|
||
ticksAndAnnotations(yActive, xActive); | ||
} | ||
|
||
|
@@ -902,6 +926,31 @@ 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]); | ||
} | ||
|
||
if(paths.length) { | ||
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) + | ||
|
@@ -924,9 +973,7 @@ function removeZoombox(gd) { | |
} | ||
|
||
function isSelectOrLasso(dragmode) { | ||
var modes = ['lasso', 'select']; | ||
|
||
return modes.indexOf(dragmode) !== -1; | ||
return dragmode === 'lasso' || dragmode === 'select'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Way better 🐎 . Thanks! |
||
} | ||
|
||
function xCorners(box, y0) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
|
||
'use strict'; | ||
|
||
var polybool = require('polybooljs'); | ||
var polygon = require('../../lib/polygon'); | ||
var throttle = require('../../lib/throttle'); | ||
var color = require('../../components/color'); | ||
|
@@ -19,6 +20,7 @@ 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; } | ||
|
@@ -39,10 +41,10 @@ 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 = zoomLayer.selectAll('path.select-outline').data([1, 2]); | ||
|
@@ -129,20 +131,18 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { | |
} | ||
}; | ||
} else { | ||
fillRangeItems = function(eventData, poly, pts) { | ||
fillRangeItems = function(eventData, currentPolygon, filterPoly) { | ||
var dataPts = eventData.lassoPoints = {}; | ||
|
||
for(i = 0; i < allAxes.length; i++) { | ||
var ax = allAxes[i]; | ||
dataPts[ax._id] = pts.filtered.map(axValue(ax)); | ||
dataPts[ax._id] = filterPoly.filtered.map(axValue(ax)); | ||
} | ||
}; | ||
} | ||
} | ||
|
||
dragOptions.moveFn = function(dx0, dy0) { | ||
var poly; | ||
|
||
x1 = Math.max(0, Math.min(pw, dx0 + x0)); | ||
y1 = Math.max(0, Math.min(ph, dy0 + y0)); | ||
|
||
|
@@ -152,46 +152,78 @@ 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]]; | ||
currentPolygon.xmin = Math.min(x0, x1); | ||
currentPolygon.xmax = Math.max(x0, x1); | ||
currentPolygon.ymin = Math.min(0, ph); | ||
currentPolygon.ymax = Math.max(0, ph); | ||
// extras to guide users in keeping a straight selection | ||
corners.attr('d', 'M' + poly.xmin + ',' + (y0 - MINSELECT) + | ||
corners.attr('d', 'M' + currentPolygon.xmin + ',' + (y0 - MINSELECT) + | ||
'h-4v' + (2 * MINSELECT) + 'h4Z' + | ||
'M' + (poly.xmax - 1) + ',' + (y0 - MINSELECT) + | ||
'M' + (currentPolygon.xmax - 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]]; | ||
currentPolygon.xmin = Math.min(0, pw); | ||
currentPolygon.xmax = Math.max(0, pw); | ||
currentPolygon.ymin = Math.min(y0, y1); | ||
currentPolygon.ymax = Math.max(y0, y1); | ||
corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + currentPolygon.ymin + | ||
'v-4h' + (2 * MINSELECT) + 'v4Z' + | ||
'M' + (x0 - MINSELECT) + ',' + (poly.ymax - 1) + | ||
'M' + (x0 - MINSELECT) + ',' + (currentPolygon.ymax - 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]]; | ||
currentPolygon.xmin = Math.min(x0, x1); | ||
currentPolygon.xmax = Math.max(x0, x1); | ||
currentPolygon.ymin = Math.min(y0, y1); | ||
currentPolygon.ymax = Math.max(y0, y1); | ||
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 && dragOptions.polygons.length) { | ||
mergedPolygons = joinPolygons(dragOptions.mergedPolygons, currentPolygon); | ||
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'); | ||
|
||
throttle.throttle( | ||
throttleID, | ||
constants.SELECTDELAY, | ||
function() { | ||
selection = []; | ||
|
||
var traceSelections = [], traceSelection; | ||
for(i = 0; i < searchTraces.length; i++) { | ||
searchInfo = searchTraces[i]; | ||
|
||
traceSelection = searchInfo.selectPoints(searchInfo, testPoly); | ||
traceSelections.push(traceSelection); | ||
|
||
var thisSelection = fillSelectionItem( | ||
searchInfo.selectPoints(searchInfo, poly), searchInfo | ||
traceSelection, searchInfo | ||
); | ||
if(selection.length) { | ||
for(var j = 0; j < thisSelection.length; j++) { | ||
|
@@ -202,14 +234,15 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { | |
} | ||
|
||
eventData = {points: selection}; | ||
fillRangeItems(eventData, poly, pts); | ||
fillRangeItems(eventData, currentPolygon, filterPoly); | ||
dragOptions.gd.emit('plotly_selecting', eventData); | ||
} | ||
); | ||
}; | ||
|
||
dragOptions.doneFn = function(dragged, numclicks) { | ||
corners.remove(); | ||
|
||
throttle.done(throttleID).then(function() { | ||
throttle.clear(throttleID); | ||
|
||
|
@@ -226,10 +259,31 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { | |
else { | ||
dragOptions.gd.emit('plotly_selected', eventData); | ||
} | ||
|
||
if(currentPolygon && dragOptions.polygons) { | ||
// save last polygons | ||
dragOptions.polygons.push(currentPolygon); | ||
|
||
// we have to keep reference to arrays container | ||
dragOptions.mergedPolygons.length = 0; | ||
[].push.apply(dragOptions.mergedPolygons, mergedPolygons); | ||
} | ||
}); | ||
}; | ||
}; | ||
|
||
function joinPolygons(list, poly) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Joining the polygons sounds very reasonable to me. But, some users might expect that selections within selections would remove the inner polygons. Perhaps we'll need to add layout options (e.g. Any thoughts about this @alexcjohnson @chriddyp @cldougl @monfera ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My gut reaction: if you start a new selection inside an existing one, it's purely subtractive, if you start outside the existing ones it's purely additive. Would have to play with it implemented to see if this really feels intuitive though... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, added subtract selection by holding Alt in e8c18f0 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
+1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is more powerful (as well as easier to code 😅 ), my only concern is whether people will be able to find it. Though I guess if they can find add they can find subtract... (and I never saw There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice solution @dfcreative . The only (potential) problem I can think of is mobile and tablets users complaining no having access to Shift and Alt keys and by consequent multi-selections. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard good point, we can use @alexcjohnson strategy for that. But I'd wait for persistent selections PR merged first |
||
var res = polybool.union({ | ||
regions: list, | ||
inverted: false | ||
}, { | ||
regions: [poly], | ||
inverted: false | ||
}); | ||
|
||
return res.regions; | ||
} | ||
|
||
function fillSelectionItem(selection, searchInfo) { | ||
if(Array.isArray(selection)) { | ||
var trace = searchInfo.cd[0].trace; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a big fan of this behavior. @dfcreative is there a particular reason why you added this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not relevant since #2135