Skip to content

[WIP] Feature: Animated 1D plots #2729

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

Closed
wants to merge 37 commits into from
Closed

[WIP] Feature: Animated 1D plots #2729

wants to merge 37 commits into from

Conversation

TomNicholas
Copy link
Member

@TomNicholas TomNicholas commented Jan 30, 2019

This is an attempt at a proof-of-principle for making animated plots in the way I suggested in #2355. (Also relevant for #2030.)

This example code:

import matplotlib.pyplot as plt
import xarray as xr

# Load data as done in plotting tutorial
airtemps = xr.tutorial.open_dataset('air_temperature')
air = airtemps.air - 273.15
air.attrs = airtemps.air.attrs
air.attrs['units'] = 'deg C'

# Downsample to make reasonably-sized gif
data = air.isel(lat=10, time=slice(None,None,40))

# Create animated plot
anim = data.plot(animate_over='time')
anim.save('line1.gif', writer='imagemagick')
plt.show()

now produces this gif:
line1
The units on the timeline are formatted incorrectly because this PR isn't merged yet

I think it looks pretty good! It even animates the title properly. The actual animation creation only takes one line to do.

This currently only works for a plot with a single line, which is animated over a coordinate dimension.
It also required some minor modifications/bugfixes to animatplot, so it probably isn't reproducible right out of the box yet. If you want to try this out then use the develop branch of my forked version of animatplot.

The reason I've put this up is because I wanted to

  1. show people the level of complexity required, and
  2. get people's opinion on the implementation.

I feel like although it required only ~100 lines extra to do this then the logic is very fragmented and scattered through the plot.line and plot._infer_line_data functions. In 2D this would get even more complicated, but I can't see a good way to abstract the case of animation out?

(@t-makaro I expect you will be interested in this)

EDIT: To-Do list:

  • Animate single line
  • Animated line and static line on same axes
  • Animate multiple lines on same axes
  • Multiple animated line plots on same figure
  • FacetGrids of multiple animated lines (will leave for a later PR)
  • Complete set of tests
  • Add animatplot as optional dependency
  • Add new CI tests using animatplot
  • New documentation page
  • Fix issues with formatting of timeline label (fixed by Intelligent slider size t-makaro/animatplot#46)

@pep8speaks
Copy link

pep8speaks commented Jan 30, 2019

Hello @TomNicholas! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2019-04-30 16:06:37 UTC

anim = Animation([line_block, title_block], timeline=timeline)
# TODO I think ax should be passed to timeline_slider args
# but that just plots a single huge timeline and no line plot?!
anim.controls(timeline_slider_args={'text': animate_over})

Choose a reason for hiding this comment

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

You can pass 'valfmt':'%s' to format datetime. It will require some code to be smarter depending on the type of data being formated. I haven't worked with datetime much, so I'm not sure the best practices about formatting them.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it's in the title; maybe it doesn't need to be in the slider?

Copy link
Member Author

Choose a reason for hiding this comment

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

You can pass 'valfmt':'%s' to format datetime.

This gives you the date in year-month-day hour:minute:seconds.milliseconds format, but doesn't truncate all the decimal points with the milliseconds. I also haven't used datetime64 objects much so I don't really know how to fix that in a general manner.

If it's in the title; maybe it doesn't need to be in the slider?

I think it should be in both, although in this example the title looks good, in my experience then once you slice high-dimensional data the title becomes really complicated and it's better to definitely have it presented nicely by the timeline too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've written in a special case for datetime objects, which truncates them thanks to pandas.to_datetime(). Unfortunately the string still gets partly obscured by the play/pause button, but I think fixing that would require either truncating the string to a fixed maximum size or changing animatplot to choose the button placement based on the size of the string. I've updated the gif in the original PR to show what it looks like now.

Copy link
Contributor

@dcherian dcherian left a comment

Choose a reason for hiding this comment

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

@TomNicholas This looks like it's on the right track. That gif is awesome!

I am in favour of including this feature. It makes sense that you need to update the _infer_* functions. I would ignore doing 2D arrays (i.e. no hue).

Re complexity: It;s probably worth extracting everything to a animate_line function in animate.py? Things should be cleaner that way. The only code you have in common is the one that sets axes scales , limits & labels I think. Everything else has already been abstracted to _infer_line_data


if ndims == 2 and add_legend:
if animate_over is not None:
# TODO how might this work for multiple lines?
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 we can ignore this for now.

ax.set_title(darray._title_for_slice())
else:
# Would be nicer if we had something like in GH issue #266
frame_titles = [darray[{animate_over: i}]._title_for_slice()
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of looping over a possibly very-large dimension, maybe write a generator?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's currently not much point in doing this, as the first thing animatplot will do with the generator is expand it into a list anyway.

anim = Animation([line_block, title_block], timeline=timeline)
# TODO I think ax should be passed to timeline_slider args
# but that just plots a single huge timeline and no line plot?!
anim.controls(timeline_slider_args={'text': animate_over})
Copy link
Contributor

Choose a reason for hiding this comment

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

If it's in the title; maybe it doesn't need to be in the slider?

@rabernat
Copy link
Contributor

I think this is a cool example, and lots of users will probably like it.

A high-level question: why was animatplot chosen over matplotlib's animation module?

@TomNicholas
Copy link
Member Author

TomNicholas commented Jan 31, 2019

Re complexity: It;s probably worth extracting everything to a animate_line function in animate.py?

I've had a go at that in the most recent commits.

I think this is a cool example, and lots of users will probably like it.

Great - it's definitely something I personally want to do all the time.

A high-level question: why was animatplot chosen over matplotlib's animation module?

Because animatplot already abstracts away the creation of the animation, so plotting an animation becomes almost a simple substitution matplotlib.plot.line() -> animatplot.blocks.Line().

Another advantage is that animatplot's block abstraction should make it easier to compose figures made of multiple animated plots, another thing people often want to do but is is awful with matplotlib.funcanimation directly.

(I haven't really looked at how animated facetgrids might work yet, but I suspect that being able to animate a large list of blocks would be helpful too.)

The logic is explained slightly more in #2355, and also looking at animatplot's docs might help you see why I think it's a good idea.

We don't have to use animatplot, but if we didn't I would be suggesting reimplementing something with a similar class structure in xarray as the best approach anyway. The disadvantages of using animatplot are that it's an extra (optional) dependency, and it's not got good unit test coverage yet so someone should probably contribute tests to animatplot upstream before xarray officially relies on it. animatplot isn't particularly large, so if we wanted to reimplement something similar in xarray then that would be feasible.

TomNicholas referenced this pull request in benjaminwoods/iplot Feb 13, 2019
_labels = kwargs.pop('_labels', True)

ax = get_axis(figsize, size, aspect, ax)
xplt_val, yplt_val, hueplt, xlabel, ylabel, huelabel = \
Copy link
Contributor

Choose a reason for hiding this comment

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

Does animate work with pd.Interval step plots?

Copy link
Member Author

Choose a reason for hiding this comment

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

I have no idea. In theory then as long as xplt_val and yplt_val get set correctly then it should?

Have you got an example, issue, or point in the code somewhere I can look to see what a pd.Interval step plot consists of?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is the bit that tests it:

class TestPlotStep(PlotTestCase):

Copy link
Contributor

Choose a reason for hiding this comment

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

If it doesn't work; just raise an informative error...

@@ -0,0 +1,166 @@
"""
Use this module directly:
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs an edit.

@TomNicholas
Copy link
Member Author

I've done some more work on this, you can now:

Plot multiple animated lines

dat2d = air.isel(lat=[10, 15, 20])

anim = dat2d.plot(animate='time', hue='lat')
anim.save('line.gif', writer='imagemagick')
plt.show()

lines

Add static lines to an animated plot

# draw animated line
anim = dat1d.plot(animate='time', label='evolving')

# draw static line
# plot will be drawn on to current axes unless ax argument given
dat1d.mean(dim='time').plot(color='g', label='average')

plt.legend(loc='upper right')

anim.save('average.gif', writer='imagemagick')
plt.show()

average

@dcherian
Copy link
Contributor

dcherian commented May 2, 2019

There's a glitch with the legend in the first gif. Maybe the location needs to be pinned to something that is not 'best'

@dcherian
Copy link
Contributor

dcherian commented May 2, 2019

This is looking great; let me know when you're ready for another review.

I recommend saving facetgrid support for future PRs. I'm personally more excited about 2D animations and am happy to implement that once all the common infrastructure is in. With that bias in mind, I think the way to proceed would be:

(this PR) → 2D animations → facetgrid support

@rabernat
Copy link
Contributor

rabernat commented May 2, 2019

Just pinging @jbusecke, who recently announced https://github.com/jbusecke/xmovie. He might have some insights on this problem.

@jbusecke
Copy link
Contributor

jbusecke commented May 2, 2019

This looks amazing! Which problem are you referring to specifically @rabernat?

@jbusecke
Copy link
Contributor

jbusecke commented May 2, 2019

Also FYI I have a PR open that will enable xmovie to write movie files (by invoking ffmpeg 'under the hood').
Just wanted to mention it since this might come in handy as another export option for this feature later on.

@TomNicholas
Copy link
Member Author

There's a glitch with the legend in the first gif. Maybe the location needs to be pinned to something that is not 'best'

Yeah, I'm not entirely sure why that happens yet, but I guess it means the legend position should be determined automatically for only the first frame, then fixed there for the other frames.

I recommend saving facetgrid support for future PRs.

Agree. I think I might also leave animated step plot support for a future PR.

I'm personally more excited about 2D animations and am happy to implement that once all the common infrastructure is in.

Help with that would be great! I also really want to get 2D animations working - I'm often trying to make animations like this:

n_over_t_visc_3 7e0

I anticipate that there might be some difficulties with cartopy integration for 2D animations, but I don't actually use cartopy in my work, so help with that would be especially appreciated.

Just pinging jbusecke, who recently announced https://github.com/jbusecke/xmovie.

Yeah we've been talking about the relationship between this and xmovie in jbusecke/xmovie#2. I think that movie output functionality can be tacked on later.

@ahuang11
Copy link
Contributor

ahuang11 commented May 4, 2019

This looks awesome; it would be really nice to have built-in xarray animation support!

I'm personally more excited about 2D animations and am happy to implement that once all the common infrastructure is in.

Also wanted to point out while that's progressing along, hvplot supports 2D animations. https://hvplot.pyviz.org/

import xarray as xr
import hvplot.xarray
import holoviews as hv
hv.extension('matplotlib')

ds = xr.tutorial.open_dataset('air_temperature').isel(time=slice(0, 15))
hmap = ds.hvplot('lon', 'lat', dynamic=False).opts(fig_size=300, clim=(230, 300))
hv.save(hmap, 'anim.html', fmt='scrubber')

# ---

from IPython.core.display import display, HTML
display(HTML('anim.html'))

anim

@spencerahill
Copy link
Contributor

Just came across this PR while trying for the first time to create an animation of xarray data. Looks like it got quite far along but then sputtered. Did it get superseded by hvplot?

@TomNicholas
Copy link
Member Author

Hi @spencerahill , sorry for the slow reply.

This functionality isn't integrated yet, but hvplot I think probably provides the better approach. You will also want to see what philipjfr came up with over on #3709 , which would allow an xarray-like syntax but with the power of holoviews.

The one remaining question for me with the holoviews approach is how easy it is to save a .gif or a video file. It seems like it might be possible already, but if you have a go then let me know how it goes! (This is something I very very much want to see working, but isn't my priority right now)

@keewis keewis closed this Jun 23, 2021
@keewis keewis deleted the branch pydata:master June 23, 2021 16:14
@keewis
Copy link
Collaborator

keewis commented Jun 24, 2021

@TomNicholas, this has been unintentionally closed when renaming main. Feel free to reopen if you still plan to work on this in the future.

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.

10 participants