-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Lasso & rectangular selections #154
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
Changes from 11 commits
d6f983e
e536a91
5cad4ff
ca60e63
76926d7
78d2867
f0e6cfb
c0a94f7
12b43c6
07e5cef
b1a1c24
1958475
3c40c41
188a0db
8d0afb5
1c5f325
d0203c8
5be72de
057e4ac
556cb83
7b07a25
2a970fc
d7abd18
7addcec
d02d612
698376e
89c1655
d8ed7a7
7d897a4
96e01b6
bc04a15
710791d
342f991
b5c090f
1d7745f
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 |
---|---|---|
@@ -0,0 +1 @@ | ||
dist | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,6 +121,24 @@ modeBarButtons.pan2d = { | |
click: handleCartesian | ||
}; | ||
|
||
modeBarButtons.select2d = { | ||
name: 'select2d', | ||
title: 'Box Select', | ||
attr: 'dragmode', | ||
val: 'select', | ||
icon: Icons.question, // TODO | ||
click: handleCartesian | ||
}; | ||
|
||
modeBarButtons.lasso2d = { | ||
name: 'lasso2d', | ||
title: 'Lasso Select', | ||
attr: 'dragmode', | ||
val: 'lasso', | ||
icon: Icons.question, // TODO | ||
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. @delekru here (and right above) are where the new icons get referenced, once they're part of the ploticon font. |
||
click: handleCartesian | ||
}; | ||
|
||
modeBarButtons.zoomIn2d = { | ||
name: 'zoomIn2d', | ||
title: 'Zoom in', | ||
|
@@ -179,6 +197,13 @@ modeBarButtons.hoverCompareCartesian = { | |
click: handleCartesian | ||
}; | ||
|
||
var DRAGCURSORS = { | ||
pan: 'move', | ||
zoom: 'crosshair', | ||
select: 'crosshair', | ||
lasso: 'crosshair' // TODO: better cursors for select and lasso? | ||
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. Crosshair is a pretty good cursor for what you're doing, it just doesn't disambiguate these from each other or from zoom. Maybe that's OK. 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 |
||
}; | ||
|
||
function handleCartesian(gd, ev) { | ||
var button = ev.currentTarget, | ||
astr = button.getAttribute('data-attr'), | ||
|
@@ -230,7 +255,7 @@ function handleCartesian(gd, ev) { | |
if(fullLayout._hasCartesian) { | ||
Plotly.Fx.setCursor( | ||
fullLayout._paper.select('.nsewdrag'), | ||
{pan:'move', zoom:'crosshair'}[val] | ||
DRAGCURSORS[val] | ||
); | ||
} | ||
Plotly.Fx.supplyLayoutDefaults(gd.layout, fullLayout, gd._fullData); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,10 +99,19 @@ function getButtonGroups(fullLayout, buttonsToRemove, buttonsToAdd) { | |
|
||
var hasCartesian = fullLayout._hasCartesian, | ||
hasGL2D = fullLayout._hasGL2D, | ||
allAxesFixed = areAllAxesFixed(fullLayout); | ||
allAxesFixed = areAllAxesFixed(fullLayout), | ||
dragModeGroup = []; | ||
|
||
if((hasCartesian || hasGL2D) && !allAxesFixed) { | ||
dragModeGroup = ['zoom2d', 'pan2d']; | ||
} | ||
if(hasCartesian) { | ||
dragModeGroup.push('select2d'); | ||
dragModeGroup.push('lasso2d'); | ||
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. @alexcjohnson (cc @chriddyp @jackparmer ) Should we show the select-box and lasso by default on all cartesian graphs and leave it up to plotly.js users or widget devs to add them using It seems strange to add two buttons that are useless for all but one trace type + mode combination on all cartesian graphs. Alternatively, we could make 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. Selection does have a use even without events or linking, for highlighting data while discussing or presenting it. And just because it's cool to play with 😎 So I wouldn't leave it entirely to developers to enable. Then re: diving into fullData, I guess it depends how fast you think we'll add other trace types. (not sure if I'll be able to do it myself, but now it's just a matter of adding 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.
It doesn't look like it will be hard to add I'd vote for a check on fullData. 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. fullData check: 7d897a4 |
||
} | ||
if(dragModeGroup.length) addGroup(dragModeGroup); | ||
|
||
if((hasCartesian || hasGL2D) && !allAxesFixed) { | ||
addGroup(['zoom2d', 'pan2d']); | ||
addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); | ||
} | ||
|
||
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. this if statement could be moved up here, right? 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. @cpsievert that would change the order which the buttons are displayed |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
.select-outline { | ||
fill: none; | ||
stroke-width: 1; | ||
shape-rendering: crispEdges; | ||
} | ||
.select-outline-1 { | ||
stroke: white; | ||
} | ||
.select-outline-2 { | ||
stroke: black; | ||
stroke-dasharray: 2px 2px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
/** | ||
* Copyright 2012-2015, Plotly, Inc. | ||
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. Merge master (after #137), run 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. slick! |
||
* All rights reserved. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
|
||
'use strict'; | ||
var dot = require('./matrix').dot; | ||
|
||
/** | ||
* Turn an array of [x, y] pairs into a polygon object | ||
* that can test if points are inside it | ||
* | ||
* @param ptsIn Array of [x, y] pairs | ||
* | ||
* @returns polygon Object {xmin, xmax, ymin, ymax, pts, contains} | ||
* (x|y)(min|max) are the bounding rect of the polygon | ||
* pts is the original array, with the first pair repeated at the end | ||
* contains is a function: (pt, omitFirstEdge) | ||
* pt is the [x, y] pair to test | ||
* omitFirstEdge truthy means points exactly on the first edge don't | ||
* count. This is for use adding one polygon to another so we | ||
* don't double-count the edge where they meet. | ||
* returns boolean: is pt inside the polygon (including on its edges) | ||
*/ | ||
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. 🐄 put the jsDoc block above polygon.tester = function tester(ptsIn) { 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. oh right... that's where it started, when this was the only export 🙈 |
||
var polygon = module.exports = {}; | ||
|
||
polygon.tester = function tester(ptsIn) { | ||
var pts = ptsIn.slice(), | ||
xmin = pts[0][0], | ||
xmax = xmin, | ||
ymin = pts[0][1], | ||
ymax = ymin; | ||
|
||
pts.push(pts[0]); | ||
for(var 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]); | ||
ymax = Math.max(ymax, pts[i][1]); | ||
} | ||
|
||
// do we have a rectangle? Handle this here, so we can use the same | ||
// tester for the rectangular case without sacrificing speed | ||
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. Initially I thought I'd make separate testers for lasso and rectangle, but it seemed easier to just branch in here and attach a simplified 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. 👍 looks clean |
||
|
||
var isRect = false, | ||
rectFirstEdgeTest; | ||
|
||
function onFirstVert(pt) { return pt[0] === pts[0][0]; } | ||
function onFirstHorz(pt) { return pt[1] === pts[0][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. Maybe inline these for readability sake. They appear to only be used once each. 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. 👍 |
||
|
||
if(pts.length === 5) { | ||
if(pts[0][0] === pts[1][0]) { // vert, horz, vert, horz | ||
if(pts[2][0] === pts[3][0] && | ||
pts[0][1] === pts[3][1] && | ||
pts[1][1] === pts[2][1]) { | ||
isRect = true; | ||
rectFirstEdgeTest = onFirstVert; | ||
} | ||
} | ||
else if(pts[0][1] === pts[1][1]) { // horz, vert, horz, vert | ||
if(pts[2][1] === pts[3][1] && | ||
pts[0][0] === pts[3][0] && | ||
pts[1][0] === pts[2][0]) { | ||
isRect = true; | ||
rectFirstEdgeTest = onFirstHorz; | ||
} | ||
} | ||
} | ||
|
||
function rectContains(pt, omitFirstEdge) { | ||
var x = pt[0], | ||
y = pt[1]; | ||
|
||
if(x < xmin || x > xmax || y < ymin || y > ymax) { | ||
// pt is outside the bounding box of polygon | ||
return false; | ||
} | ||
if(omitFirstEdge && rectFirstEdgeTest(pt)) return false; | ||
|
||
return true; | ||
} | ||
|
||
function contains(pt, omitFirstEdge) { | ||
var x = pt[0], | ||
y = pt[1]; | ||
|
||
if(x < xmin || x > xmax || y < ymin || y > ymax) { | ||
// pt is outside the bounding box of polygon | ||
return false; | ||
} | ||
|
||
var imax = pts.length, | ||
x1 = pts[0][0], | ||
y1 = pts[0][1], | ||
crossings = 0, | ||
i, | ||
x0, | ||
y0, | ||
xmini, | ||
ycross; | ||
|
||
for(i = 1; i < imax; i++) { | ||
// find all crossings of a vertical line upward from pt with | ||
// polygon segments | ||
// crossings exactly at xmax don't count, unless the point is | ||
// exactly on the segment, then it counts as inside. | ||
x0 = x1; | ||
y0 = y1; | ||
x1 = pts[i][0]; | ||
y1 = pts[i][1]; | ||
xmini = Math.min(x0, x1); | ||
|
||
// outside the bounding box of this segment, it's only a crossing | ||
// if it's below the box. | ||
if(x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) { | ||
continue; | ||
} | ||
else if(y < Math.min(y0, y1)) { | ||
// don't count the left-most point of the segment as a crossing | ||
// because we don't want to double-count adjacent crossings | ||
// UNLESS the polygon turns past vertical at exactly this x | ||
// Note that this is repeated below, but we can't factor it out | ||
// because | ||
if(x !== xmini) crossings++; | ||
} | ||
// inside the bounding box, check the actual line intercept | ||
else { | ||
// vertical segment - we know already that the point is exactly | ||
// on the segment, so mark the crossing as exactly at the point. | ||
if(x1 === x0) ycross = y; | ||
// any other angle | ||
else ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0); | ||
|
||
// exactly on the edge: counts as inside the polygon, unless it's the | ||
// first edge and we're omitting it. | ||
if(y === ycross) { | ||
if(i === 1 && omitFirstEdge) return false; | ||
return true; | ||
} | ||
|
||
if(y <= ycross && x !== xmini) crossings++; | ||
} | ||
} | ||
|
||
// if we've gotten this far, odd crossings means inside, even is outside | ||
return crossings % 2 === 1; | ||
} | ||
|
||
return { | ||
xmin: xmin, | ||
xmax: xmax, | ||
ymin: ymin, | ||
ymax: ymax, | ||
pts: pts, | ||
contains: isRect ? rectContains : contains, | ||
isRect: isRect | ||
}; | ||
}; | ||
|
||
/** | ||
* Test if a segment of a points array is bent or straight | ||
* | ||
* @param pts Array of [x, y] pairs | ||
* @param start the index of the proposed start of the straight section | ||
* @param end the index of the proposed end point | ||
* @param tolerance the max distance off the line connecting start and end | ||
* before the line counts as bent | ||
* @returns boolean: true means this segment is bent, false means straight | ||
*/ | ||
var isBent = polygon.isSegmentBent = function isBent(pts, start, end, tolerance) { | ||
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. Used to simplify the lasso polygon. This is pretty similar to the scatter line decimation algorithm, so not super 🌴 but line decimation has a few other quirks that made it seem annoying to combine. 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. polygon.js is ~150 lines of code. Nothing to worry here. |
||
var startPt = pts[start], | ||
segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]], | ||
segmentSquared = dot(segment, segment), | ||
segmentLen = Math.sqrt(segmentSquared), | ||
unitPerp = [-segment[1] / segmentLen, segment[0] / segmentLen], | ||
i, | ||
part, | ||
partParallel; | ||
|
||
for(i = start + 1; i < end; i++) { | ||
part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]]; | ||
partParallel = dot(part, segment); | ||
|
||
if(partParallel < 0 || partParallel > segmentSquared || | ||
Math.abs(dot(part, unitPerp)) > tolerance) return true; | ||
} | ||
return false; | ||
}; | ||
|
||
/** | ||
* Make a filtering polygon, to minimize the number of segments | ||
* | ||
* @param pts Array of [x, y] pairs (must start with at least 1 pair) | ||
* @param tolerance the maximum deviation from straight allowed for | ||
* removing points to simplify the polygon | ||
* | ||
* @returns Object {addPt, raw, filtered} | ||
* addPt is a function(pt: [x, y] pair) to add a raw point and | ||
* continue filtering | ||
* raw is all the input points | ||
* filtered is the resulting filtered Array of [x, y] pairs | ||
*/ | ||
polygon.filter = function filter(pts, tolerance) { | ||
var ptsFiltered = [pts[0]], | ||
doneRawIndex = 0, | ||
doneFilteredIndex = 0; | ||
|
||
function addPt(pt) { | ||
pts.push(pt); | ||
var prevFilterLen = ptsFiltered.length, | ||
iLast = doneRawIndex; | ||
ptsFiltered.splice(doneFilteredIndex + 1); | ||
|
||
for(var i = iLast + 1; i < pts.length; i++) { | ||
if(i === pts.length - 1 || isBent(pts, iLast, i + 1, tolerance)) { | ||
ptsFiltered.push(pts[i]); | ||
if(ptsFiltered.length < prevFilterLen - 2) { | ||
doneRawIndex = i; | ||
doneFilteredIndex = ptsFiltered.length - 1; | ||
} | ||
iLast = i; | ||
} | ||
} | ||
} | ||
|
||
if(pts.length > 1) { | ||
var lastPt = pts.pop(); | ||
addPt(lastPt); | ||
} | ||
|
||
return { | ||
addPt: addPt, | ||
raw: pts, | ||
filtered: ptsFiltered | ||
}; | ||
}; |
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.
@alexcjohnson your
ack
days are over? Can I ask why?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 think ack disappeared from my system in an OS upgrade or something... and I tried
ag
after remembering I think @theengineear mentioned it a while back. I was never really bothered byack
butag
does seem to live up to its claim of speed. Otherwise it seems pretty similar but I'm pretty basic in how I use these tools.