Animate the Boring Stuff with Matplotlib
Want to animate a matplotlib plot without converting into a graph in plotly or bokeh? this article is for you. You can animate beautiful plots in matplotlib with the FuncAnimation class.
This article covers animation of two matplotlib plots, a scatter map and a line chart.
Behind the scenes of Animations: the animation class creates different graphs for each frame(year in this case) and combines it into one big picture. Think about it as looping from one visual to the next, but in a quick customized fashion.
Let’s get to it.
Scatter Animation:
- First, import the necessary modules.
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
- To create an animation, you’ll need a list of values which will control how the animation is displayed (frames), in this case I shall be using a year column. First sort the values in order as matplotlib will not automatically do that for you.
unique_years = sorted(hosp["year"].unique())
The datasets employed in this article are: “geo_df”: a shapefile of the country, Nigeria. & “hosp”: a data of all the hospitals in the country.
NOTE: the data “hosp” has prior been cleaned and sampled for the purpose of this article. Here’s a random snapshot of the columns used:
- Next, create a matplotlib figure object, think of the figure as a container for your visual, and the axes as the artist, holding the contents of that container. I’ll be creating a figure of width 11, and height 10.
fig, ax = plt.subplots(figsize=(11, 10))
- Define a function which will be passed to FuncAnimation. Simply put, this function tells matplotlib what to do on each frame of the animation.
def update(year):
ax.clear()
hosp_df = hosp[hosp[‘year’] == year]
geo_df.plot(ax=ax, color="#f9ffe7", edgecolor="black")
hosp_df.plot(ax= ax, kind="scatter",x="longitude", y="latitude",marker = "o", color ="#ffc0cb", alpha=0.5)
for i, row in nigeria.iterrows():
ax.text(row.geometry.centroid.x, row.geometry.centroid.y, row["state"], fontsize=8, ha="center")
ax.set_axis_off()
plt.gca().set_aspect("equal", adjustable="box")
plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
ax.set_title(f"Nigerian hospitals in {year}", size=14, weight="bold")
Explanation of the code:
- ax.clear(): this is very important in creating the plot, it clears the figure so that the frames do not overlap each other.
- hosp[hosp[“year”] ==year]: here we subset our data for where the value in the year column equals to the current frame of the animation.
- geo_df.plot: this plots a map from the country geodataframe. The for loop iterates through each row and places a label “state” in the center of each geometry.
- hosp_df.plot: this plots the hospital data of the current frame as a scatter plot. By setting the argument ax=ax, we layer the plot on top of the geo_df map.
- The remaining part of the code removes the axis for the plot, adjusts the plot to the middle, and creates a title.
To create an animation with FuncAnimation, essential arguments needed are the matplotlib figure name, a function that defines what happens on each frame, a frame, and an interval i.e. how long each frame lasts in milliseconds.
animation = FuncAnimation(fig, update, frames=unique_years, interval=1000)
Lastly, the animation can be saved as an mp4 video, gif or displayed as html.
animation.save('hospital_animation.mp4')
animation.save('hospital_animation.gif', writer='pillow', fps=1)
from IPython.display import HTML
html_output = animation.to_html5_video()
HTML(html_output)
Line Animation:
For this animation, I’ll be using the python package, celluloid, to animate a matplotlib figure. Begin by installing celluloid, installation varies according to the IDE being used, I am using google colab notebook.
!pip install celluloid
- The “hosp” dataframe will be grouped by year along with the frequency of each year using .size ().
year_count = hosp.groupby("year").size().reset_index()
year_count.columns = ("year", "counts")
- Define x and y values for the line chart, which are the “year” and “counts” column for this data.
x = year_count["year"]
y = year_count["counts"]
- Import Camera class from celluloid. What does Camera do? Basically it takes a picture. Remember the behind the scenes explanation of animations explained earlier, Camera works the same way. When it iterates through each frame of a matplotlib plot, it creates a snapshot of the frame.
from celluloid import Camera
- Then create a matplotlib figure object, and initialize a Camera object, by associating it with the matplotlib figure.
fig = plt.figure(dpi=300)
camera = Camera(fig)
- The dpi argument controls the display resolution of the plot.
Next, iterate through the year values(ie x and plot the values on x and y axis up until the current index). Note that if adding annotations ie text, specify to start displaying the text, only after first line has been plotted, hence the need for the if statement. This ensures accurate placement of text on the plot.
for i in range(len(x)):
plt.plot(x[:i], y[:i], c="royalblue")
if i > 0:
plt.text(x[i - 1], y[i - 1], f"{y[i - 1]}", ha='left', va='bottom', color='black', fontsize=8)
camera.snap()
- Lastly, create the animation using camera’s animate function and save.
animation = camera.animate(blit=False, interval=1000)
animation.save("line_animate.gif", writer='pillow', fps=5)