Skip to content

Cache the last mousemove event #1304

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

Merged
merged 11 commits into from
Feb 15, 2017
Merged

Cache the last mousemove event #1304

merged 11 commits into from
Feb 15, 2017

Conversation

rreusser
Copy link
Contributor

@rreusser rreusser commented Jan 16, 2017

This PR attempts to solve #1274 by caching the mousemove event globally and then using it to re-set the hoverdata when the plot is clicked.

I don't think this is really satisfactory, but I didn't manage to come up with a more pleasant solution. The problem is that animations may change the data, but the hoverdata is only set when the mouse moves, so that if the data changes and you click before moving the mouse, hoverdata won't be correct. The next best solution seems to be to use the click event data to compute the hover data, except clientX and clientY aren't available on the click event; they're only available on the mousemove event. So the solution here is to simply use the most recent mouse move event, if available. This could work, but I'm concerned about what it means to only consider a single mousemove event. Does clientX mean the same thing against all subplots that might call this?

I don't think it's a good idea to merge this, but submitting it for feedback so we can come up with a better solution.

cc @etpinard @alexcjohnson @bpostlethwaite

@rreusser
Copy link
Contributor Author

rreusser commented Jan 16, 2017

Instead of totally rethinking this, the other option is to just be more careful about scoping, caching and reusing the last hover data. The more I look at it and think about it, the more I think this might actually be a decent option. As long as it's robust, it seems much simpler than refactoring things to really just accomplish the same thing.

@etpinard etpinard added status: in progress bug something broken labels Jan 16, 2017
@etpinard
Copy link
Contributor

etpinard commented Jan 16, 2017

This patch looks solid. I'm a little concerned about graphs with multiple cartesian subplots. @rreusser have you tried manually testing multi-subplots mocks?

Moreover, by the looks of it #1274 should only affect cartesian subplots. Is this that case?

@rreusser
Copy link
Contributor Author

rreusser commented Jan 16, 2017

Yeah, multiple subplots and different trace types were my main concern here. Wanted to get feedback on if this is even reasonable or if there's a magic shortcut before going too far with it. This will probably require some refinement to ensure it's fully robust and only affects what it should affect.

@bpostlethwaite
Copy link
Member

@alexcjohnson can you continue to review this one for us?

@alexcjohnson
Copy link
Collaborator

thanks @bpostlethwaite

@rreusser I like the concept of caching the event - but I think rather than re-evaluating it on click, I think it needs to happen after any change to the plot. That way if animation actually changes which point you're over, this will be captured by the displayed hover info (and therefore by the click too).

Note that this issue has existed forever (though never bubbled up into an issue...) in the case that something else that wasn't directly initiated by the user changes the plot while the mouse is over it - such as streaming, or restyle/relayout/whatever calls coming from some larger javascript app that the plot is embedded in. Would be great if whatever we do here can solve those cases too.

@rreusser
Copy link
Contributor Author

rreusser commented Jan 20, 2017

Thanks for the context, @alexcjohnson. I think the path forward then is:

  1. understand whether hover is attached to the whole plot or to specific subplots so that I can be sure to get subplot behavior correct, and
  2. figure out the right hook for reevaluating. In order to get called with minimal additional effort and at the right times, I feel like maybe it should be called at the end of the calc step for baseplot=cartesian type traces. (this could get exposed as module.rehover or something, but I hate always adding more surface area to the module APIs (though it generally feels maintainable and straightforward))

@rreusser
Copy link
Contributor Author

rreusser commented Jan 30, 2017

I've very slightly modified this to cache the last hover handler call instead. Whenever it runs calcdata, it now executes the last hover callback. I still need to figure out whether hoverdata is being updated at the correct time or not.

The gif below shows that it updates the hover to reflect new data even when the mouse hasn't moved. It's not shown here, but if you click repeatedly, the correct (that is, current) curve and point are returned in the click handler.

Live example here: https://rreusser.github.io/demos/plotly-unsupported/outdated-hover.html

ezgif com-052d59733f

@rreusser
Copy link
Contributor Author

In order to finish this, I the main thing I need is hints regarding the cases in which it's still likely to fail. Here's what I can come up with:

  1. is the end of calcdata the correct place to trigger this? It seemed to make sense since that's identically the same as the data changing, which is when this needs a recheck.
  2. does it need to be unset? like if the visibility of a trace is toggled? Or perhaps it should be globally unset at the beginning of each replot?
  3. other corner cases related to plot types or otherwise?

cc @alexcjohnson @etpinard

@alexcjohnson
Copy link
Collaborator

It seems a little funny to me to have something as presentational as hover take place during the calc stage. And there may be some changes that affect the hover label but don't recalc - like changing trace color? I think Plots.style gets called near the end of all of these paths, though that also seems like a little bit of a nonintuitive place to put it. Maybe checking for rehover actually warrants being an explicit step that Plotly.plot, Plotly.restyle, Plotly.relayout, and Plotly.update all tack onto their queues?

Another thing I worry about is whether maindrag always still exists and represents the actual dragger at the end of the update. We're probably not handling its creation and update using regular d3 idioms... until we do, might rehover fail if you do something like change a subplot size in the update - like Plotly.relayout(gd, {xaxis.domain: [0, 0.5]})? In a case like that I'd be fine if we just drop the hover label entirely - something like try { hover(...) } except { unhover(...) }.

Anyway the general behavior your demo shows (hover label jumps while animation is smooth) seems totally reasonable.

@rreusser
Copy link
Contributor Author

rreusser commented Jan 30, 2017

Agreed. Pardon the burden, but that's exactly the feedback I was hoping for while it was still only a five-line patch. It would be possible to unpack values from the event so everything is definitely current when used later, but I think some of the references (maindrag?) were only accessible in the closure, which is where the out-of-date reference issue becomes tricky to resolve.

I'll iterate on this and see if I can make it a bit more satisfying. Thanks!

@rreusser
Copy link
Contributor Author

@alexcjohnson I've taken another shot at refining this by explicitly appending it to the async queue where needed. I've also added matching of the subplot and unsetting the name of the hovered subplot when the mouse leaves maindrag. This prevents some corner cases like when it would reapply the hover inappropriately after the mouse has fully left the plot.

I think it's doing a bit of unnecessary work by applying the hover more often than necessary. It's possible this could be optimized by more carefully relying on flags like docalc etc., but I'm hesitant to get too elaborate. Cost = dev time, code complexity, extra processing, Benefit = calling it more often than necessary ensures it's more likely to be correct and not missed.

var seq = [plots.previousPromises, interruptPreviousTransitions, prepareTransitions, executeTransitions];
function rehover() {
plots.rehover(gd);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This wrapper isn't necessary, as all the items get gd passed as an arg.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh, thanks. Wondered about that but assumed the opposite.

// When the mouse leaves this maindrag, unset the hovered subplot.
// This may cause problems if it leaves the subplot directly *onto*
// another subplot, but that's a tiny corner case at the moment.
gd._fullLayout._hoversubplot = null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Inset plots seem like a not-so-tiny corner case. But does this actually cause problems? Presumably mouseout happens before mouseover in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, which happens first was the question. If mouseover and then mouseout, it would end up unset, but the only time that could happen is if you move exactly one pixel outside of the inset region, which is where it started to seem like a small corner case. The second pixel moved will immediately fix things.

gd._fullLayout._hoversubplot = plotinfo.id;

gd._fullLayout._rehover();

fx.hover(gd, evt, subplot);
fullLayout._lasthover = maindrag;
fullLayout._hoversubplot = subplot;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are this fx.hover and fullLayout._hoversubplot = subplot still necessary then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops! Good catch. I change this back and forth and accidentally left both. Fixed in the commit below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. Yeah, you're right. There's still an extra item or two there. Fixing (and looking at the full diff when doing so this time)

@rreusser
Copy link
Contributor Author

rreusser commented Feb 13, 2017

@alexcjohnson Thanks for the quick review. I made the changes you pointed out. I can add a test that catches the straightforward cases if you think the proposed fix is starting to look reasonable.

It now looks like this, which removes unneeded lines and is a bit more precise with fullLayout vs. gd._fullLayout references:

            maindrag.onmousemove = function(evt) {
                // This is on `gd._fullLayout`, *not* fullLayout because the reference
                // changes by the time this is called again.
                gd._fullLayout._rehover = function() {
                    if(gd._fullLayout._hoversubplot === subplot) {
                        fx.hover(gd, evt, subplot);
                    }
                };

                fx.hover(gd, evt, subplot);

                // Note that we have *not* used the cached fullLayout variable here
                // since that may be outdated when this is called as a callback later on
                gd._fullLayout._lasthover = maindrag;
                gd._fullLayout._hoversubplot = subplot;
            };

@alexcjohnson
Copy link
Collaborator

Yes, this looks great! 🔒 it down with some tests and we should be ready to roll.

It's possible this could be optimized by more carefully relying on flags like docalc etc., but I'm hesitant to get too elaborate

Agreed 100%

@rreusser
Copy link
Contributor Author

Added test to verify it's updated when data changes (and nothing else, i.e. no replot, no mouse event). Used animation to accomplish this. That doesn't exhaustively test every case, but it at least verifies the fix is occurring.

@alexcjohnson
Copy link
Collaborator

Could you add a test where an update causes the hover label to disappear, then another update causes one to appear again? Maybe a relayout changing an axis range, so we're also testing a route that app developers are likely to use when altering the data in their plots, and similar to the route used by streaming?

@rreusser
Copy link
Contributor Author

I've expanded the test so that it now:

  1. shows label
  2. animates data (no replot) and confirms point in other trace now hovered
  3. relayout to move data offscreen & verify hover label removed
  4. relayout to restore original axis limits & verify hover label restored

@alexcjohnson
Copy link
Collaborator

Awesome, thanks! 💃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants