import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib import rc
Animations
Sometimes explaining a complex topic becomes a lot easier by using an animation of a plot. It could showing how the graph changes over a paramter. It is even more helpful if we try to do combine more than one plot.
Simple Animations
Matplotlib plots are made of visible objects called artists. Each line, curve, point that we draw on a plot is called an artist. To make an animation, we need to create a plot and write a function that updates the required artists.
# NOTE: This is important. The animation will not play without this.
# configure matplotlib to show html animations
'animation', html='html5') rc(
As an example, we’ll plot a parabola using the formula \(y = a x^2\) with \(a\) going from \(0\) to \(1\).
# define a, x and y
= 1.0
a = np.linspace(-10, 10, 1000)
x = a*x*x y
# create a figure
= plt.figure()
fig
# plot returns a list of lines, we need to remember the first one, so that we can modify it
= plt.plot(x, y)
lines = lines[0]
line
# We also want to remember the title so that we can update it later.
= plt.title("$y = {a}x^2$")
title
plt.grid()
We got a plot, but what we really need an animation, not a staic plot.
In the following cell, we’ll write a function update
that takes the frame number and updates the data for the line and the title for each frame.
= 100
num_frames
def update(frame):
"""Function to update the figure for given frame number.
"""
# a is going from 0 to 1 and we have 100 frames
# dividing the curent frame number by num_frames will give a value between 0 and 1
= frame/num_frames
a
# compute x and y again
= np.linspace(-10, 10, 1000)
x = a*x*x
y
# update the data for the line (or the parabola)
line.set_data(x, y)
# update thext of the title
f"$y = {a} x^2$") title.set_text(
# Create an animation
# It will take a while to render all the frames and make it into a video
=num_frames) animation.FuncAnimation(fig, update, frames
Example: Moving over a circle
Let’s look at a more complex example of animation now. We are going to look at motion around a circle and how the x and y coordinates change and correlate that with \(sin\) and \(cos\).
# Let's work with angles and it will be easier to understand
# We are dividing [0, 360] into 10 degree intervals, so we'll have 37 points in total as we are coming back to the starting point
= np.linspace(0, 360, 37)
angles
= np.radians(angles)
theta = 1
r
= r * np.cos(theta)
x = r * np.sin(theta)
y
= plt.figure(figsize=(4, 4))
fig
plt.plot(x, y) plt.grid()
As the first step, we’ll make a point go over the circle.
# Let's work with angles and it will be easier to understand
# We are dividing [0, 360] into 10 degree intervals, so we'll have 37 points in total as we are coming back to the starting point
= np.linspace(0, 360, 37)
angles
= np.radians(angles)
theta = 1
r
= r * np.cos(theta)
x = r * np.sin(theta)
y
= plt.figure(figsize=(4, 4))
fig
plt.plot(x, y)
plt.grid()
= 30
angle = np.cos(np.radians(angle))
px = np.sin(np.radians(angle))
py
# plot a red color, circle marker
# see https://matplotlib.org/stable/api/markers_api.html
'ro')
plt.plot(px, py,
# Let's also mark origin
0, 0, 'ro')
plt.plot(
# and connect them using a dotted line
0, px], [0, py], 'r--') plt.plot([
To turn this into an animation, we need to remember the artists and update them for each frame.
# Let's work with angles and it will be easier to understand
# We are dividing [0, 360] into 10 degree intervals, so we'll have 37 points in total as we are coming back to the starting point
= np.linspace(0, 360, 37)
angles
= np.radians(angles)
theta = 1
r
= r * np.cos(theta)
x = r * np.sin(theta)
y
= plt.figure(figsize=(4, 4))
fig
plt.plot(x, y)
plt.grid()
= 0
angle = np.cos(np.radians(angle))
px = np.sin(np.radians(angle))
py
# plot a red color, circle marker
# see https://matplotlib.org/stable/api/markers_api.html
= plt.plot(px, py, 'ro')
p1,
# Let's also mark origin
= plt.plot(0, 0, 'ro')
p2,
# and connect them using a dotted line
= plt.plot([0, px], [0, py], 'r--')
line1,
= plt.title("") title
# move by 15 degrees
= 24
num_frames
def update(frame):
= frame/num_frames*360
angle
= np.cos(np.radians(angle))
px = np.sin(np.radians(angle))
py
p1.set_data(px, py)0, px], [0, py])
line1.set_data([
rf"$\theta = {angle}\degree$") title.set_text(
=num_frames, interval=500) animation.FuncAnimation(fig, update, frames
/tmp/ipykernel_25710/1378687440.py:10: MatplotlibDeprecationWarning: Setting data with a non sequence type is deprecated since 3.7 and will be remove two minor releases later
p1.set_data(px, py)
= plt.subplots()
fig, ax
def draw(ax):
# Let's work with angles and it will be easier to understand
# We are dividing [0, 360] into 10 degree intervals, so we'll have 37 points in total as we are coming back to the starting point
= np.linspace(0, 360, 37)
angles
= np.radians(angles)
theta = 1
r
= r * np.cos(theta)
x = r * np.sin(theta)
y
= plt.figure(figsize=(4, 4))
fig
plt.plot(x, y)
plt.grid()
= 30
angle = np.cos(np.radians(angle))
px = np.sin(np.radians(angle))
py
# plot a red color, circle marker
# see https://matplotlib.org/stable/api/markers_api.html
'ro')
plt.plot(px, py,
# Let's also mark origin
0, 0, 'ro')
plt.plot(
# and connect them using a dotted line
0, px], [0, py], 'r--') plt.plot([
# plt.ioff()
= plt.subplots()
fig, ax = np.random.default_rng(19680801)
rng = np.array([20, 20, 20, 20])
data = np.array([1, 2, 3, 4])
x
= []
artists = ['tab:blue', 'tab:red', 'tab:green', 'tab:purple']
colors for i in range(20):
ax.clear()+= rng.integers(low=0, high=10, size=data.shape)
data = ax.barh(x, data, color=colors)
container = ax.set_title(f"i={i}")
title
artists.append([container])
# plt.ion()
=fig, artists=artists, interval=400) animation.ArtistAnimation(fig
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) File ~/.local/lib/python3.10/site-packages/IPython/core/formatters.py:342, in BaseFormatter.__call__(self, obj) 340 method = get_real_method(obj, self.print_method) 341 if method is not None: --> 342 return method() 343 return None 344 else: File ~/.local/lib/python3.10/site-packages/matplotlib/animation.py:1361, in Animation._repr_html_(self) 1359 fmt = mpl.rcParams['animation.html'] 1360 if fmt == 'html5': -> 1361 return self.to_html5_video() 1362 elif fmt == 'jshtml': 1363 return self.to_jshtml() File ~/.local/lib/python3.10/site-packages/matplotlib/animation.py:1288, in Animation.to_html5_video(self, embed_limit) 1284 Writer = writers[mpl.rcParams['animation.writer']] 1285 writer = Writer(codec='h264', 1286 bitrate=mpl.rcParams['animation.bitrate'], 1287 fps=1000. / self._interval) -> 1288 self.save(str(path), writer=writer) 1289 # Now open and base64 encode. 1290 vid64 = base64.encodebytes(path.read_bytes()) File ~/.local/lib/python3.10/site-packages/matplotlib/animation.py:1090, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback) 1085 with mpl.rc_context({'savefig.bbox': None}), \ 1086 writer.saving(self._fig, filename, dpi), \ 1087 cbook._setattr_cm(self._fig.canvas, 1088 _is_saving=True, manager=None): 1089 for anim in all_anim: -> 1090 anim._init_draw() # Clear the initial frame 1091 frame_number = 0 1092 # TODO: Currently only FuncAnimation has a save_count 1093 # attribute. Can we generalize this to all Animations? File ~/.local/lib/python3.10/site-packages/matplotlib/animation.py:1498, in ArtistAnimation._init_draw(self) 1496 for f in self.new_frame_seq(): 1497 for artist in f: -> 1498 artist.set_visible(False) 1499 artist.set_animated(self._blit) 1500 # Assemble a list of unique figures that need flushing AttributeError: 'BarContainer' object has no attribute 'set_visible'
1 + 2
3
Animation Utilities
# Utilities to draw
class Animation:
def __init__(self, fig, frames, datafunc, **kwargs):
"""Creates an animation.
Parameters
----------
fig
The matplotlib figure to animate on.
frames
Number of frames
datafunc
A function that takes theta and returns x and y
kwargs
Optional keyworkd arguments passed to matplotlib.animation.FuncAnimation.
"""
self.fig = fig
self.frames = frames
self.datafunc = datafunc
self.kwargs = kwargs
self.objects = []
self._anim = None
def draw_frame(self, frame):
= frame/self.frames*2*np.pi
angle = self.datafunc(angle)
x, y = Context(angle=angle, x=x, y=y)
ctx return [obj.update(ctx) for obj in self.objects]
def render(self):
self._anim = animation.FuncAnimation(self.fig, self.draw_frame, self.frames, **self.kwargs)
def add_line(self, ax, fmt, datafunc):
"""Adds a line to the animation.
Parameters
----------
ax
The axis to draw the line on
fmt
The format of the line to draw. e.g. 'r-'
datafunc:
A function that takes the context as argument and returns [x1, y1, x2, y2].
"""
= Line(ax, fmt, datafunc)
line self.objects.append(line)
def add_point(self, ax, fmt, datafunc):
"""Adds a point to the animation.
Parameters
----------
ax
The axis to draw the line on
fmt
The format of the line to draw. e.g. 'r-'
datafunc:
A function that takes the context as argument and returns [x1, y1].
"""
= Point(ax, fmt, datafunc)
p self.objects.append(p)
class Context:
def __init__(self, angle=0, x=0, y=0):
self.angle = angle
self.x = x
self.y = y
class Line:
def __init__(self, ax, fmt, datafunc):
self.datafunc = datafunc
self.obj, = ax.plot([0, 0], [0, 0], fmt)
def update(self, ctx):
= self.datafunc(ctx)
x1, y1, x2, y2 self.obj.set_data([x1, x2], [y1, y2])
return self.obj
class Point:
def __init__(self, ax, fmt, datafunc):
self.datafunc = datafunc
self.obj, = ax.plot(0, 0, fmt)
def update(self, ctx):
= self.datafunc(ctx)
x, y self.obj.set_data([x], [y])
return self.obj
class ParametericAnimation(Animation):
def __init__(self, func, frames=36):
= plt.figure(figsize=(8, 8))
fig super().__init__(fig, frames, func)
self.draw()
def draw(self):
= plt.subplot(2, 2, 3)
ax0 = plt.subplot(2, 2, 1)
ax1 = plt.subplot(2, 2, 4)
ax2
# ticks = np.linspace(0, 2*np.pi, 9)
# tick_labels = [
# r"$0$", r"$\frac{\pi}{4}$",
# r"$\frac{\pi}{2}$", r"$\frac{3\pi}{4}$",
# r"$\pi$", r"$\frac{5\pi}{4}$",
# r"$\frac{3\pi}{2}$", r"$\frac{7\pi}{4}$",
# r"$2\pi$"]
= np.linspace(0, 2*np.pi, 5)
ticks = [
tick_labels r"$0$", r"$\frac{\pi}{2}$",
r"$\pi$", r"$\frac{3\pi}{2}$", r"$2\pi$"]
= np.linspace(0, 2*np.pi, 1000)
t = self.datafunc(t)
x, y
ax0.plot(x, y)
ax0.grid()
'r-')
ax1.plot(x, t,
ax1.grid()0, 0], [0, 2*np.pi], color='gray')
ax1.axline([
ax1.set_yticks(ticks, tick_labels)
'g-')
ax2.plot(t, y,
ax2.grid()0, 0], [2*np.pi, 0], color='gray')
ax2.axline([
ax2.set_xticks(ticks, tick_labels)
self.add_line(ax0, 'b--', lambda ctx: [0, 0, ctx.x, ctx.y])
self.add_line(ax0, 'r--', lambda ctx: [0, 0, ctx.x, 0])
self.add_line(ax0, 'g--', lambda ctx: [ctx.x, 0, ctx.x, ctx.y])
self.add_point(ax0, 'bo', lambda ctx: [0, 0])
self.add_point(ax0, 'bo', lambda ctx: [ctx.x, ctx.y])
self.add_point(ax1, 'ro', lambda ctx: [ctx.x, ctx.angle])
self.add_line(ax1, 'r--', lambda ctx: [0, ctx.angle, ctx.x, ctx.angle])
self.add_point(ax2, 'go', lambda ctx: [ctx.angle, ctx.y])
self.add_line(ax2, 'g--', lambda ctx: [ctx.angle, 0, ctx.angle, ctx.y])
def func(t):
= np.cos(t)
x = np.sin(t)
y return x, y
= ParametericAnimation(func)
ani ani.render()