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

Throttle selectPoints #2040

Merged
merged 10 commits into from
Sep 28, 2017
Merged

Throttle selectPoints #2040

merged 10 commits into from
Sep 28, 2017

Conversation

alexcjohnson
Copy link
Collaborator

closes #2005 - adds throttling to box and lasso selection. I opted to throttle just the selectPoints portion, because It's where nearly all the expense of that drag handler lives, so this way keeps the selection region feeling snappy even if it takes some time for the selected points to catch up.

Here's what it looked like before:
unthrottled-select
and now with throttling:
throttled-select
Using this plot:

var x = new Array(3e5), y = new Array(x.length);
for(var i = 0; i < x.length; i++) { x[i] = Math.random(); y[i] = Math.random(); }
Plotly.newPlot(gd,
    [{x: x, y: y, type: 'scattergl', mode: 'markers', marker: {opacity: 0.1}}],
    {width: 600, height: 600})

Lib.throttle(
function() { _hover(gd, evt, subplot, noHoverEvent); },
constants.HOVERMINTIME,
gd._fullLayout._uid + constants.HOVERID
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using fullLayout._uid to construct the key to what we're throttling means we can 🔪 gd._lastHoverTime and gd._hoverTimer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loving this ❤️

// Get the container div: we store all variables for this plot as
// properties of this div
// some callers send this in by DOM element, others by id (string)
module.exports = function(gd) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, this file was supposed to go in the next commit aa434dd

docs cleaned up in 0861736

@@ -186,9 +186,15 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
selection = [];
for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
[].push.apply(selection, fillSelectionItem(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a reported bug AFAIK, but this use of push.apply overloads the stack for large plots. Doesn't look to me as though any of the other places we use push.apply are susceptible to this issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyone interested in writing a syntax test to 🔒 this down?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do use push.apply in a few other places - but they get max a few items in the array so it's not a problem - so if we wanted to 🔒 it, we'd have to also remove those other uses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson funny enough I stumbled upon this stack overflow in selection for regl. I replaced it with .concat here https://github.com/plotly/plotly.js/pull/1869/files#diff-8c5d8f688f2bb41a18d35397e5f58864R211, since it is being called only as many times as there is traces per subplot, which is small. (related tweet)

searchInfo = searchTraces[i];
searchInfo.selectPoints(searchInfo, false);
}
throttle.done(throttleID).then(function() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to wait for any throttling-delayed move event before we process the mouseup event, so we have the correct data.

.then(function() {
// deselect gives an extra plotly_selected event on the first click
// event data is undefined
checkEventCounts(1, 2, 1, 'deselect');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gd.once we were using in this test before hid this behavior - which exists on master as well as in this branch. I imagine we would prefer to not emit these events with undefined data - which happen I guess when we get here but dragged === false. @etpinard shall I remove that extra event and put this test back as it appeared to be previously?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we would prefer to not emit these events with undefined data

Hmm. I'm thinking some users might find it useful to have plotly_selected fired even when the selection polygon is smaller than the minimum allowed. In this case, plotly_selected essentially mimics plotly_deselect - which some users might not know about.

So, I'd vote for keeping the current behavior and updating the test (good catch by the way 🏅 )

But, maybe this is something we might want to change in v2, especially now that click selections are on the table for future development.

assertPoints([{lon: 20, lat: 20}]);
assertLassoPoints([
[13.28, 25.97], [13.28, 14.33], [25.71, 14.33], [25.71, 25.97], [13.28, 25.97]
]);
checkEventCounts(4, 1, 0, 'lasso');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 plotly_selecting events is real - because there are 5 points in the lasso path.

@rreusser
Copy link
Contributor

Interestingly, I noticed today that selecting a box + heatmapgl can be very slow. I'm curious whether that's the same pathway.

@etpinard etpinard added this to the v1.31.0 milestone Sep 28, 2017
@etpinard
Copy link
Contributor

etpinard commented Sep 28, 2017

that selecting a box + heatmapgl can be very slow

I didn't know you could select heatmapgl traces 😕

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. That new lib/throttle.js module is very very nice.

Your patches in select_test.js scare me a little bit as they will conflict with e53a908.

Lib.throttle(
function() { _hover(gd, evt, subplot, noHoverEvent); },
constants.HOVERMINTIME,
gd._fullLayout._uid + constants.HOVERID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loving this ❤️

@@ -1327,8 +1327,6 @@ plots.purge = function(gd) {
delete gd.hmlumcount;
delete gd.hmpixcount;
delete gd.numboxes;
delete gd._hoverTimer;
delete gd._lastHoverTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@@ -446,7 +446,7 @@ describe('Event data:', function() {
function _hover(px, py) {
return new Promise(function(resolve, reject) {
gd.once('plotly_hover', function(d) {
delete gd._lastHoverTime;
Lib.clearThrottle();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smooth ⛵


var hoverConstants = require('../fx/constants');

var unhover = module.exports = {};


unhover.wrapped = function(gd, evt, subplot) {
if(typeof gd === 'string') gd = document.getElementById(gd);
gd = getGraphDiv(gd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIce 🌴

@@ -186,9 +186,15 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
selection = [];
for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
[].push.apply(selection, fillSelectionItem(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyone interested in writing a syntax test to 🔒 this down?

SELECTDELAY: 100,

// cache ID suffix for throttle
SELECTID: '-select',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but we should start thinking about moving selection things out of plots/cartesian/ and into e.g. component/fx/ or a new component/select module.

if(selection.length) {
for(var j = 0; j < thisSelection.length; j++) {
selection.push(thisSelection[j]);
throttle.throttle(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

searchInfo.selectPoints(searchInfo, false);
}
throttle.done(throttleID).then(function() {
throttle.clear(throttleID);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe throttle.done() should call throttle.clear() before returning a promise?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe throttle.done() should call throttle.clear() before returning a promise?

I don't think so. It doesn't come into play here, but you could imagine wanting to finish whatever event had been throttled when done was called, without cutting off throttling of future events in the train. I guess that's saying I didn't mean done to mean you were necessarily done forever, just done with whatever is in the queue right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks!

var selectingCnt, selectingData, selectedCnt, selectedData, deselectCnt, doubleClickData;
var selectedPromise, deselectPromise;

function resetEvents(gd) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. This will conflict massively with e53a908

Not sure who should get priority here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2030 is epic enough already, you don't need to add a big merge resolution to it. You can merge first.

* @param {number} minInterval: minimum time, in milliseconds, between
* invocations of `callback`
*/
exports.throttle = function throttle(callback, minInterval, id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think throttle(id, minInterval, callback) would a call signature more familiar to most (node.)js devs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> b6d64be

_clearTimeout(cache);

function exec() {
callback();
Copy link
Contributor

@etpinard etpinard Sep 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So callback can't itself be async - which is fine but deserves a mention in the docstring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> b6d64be

x1 = 150,
y1 = 250,
x2 = 50,
y2 = 50;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(x2, y2) is in the top margin of this plot, so the doubleclick didn't do anything. But since these tests didn't actually check that the handlers ever got called, it didn't cause a failure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice investigation work. Thanks!

@alexcjohnson
Copy link
Collaborator Author

@etpinard I had to do (as expected) a fair amount of work pruning merge conflicts in select_test - would you mind giving it a look to make sure I preserved your intentions?

@etpinard
Copy link
Contributor

  • would you mind giving it a look to make sure I preserved your intentions?

select_test.js is looking very nice. That resetEvents abstraction is 💯

I'm looking forward to adding tests for bar/histogram traces tomorrow. 🍻

and increase tolerance to get all tests passing locally
@etpinard
Copy link
Contributor

Great. Tests are ✅ ↪️ 💃

@alexcjohnson alexcjohnson merged commit e7c7149 into master Sep 28, 2017
@alexcjohnson alexcjohnson deleted the throttle-select branch September 28, 2017 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Throttle box/lasso selection
4 participants