I flip-flop between Python, R, and D3 for my data visualizations depending on what exactly I’m doing. My default, though, is definitely Python. One of the most well-established data visualization libraries in Python is Matplotlib. If you dig deep enough in it, you can find a wide variety of features beyond standard graphs. One of the less well-documented of these features is the animation library. The
FuncAnimation class in particular is quite powerful, allowing you to programmatically generate the frames for your animation and compile them together. Jake VanderPlas has a great tutorial on using
FuncAnimation which I’m not going to try to duplicate. Here, I’m just going to focus on a small but critical aspect of using
FuncAnimation that is glossed over elsewhere: blitting.
Here’s how critical blitting is: My first attempted Matplotlib animation took around an hour to render. That wasn’t going to work. Thanks to blitting, I can now render the same animation in under a minute.
The basic idea behind
FuncAnimation is that you pass it a function that it will use to generate new frames. If you aren’t using blitting, this function generates each one of your frames from scratch. Unsurprisingly, that takes a super long time (okay, I’m lying: I was surprised by how long it took at first). Fortunately, if you are making an animation, chances are your frames aren’t 100% different from each other. In fact, it’s likely that there is just one element or set of elements that is changing. Blitting takes advantage of this by just changing the plot elements that need to be changed. However, this completely changes the way that the function you pass to
FuncAnimation needs to work.
To demonstrate this, I’ll use my code as an example. Here’s the animation I was trying to make. It has a fixed background with colored circles that appear on top and change color from frame to frame:
First we load in the data and set stuff up:
import matplotlib.pyplot as plt
from matplotlib import animation
world_size = 59 #The circles appear on a 59 X 59 grid
world = load_environment(filename) #Don't worry about this it's just
#loading a 2D array of numbers to pass to imshow() to make the background
circle_colors = load_color_data(filename) #And this is loading a 3D array
#Each XY coordinate contains a list of RGB colors indicating the series
#of colors the circle should take on in each frame (so the length of
#these lists is equal to the number of frames)
Next, we create a figure object to pass to the
#Create figure to do plotting on
fig = plt.figure(figsize=(20,20))
Now we need to create a list of objects that will change from frame to frame. These objects need to be “drawables.” The Matplotlib documentation doesn’t clearly define what drawables are, but they seem to at least include all artist objects. Here, we will use Circles (for those keeping track at home, these are matplotlib.patches objects, which inherit from artists), but you could do this with lines, polygons, or a wide variety of other shapes.
#Create list of circles, one for every grid cell in the environment
patches = 
for i in range(world_size):
for j in range(world_size):
patches.append(plt.Circle((j,i), radius=.3, lw=2, ec="black", facecolor=None))
#(j,i) are the (x,y) coordinates, lw is the line width, ec is the color of the
#outline, setting facecolor to None makes these circles invisible for now
Now we’re ready to start defining functions to pass to
FuncAnimation! The first one is the
init function. It plots the parts of the image that don’t change between frames, and adds all of the parts that will change to the axes of this image.
plt.imshow(world, interpolation="none", hold=True)
axes = plt.gca()
#Add patches to axes
for p in patches:
At last, it’s time to write the function that actually makes all of the frames. This function will basically loop through all of the circles and assign them the appropriate color. The function will automatically get passed one argument: the frame number currently being generated. Additional arguments can be specified using the
fargs argument in
#Note that this needs to be in the same scope as circle_colors
for i in range(world_size):
for j in range(world_size):
if circle_colors[i][j][n] == 0:
#Here, I'm using 0 as code for non-existent
#So I make these circles invisible
patches[i * world_size + j].set_visible(False)
else: #Otherwise we set the color to the one indicated by circle_colors
#and make sure the circle is set to visible
patches[i*world_size + j].set_facecolor(circle_colors[i][j][n])
patches[i*world_size + j].set_visible(True)
#We haven't actually changed patches, but FuncAnimation is
#still expecting us to return it. Don't forget the comma.
All that’s left now is to finally create the animation object and let it do its (now dramatically faster) rendering.
anim = animation.FuncAnimation(fig, animate, init_func=init,
#remember to set blit=True
anim.save(filename, writer="mencoder", fps=2) #You can also save your animation!
#Appropriate writer will vary based on your computer
You should now be able to create spiffy animations that don’t take hours to render! Note that there seems to be a bug with some versions of VLC where the animated parts of the picture don’t show up. This may or may not lead to your adviser being perplexed that you sent them an animation that doesn’t do anything.
Other resources that I found helpful in figuring out how this works include: these stackoverflow posts (although I never did get the PatchCollection class mentioned in the second to work), this sample script, and this tutorial.