General way to animate any artist in matplotlib? - python

I have tried to animate two different artists plt.quiver() and plt.hist() in matplotlib recently and both times I ran into the same problem. Apparently those classes (I hope my OOP literacy is holding up) both don't have a set_data like method. Well, technically plt.quiver() does have set_UVC, but that doesn't work with Line3D instances, only with Line2D. Also, there is an example for animating a histogram, but it seemed like some serious jerry-rigging to me. I tried to simply define my artist with new values in the update() function and then just return the new artist instead of defining the artist outside the update() and then updating the data of the artist using a set_data() method. But this only results in an animation in which all frames are kept in the plot and overlap. Below are the animations for both the Histogram and the Quiver plot.
Histogram:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
"""
evolution of mean values produced by 1000 dice rolls with
more and more dices, which lead to a narrowing variance
with a growing number of dices.
"""
fig, ax = plt.subplots()
def update(i):
k = [np.mean(np.random.randint(0,7,i)) for j in range(1000)]
lol = ax.hist(k,bins=20)
return lol
ani = FuncAnimation(fig, update, frames=(1,2,10,100,1000))
plt.show()
Quiver:
from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
def rot_z(angle):
o = 2*np.pi*(angle/360)
mat = np.array(((np.cos(o),-np.sin(o),0),
(np.sin(o), np.cos(o),0),
( 0 , 0 ,0)))
return mat
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_xlim(-1.5,1.5)
ax.set_ylim(-1.5,1.5)
ax.set_zlim(-1.5,1.5)
def update(frame):
x,y,z = rot_z(frame).dot(np.array((1,1,1)))
quiv = ax.quiver(0,
0,
0,
x,
y,
z,
length=1)
return quiv
ani = FuncAnimation(fig, update, frames=np.linspace(0,360,100))
plt.show()
If you run them, you can see the issue. So I wanted to know: Isn't there an easier, abstractable way of animating artists, or am I at the mercy of potentially non-existent setters? I have checked both dir(plt.quiver), dir(plt.hist) to see if I was simply overlooking those methods in the docs, but the example of the animated histogram seemed to confirm my fears.

You could try to clear the image at every update with ax.clear(). Maybe the histogram animation would be more smooth if you would extend an array of throws instead of restarting from scratch at each frame?
Edit: the code below includes a test to reuse the same samples
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
randnums = [np.random.randint(0,7,1000) for j in range(1000)]
def update(i):
k = [np.mean(randnums[j][:i]) for j in range(1000)]
ax.clear()
lol = ax.hist(k,bins=20)
return lol
ani = FuncAnimation(fig, update, frames=[2**t for t in range(11)])
plt.show()

Related

Matplotlib FuncAnimation Step-by-Step Animation Function

I am trying to use matplotlib's FuncAnimation to make an animated video. Each frame is just a boolean n x n array visualised as white/black squares. I can do this successfully by defining all the arrays in advance and then going through them one by one. This uses code similar to matplotlib's example.
My items are rather large and I want to run the simulation for a long time. I thus don't want to create the entire list of arrays then go through them one by one. Instead, I want to define the animate function to do each step. Let me explain with a minimal non-working example. My actual example includes far larger arrays!
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
def create_video(n):
global X
X = np.random.binomial(1, 0.3, size = (n,n))
fig = plt.figure()
im = plt.imshow(X, cmap = plt.cm.gray)
def animate(t):
global X
X = np.roll(X, +1, axis = 0)
im.set_array(X)
anim = FuncAnimation(
fig,
animate,
frames = 100,
interval = 1000 / 30,
blit = True
)
return anim
anim = create_video(10)
This initialises some random 10 x 10 set of 0/1s then just 'rolls' it at each step. I get an error.
RuntimeError: The animation function must return a sequence of Artist objects.
If I remove the return anim, replacing it with pass, and replacing anim = create_video(10) with create_video(10), then I get a warning.
UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation.
Clearly, I don't understand well enough FuncAnimation. What I want to happen is for the function animate to update the array X, by 'rolling' it one step, as well as doing im.set_array(X).
As explained in this answer:
As the error suggests, and as can be seen e.g. in the
simple_animation example, but also from the FuncAnimation
documentation, the init_func as well as the updating func are
supposed to return an iterable of artists to animate.
The documentation does not say that this is actually only needed when
using blit=True, but since you are using blitting here, it is
definitely needed.
So you have two ways:
add
return im,
to animate function
set blit = False in FuncAnimation
Complete Code
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
def create_video(n):
global X
X = np.random.binomial(1, 0.3, size = (n, n))
fig = plt.figure()
im = plt.imshow(X, cmap = plt.cm.gray)
def animate(t):
global X
X = np.roll(X, +1, axis = 0)
im.set_array(X)
return im,
anim = FuncAnimation(
fig,
animate,
frames = 100,
interval = 1000/30,
blit = True
)
plt.show()
return anim
anim = create_video(10)

Plot a rolling window with Matplotlib

I want to plot a time series in a while loop as a rolling window: The graph should always show the 10 most recent observations.
My idea was to use a deque object with maxlen=10 and plot it in every step.
To my great surprise the plot appends new values to the old plot; apparently it remembers values that are no longer inside the deque! Why is that and how can I switch it off?
This is a minimal example of what I am trying to do. The plotting part is based on this post (although plt.ion() did not change anything for me, so I left it out):
from collections import deque
import matplotlib.pyplot as plt
import numpy as np
x = 0
data = deque(maxlen=10)
while True:
x += np.abs(np.random.randn())
y = np.random.randn()
data.append((x, y))
plt.plot(*zip(*data), c='black')
plt.pause(0.1)
I also tried to use Matplotlib's animation functions instead, but could not figure out how to do that in an infinite while loop...
Nowadays, it's much easier (and offers much better performance) to use the animation module than to use multiple calls to plt.plot:
from collections import deque
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
def animate(i):
global x
x += np.abs(np.random.randn())
y = np.random.randn()
data.append((x, y))
ax.relim()
ax.autoscale_view()
line.set_data(*zip(*data))
fig, ax = plt.subplots()
x = 0
y = np.random.randn()
data = deque([(x, y)], maxlen=10)
line, = plt.plot(*zip(*data), c='black')
ani = animation.FuncAnimation(fig, animate, interval=100)
plt.show()

Redrawing Seaborn Figures for Animations

Some seaborn methods like JointPlot create new figures on each call. This makes it impossible to create a simple animation like in matplotlib where iterative calls to plt.cla() or plt.clf() allow to update the contents of a figure without closing/opening the window each time.
The only solution I currently see is:
for t in range(iterations):
# .. update your data ..
if 'jp' in locals():
plt.close(jp.fig)
jp = sns.jointplot(x=data[0], y=data[1])
plt.pause(0.01)
This works because we close the previous window right before creating a new one. But of course, this is far from ideal.
Is there a better way? Can the plot somehow be done directly on a previously generated Figure object? Or is there a way to prevent these methods to generate new figures on each call?
sns.jointplot creates a figure by itself. In order to animate the jointplot, one might therefore reuse this created figure instead of recreating a new one in each iteration.
jointplot internally creates a JointGrid, so it makes sense to directly use this and plot the joint axes and the marginals individually. In each step of the animation one would then update the data, clear the axes and set them up just as during creation of the grid. Unfortunately, this last step involves a lot of code lines.
The final code may then look like:
import matplotlib.pyplot as plt
import matplotlib.animation
import seaborn as sns
import numpy as np
def get_data(i=0):
x,y = np.random.normal(loc=i,scale=3,size=(2, 260))
return x,y
x,y = get_data()
g = sns.JointGrid(x=x, y=y, size=4)
lim = (-10,10)
def prep_axes(g, xlim, ylim):
g.ax_joint.clear()
g.ax_joint.set_xlim(xlim)
g.ax_joint.set_ylim(ylim)
g.ax_marg_x.clear()
g.ax_marg_x.set_xlim(xlim)
g.ax_marg_y.clear()
g.ax_marg_y.set_ylim(ylim)
plt.setp(g.ax_marg_x.get_xticklabels(), visible=False)
plt.setp(g.ax_marg_y.get_yticklabels(), visible=False)
plt.setp(g.ax_marg_x.yaxis.get_majorticklines(), visible=False)
plt.setp(g.ax_marg_x.yaxis.get_minorticklines(), visible=False)
plt.setp(g.ax_marg_y.xaxis.get_majorticklines(), visible=False)
plt.setp(g.ax_marg_y.xaxis.get_minorticklines(), visible=False)
plt.setp(g.ax_marg_x.get_yticklabels(), visible=False)
plt.setp(g.ax_marg_y.get_xticklabels(), visible=False)
def animate(i):
g.x, g.y = get_data(i)
prep_axes(g, lim, lim)
g.plot_joint(sns.kdeplot, cmap="Purples_d")
g.plot_marginals(sns.kdeplot, color="m", shade=True)
frames=np.sin(np.linspace(0,2*np.pi,17))*5
ani = matplotlib.animation.FuncAnimation(g.fig, animate, frames=frames, repeat=True)
plt.show()
using the celluloid package (https://github.com/jwkvam/celluloid) I was able to animate seaborn plots without much hassle:
import numpy as np
from celluloid import Camera
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
fig = plt.figure()
camera = Camera(fig)
# animation draws one data point at a time
for i in range(0, data.shape[0]):
plot = sns.scatterplot(x=data.x[:i], y=data.y[:i])
camera.snap()
anim = camera.animate(blit=False)
anim.save('animation.mp4')
I'm sure similar code could be written for jointplots

FuncAnimation with a matrix

I would like to use FuncAnimation to animate a matrix that will evolve. I tried to use a very simple matrix before using a complex one but I don't manage to use FuncAnimation with the simple one. I tried looking on other posts but I can't adapt them to what I want to do. Here's what I tried to do but it doesn't work
from numpy import *
import matplotlib.pyplot as plt
import matplotlib.animation as animation
M=array([[0,0,100,100,100,100,100,100,300,300,300,300,300,300,500,500,500,500,500,500,1000,1000,1000,1000] for i in range(0,20)])
def update(i):
M[7,i] =1000
M[19-i,10]=500
mat.set_array(modif(i,M))
return mat
fig, ax = plt.subplots()
matrice = plt.matshow(mat)
plt.colorbar(matrice)
ani = animation.FuncAnimation(fig, update, frames=19, interval=1500)
plt.show()
I would just like to see the matrix moving to see its evolution.
If you have any ideas, please let me know (even if I'm a little inexperienced).
Thanks !
The reason your code doesn't work: You have not defined mat nor modif.
Also, you should plot to the axes (ax.matshow()) instead of creating a new plot (plt.matshow()).
The following should do what you want.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
M=np.array([[0,0,100,100,100,100,100,100,300,300,300,300,300,300,500,500,500,500,500,500,1000,1000,1000,1000] for i in range(0,20)])
def update(i):
M[7,i] = 1000
M[19-i,10] = 500
matrice.set_array(M)
fig, ax = plt.subplots()
matrice = ax.matshow(M)
plt.colorbar(matrice)
ani = animation.FuncAnimation(fig, update, frames=19, interval=500)
plt.show()

Animation based on only updating colours in a plot

I have a plot which consists of great number of lines. At each step the colours of lines should get updated in the animation, but doing a for loop on lines seems to be really costly. Is there any better way to do that?
Here is my code:
import numpy as np
lines=[]
from matplotlib import pyplot as plt
import matplotlib.animation as animation
#initial plot
fig=plt.figure()
ax=plt.subplot(1,1,1)
for i in range(10):
lines.append([])
for j in range(10):
lines[i].append(ax.plot([i,j],color='0.8'))
lines=np.asarray(lines)
##Updating the colors 10 times
im=[]
for steps in range(10):
colors=np.random.random(size=(10,10))
for i in range(10):
for j in range(10):
lines[i,j][0].set_color(str(colors[i,j]))
plt.draw()
# im.append(ax)
plt.pause(.1)
#ani = animation.ArtistAnimation(fig, im, interval=1000, blit=True,repeat_delay=1000)
plt.show()
Plus I couldn't make it to work with animation artist! I used draw. What is wrong with the animation lines
Now increasing those 10s to 100 makes the program terribly slow:
import numpy as np
lines=[]
from matplotlib import pyplot as plt
import matplotlib.animation as animation
#initial plot
fig=plt.figure()
ax=plt.subplot(1,1,1)
for i in range(100):
lines.append([])
for j in range(100):
lines[i].append(ax.plot([i,j],color='0.8'))
lines=np.asarray(lines)
##Updating the colors 10 times
im=[]
for steps in range(10):
colors=np.random.random(size=(100,100))
for i in range(100):
for j in range(100):
lines[i,j][0].set_color(str(colors[i,j]))
plt.draw()
# im.append(ax)
plt.pause(.1)
#ani = animation.ArtistAnimation(fig, im, interval=1000, blit=True,repeat_delay=1000)
plt.show()
As I said I want to run it side by side with an animation. Therefore I prefer to make it an animation. I think that would solve the lagging problem at least after the animation starts but right now the way I defined it, it doesn't work.
It's easiest to use a LineCollection for this. That way you can set all of the colors as a single array and generally get much better drawing performance.
The better performance is mostly because collections are an optimized way to draw lots of similar objects in matplotlib. Avoiding the nested loops to set the colors is actually secondary in this case.
With that in mind, try something more along these lines:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
import matplotlib.animation as animation
lines=[]
for i in range(10):
for j in range(10):
lines.append([(0, i), (1, j)])
fig, ax = plt.subplots()
colors = np.random.random(len(lines))
col = LineCollection(lines, array=colors, cmap=plt.cm.gray, norm=plt.Normalize(0,1))
ax.add_collection(col)
ax.autoscale()
def update(i):
colors = np.random.random(len(lines))
col.set_array(colors)
return col,
# Setting this to a very short update interval to show rapid drawing.
# 25ms would be more reasonable than 1ms.
ani = animation.FuncAnimation(fig, update, interval=1, blit=True,
init_func=lambda: [col])
# Some matplotlib versions explictly need an `init_func` to display properly...
# Ideally we'd fully initialize the plot inside it. For simplicitly, we'll just
# return the artist so that `FuncAnimation` knows what to draw.
plt.show()
If you want to speed up a for loop, there are several good ways to do that. The best one for what you are trying to do, generator expressions, is probably like this:
iterator = (<variable>.upper() for <samevariable> in <list or other iterable object>)
(for more specific information on these there is documentation at http://www.python.org/dev/peps/pep-0289/ and https://wiki.python.org/moin/Generators)
There are also other, non-for loop ways to update color, but they are unlikely to be any faster than a generator. You could create some form of group for the lines, and call something like:
lines.update()
on all of them.

Categories

Resources