Embedding Matplotlib Animations in IPython Notebooks

This notebook first appeared as a blog post on Pythonic Perambulations.

License: BSD (C) 2013, Jake Vanderplas. Feel free to use, distribute, and modify with the above attribution.

I've spent a lot of time on this blog working with matplotlib animations (see the basic tutorial [here](http://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/), as well as my examples of animating [a quantum system](http://jakevdp.github.io/blog/2012/09/05/quantum-python/), [an optical illusion](http://jakevdp.github.io/blog/2012/09/26/optical-illusions-in-matplotlib/), [the Lorenz system in 3D](http://jakevdp.github.io/blog/2013/02/16/animating-the-lorentz-system-in-3d/), and [recreating Super Mario](http://jakevdp.github.io/blog/2013/01/13/hacking-super-mario-bros-with-python/)). Up until now, I've not have not combined the animations with IPython notebooks. The problem is that so far the integration of IPython with matplotlib is entirely static, while animations are by their nature dynamic. There are some efforts in the IPython and matplotlib development communities to remedy this, but it's still not an ideal setup. I had an idea the other day about how one might get around this limitation in the case of animations. By creating a function which saves an animation and embeds the binary data into an HTML string, you can fairly easily create automatically-embedded animations within a notebook.

The Animation Display Function

As usual, we'll start by enabling the pylab inline mode to make the notebook play well with matplotlib.


In [1]:
%pylab inline


Populating the interactive namespace from numpy and matplotlib

Now we'll create a function that will save an animation and embed it in an html string. Note that this will require ffmpeg or mencoder to be installed on your system. For reasons entirely beyond my limited understanding of video encoding details, this also requires using the libx264 encoding for the resulting mp4 to be properly embedded into HTML5.


In [2]:
from tempfile import NamedTemporaryFile

VIDEO_TAG = """<video controls>
 <source src="data:video/x-m4v;base64,{0}" type="video/mp4">
 Your browser does not support the video tag.
</video>"""

def anim_to_html(anim):
    if not hasattr(anim, '_encoded_video'):
        with NamedTemporaryFile(suffix='.mp4') as f:
            anim.save(f.name, fps=20, extra_args=['-vcodec', 'libx264'])
            video = open(f.name, "rb").read()
        anim._encoded_video = video.encode("base64")
    
    return VIDEO_TAG.format(anim._encoded_video)

With this HTML function in place, we can use IPython's HTML display tools to create a function which will show the video inline:


In [3]:
from IPython.display import HTML

def display_animation(anim):
    plt.close(anim._fig)
    return HTML(anim_to_html(anim))

Example of Embedding an Animation

The result looks something like this -- we'll use a basic animation example taken from my earlier Matplotlib Animation Tutorial post:


In [4]:
from matplotlib import animation

# First set up the figure, the axis, and the plot element we want to animate
fig = plt.figure()
ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
line, = ax.plot([], [], lw=2)

# initialization function: plot the background of each frame
def init():
    line.set_data([], [])
    return line,

# animation function.  This is called sequentially
def animate(i):
    x = np.linspace(0, 2, 1000)
    y = np.sin(2 * np.pi * (x - 0.01 * i))
    line.set_data(x, y)
    return line,

# call the animator.  blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=100, interval=20, blit=True)

# call our new function to display the animation
display_animation(anim)


---------------------------------------------------------------------------
BrokenPipeError                           Traceback (most recent call last)
<ipython-input-4-b51bfc0123f6> in <module>()
     23 
     24 # call our new function to display the animation
---> 25 display_animation(anim)

<ipython-input-3-63a8cd04f4be> in display_animation(anim)
      3 def display_animation(anim):
      4     plt.close(anim._fig)
----> 5     return HTML(anim_to_html(anim))

<ipython-input-2-e4c246e992b2> in anim_to_html(anim)
      9     if not hasattr(anim, '_encoded_video'):
     10         with NamedTemporaryFile(suffix='.mp4') as f:
---> 11             anim.save(f.name, fps=20, extra_args=['-vcodec', 'libx264'])
     12             video = open(f.name, "rb").read()
     13         anim._encoded_video = video.encode("base64")

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/animation.py in save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs)
    808                     # TODO: Need to see if turning off blit is really necessary
    809                     anim._draw_next_frame(d, blit=False)
--> 810                 writer.grab_frame(**savefig_kwargs)
    811 
    812         # Reconnect signal for first draw if necessary

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/animation.py in grab_frame(self, **savefig_kwargs)
    228             # frame format and dpi.
    229             self.fig.savefig(self._frame_sink(), format=self.frame_format,
--> 230                              dpi=self.dpi, **savefig_kwargs)
    231         except RuntimeError:
    232             out, err = self._proc.communicate()

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/figure.py in savefig(self, *args, **kwargs)
   1563             self.set_frameon(frameon)
   1564 
-> 1565         self.canvas.print_figure(*args, **kwargs)
   1566 
   1567         if frameon:

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, **kwargs)
   2230                 orientation=orientation,
   2231                 bbox_inches_restore=_bbox_inches_restore,
-> 2232                 **kwargs)
   2233         finally:
   2234             if bbox_inches and restore_bbox:

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/backends/backend_agg.py in print_raw(self, filename_or_obj, *args, **kwargs)
    517             close = False
    518         try:
--> 519             fileobj.write(renderer._renderer.buffer_rgba())
    520         finally:
    521             if close:

BrokenPipeError: [Errno 32] Broken pipe

Making the Embedding Automatic

We can go a step further and use IPython's display hooks to automatically represent animation objects with the correct HTML. We'll simply set the _repr_html_ member of the animation base class to our HTML converter function:


In [5]:
animation.Animation._repr_html_ = anim_to_html

Now simply creating an animation will lead to it being automatically embedded in the notebook, without any further function calls:


In [6]:
animation.FuncAnimation(fig, animate, init_func=init,
                        frames=100, interval=20, blit=True)


---------------------------------------------------------------------------
BrokenPipeError                           Traceback (most recent call last)
/home/chris/anaconda3/lib/python3.5/site-packages/IPython/core/formatters.py in __call__(self, obj)
    339             method = _safe_get_formatter_method(obj, self.print_method)
    340             if method is not None:
--> 341                 return method()
    342             return None
    343         else:

<ipython-input-2-e4c246e992b2> in anim_to_html(anim)
      9     if not hasattr(anim, '_encoded_video'):
     10         with NamedTemporaryFile(suffix='.mp4') as f:
---> 11             anim.save(f.name, fps=20, extra_args=['-vcodec', 'libx264'])
     12             video = open(f.name, "rb").read()
     13         anim._encoded_video = video.encode("base64")

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/animation.py in save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs)
    808                     # TODO: Need to see if turning off blit is really necessary
    809                     anim._draw_next_frame(d, blit=False)
--> 810                 writer.grab_frame(**savefig_kwargs)
    811 
    812         # Reconnect signal for first draw if necessary

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/animation.py in grab_frame(self, **savefig_kwargs)
    228             # frame format and dpi.
    229             self.fig.savefig(self._frame_sink(), format=self.frame_format,
--> 230                              dpi=self.dpi, **savefig_kwargs)
    231         except RuntimeError:
    232             out, err = self._proc.communicate()

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/figure.py in savefig(self, *args, **kwargs)
   1563             self.set_frameon(frameon)
   1564 
-> 1565         self.canvas.print_figure(*args, **kwargs)
   1566 
   1567         if frameon:

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, **kwargs)
   2230                 orientation=orientation,
   2231                 bbox_inches_restore=_bbox_inches_restore,
-> 2232                 **kwargs)
   2233         finally:
   2234             if bbox_inches and restore_bbox:

/home/chris/anaconda3/lib/python3.5/site-packages/matplotlib/backends/backend_agg.py in print_raw(self, filename_or_obj, *args, **kwargs)
    517             close = False
    518         try:
--> 519             fileobj.write(renderer._renderer.buffer_rgba())
    520         finally:
    521             if close:

BrokenPipeError: [Errno 32] Broken pipe
Out[6]:
<matplotlib.animation.FuncAnimation at 0x7f2e752d2198>

So simple! I hope you'll find this little hack useful!

This post was created entirely in IPython notebook. Download the raw notebook here, or see a static view on nbviewer.