From b800e5537fc67d5b0493b33a57418c0a3a2fff9f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 2 Nov 2017 12:48:20 -0400 Subject: [PATCH 1/9] Add multiselect --- package.json | 1 + src/lib/polygon.js | 58 ++++++++++++++++++++++++-- src/plots/cartesian/dragbox.js | 53 ++++++++++++++++++++++-- src/plots/cartesian/select.js | 75 +++++++++++++++++++++++----------- 4 files changed, 156 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 49556953bed..728a1d1fc18 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,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", diff --git a/src/lib/polygon.js b/src/lib/polygon.js index d30d1fda104..a8305513ddc 100644 --- a/src/lib/polygon.js +++ b/src/lib/polygon.js @@ -31,14 +31,17 @@ 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, ymin = pts[0][1], - ymax = ymin; + ymax = ymin, + i; pts.push(pts[0]); - for(var i = 1; i < pts.length; i++) { + for(i = 1; i < pts.length; i++) { xmin = Math.min(xmin, pts[i][0]); xmax = Math.max(xmax, pts[i][0]); ymin = Math.min(ymin, pts[i][1]); @@ -149,6 +152,16 @@ polygon.tester = function tester(ptsIn) { return crossings % 2 === 1; } + // detect if poly is degenerate + var degenerate = true; + var lastPt = pts[0]; + for(i = 1; i < pts.length; i++) { + if(lastPt[0] !== pts[i][0] || lastPt[1] !== pts[i][1]) { + degenerate = false; + break; + } + } + return { xmin: xmin, xmax: xmax, @@ -156,7 +169,46 @@ polygon.tester = function tester(ptsIn) { ymax: ymax, pts: pts, contains: isRect ? rectContains : contains, - isRect: isRect + isRect: isRect, + degenerate: degenerate + }; +}; + +/** + * 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, + degenerate: false }; }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 4bf2881191e..789efba65d2 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -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,6 +171,19 @@ 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); } } @@ -175,6 +191,11 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { 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]); + ticksAndAnnotations(yActive, xActive); } @@ -902,6 +926,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) + @@ -924,9 +971,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) { diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 98afbcd5e84..0dae4a72142 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -9,6 +9,7 @@ 'use strict'; +var polybool = require('poly-bool'); 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]); @@ -115,34 +117,33 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { fillRangeItems = plotinfo.fillRangeItems; } else { if(mode === 'select') { - fillRangeItems = function(eventData, poly) { + fillRangeItems = function(eventData, currentPolygon) { var ranges = eventData.range = {}; for(i = 0; i < allAxes.length; i++) { var ax = allAxes[i]; var axLetter = ax._id.charAt(0); + var x = axLetter === 'x'; ranges[ax._id] = [ - ax.p2d(poly[axLetter + 'min']), - ax.p2d(poly[axLetter + 'max']) + ax.p2d(currentPolygon[0][x ? 0 : 1]), + ax.p2d(currentPolygon[2][x ? 0 : 1]) ].sort(ascending); } }; } 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 +153,66 @@ 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'); 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,7 +223,7 @@ 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); } ); @@ -226,6 +247,12 @@ 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)); }); }; }; From 05b0dca107cf3766e062290808fe74eba7ba3e7e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 2 Nov 2017 15:36:27 -0400 Subject: [PATCH 2/9] Fix failing cases --- src/plots/cartesian/dragbox.js | 12 +++++++----- src/plots/cartesian/select.js | 32 +++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 789efba65d2..6bd5ff2bff3 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -942,11 +942,13 @@ function showSelect(zoomlayer, dragOptions) { 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'); + 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) { diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 0dae4a72142..246c2f05b1b 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -117,17 +117,16 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { fillRangeItems = plotinfo.fillRangeItems; } else { if(mode === 'select') { - fillRangeItems = function(eventData, currentPolygon) { + fillRangeItems = function(eventData, poly) { var ranges = eventData.range = {}; for(i = 0; i < allAxes.length; i++) { var ax = allAxes[i]; var axLetter = ax._id.charAt(0); - var x = axLetter === 'x'; ranges[ax._id] = [ - ax.p2d(currentPolygon[0][x ? 0 : 1]), - ax.p2d(currentPolygon[2][x ? 0 : 1]) + ax.p2d(poly[axLetter + 'min']), + ax.p2d(poly[axLetter + 'max']) ].sort(ascending); } }; @@ -154,6 +153,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { if(dy < Math.min(dx * 0.6, MINSELECT)) { // horizontal motion: make a vertical box 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' + Math.min(x0, x1) + ',' + (y0 - MINSELECT) + 'h-4v' + (2 * MINSELECT) + 'h4Z' + @@ -164,6 +167,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { else if(dx < Math.min(dy * 0.6, MINSELECT)) { // vertical motion: make a horizontal box 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) + ',' + Math.min(y0, y1) + 'v-4h' + (2 * MINSELECT) + 'v4Z' + 'M' + (x0 - MINSELECT) + ',' + (Math.max(y0, y1) - 1) + @@ -172,6 +179,10 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { else { // diagonal motion 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'); } } @@ -181,7 +192,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { } // create outline & tester - if(dragOptions.polygons.length) { + if(dragOptions.polygons && dragOptions.polygons.length) { mergedPolygons = polybool(dragOptions.mergedPolygons, [currentPolygon], 'or'); testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon])); } @@ -231,6 +242,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { dragOptions.doneFn = function(dragged, numclicks) { corners.remove(); + throttle.done(throttleID).then(function() { throttle.clear(throttleID); @@ -248,11 +260,13 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { dragOptions.gd.emit('plotly_selected', eventData); } - // save last polygons - dragOptions.polygons.push(currentPolygon); + if(currentPolygon && dragOptions.polygons) { + // 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)); + // we have to keep reference to arrays, therefore just replace items + dragOptions.mergedPolygons.splice.apply(dragOptions.mergedPolygons, [0, dragOptions.mergedPolygons.length].concat(mergedPolygons)); + } }); }; }; From 40fbca9d5002979583556f67a18b74e252e27a2e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 2 Nov 2017 15:39:27 -0400 Subject: [PATCH 3/9] Avoid overcalculation --- src/plots/cartesian/select.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 246c2f05b1b..e709ee164d0 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -158,9 +158,9 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { 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' + Math.min(x0, x1) + ',' + (y0 - MINSELECT) + + corners.attr('d', 'M' + currentPolygon.xmin + ',' + (y0 - MINSELECT) + 'h-4v' + (2 * MINSELECT) + 'h4Z' + - 'M' + (Math.max(x0, x1) - 1) + ',' + (y0 - MINSELECT) + + 'M' + (currentPolygon.xmax - 1) + ',' + (y0 - MINSELECT) + 'h4v' + (2 * MINSELECT) + 'h-4Z'); } @@ -171,9 +171,9 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { currentPolygon.xmax = Math.max(0, pw); currentPolygon.ymin = Math.min(y0, y1); currentPolygon.ymax = Math.max(y0, y1); - corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + Math.min(y0, y1) + + corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + currentPolygon.ymin + 'v-4h' + (2 * MINSELECT) + 'v4Z' + - 'M' + (x0 - MINSELECT) + ',' + (Math.max(y0, y1) - 1) + + 'M' + (x0 - MINSELECT) + ',' + (currentPolygon.ymax - 1) + 'v4h' + (2 * MINSELECT) + 'v-4Z'); } else { From 473d34d09f00585ab03039e2e9af40871bdafb0e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 2 Nov 2017 18:44:05 -0400 Subject: [PATCH 4/9] Replace poly-bool --- package.json | 2 +- src/plots/cartesian/select.js | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 728a1d1fc18..303f6c4993e 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "ndarray-fill": "^1.0.2", "ndarray-homography": "^1.0.0", "ndarray-ops": "^1.2.2", - "poly-bool": "^1.0.0", + "polybooljs": "^1.2.0", "regl": "^1.3.0", "right-now": "^1.0.0", "robust-orientation": "^1.1.3", diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index e709ee164d0..dc0a3d60c97 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -9,7 +9,7 @@ 'use strict'; -var polybool = require('poly-bool'); +var polybool = require('polybooljs'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); var color = require('../../components/color'); @@ -193,7 +193,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { // create outline & tester if(dragOptions.polygons && dragOptions.polygons.length) { - mergedPolygons = polybool(dragOptions.mergedPolygons, [currentPolygon], 'or'); + mergedPolygons = joinPolygons(dragOptions.mergedPolygons, currentPolygon); testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon])); } else { @@ -271,6 +271,18 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { }; }; +function joinPolygons(list, poly) { + 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; From 02db56fa42554c45ba8cb1084056ea5144622809 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 2 Nov 2017 19:22:21 -0400 Subject: [PATCH 5/9] Enhance readability --- src/plots/cartesian/select.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index dc0a3d60c97..973cdf337c8 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -264,8 +264,9 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { // 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)); + // we have to keep reference to arrays container + dragOptions.mergedPolygons.length = 0; + [].push.apply(dragOptions.mergedPolygons, mergedPolygons); } }); }; From e8c18f03ed8c1e5a3d0ac61be97f5b2f131ef36a Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 3 Nov 2017 13:56:56 -0400 Subject: [PATCH 6/9] Add subtraction mode --- src/lib/polygon.js | 10 ++++++++-- src/plots/cartesian/dragbox.js | 6 +++--- src/plots/cartesian/select.js | 25 +++++++++++++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/lib/polygon.js b/src/lib/polygon.js index a8305513ddc..b28dd0d8aff 100644 --- a/src/lib/polygon.js +++ b/src/lib/polygon.js @@ -186,6 +186,7 @@ polygon.multitester = function multitester(list) { for(var i = 0; i < list.length; i++) { var tester = polygon.tester(list[i]); + tester.subtract = list[i].subtract; testers.push(tester); xmin = Math.min(xmin, tester.xmin); xmax = Math.max(xmax, tester.xmax); @@ -194,10 +195,15 @@ polygon.multitester = function multitester(list) { } function contains(pt, arg) { + var yes = false; for(var i = 0; i < testers.length; i++) { - if(testers[i].contains(pt, arg)) return true; + if(testers[i].contains(pt, arg)) { + // if contained by subtract polygon - exclude the point + yes = testers[i].subtract === false; + } } - return false; + + return yes; } return { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 6bd5ff2bff3..4b9c07d3760 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -173,15 +173,15 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { dragOptions.yaxes = ya; // take over selection polygons from prev mode, if any - if(e.shiftKey && plotinfo.selection.polygons && !dragOptions.polygons) { + if((e.shiftKey || e.altKey) && 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)) { + else if((!e.shiftKey && !e.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection.polygons)) { plotinfo.selection = {}; plotinfo.selection.polygons = dragOptions.polygons = []; - dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons = []; + plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; } prepSelect(e, startX, startY, dragOptions, dragModeNow); diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 973cdf337c8..b315c226086 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -41,7 +41,8 @@ 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), - filterPoly, testPoly, mergedPolygons, currentPolygon; + filterPoly, testPoly, mergedPolygons, currentPolygon, + subtract = e.altKey; if(mode === 'lasso') { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); @@ -193,7 +194,8 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { // create outline & tester if(dragOptions.polygons && dragOptions.polygons.length) { - mergedPolygons = joinPolygons(dragOptions.mergedPolygons, currentPolygon); + mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract); + currentPolygon.subtract = subtract; testPoly = multipolygonTester(dragOptions.polygons.concat([currentPolygon])); } else { @@ -262,6 +264,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { if(currentPolygon && dragOptions.polygons) { // save last polygons + currentPolygon.subtract = subtract; dragOptions.polygons.push(currentPolygon); // we have to keep reference to arrays container @@ -272,8 +275,22 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { }; }; -function joinPolygons(list, poly) { - var res = polybool.union({ +function mergePolygons(list, poly, subtract) { + var res; + + if(subtract) { + res = polybool.difference({ + regions: list, + inverted: false + }, { + regions: [poly], + inverted: false + }); + + return res.regions; + } + + res = polybool.union({ regions: list, inverted: false }, { From ca4aaae2ed1e45ccc3ac9a45af874058721e8beb Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 3 Nov 2017 14:07:10 -0400 Subject: [PATCH 7/9] Fix initial-shift selection --- src/plots/cartesian/dragbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 4b9c07d3760..34ecb33fcb1 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -173,12 +173,12 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { dragOptions.yaxes = ya; // take over selection polygons from prev mode, if any - if((e.shiftKey || e.altKey) && plotinfo.selection.polygons && !dragOptions.polygons) { + if((e.shiftKey || e.altKey) && (plotinfo.selection && 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.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection.polygons)) { + else if((!e.shiftKey && !e.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) { plotinfo.selection = {}; plotinfo.selection.polygons = dragOptions.polygons = []; plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; From 39a4dc49dff2acd95af05045ac429f36289cf7d0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 6 Nov 2017 12:38:11 -0500 Subject: [PATCH 8/9] Add multiselection test --- test/jasmine/tests/select_test.js | 95 ++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 3bf53a8337e..db16fe098a6 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -20,26 +20,26 @@ function drag(path, options) { Lib.clearThrottle(); if(options.type === 'touch') { - touchEvent('touchstart', path[0][0], path[0][1]); + touchEvent('touchstart', path[0][0], path[0][1], options); path.slice(1, len).forEach(function(pt) { Lib.clearThrottle(); - touchEvent('touchmove', pt[0], pt[1]); + touchEvent('touchmove', pt[0], pt[1], options); }); - touchEvent('touchend', path[len - 1][0], path[len - 1][1]); + touchEvent('touchend', path[len - 1][0], path[len - 1][1], options); return; } - mouseEvent('mousemove', path[0][0], path[0][1]); - mouseEvent('mousedown', path[0][0], path[0][1]); + mouseEvent('mousemove', path[0][0], path[0][1], options); + mouseEvent('mousedown', path[0][0], path[0][1], options); path.slice(1, len).forEach(function(pt) { Lib.clearThrottle(); - mouseEvent('mousemove', pt[0], pt[1]); + mouseEvent('mousemove', pt[0], pt[1], options); }); - mouseEvent('mouseup', path[len - 1][0], path[len - 1][1]); + mouseEvent('mouseup', path[len - 1][0], path[len - 1][1], options); } function assertSelectionNodes(cornerCnt, outlineCnt) { @@ -157,6 +157,43 @@ describe('Test select box and lasso in general:', function() { drag(selectPath); + selectedPromise.then(function() { + expect(selectedCnt).toBe(1, 'with the correct selected count'); + assertEventData(selectedData.points, [{ + curveNumber: 0, + pointNumber: 0, + x: 0.002, + y: 16.25, + id: 'id-0.002', + customdata: 'customdata-16.25' + }, { + curveNumber: 0, + pointNumber: 1, + x: 0.004, + y: 12.5, + id: 'id-0.004', + customdata: 'customdata-12.5' + }], 'with the correct selected points (2)'); + assertRange(selectedData.range, { + x: [0.002000, 0.0046236], + y: [0.10209191961595454, 24.512223978291406] + }, 'with the correct selected range'); + + return doubleClick(250, 200); + }) + .then(deselectPromise) + .then(function() { + expect(doubleClickData).toBe(null, 'with the correct deselect data'); + }) + .catch(fail) + .then(done); + }); + + it('should handle add/sub selection', function(done) { + resetEvents(gd); + + drag(selectPath); + selectedPromise.then(function() { expect(selectingCnt).toBe(1, 'with the correct selecting count'); assertEventData(selectingData.points, [{ @@ -178,8 +215,14 @@ describe('Test select box and lasso in general:', function() { x: [0.002000, 0.0046236], y: [0.10209191961595454, 24.512223978291406] }, 'with the correct selecting range'); - expect(selectedCnt).toBe(1, 'with the correct selected count'); - assertEventData(selectedData.points, [{ + }) + .then(function() { + // add selection + drag([[193, 193], [213, 193]], {shiftKey: true}); + }) + .then(function() { + expect(selectingCnt).toBe(2, 'with the correct selecting count'); + assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 0, x: 0.002, @@ -193,15 +236,37 @@ describe('Test select box and lasso in general:', function() { y: 12.5, id: 'id-0.004', customdata: 'customdata-12.5' - }], 'with the correct selected points (2)'); - assertRange(selectedData.range, { - x: [0.002000, 0.0046236], - y: [0.10209191961595454, 24.512223978291406] - }, 'with the correct selected range'); + }, { + curveNumber: 0, + pointNumber: 4, + x: 0.013, + y: 6.875, + id: 'id-0.013', + customdata: 'customdata-6.875' + }], 'with the correct selecting points (1)'); + }) + .then(function() { + // sub selection + drag([[219, 143], [219, 183]], {altKey: true}); + }).then(function() { + assertEventData(selectingData.points, [{ + curveNumber: 0, + pointNumber: 0, + x: 0.002, + y: 16.25, + id: 'id-0.002', + customdata: 'customdata-16.25' + }, { + curveNumber: 0, + pointNumber: 1, + x: 0.004, + y: 12.5, + id: 'id-0.004', + customdata: 'customdata-12.5' + }], 'with the correct selecting points (1)'); return doubleClick(250, 200); }) - .then(deselectPromise) .then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); }) From ab92fd284c9f71486bdde9d19112373c1a35f365 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 13 Nov 2017 13:24:52 -0500 Subject: [PATCH 9/9] Fix review comments --- src/plots/cartesian/dragbox.js | 45 ---------------------------------- src/plots/cartesian/select.js | 13 ++++++++++ 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 34ecb33fcb1..4a81f4a625f 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -171,19 +171,6 @@ 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 || e.altKey) && (plotinfo.selection && 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.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) { - plotinfo.selection = {}; - plotinfo.selection.polygons = dragOptions.polygons = []; - plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; - } - prepSelect(e, startX, startY, dragOptions, dragModeNow); } } @@ -191,11 +178,6 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { 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, @@ -548,8 +530,6 @@ 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]); - ticksAndAnnotations(yActive, xActive); } @@ -926,31 +906,6 @@ 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) + diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index b315c226086..0f1a1f88d3e 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -44,6 +44,19 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { filterPoly, testPoly, mergedPolygons, currentPolygon, subtract = e.altKey; + + // take over selection polygons from prev mode, if any + if((e.shiftKey || e.altKey) && (plotinfo.selection && 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.altKey) || ((e.shiftKey || e.altKey) && !plotinfo.selection)) { + plotinfo.selection = {}; + plotinfo.selection.polygons = dragOptions.polygons = []; + plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; + } + if(mode === 'lasso') { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); }