This notebook is to demonstrate animation and interactive elements in python + Jupyter.
The animation is a part of matplotlib. For the animation, we create a function that generates each frame. This function is passed to the matplotlib.animation class creator FuncAnimation to generate the animation. We can then display that animation in the notebook and/or save it to a file. Some good resources to understand animation using matplotlib are:
The imports from ipywidgets allow us to create interactive sliders and other elements in the notebook. Some good information on this is at
For this demonstration, we need to import several modules.
import numpy as np
%matplotlib inline
import matplotlib
from matplotlib import pyplot as plt
from matplotlib import animation
matplotlib.rc('animation', html='html5')
from ipywidgets import interact, interactive, widgets
from IPython.display import display
A good explanation of why we are importing pyplot as plt rather importing pylab is found on the matplotblib documentation and in the documentation on using matplotlib in Jupyter. Esentially, since we are doing an animation we need the figure object to persit past a single line. This way we keep access to the same figure and axes. Also, the axes are the basic object in matplotlib, not figure. The figure is a container for the axes, and there could be muliple sets of axes. The plots (actually matplotlib line objects) are objects of the axes, not the figure.
This example is based mainly on this example.
In the code below:
MyAxes
variable is an instance of a matplotlib axes object.set_xlim
and set_ylim
set the x and y axis ranges..plot([], [], lw=2)
function call:
','
in "MyLine, =".
[]
.'lw=2
argument sets the linewidth.MyFigure, MyAxes = plt.subplots()
MyAxes.set_xlim(( 0, 2*np.pi))
MyAxes.set_ylim((-1.5, 1.5))
MyLine, = MyAxes.plot([], [], lw=2)
x=np.linspace(0,2*np.pi)
AnimationFrames = 100 #total number of frames in animation.
DelayBetweenFrames = 20 #in msec (20 gives 50 fps)
AnimationTime = AnimationFrames * DelayBetweenFrames/1000
print ("The animation will be {:.1f} seconds long.".format(AnimationTime))
The animation will be 2.0 seconds long.
In the following cell, we define the animation function or the function that will be called for each frame. This function should return a touple of anything that has changed from the previous frame. The function accepts one argument, the number of the animation frame.
Since we want a sin wave that looks like it is continously moving, we will adjust the phase. We adujst the phase by exactly $2\pi$ over one animation loop so that the animation looks continous.
def a(i):
y = np.sin(x + 2*np.pi*i/AnimationFrames)
MyLine.set_data(x, y)
return (MyLine,)
FuncAnimation creates the animation sequence. The option blit=true
speeds up the drawing process, and requires a
to return a list of items which have changed as our a
function does.
FuncAnimation
returns a FuncAnimation object. We could save this animation using the save method. In this case we will display the animation in this notebook. Since we have set the parameter for displaying animations to html5 with matplotlib.rc('animation', html='html5')
in the initilization section, we simply need to display the animation object.
MyAnimation = animation.FuncAnimation(MyFigure, a,
frames=AnimationFrames, interval=DelayBetweenFrames, blit=True)
MyAnimation
Let us look at another animation. In this case we will fit a line to some random data and display the data and fit line. We will animate this figure by changing the number of data points generated.
First, we set the parameters for the data generation
maxx = 25
stddev = 10
mtarget = 3.0
btarget = 2.0
maxy = 1.1*(mtarget*maxx + btarget)
Now, we again setup a figure, axes object, and the data series we want to plot. In this case there are two data series or lines.
DFFig, DFAxes = plt.subplots()
DFAxes.set_xlim(( 0, maxx))
DFAxes.set_ylim((0, maxy))
DFLineData, = DFAxes.plot([], [], marker='H', ls='none', MFC=(1,0,0,0),mec=(1,0,0,.25), label='Data')
DFLineFit, = DFAxes.plot([], [], marker='None', ls='-', label='Fit')
Next, we set the parameters for the animation so that we don't have magic numbers floating around.
AnimationFrames = 400 #total number of frames in animation.
DelayBetweenFrames = 41 #in msec (41.6 gives 24 fps)
MinDataPoints = 2
MaxDataPoints = 10000
NMultiplier = (MaxDataPoints - MinDataPoints)/AnimationFrames
AnimationTime = AnimationFrames * DelayBetweenFrames/1000
print ("The animation will be {:.1f} seconds long.".format(AnimationTime))
The animation will be 16.4 seconds long.
xx=np.linspace(0, maxx)
def DFData(n):
x = np.random.uniform(0., maxx, n)
y = mtarget * x + btarget + np.random.normal(0., stddev, n)
return (x,y)
def DFAnimator(n=10):
n=int(NMultiplier*n + MinDataPoints)
x,y = DFData(n)
A = np.vstack([x,np.ones(len(x))]).T
mfit, bfit = np.linalg.lstsq(A, y)[0]
DFLineFit.set_data(xx,mfit*xx + bfit)
DFLineData.set_data(x, y)
return [DFLineData,DFLineFit]
DFAnim = animation.FuncAnimation(DFFig, DFAnimator ,
frames=AnimationFrames, interval=DelayBetweenFrames, blit=False)
DFAnim
In the initilization section above, we gave some resources for learning about interactive widgets. Armed with this information, we will again consider curve fitting to random data. This time, we will vary the number of data points with an interactive slider widget using interactive.
def DFFigure(n=10):
x,y = DFData(n)
A = np.vstack([x,np.ones(n)]).T
mfit, bfit = np.linalg.lstsq(A, y)[0]
DFLineFit.set_data(xx,mfit*xx + bfit)
DFLineData.set_data(x, y)
display(DFFig)
DFw=interactive(DFFigure,n=(MinDataPoints,MaxDataPoints),continuous_update=True)
display(DFw)
We can also create a widget with two sliders. In this case, we will vary the number of points and the standard deviation in our generated data. To do this, we will first create a FloatSlider and IntSlider widgets and then use those in our call to interactive. The function we call from interactive must accept two arguments.
stddev_widget = widgets.FloatSlider(min=0.05, max=30.0, step=0.05, value=20.0)
n_widget = widgets.IntSlider(min=MinDataPoints,max=MaxDataPoints,step=10,value=(MinDataPoints+MaxDataPoints)/2)
def DFDataTwo(n, sd):
x = np.random.uniform(0., maxx, n)
y = mtarget * x + btarget + np.random.normal(0., sd, n)
return (x,y)
def DFFigure(n, sd):
x,y = DFDataTwo(n, sd)
A = np.vstack([x,np.ones(n)]).T
mfit, bfit = np.linalg.lstsq(A, y)[0]
DFLineFit.set_data(xx,mfit*xx + bfit)
DFLineData.set_data(x, y)
display(DFFig)
DFwnew=interactive(DFFigure,n=n_widget,sd=stddev_widget,continuous_update=True)
display(DFwnew)