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

.animate method that can handle any type of figure diff #1849

Closed
chriddyp opened this issue Jul 4, 2017 · 14 comments · Fixed by #3217
Closed

.animate method that can handle any type of figure diff #1849

chriddyp opened this issue Jul 4, 2017 · 14 comments · Fixed by #3217
Assignees
Labels
feature something new
Milestone

Comments

@chriddyp
Copy link
Member

chriddyp commented Jul 4, 2017

In Dash, I'd like users to be able to toggle on and off an animate property which would either smoothly transition points (if available) or just update the chart.

Right now, Plotly.animate doesn't work with all chart transitions and data transitions. While I could program this myself in the Graph component, it would be great if plotly.js could do this so that everyone could benefit. We could even use this in the workspace to smooth out transitions!

Ideally, I would just replace the .newPlot(id, data, layout) call with a .animate(id, data, layout) call and call it a day.

In particular, here are some of the edge cases:

  • .animate to work before .plot is called
  • .animate to work across all chart types (even if it's not a smooth animation)
  • .animate to autorange (expand axes if the new data was larger than the existing data)
  • .animate when new traces are added or removed
  • .animate when chart types have been changed

I'm OK if this is a new method or a new config option or whatever.


cc @rreusser @alexcjohnson @etpinard @cpsievert @monfera @jackparmer

@etpinard
Copy link
Contributor

etpinard commented Jul 4, 2017

.animate to autorange (expand axes if the new data was larger than the existing data)

That's arguably not always the desired behavior. We'll probably need another attribute.

Related:

@chriddyp
Copy link
Member Author

chriddyp commented Jul 5, 2017

.animate to autorange (expand axes if the new data was larger than the existing data)
That's arguably not always the desired behavior. We'll probably need another attribute.

If autosize=true, I think that I'd argue that this is the expected behaviour. If the user wants to restrict the range to a certain view, they can set the range manually.

@monfera
Copy link
Contributor

monfera commented Jul 5, 2017

I 👍 -d @etpinard's comment b/c sometimes expansion (or shrinking) is not the intent - indeed, keeping the range(s) constant is achievable by explicitly setting the range. I was thinking of a boolean stickiness attribute (true: retain the initial autodomain vs. false: expand/shrink as needed) but it may be unnecessary complication as the range can be directly set as you suggest. It's likely I didn't have the same thing in mind as @etpinard, just wanted to add my thought.

Btw. it might occur more often that data is removed, yet one wants to preserve the original full domain (eg. when eliminating points via crossfiltering), than that data is added, yet one would not want to see them.

Even with crossfiltering point elimination, setting an explicit range would work, though it's slightly more convenient to rely on plotly.js for the initial (full) range calculation than doing it in userland (or crossfiltering) code as it'd just duplicate aggregation (extent calculation) logic.

@etpinard
Copy link
Contributor

etpinard commented Jul 5, 2017

If autosize=true, I think that I'd argue that this is the expected behaviour.

Right, if a user explicitly set autosize to true, I agree. But, autosize defaults to true when the axis ranges aren't valid. In that case, I suspect keeping the first view when animating is more desirable.

@alexcjohnson
Copy link
Collaborator

keeping the first view when animating is more desirable

Doesn't that mean the rendered plot will be out of sync with the input state? I know we push ranges back to the input state, but autorange: true is supposed to mean that it will recalculate the ranges to fit the given data, and indeed if you copy the figure json and make a new plot with it that will happen. Or if you change the data using restyle for that matter.

Perhaps we need a new autorange setting (autorange: 'once'?) that will run autorange once and then revert to false (pushed back to the input state), to account for all these desires.

@nicolaskruchten
Copy link
Contributor

FWIW I would favour having an animate flag somewhere such that .react() would animate a transition or something like that, rather than a .animate() method.

@vianamp
Copy link

vianamp commented Sep 18, 2018

Not sure if my question is quite related to this issue, but I want to avoid the axes to be recalculated when the user deselect a particular trace by clicking on the legend. Is this currently possible? Thanks.

@etpinard etpinard self-assigned this Oct 4, 2018
@nicolaskruchten
Copy link
Contributor

Would it make sense for this new react-with-animate flag to be in config or layout ?

@etpinard
Copy link
Contributor

Here are a few thoughts on the topic:


The premise of making Plotly.react "call" Plotly.animate is somewhat wrong:

  • Plotly.react diffs its input with the current state and executes the required subroutines to reach the state as described in the input. Upon completion, the "old" state is lost.
  • Plotly.animate applies frames -- which work as data/layout patches, not full descriptions -- to a "base" state. This "base" state can be the "current" state or it can combine a baseframe.

For example, given

var fig = {
  data: [{y: [1,2,1], marker: {size: 20} }]
}
var fig2 = {
  data: [{y: [2,1,2], marker: {color: 'red'} }]
}

then

Plotly.react(gd, fig)
Plotly.react(gd, fig2)

gives a scatter trace with red markers of size 6. Whereas,

Plotly.newPlot(gd, {
  data: fig.data,
  frames: [{data: fig2.data}]
})
Plotly.animate(gd)

gives a scatter trace with red markers but of size 20.


I can't think a use-case for the frames API for Plotly.react users. Frames were added to make animations from layout sliders and updatemenus fully JSON-serializable. Plotly.react users most likely have a more imperative set of interactions (please, correct me if I'm wrong!). In other words, they compute their "frames" on-the-fly.

In plotly.js lingo, this ticket is about making Plotly.react transition from "old" to "new" data/layout. More technically, Plotly.react should call Plots.transition when a transition is desired. I'm proposing initiating transitions via Plotly.react with:

Plotly.react(gd, fig, transitionOpts);

which doesn't conflict with the react(gd, data, layout, config) signature and where the transitionOpts are:

transition: {
duration: {
valType: 'number',
role: 'info',
min: 0,
dflt: 500,
description: [
'The duration of the transition, in milliseconds. If equal to zero,',
'updates are synchronous.'
].join(' ')
},
easing: {
valType: 'enumerated',
dflt: 'cubic-in-out',
values: [
'linear',
'quad',
'cubic',
'sin',
'exp',
'circle',
'elastic',
'back',
'bounce',
'linear-in',
'quad-in',
'cubic-in',
'sin-in',
'exp-in',
'circle-in',
'elastic-in',
'back-in',
'bounce-in',
'linear-out',
'quad-out',
'cubic-out',
'sin-out',
'exp-out',
'circle-out',
'elastic-out',
'back-out',
'bounce-out',
'linear-in-out',
'quad-in-out',
'cubic-in-out',
'sin-in-out',
'exp-in-out',
'circle-in-out',
'elastic-in-out',
'back-in-out',
'bounce-in-out'
],
role: 'info',
description: 'The easing function used for the transition'
},
}

The other animation attributes in that same file have no meaning when we don't consider frames (with the exception of frame.redraw, which I'm thinking of mapping to the transition attribute container).

So here, we're not adding any config or layout attribute, we're asking Plotly.react users to describe how each update transitions from old to new state.


If performance isn't a concern, resolving this issue essentially sums up to adding a version of Plots.transition that replaces "old" with "new" instead of merging "new" with "old".


While working on this, I'll probably for sure discover animation bugs/regressions which will slow down progress. But, all in all, I should be able to make a PR for this by the end of October.

@chriddyp
Copy link
Member Author

Looks good to me @etpinard !

@alexcjohnson
Copy link
Collaborator

Plotly.react(gd, fig, transitionOpts);

Just trying to figure out how this will work in practice... if the graph is expected to simply receive a new figure and adapt to it, how will the app know when to provide transitionOpts and when not to? One thing that comes to mind is a declarative spec like "if changes are within this set of attributes, do this transition; if they're in this other set of attributes, do this other transition; anything else, no transition". That could take a form like (in layout or figure):

transitions: [
  {
    // scatter -> any scatter trace? or maybe 'data[0].x'?
    // can't animate name so this would invoke redraw after animating x/y
    attributes: ['scatter.x', 'scatter.y', 'scatter.name'],
    duration: 1000,
    easing: 'bounce'
  },
  {
    // some other set, in this case including the first set and more
    attributes: ['scatter.x', 'scatter.y', 'scatter.name', 'layout.xaxis.range', 'layout.yaxis.range'],
    duration: 1500,
    easing: 'cubic-in-out'
  }
]

That way if you have an app that has controls like log/linear axes, or changing colors or something, those will just happen with no transition, but when you update the pieces that are expected to transition, they will do so automatically.

We could probably start with transitionOpts and then add something like this later, but it seems like a declarative version would be a lot easier to manage.

with the exception of frame.redraw

Again, ideally we just determine this from the diff, perhaps by marking each schema attribute that does support animation as animatable: true, and if any attribute has a diff and does not describe itself as animatable, we need to redraw.

@nicolaskruchten
Copy link
Contributor

From @alexcjohnson:

Just trying to figure out how this will work in practice... if the graph is expected to simply receive a new figure and adapt to it, how will the app know when to provide transitionOpts and when not to?

Yep, pretty much exactly how I felt when I read that :)

@chriddyp this would be problematic for Dash too right?

@etpinard
Copy link
Contributor

Ok, I'm starting to think @alexcjohnson 's layout.transitions attribute container as described in #1849 (comment) is what we want here. In similar fashion to plotly/dash-core-components#198, putting new things layout seems to make users of the Plotly.react workflow happy.

Moreover, layout.transitions may become useful to users not using react for example,

  • restyle(gd, 'marker.size', 20) would transition if gd.layout.transitions includes transition options for marker.size attributes, which maybe more intuition for some than using frames and Plotly.animate
  • updatemenus and sliders could specify transitioning updates using restyle w/o having to worry about declaring frames

As @alexcjohnson points, it'll become important to list the attributes that do support animations in the schema so that diffData and diffLayout can pick them out. The current _module.animable isn't enough.

Now, supporting multiple transitions options per graph will be nice, but I'm a little worried about update calls that would trigger multiple transitions. Consider,

layout.transitions = [{
  attributes: ['marker.size'],
  duration: 400,
  easing: 'linear'
}, {
  attributes: ['yaxis.range'],
  duration: 1000,
  easing: 'cubic-in-out'
}]

here in general marker.size changes can also trigger xaxis.range changes (when autorange is turned on). At the moment, Plotly.animate seems to only transition the axis range (see https://codepen.io/etpinard/pen/ReXaKz)

@nicolaskruchten
Copy link
Contributor

Woot!

How often will people really want to specify different transitions for different aspects of the chart? Is this a critical thing to be able to support? My feeling is that most people will want to just set some basic transition options once for everything and leave it at that...

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

Successfully merging a pull request may close this issue.

6 participants