-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
1852 persistent click #2824
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
1852 persistent click #2824
Conversation
- Just a proof of concept only working for scatter trace type and with single trace plots only.
- Selection state doesn't move back to nothing selected state though.
- Note: discussion needed if new behavior is wanted.
- Given multiple points selected, deselecting one of those should start a new selection, or when Shift is pressed the point should be removed from retained selection.
- If the Shift key is down, data points previously selected by a click are now retained when user adds to a selection through box or lasso select mode.
- Reason: new approach is needed to support proper interplay of polygon and click select. See #1852 for a discussion. - New approach separates the two concerns 'what was selected' and 'set selected state of data points', that were previously handled by `selectPoints`, into multiple functions. - Also the polygons now longer implicitly carry selection state in cartesian/select.js but are only visual cues anymore. - Introduce a new Lib function for calculating the set difference of two arrays.
- Select-on-click wasn't checking if an outlines object was passed which is the case in zoom and pan mode for example.
- Instead of four functions only have two anymore. - Also update modebar to detect a selectable trace. - Also update supplyDefaults code.
- Just rename a parameter name.
- Reason: DRY with selectOnClick, whereas extracted method has logic in it to determine if a trace is selectable. - Prepare scattergl as next target of new selection interface migration.
- Thereby had to change controlling code in cartesian/select#selectOnClick. - No longer restrict selected mode rendering to lasso/box select mode.
- Account for difference in hoverData having no pointNumber but binNumber and pointNumbers in case of histogram. - Created a messy entireSelectionToBeCleared function
- Reason: more efficient than the extra hack for scattergl for the case when no point is selected at all.
- When drawing lasso bar and scatter trace weren't showing their points as deselected.
- Needs cleanup in select.js - Controlling code in cartesian/select.js needs a change due to a false assumption about hoverData: box produces a multi-element hoverData array whereas only the first element is really the clicked point
- Reason: box trace type stuffing not even points but also hoverData about boxes which appears on mouseover into gd._hoverdata. - So based on analysis of fx/hover.js, which sorts the hoverdata array so that the closest point comes first, it is assumed now that only the first element in hoverData represents a clicked point. - For box trace type particulary if a box is clicked the first hoverdata element is not a point but a box with pointnumber being the index of the point group that's represented as a box plot element visually.
- By introducing a hoverOnBox property on hoverData. - Still needs cleaning things up. - Left over misbehaviour: clicking a box deselects all points and instead should not change the current selection at all.
- Reason: multiPolygon tester no longer needed due to changes how selection code works.
- Until clickMode attribute is not introduced in public API this hack needs to be there to satisfy ESLint.
- Also adapt the new `toggleSelected` function to style text data points properly.
- Violin uses box trace type select implementation.
- Thereby fix failing geo_point-selection.json image test.
- Only the proper mapping to scatter's select interface was needed.
Since this is a PR in progress, here's a quick overview of what is missing:
|
src/components/modebar/manage.js
Outdated
@@ -194,7 +193,7 @@ function isSelectable(fullData) { | |||
|
|||
var trace = fullData[i]; | |||
|
|||
if(!trace._module || !trace._module.selectPoints) continue; | |||
if(!trace._module || !trace._module.getPointsIn || !trace._module.toggleSelected) continue; |
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.
It might be better to make Plotly.register
set a boolean _module.selected
somewhere here and use that boolean downstream (e.g. in Plots.supplyDefaults
) instead of looking for getPointsIn
and toggleSelected
.
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.
Neat! Would _module.selectable
even be a better name?
src/lib/set_operations.js
Outdated
return a; | ||
} | ||
return a.filter(function(e) { | ||
return b.indexOf(e) < 0; |
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.
Looks like Lib.difference
is only called on array of number. We could take advantage of that by using a hash table instead of indexOf
which can be slow for arrays with length > 1e5.
Something like:
var small;
var large;
var hash = {};
var out = [];
var i;
if(a.length < b.length) {
small = a;
large = b;
} else {
small = b;
large = a;
}
for(i = 0; i < small.length; i++) {
hash[small[i]] = 1;
}
for(i = 0; i < large.length; i++) {
var l = large[i];
if(hash[l]) {
out.push(l)
}
}
return out;
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.
... it might be nice to look at the lodash's implementation.
src/plots/cartesian/select.js
Outdated
!trace._module.toggleSelected || !trace._module.getPointsIn) continue; | ||
|
||
// TODO is dragOptions.subplot is ever set? If not, delete. | ||
// if(dragOptions.subplot) { |
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.
Looks like ternary subplots still set that subplot field as well as geo and mapbox subplots.
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.
Thx! Really great you're also going over my TODOs because some of them would have required your or AJ's input anyways!
src/traces/scattergl/index.js
Outdated
@@ -590,32 +592,30 @@ function plot(gd, subplot, cdata) { | |||
null; | |||
}); | |||
|
|||
if(selectMode) { |
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.
Ah no. We need to keep that selectMode
condition.
As creating one regl context takes about ~300ms, we don't want to create the selection regl context when in non-select interaction modes.
But perhaps, you only remove it before implementing clickmode
. Please ignore if that's the case.
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.
Oh, I'll have to dig into that again. With if(selectMode)
scattergl traces weren't updated on select-on-click when in zoom/pan dragMode. I guess I'll have to adapt the logic when selectMode
flag is set to true
.
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.
As far as I understand, the selection regl context is created by a call to createScatter()
. When user switches to lasso or box mode, the plot is re-rendered for some reason and regl contexts are creating again.
I think you are absolutely right to take clickMode
into account to decide if scene.select2d
context needs to be created.
For now, I'll reintroduce the selectMode
condition and add a TODO for later when I introduce clickmode
.
// console.log('els : ' + JSON.stringify(scene.selectBatch[stash.index])); | ||
// console.log('unels: ' + JSON.stringify(scene.unselectBatch[stash.index])); | ||
// console.log('selection: ' + JSON.stringify(selection)); | ||
return selection; |
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 have some bad news. The new selection algo perform very poorly above 1e5 points (which in practice only affects scattergl
). From
var N = 1e5
var x = []
var y = []
var ms = []
for(var i = 0; i < N; i++) {
x.push(Math.random())
y.push(Math.random())
ms.push(i % 2 ? 20 : 10)
}
console.time(N)
Plotly.newPlot(gd, [{
type: 'scattergl',
mode: 'markers',
x: x,
y: y,
marker: {size: ms}
}], {
dragmode: 'select',
})
console.timeEnd(N)
selecting is painfully slow.
I'll try to pin down what's causing this slow down later today.
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.
Okay, thanks! Let me know if I can be of any assistance, e.g. if some of my code isn't clear...
BTW I didn't know console.time
and console.timeEnd
! Awesome!!
gd.emit('plotly_deselect', null); | ||
} | ||
else { | ||
// TODO What to do with the code below because we now have behavior for a single click |
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.
Thanks for writing down that TODO
.
I'm thinking when clickmode: 'select'
we'll emit plotly_selected
with the selected points info. Otherwise, we'll have to keep that dummy event below (until v2).
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.
Thx.
src/plots/cartesian/select.js
Outdated
} | ||
|
||
updateSelectedState(gd, searchTraces); | ||
|
||
// clear visual boundaries of selection area if displayed | ||
outlines.remove(); |
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.
Why did you move this after updateSelectedState
?
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.
TBH, I'm not sure. Fun fact: I even had it beneath emitting plotly_selected
event which then broke some unit tests that assume outline elements are no longer in DOM when plotly_selected
event is received.
But why did I really moved it below updateStelectedState
? As far as I can remember, in the early commits, I thought I could move the line before the if(numClicks === 2)
condition. For some reason that wasn't correct and I moved it back into the if block. So I guess no rationale behind it other than me not putting it back where it always should have been.
// Determine clicked points, | ||
// call selection modification functions of the trace's module | ||
// and collect the resulting set of selected points | ||
clickedPts = clickedPtsFor(searchInfo, hoverData); |
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.
Does the clickedPts
array ever have more than one item?
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.
At the moment not. I changed it back when I was about to integrate the box
trace type. See 231fa6d.
I figured it'd be a good idea to be ready for hoverdata
one day containing more than one valid data point. E.g. when multiple data points are at the same location that was clicked.
Is allocating an array costly in terms of performance? Happy to change it back to non-array style if that is the case! Or, if we want to keep it, I could add a comment!
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.
Is allocating an array costly in terms of performance?
It can be costly, but I'll have to test on some real-world data to know for sure.
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.
For now I postpone moving back to a non-array return type until there's proof that it hurts performance considerably.
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.
This PR is very impressive. 🎉 💪 🥇
Thanks very much @rmoestl for taking the time to improve parts of our internal API. Selections have been suffering from some growing pains in the last few months, this is for sure a step in the right direction.
I do have a few concerns about performance. Hopefully, speeding up Lib.difference
will suffice. I'll report back what I found later today.
var out = []; | ||
var i; | ||
|
||
for(i = 0; i < b.length; i++) { |
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.
This commit makes selection of 1e5 scattergl markers work ok, but still about an order of magnitude too slow.
I'll have to dig deeper tomorrow.
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.
Have you dug deeper since then?
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 haven't had the time unfortunately.
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'll find some time to review it before the meeting on Thursday though.
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.
No problem. There's no hurry at the moment.
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.
Ok very briefly just to get a conversion started (and not break my promise):
Doing some order of operation analysis in the hot selection code path comparing:
plotly.js/src/plots/cartesian/select.js
Lines 283 to 286 in def6aa5
searchInfo = searchTraces[i]; | |
traceSelection = searchInfo._module.selectPoints(searchInfo, testPoly); | |
traceSelections.push(traceSelection); |
on master to this branch's
plotly.js/src/plots/cartesian/select.js
Lines 242 to 254 in bd04fd8
searchInfo = searchTraces[i]; | |
module = searchInfo._module; | |
if(!retainSelection) module.toggleSelected(searchInfo, false); | |
var currentPolygonTester = polygonTester(currentPolygon); | |
var pointsInCurrentPolygon = module.getPointsIn(searchInfo, currentPolygonTester); | |
module.toggleSelected(searchInfo, !subtract, pointsInCurrentPolygon); | |
var pointsNoLongerSelected = difference(pointsInPolygon[i], pointsInCurrentPolygon); | |
traceSelection = module.toggleSelected(searchInfo, false, pointsNoLongerSelected); | |
pointsInPolygon[i] = pointsInCurrentPolygon; |
we can see why this branch is roughly an order of magnitude slower:
On master that block scales as O(n). To handle addition/subtraction/merge selections, we use an algo acting on the selections polygon with I believe is O(number-of-vertices^2) but results nonetheless in not that many computations in most use cases (e.g. rectangles with 4 vertices)
On this branch, we have
if(!retain) toggleSelected('clear')
// which is currently O(2*n)
getPolyonTester()
// not sure why this one is called in the hot code path
getPointsIn()
// this one is O(n) assuming 'fast' is-in-polygon computations
toggleSelected('add or substract')
// this one currently is O(n + p)
difference()
// O(n + p)
toggleSelected('clear')
// this one is O(n + (n-p))
which when added all up gives a total of O(6n + p) where n is the number of points and p is number of selected points ~ roughly one order of magnitude.
So, we should try to combine all the toggleSelected
loops into one. Don't get me wrong, I don't think splitting up selectedPoints
into toggleSelected
and getPointsIn
was a bad idea, but perhaps we could bring a version of selectPoints
that reuses getPointsIn
? We can talk more about this tomorrow during the meeting. Thanks for your hard work!
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.
In hindsight it seams the new approach of splitting up selectedPoints
wasn't such a good idea :-( I'm not sure if it's possible to speed things up that much with that approach. To be honest, it feels like to be at square one again.
Thinking back, the reason for splitting up selectPoints
was to cover a couple of edge cases when users would use click-to-select and polygon-select interchangeably. Back then we discussed possible approaches here.
Let me jot down some thoughts for the upcoming meeting:
Current approach on master
The approach to selections before tackling persistent select was as follows: each trace type module supporting selection exposes a selectPoints
function that accepts a searchInfo
parameter describing the actual trace and a polygon
object that offers a contains
function. selectPoints
iterates through the data points of the passed trace (identified by searchInfo
) and calls polygon.contains
for each data point to see if the data point is within the selection area. Adding/subtracting from an existing selection is supported by wrapping multiple polygons
into one multiPolygon and passing that to selectPoints
.
Possible new approach to support click-to-select
The whole polygon testing approach is covered in lib/polygon.js
which exposes tester
(for testing a single polygon) and multiTester
(wrapper for multiple tester
objects). What about having a third tester
type that tests by data point index? What would that mean for the client code?
- Go back to
selectPoints
in trace type modules - Add data point index as a parameter to the signature of
contains
- In
selectPoints
implementations do not only pass thex
andy
(orlan
,lat
etc.) of a data point when callingcontains
but also the data point index - Adapt
multiTester
to be able to wrap the new index-based tester
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.
Your new approach sounds very promising! I'd say give it a shot with scatter
and scattergl
and ping me so that I can quickly review and perf-test it. 👌
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.
@etpinard proof-of-concept is ready to review. It was easier to start from scratch. So the new approach can be found at https://github.com/plotly/plotly.js/tree/1852-persistent-click-via-selectPoints. (Although it would make reviewing easier, I figured code is not quite ready for a new PR).
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 made a few comments on 2fbaaaa . Your solution is looking great! 💎
I'd say go ahead and try to implement it for all selectable trace modules. 🚀
Thanks @etpinard for taking the time to do such a thorough review! 🙇♂️ |
- Reason: code for checking if a trace supports selection was duplicated across the code base.
- Reason: to prevent confusion when comparing code to previous versions. - See comment in PR #2824 for more info.
- Reason: to spare the creation of an unnecessary regl context when selection can't take place. - TODO: use the not yet introduced clickmode layout attribute to set the select mode.
@etpinard (CC @alexcjohnson) I've incorporated your feedback as much as possible at this stage and left TODOs etc. where implementation makes more sense later. Next steps: I'll let CI run the tests and then tackle failing tests. That'll involve transitioning further trace types to the new selection interface. After that I'll start to add my own tests. |
- Reason: mocking of `gd._fullData._module` was missing the `selectable` property introduced a few commits earlier to centralize checking if a trace supports selection.
- Reason: `cartesian/select.js.prepSelect` expects `dragOptions.plotinfo` to have an `id` property set. Mapbox base plot wasn't setting this id property and thus cartesian select always cleared existing outlines (user holding Shift key)
- Reason: until clickmode attribute is not introduced, Jasmine spies in gl2d_plot_interact_test are expecting different default behavior.
Now in -> #2944 |
Though not complete, this pull request will introduce the select-on-click / persistent selection feature discussed in issue #1852.
The feature requires a change of the trace selection interface and how selection state is determined and controlled in general.
Tests (image and Jasmine alike) are primarily caused by trace types not yet migrated to the new selection interface.