A Javascript Viewer for Matplotlib Animations
I'll cut to the chase: here's what I've created: a javascript-based animation viewer, with hooks to embed it in IPython. It's best viewed in a modern browser (unfortunately Firefox does not currently qualify as "modern" due to its lack of HTML5 support)
%pylab inline
# get the JSAnimation import at https://github.com/jakevdp/JSAnimation
from JSAnimation import examples
examples.basic_animation()
I think the result is pretty good, if I do say so myself.
The Background¶
Last week, Fernando Perez visited UW to give a talk for the eScience institute. Over lunch we were discussing the possibility of building a Javascript-based animation viewer which could be embedded in IPython notebooks. I had written a short hack to embed mp4 movies in IPython, which works quite well: Michael Kuhlen of Berkeley ran with the idea and made this notebook, which embeds a 3D rendering of orbits within an N-body simulation.
The problem with this mp4 approach is that it requires installation of ffmpeg or mencoder with the proper video codec libraries. What we wanted was something that only requires Python and a web browser: something that could use Javascript to display frames rendered by Matplotlib.
Above you see the result of a week's worth of evenings hacking on Python, html, and Javascript -- my first real foray into the latter. The result is a small python package, available on my github page: https://github.com/jakevdp/JSAnimation. See the README and examples on that page for details of how this can be used.
So what's going on here?¶
You can dig into the code to see how it works, but here's the short version:
The package adds an IPython representation hook to the animation object, similar
to the one I showed here.
When the animation is displayed, IPython calls the new HTMLWriter
to convert
the animation to an embeddable html document. This writer is capable of saving any
animation to a stand-alone HTML file, with the frames either embedded or in a separate
directory: this stand-alone file is created, read-in, and embedded into the document
as raw HTML.
For IPython, the animation creates frames that are embedded directly in the HTML source via the base-64 representation. A base-64 representation is a standard way of encoding binary data to a normal string of text, which looks like this:
fig, ax = plt.subplots()
ax.plot(random.rand(100))
# write the figure to a temporary file, and encode the results to base64
import tempfile
with tempfile.NamedTemporaryFile(suffix='.png') as f:
fig.savefig(f.name)
data = open(f.name, 'rb').read().encode('base64')
# close the figure and display the data
plt.close(fig)
print data[:460]
We only print the first few sections of the data, as it is a rather large string. The magic of this is that contained in that string is all the information needed to reconstruct the original PNG image. We can see that directly by inserting the string into an HTML image tag, which results in a frame embedded in the document itself:
from IPython.display import HTML
HTML('<img src="data:image/png;base64,{0}">'.format(data))
This sort of thing is similar to what goes on in the background every time you use embedded figures in an IPython notebook.
By embedding all the frames this way, we're able to use Javascript to switch
between them at a given frame-rate using the javascript setInterval()
function. The rest is just straightforward javascript event handling.
Embedding Your Own Animation¶
If you'd like to use this to create your own animation, you can follow the
suggestions in the animation tutorial.
To embed your animation in the notebook, import IPython_display
from the JSAnimation
package. This will add the _repr_html_
method to the animation class, so that creating
the animation will lead to it being displayed via the Javascript embedding.
Here is an example showing how the above animation was created:
from matplotlib import animation
from JSAnimation import IPython_display
fig = plt.figure()
ax = plt.axes(xlim=(0, 10), ylim=(-2, 2))
line, = ax.plot([], [], lw=2)
def init():
line.set_data([], [])
return line,
def animate(i):
x = np.linspace(0, 10, 1000)
y = np.cos(i * 0.02 * np.pi) * np.sin(x - i * 0.02 * np.pi)
line.set_data(x, y)
return line,
animation.FuncAnimation(fig, animate, init_func=init,
frames=100, interval=30)
Remaining Issues¶
One of my goals in this was to make it relatively lightweight: For this reason, it doesn't depend on JQuery, JQuery-UI, and other nice packages that might be suited for this type of application.
For that reason, the frame slider uses the HTML5 slider element, which is not yet supported by all browsers. In particular, if you're using older versions of IE or Firefox, the frame dragger will appear as an ugly numerical input box. Making this compatible with non-HTML5-compliant browsers would be possible, but would require a lot more javascript hacking.
Second, this does not scale well to large animations. Because each frame is individually embedded in the document, the size of the notebook can become very large very quickly. Typical web-ready video formats involve a lot of image compression. This tends to be easy for video: most frames look very similar to the last, with just a few changes. Implementing this sort of compression in Javascript would certainly be possible, but is well beyond my minimal Javascript hacking abilities. It would be very cool if someone could run with this idea and create what would amount to a Javascript video codec to reduce the size of these embedded animations. I think it could be done with some effort.
As it is, though, I think this is a pretty neat widget and will definitely find good uses.