Matplotlib animate objects with different markers - python

I need to do an animation in matplotlib where I want to plot a moving particle (as a point) and a line (corresponding to the magnitude and direction of its speed)
I have taken a look at this website,were there are ifferent examples of animations.
https://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/
Basically what they do in the examples is initializing line as a plot with certain markers:
line, = ax.plot([], [], 'o') # for points
line2, = ax.plot([], [], '-') # for lines
and in the animate function they just use set_data to update the position of the particle for the next frame
def animate(i):
line.set_data(x, y)
return line,
However in the examples, they either use lines (eg: the pendulum example) OR dots (the particles in a box example), but not both.
How can I first plot the particle as a dot and then add a speed vector as a line?
The animate function has to return line so if I use line and line2 I need a way to merge them.
Something like:
plot(x, y, 'o') # position
plot([x0, x1], [y0, y1], '-') # speed
Thanks

You can define two "lines", one which consists of marked points, but no lines in between (ls="") and one with only a line but no markers (marker="")
points, = ax.plot([1,2], [3,4], marker='o', ls="" ) # for points
line, = ax.plot([2,1], [4,3], marker="" , ls="-") # for lines
The animate function would then return both "lines":
def animate(i):
# x = ...; y = ...
line.set_data(x, y)
# x2 = ...; y2 = ...
points.set_data(x2, y2)
return points, line

Related

Matplotlib: Include certain points during animation

I wanted to simulate the trajectory of a projectile which is fired from a certain position and show the highest point and the position where it hits the ground during the animation. My problem is that I can´t figure out how to add these points dynamically while the animation takes place.
Here´s how I animate the path:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation, rc
from IPython.display import HTML
# First set up the figure, the axis, and the plot element we want to animate:
## Get Figure and Axes to plot in
fig, ax = plt.subplots()
## Limits the x- and y-axis
ax.set_xlim((0, 30))
ax.set_ylim((0, 25))
line, = ax.plot([], [], lw=2)
# Setup interval for plotting
interval = np.linspace(0, 29.43, 100)
# Defines the path function of the projectile
def path_of_projectile(x):
return (-(x - 9.81)**2 / 19.62) + 19.62
# Defines the highest point of the projectile
def get_highest_point(x):
return 14.715 + 9.81 * x - 4.905 * (1)**2
# Define the initialization function, which plots the background of each frame:
def init():
line.set_data([], [])
return (line,)
y = path_of_projectile((interval))
# Define the animation function, which is called for each new frame:
def animate(i):
line.set_data(interval[0:i], y[0:i])
return (line,)
# Compile the animation
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=100, interval=20,
blit=True)
# Plot it
HTML(anim.to_jshtml())
The function get_highest_point returns the highest point of the projectile f.e. How can I include it to show up during the animation?
As I also commented, I don't understand the relationship between the function to get the highest point and the Y-value; by indicating the highest point of the Y-value as a point, I think the same technique can be used to handle the highest point obtained from the function. The point is that since the highest point is known before drawing, only the same value as the highest point should be drawn on the graph.
def animate(i):
line.set_data(interval[0:i], y[0:i])
if y[i] == max(y):
ax.scatter(interval[i], y[i], s=25, color='r')
return (line,)

How to Animate Text in 3D Scatter Plot?

I would like to animate a 3D scatter plot where each data point has a text label that moves along with it.
Right now the text labels do follow the data points as I want, but they persist through each frame; the text does not disappear from the last animation update. See image below. The data points themselves are moving just fine.
2 Questions whose answers might help me >
Is there some way to clear the text without clearing the data points?
My implementation seems a bit clunky. Is there a hidden function similar to _offset3d that works for text objects.
Here's the graphing function:
def graph(data, update_cnt):
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
dots = ax.scatter(data[0][0][:], data[1][0][:], data[2][0][:])
dot_txt = nmp.ones(nmp.size(data,2), dtype=str)
for n in range(0,nmp.size(data, 2)):
dot_txt[n] = ax.text(data[0][0][n], data[1][0][n], data[2][0][n],'%s'%(n))
ani = animation.FuncAnimation(fig, update, update_cnt, fargs=(dots, data, dot_txt, ax), interval=300)
plt.show()
and the animation update function:
def update(num, dots, data, dot_txt, ax):
y = data[0][num][:]
x = data[1][num][:]
z = data[2][num][:]
dots._offsets3d = (x, y, z)
#dot_txt._something_to_update_text_here()
dot_txt = nmp.ones(nmp.size(data,2), dtype=str)
for n in range(0,nmp.size(data, 2)):
dot_txt[n] = ax.text(data[1][num][n], data[0][num][n], data[2][num][n],'%s'%(n))
and the current plot output:
I found a solution.
I think it's important to note that I could not use the more common solutions for 3D animated scatter plots because I need different marker styles for various points. This forces me to iteratively plot each scatter point, rather than passing a list to the update function. However in doing so, the problem of animating the text is solved nicely.
frame_list contains the x,y,z coordinates and styling for each data point in every frame.
def graph(frame_list):
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
frame_cnt = len(frame_list)
ani = animation.FuncAnimation(fig, update_graph, frame_cnt,
fargs=(frame_list, ax, frame_cnt), interval=600)
plt.show()
The biggest contributor the success of this is the ax.clear() call before every frame.
def update_graph(f, frame_list, ax, cnt):
ax.clear()
f = nmp.mod(f, cnt)
frame = frame_list[f]
for n in range(len(frame.marker)):
x, y, z, s, c, m, name = frame.get_params(n)
ax.scatter(x, y, z, s=s, c=c, marker=m)
ax.text(x, y, z, '%s'%(name))
The get_params function returns all of the relevant data for that frame.

Python: Fill space between two lines drawn on a Basemap

I have drawn two lines on a Basemap created in python. Each line is created with two points (start and end points). Both lines originate from the same point.
m = Basemap(llcrnrlon=119.46,llcrnrlat=21.62,urcrnrlon=121.406,urcrnrlat=23.43, resolution = 'i', epsg=3825)
m.drawcoastlines()
m.plot([x, x1], [y, y1])
m.plot([x, x2], [y, y2])
Resulting in a plot like this:
I would like to shade in the area between these two lines (the larger slice on the lower left). I know it involves some use of fill_between() and/or fill_betweenx(), but I can't figure it out.
More generally:
I have two lines originating from a center point. The lines represent the sweep range of a radar. I want to fill in the area NOT included in this sweep range. This needs to work for any two lines (any sweep range). I can also pull out the beginning and ending azimuths in degrees of the sweep, if we need that.
Thanks for your help.
Here is my solution. Wish it help!
from matplotlib.patches import Polygon
m = Basemap(llcrnrlon=119.46,llcrnrlat=21.62,urcrnrlon=121.406,urcrnrlat=23.43, resolution = 'i', epsg=3825)
m.drawcoastlines()
x,y = (119.46 + 121.406)/2.0,(21.62+23.43)/2.0
x1,y1 = 120.0,24.0
x2,y2 = 124.0,22.0
lons = np.array([x1,x, x2, x2,x1])
lats = np.array([y1, y, y2, y1,y1])
x, y = m( lons, lats )
xy = zip(x,y)
poly = Polygon( xy, facecolor='b', alpha=0.75 ,edgecolor = 'r', zorder =15,linewidth = 2)
plt.gca().add_patch(poly)
m.drawparallels(np.arange(21.0,24.0,0.5),labels=[1,0,0,1],size=12,linewidth=0,color= '#FFFFFF')
m.drawmeridians(np.arange(119.8,121.5,0.5),labels=[1,0,0,1],size=12,linewidth=0)
I solved this by using a wedge patch object and using the azimuths of the radar sweeps.
fig, ax = plt.subplots()
m = Basemap(llcrnrlon=119.46,llcrnrlat=21.62,urcrnrlon=121.406,urcrnrlat=23.43, resolution = 'i', epsg=3825)
m.drawcoastlines()
wedge = Wedge((x,y), 200000, az2, az1, edgecolor="none", color = 'grey', alpha = 0.2)
ax.add_patch(wedge)

How to annotate multiple lines in matplotlib? [duplicate]

Have a plot with several diagonal lines with different slopes. I would like to annotate these lines with a text label that matches the slope of the lines.
Something like this:
Is there a robust way to do this?
I've tried both text's and annotate's rotation parameters, but those are in screen coordinates, not data coordinates (i.e. it's always x degrees on the screen no matter the xy ranges). My x and y ranges differ by orders of magnitude, and obviously the apparent slope is affected by viewport size among other variables, so a fixed-degree rotation doesn't do the trick. Any other ideas?
This is the exact same process and basic code as given by #Adam --- it's just restructured to be (hopefully) a little more convenient.
def label_line(line, label, x, y, color='0.5', size=12):
"""Add a label to a line, at the proper angle.
Arguments
---------
line : matplotlib.lines.Line2D object,
label : str
x : float
x-position to place center of text (in data coordinated
y : float
y-position to place center of text (in data coordinates)
color : str
size : float
"""
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
ax = line.get_axes()
text = ax.annotate(label, xy=(x, y), xytext=(-10, 0),
textcoords='offset points',
size=size, color=color,
horizontalalignment='left',
verticalalignment='bottom')
sp1 = ax.transData.transform_point((x1, y1))
sp2 = ax.transData.transform_point((x2, y2))
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = np.degrees(np.arctan2(rise, run))
text.set_rotation(slope_degrees)
return text
Used like:
import numpy as np
import matplotlib.pyplot as plt
...
fig, axes = plt.subplots()
color = 'blue'
line, = axes.plot(xdata, ydata, '--', color=color)
...
label_line(line, "Some Label", x, y, color=color)
Edit: note that this method still needs to be called after the figure layout is finalized, otherwise things will be altered.
See: https://gist.github.com/lzkelley/0de9e8bf2a4fe96d2018f1b1bd5a0d3c
I came up with something that works for me. Note the grey dashed lines:
The rotation must be set manually, but this must be done AFTER draw() or layout. So my solution is to associate lines with annotations, then iterate through them and do this:
get line's data transform (i.e. goes from data coordinates to display coordinates)
transform two points along the line to display coordinates
find slope of displayed line
set text rotation to match this slope
This isn't perfect, because matplotlib's handling of rotated text is all wrong. It aligns by the bounding box and not by the text baseline.
Some font basics if you're interested about text rendering: http://docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html
This example shows what matplotlib does: http://matplotlib.org/examples/pylab_examples/text_rotation.html
The only way I found to have a label properly next to the line is to align by center in both vertical and horizontal. I then offset the label by 10 points to the left to make it not overlap. Good enough for my application.
Here is my code. I draw the line however I want, then draw the annotation, then bind them with a helper function:
line, = fig.plot(xdata, ydata, '--', color=color)
# x,y appear on the midpoint of the line
t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color)
text_slope_match_line(t, x, y, line)
Then call another helper function after layout but before savefig (For interactive images I think you'll have to register for draw events and call update_text_slopes in the handler)
plt.tight_layout()
update_text_slopes()
The helpers:
rotated_labels = []
def text_slope_match_line(text, x, y, line):
global rotated_labels
# find the slope
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})
def update_text_slopes():
global rotated_labels
for label in rotated_labels:
# slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates
text, line = label["text"], label["line"]
p1, p2 = label["p1"], label["p2"]
# get the line's data transform
ax = line.get_axes()
sp1 = ax.transData.transform_point(p1)
sp2 = ax.transData.transform_point(p2)
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = math.degrees(math.atan(rise/run))
text.set_rotation(slope_degrees)
Even though this question is old, I keep coming across it and get frustrated, that it does not quite work. I reworked it into a class LineAnnotation and helper line_annotate such that it
uses the slope at a specific point x,
works with re-layouting and resizing, and
accepts a relative offset perpendicular to the slope.
x = np.linspace(np.pi, 2*np.pi)
line, = plt.plot(x, np.sin(x))
for x in [3.5, 4.0, 4.5, 5.0, 5.5, 6.0]:
line_annotate(str(x), line, x)
I originally put it into a public gist, but #Adam asked me to include it here.
import numpy as np
from matplotlib.text import Annotation
from matplotlib.transforms import Affine2D
class LineAnnotation(Annotation):
"""A sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
fig, ax = subplots()
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
ax.add_artist(LineAnnotation("text", line, 1.5))
"""
def __init__(
self, text, line, x, xytext=(0, 5), textcoords="offset points", **kwargs
):
"""Annotate the point at *x* of the graph *line* with text *text*.
By default, the text is displayed with the same rotation as the slope of the
graph at a relative position *xytext* above it (perpendicularly above).
An arrow pointing from the text to the annotated point *xy* can
be added by defining *arrowprops*.
Parameters
----------
text : str
The text of the annotation.
line : Line2D
Matplotlib line object to annotate
x : float
The point *x* to annotate. y is calculated from the points on the line.
xytext : (float, float), default: (0, 5)
The position *(x, y)* relative to the point *x* on the *line* to place the
text at. The coordinate system is determined by *textcoords*.
**kwargs
Additional keyword arguments are passed on to `Annotation`.
See also
--------
`Annotation`
`line_annotate`
"""
assert textcoords.startswith(
"offset "
), "*textcoords* must be 'offset points' or 'offset pixels'"
self.line = line
self.xytext = xytext
# Determine points of line immediately to the left and right of x
xs, ys = line.get_data()
def neighbours(x, xs, ys, try_invert=True):
inds, = np.where((xs <= x)[:-1] & (xs > x)[1:])
if len(inds) == 0:
assert try_invert, "line must cross x"
return neighbours(x, xs[::-1], ys[::-1], try_invert=False)
i = inds[0]
return np.asarray([(xs[i], ys[i]), (xs[i+1], ys[i+1])])
self.neighbours = n1, n2 = neighbours(x, xs, ys)
# Calculate y by interpolating neighbouring points
y = n1[1] + ((x - n1[0]) * (n2[1] - n1[1]) / (n2[0] - n1[0]))
kwargs = {
"horizontalalignment": "center",
"rotation_mode": "anchor",
**kwargs,
}
super().__init__(text, (x, y), xytext=xytext, textcoords=textcoords, **kwargs)
def get_rotation(self):
"""Determines angle of the slope of the neighbours in display coordinate system
"""
transData = self.line.get_transform()
dx, dy = np.diff(transData.transform(self.neighbours), axis=0).squeeze()
return np.rad2deg(np.arctan2(dy, dx))
def update_positions(self, renderer):
"""Updates relative position of annotation text
Note
----
Called during annotation `draw` call
"""
xytext = Affine2D().rotate_deg(self.get_rotation()).transform(self.xytext)
self.set_position(xytext)
super().update_positions(renderer)
def line_annotate(text, line, x, *args, **kwargs):
"""Add a sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
line_annotate("sin(x)", line, 1.5)
See also
--------
`LineAnnotation`
`plt.annotate`
"""
ax = line.axes
a = LineAnnotation(text, line, x, *args, **kwargs)
if "clip_on" in kwargs:
a.set_clip_path(ax.patch)
ax.add_artist(a)
return a
New in matplotlib 3.4.0
There is now a built-in parameter transform_rotates_text for rotating text relative to a line:
To rotate text with respect to a line, the correct angle won't be the angle of that line in the plot coordinate system, but the angle that the line appears in the screen coordinate system. This angle can be determined automatically by setting the new parameter transform_rotates_text.
So now we can just pass the raw data angle to plt.text and let matplotlib automatically transform it to the correct visual angle by setting transform_rotates_text=True:
# plot line from (1, 4) to (6, 10)
x = [1, 6]
y = [4, 10]
plt.plot(x, y, 'r.-')
# compute angle in raw data coordinates (no manual transforms)
dy = y[1] - y[0]
dx = x[1] - x[0]
angle = np.rad2deg(np.arctan2(dy, dx))
# annotate with transform_rotates_text to align text and line
plt.text(x[0], y[0], f'rotation={angle:.2f}', ha='left', va='bottom',
transform_rotates_text=True, rotation=angle, rotation_mode='anchor')
This approach is robust against the figure and axes scales. Even if we modify the figsize or xlim after placing the text, the rotation stays properly aligned:
# resizing the figure won't mess up the rotation
plt.gcf().set_size_inches(9, 4)
# rescaling the axes won't mess up the rotation
plt.xlim(0, 12)

Using matplotlib.animate to animate a contour plot in python

I have a 3D array of data (2 spatial dimensions and 1 time dimension) and I'm trying to produce an animated contour plot using matplotlib.animate. I'm using this link as a basis:
http://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/
And here's my attempt:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from numpy import array, zeros, linspace, meshgrid
from boutdata import collect
# First collect data from files
n = collect("n") # This is a routine to collect data
Nx = n.shape[1]
Nz = n.shape[2]
Ny = n.shape[3]
Nt = n.shape[0]
fig = plt.figure()
ax = plt.axes(xlim=(0, 200), ylim=(0, 100))
cont, = ax.contourf([], [], [], 500)
# initialisation function
def init():
cont.set_data([],[],[])
return cont,
# animation function
def animate(i):
x = linspace(0, 200, Nx)
y = linspace(0, 100, Ny)
x,y = meshgrid(x,y)
z = n[i,:,0,:].T
cont.set_data(x,y,z)
return cont,
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=200, interval=20, blit=True)
plt.show()
But when I do this, I get the following error:
Traceback (most recent call last):
File "showdata.py", line 16, in <module>
cont, = ax.contourf([], [], [], 500)
File "/usr/lib/pymodules/python2.7/matplotlib/axes.py", line 7387, in contourf
return mcontour.QuadContourSet(self, *args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1112, in __init__
ContourSet.__init__(self, ax, *args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 703, in __init__
self._process_args(*args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1125, in _process_args
x, y, z = self._contour_args(args, kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1172, in _contour_args
x,y,z = self._check_xyz(args[:3], kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1204, in _check_xyz
raise TypeError("Input z must be a 2D array.")
TypeError: Input z must be a 2D array.
So I've tried replacing all the [] by [[],[]] but this then produces:
Traceback (most recent call last):
File "showdata.py", line 16, in <module>
cont, = ax.contourf([[],[]], [[],[]], [[],[]],500)
File "/usr/lib/pymodules/python2.7/matplotlib/axes.py", line 7387, in contourf
return mcontour.QuadContourSet(self, *args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1112, in __init__
ContourSet.__init__(self, ax, *args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 703, in __init__
self._process_args(*args, **kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1125, in _process_args
x, y, z = self._contour_args(args, kwargs)
File "/usr/lib/pymodules/python2.7/matplotlib/contour.py", line 1177, in _contour_args
self.zmax = ma.maximum(z)
File "/usr/lib/python2.7/dist-packages/numpy/ma/core.py", line 5806, in __call__
return self.reduce(a)
File "/usr/lib/python2.7/dist-packages/numpy/ma/core.py", line 5824, in reduce
t = self.ufunc.reduce(target, **kargs)
ValueError: zero-size array to maximum.reduce without identity
Thanks in advance!
Felix Schneider is correct about the animation becoming very slow. His solution of setting ax.collections = [] removes all old (and superseded) "artist"s. A more surgical approach is to only remove the artists involved in the drawing the contours:
for c in cont.collections:
c.remove()
which is useful in more complicated cases, in lieu of reconstructing the entire figure for each frame. This also works in Rehman Ali's example; instead of clearing the entire figure with clf() the value returned by contourf() is saved and used in the next iteration. Here is an example code similar to Luke's from Jun 7 '13, demonstrating removing the contours only:
import pylab as plt
import numpy
import matplotlib.animation as animation
#plt.rcParams['animation.ffmpeg_path'] = r"C:\some_path\ffmpeg.exe" # if necessary
# Generate data for plotting
Lx = Ly = 3
Nx = Ny = 11
Nt = 20
x = numpy.linspace(0, Lx, Nx)
y = numpy.linspace(0, Ly, Ny)
x,y = numpy.meshgrid(x,y)
z0 = numpy.exp(-(x-Lx/2)**2-(y-Ly/2)**2) # 2 dimensional Gaussian
def some_data(i): # function returns a 2D data array
return z0 * (i/Nt)
fig = plt.figure()
ax = plt.axes(xlim=(0, Lx), ylim=(0, Ly), xlabel='x', ylabel='y')
cvals = numpy.linspace(0,1,Nt+1) # set contour values
cont = plt.contourf(x, y, some_data(0), cvals) # first image on screen
plt.colorbar()
# animation function
def animate(i):
global cont
z = some_data(i)
for c in cont.collections:
c.remove() # removes only the contours, leaves the rest intact
cont = plt.contourf(x, y, z, cvals)
plt.title('t = %i: %.2f' % (i,z[5,5]))
return cont
anim = animation.FuncAnimation(fig, animate, frames=Nt, repeat=False)
anim.save('animation.mp4', writer=animation.FFMpegWriter())
This is what I got to work:
# Generate grid for plotting
x = linspace(0, Lx, Nx)
y = linspace(0, Ly, Ny)
x,y = meshgrid(x,y)
fig = plt.figure()
ax = plt.axes(xlim=(0, Lx), ylim=(0, Ly))
plt.xlabel(r'x')
plt.ylabel(r'y')
# animation function
def animate(i):
z = var[i,:,0,:].T
cont = plt.contourf(x, y, z, 25)
if (tslice == 0):
plt.title(r't = %1.2e' % t[i] )
else:
plt.title(r't = %i' % i)
return cont
anim = animation.FuncAnimation(fig, animate, frames=Nt)
anim.save('animation.mp4')
I found that removing the blit=0 argument in the FuncAnimation call also helped...
This is the line:
cont, = ax.contourf([], [], [], 500)
change to:
x = linspace(0, 200, Nx)
y = linspace(0, 100, Ny)
x, y = meshgrid(x, y)
z = n[i,:,0,:].T
cont, = ax.contourf(x, y, z, 500)
You need to intilize with sized arrays.
Here is another way of doing the same thing if matplotlib.animation don't work for you. If you want to continuously update the colorbar and everything else in the figure, use plt.ion() at the very beginning to enable interactive plotting and use a combo of plt.draw() and plt.clf() to continuously update the plot.
import matplotlib.pyplot as plt
import numpy as np
plt.ion(); plt.figure(1);
for k in range(10):
plt.clf(); plt.subplot(121);
plt.contourf(np.random.randn(10,10)); plt.colorbar();
plt.subplot(122,polar=True)
plt.contourf(np.random.randn(10,10)); plt.colorbar();
plt.draw();
Note that this works with figures containing different subplots and various types of plots (i.e. polar or cartesian)
I used Lukes approach (from Jun 7 '13 at 8:08 ), but added
ax.collections = []
right before
cont = plt.contourf(x, y, z, 25).
Otherwise I experienced that creating the animation will become very slow for large frame numbers.
I have been looking at this a while ago. I my situation I had a few subplots with contours which I wanted to animate. I did not want to use the plt.clf() solution as Rehman ali suggest as I used some special setup of my axis (with pi symbols etc) which would be cleaned as well, so I preferred the 'remove()' approach suggest be Felix. The thing is that only using 'remove' does not clean up memory and will clog your computer eventually, so you need to explicitly delete of the contours by setting it to an empty list as well.
In order to have a generic remove routine which is able to take away contours as well as text, I wrote the routine 'clean_up_artists' which you should use on every time step on all the axis.
This routine cleans up the artists which are passed in a list called 'artist_list' in a given axis 'axis'. This means that for animating multiple subplots, we need to store the lists of artists for each axis which we need to clean every time step.
Below the full code to animate a number of subplots of random data. It is pretty self-explanatory, so hopefully it becomes clear what happens. Anyhow, I just thought to post it, as it combines several ideas I found on stack overflow which I just to come up with this working example.
Anybody with suggestions to improve the code, please shoot-)
import matplotlib.pyplot as plt
from matplotlib import cm
import matplotlib.animation as animation
import string
import numpy as np
def clean_up_artists(axis, artist_list):
"""
try to remove the artists stored in the artist list belonging to the 'axis'.
:param axis: clean artists belonging to these axis
:param artist_list: list of artist to remove
:return: nothing
"""
for artist in artist_list:
try:
# fist attempt: try to remove collection of contours for instance
while artist.collections:
for col in artist.collections:
artist.collections.remove(col)
try:
axis.collections.remove(col)
except ValueError:
pass
artist.collections = []
axis.collections = []
except AttributeError:
pass
# second attempt, try to remove the text
try:
artist.remove()
except (AttributeError, ValueError):
pass
def update_plot(frame_index, data_list, fig, axis, n_cols, n_rows, number_of_contour_levels, v_min, v_max,
changed_artists):
"""
Update the the contour plots of the time step 'frame_index'
:param frame_index: integer required by animation running from 0 to n_frames -1. For initialisation of the plot,
call 'update_plot' with frame_index = -1
:param data_list: list with the 3D data (time x 2D data) per subplot
:param fig: reference to the figure
:param axis: reference to the list of axis with the axes per subplot
:param n_cols: number of subplot in horizontal direction
:param n_rows: number of subplot in vertical direction
:param number_of_contour_levels: number of contour levels
:param v_min: minimum global data value. If None, take the smallest data value in the 2d data set
:param v_max: maximum global data value. If None, take the largest value in the 2d data set
:param changed_artists: list of lists of artists which need to be updated between the time steps
:return: the changed_artists list
"""
nr_subplot = 0 # keep the index of the current subplot (nr_subplot = 0,1, n_cols x n_rows -1)
# loop over the subplots
for j_col in range(n_cols):
for i_row in range(n_rows):
# set a short reference to the current axis
ax = axis[i_row][j_col]
# for the first setup call, add and empty list which can hold the artists belonging to the current axis
if frame_index < 0:
# initialise the changed artist list
changed_artists.append(list())
else:
# for the next calls of update_plot, remove all artists in the list stored in changed_artists[nr_subplot]
clean_up_artists(ax, changed_artists[nr_subplot])
# get a reference to 2d data of the current time and subplot
data_2d = data_list[nr_subplot][frame_index]
# manually set the levels for better contour range control
if v_min is None:
data_min = np.nanmin(data_2d)
else:
data_min = v_min
if v_max is None:
data_max = np.nanmax(data_2d)
else:
data_max = v_max
# set the contour levels belonging to this subplot
levels = np.linspace(data_min, data_max, number_of_contour_levels + 1, endpoint=True)
# create the contour plot
cs = ax.contourf(data_2d, levels=levels, cmap=cm.rainbow, zorder=0)
cs.cmap.set_under("k")
cs.cmap.set_over("k")
cs.set_clim(v_min, v_max)
# store the contours artists to the list of artists belonging to the current axis
changed_artists[nr_subplot].append(cs)
# set some grid lines on top of the contours
ax.xaxis.grid(True, zorder=0, color="black", linewidth=0.5, linestyle='--')
ax.yaxis.grid(True, zorder=0, color="black", linewidth=0.5, linestyle='--')
# set the x and y label on the bottom row and left column respectively
if i_row == n_rows - 1:
ax.set_xlabel(r"Index i ")
if j_col == 0:
ax.set_ylabel(r"Index j")
# set the changing time counter in the top left subplot
if i_row == 0 and j_col == 1:
# set a label to show the current time
time_text = ax.text(0.6, 1.15, "{}".format("Time index : {:4d}".format(frame_index)),
transform=ax.transAxes, fontdict=dict(color="black", size=14))
# store the artist of this label in the changed artist list
changed_artists[nr_subplot].append(time_text)
# for the initialisation call only, set of a contour bar
if frame_index < 0:
# the first time we add this (make sure to pass -1 for the frame_index
cbar = fig.colorbar(cs, ax=ax)
cbar.ax.set_ylabel("Random number {}".format(nr_subplot))
ax.text(0.0, 1.02, "{}) {}".format(string.ascii_lowercase[nr_subplot],
"Random noise {}/{}".format(i_row, j_col)),
transform=ax.transAxes, fontdict=dict(color="blue", size=12))
nr_subplot += 1
return changed_artists
def main():
n_pixels_x = 50
n_pixels_y = 30
number_of_time_steps = 100
number_of_contour_levels = 10
delay_of_frames = 1000
n_rows = 3 # number of subplot rows
n_cols = 2 # number of subplot columns
min_data_value = 0.0
max_data_value = 1.0
# list containing the random plot per sub plot. Insert you own data here
data_list = list()
for j_col in range(n_cols):
for i_row in range(n_rows):
data_list.append(np.random.random_sample((number_of_time_steps, n_pixels_x, n_pixels_y)))
# set up the figure with the axis
fig, axis = plt.subplots(nrows=n_rows, ncols=n_cols, sharex=True, sharey=True, figsize=(12,8))
fig.subplots_adjust(wspace=0.05, left=0.08, right=0.98)
# a list used to store the reference to the axis of each subplot with a list of artists which belong to this subplot
# this list will be returned and will be updated every time plot which new artists
changed_artists = list()
# create first image by calling update_plot with frame_index = -1
changed_artists = update_plot(-1, data_list, fig, axis, n_cols, n_rows, number_of_contour_levels,
min_data_value, max_data_value, changed_artists)
# call the animation function. The fargs argument equals the parameter list of update_plot, except the
# 'frame_index' parameter.
ani = animation.FuncAnimation(fig, update_plot, frames=number_of_time_steps,
fargs=(data_list, fig, axis, n_cols, n_rows, number_of_contour_levels, min_data_value,
max_data_value, changed_artists),
interval=delay_of_frames, blit=False, repeat=True)
plt.show()
if __name__ == "__main__":
main()
Removing the blit=0 or blit = True argument in the FuncAnimation call also helped
is important!!!

Categories

Resources