How to plot an histogram or bar animation with matplotlib ArtistAnimation? - python

I'm trying to create histogram animation that stacks up samples one by one.
I thought the following code would work, but it doesn't.
import matplotlib.pyplot as plt
from matplotlib.animation import ArtistAnimation
ims = []
fig = plt.figure()
x =[1,2,3,3,4,5,5,5,5,6,7,8,9,9,9,9,9,10]
for i in range(len(x)):
img = plt.hist(x[:i])
ims.append(img)
ani = ArtistAnimation(fig, ims, interval=100)
plt.show()
Changing plt.hist to plt.plot shows animation. Don't know what the difference is.
Thanks for reading.

plt.hist does not return an artist but a tuple with binning results and the artist (see here).
This is also discussed in this thread in the Matplotlib mailing list:
ArtistAnimation expects to be given a list of lists (or tuples), where
the inner collection contains all of the artists that should be
rendered for a given frame. In the case of bar, it returns a
BarCollection object (which I just learned), is a subclass of tuple.
This explains why it works (by itself), when directly appended to the
list given to ArtistAnimation; the BarCollection acts as the
collection of artists that ArtistAnimation is expecting. In the case
of the second example, ArtistAnimation is being given a
list([BarCollection, Image]); because BarCollection isn't actually an
Artist, it causes the problem.
The problem mentioned there uses different plot types:
im1 = ax1.imshow(f(xm, ym), aspect='equal', animated=True)
im2 = ax2.bar(xx, a, width=0.9*(b[1]-b[0]), color='C1')
and the solution in that case is
ims.append([im1] + list(im2))
The way to make this work in your case is to look at the return values of plt.hist and find out where the artists are being returned and put them in the correct list of lists format.
This works:
import matplotlib.animation as animation
fig = plt.figure()
# ims is a list of lists, each row is a list of artists to draw in the
# current frame; here we are just animating one artist, the image, in
# each frame
ims = []
for i in range(60):
# hist returns a tuple with two binning results and then the artist (patches)
n, bins, patches = plt.hist((np.random.rand(10),))
# since patches is already a list we can just append it
ims.append(patches)
ani = animation.ArtistAnimation(fig, ims, interval=50, blit=True,
repeat_delay=1000,)
plt.show()

Related

Matplotlib function animation not animating for pre-calculated data

I am working on some animated scatter plots in python with matplotlib. I currently have this code:
def calulateStep():
# Math stuff ....
# Changes values in in 'circpos' Nx2 array
fig, ax = plt.subplots(figsize=(5, 5))
ax.set(xlim=(-WELLRADIUS,WELLRADIUS), ylim=(-WELLRADIUS,WELLRADIUS))
[x,y] = np.hsplit(circpos,2)
scat = ax.scatter(x.flatten(),y.flatten())
def animate(i):
calculateStep()
scat.set_offsets(circpos)
return scat,
anim = FuncAnimation(fig, animate, frames=60)
anim.save('test2.gif',writer='imagemagick')
plt.draw()
plt.show()
The function calculateStep calculates new x,y values for the scatter. circpos contains the data array at each step. this works well and produces an animated gif ofthe scatter plot as expected. However the function is a rather slow numerical calculation and many many steps are required to produce stable output, so I would rather calculate all before and then animate only select frames. So I tried this.
results = [circpos]
for h in range(61):
calculateStep()
results.append(circpos)
fig, ax = plt.subplots(figsize=(5, 5))
ax.set(xlim=(-WELLRADIUS,WELLRADIUS), ylim=(-WELLRADIUS,WELLRADIUS))
[x,y] = np.hsplit(results[0],2)
scat = ax.scatter(x.flatten(),y.flatten())
def animate(i):
scat.set_offsets(results.pop(0))
return scat,
anim = FuncAnimation(fig, animate, frames=60)
anim.save('test2.gif',writer='imagemagick')
plt.draw()
plt.show()
However with this method the generated gif contains only the final frame of the animation. If I print the data from within the animate function I find that the correct numerical values are being popped from the results list but for some reason only the final value is there in the gif. I have also tried using results[i] rather than results.pop(0) I am at a loss to understand this behavior.
Well it seems I solved my own problem. When I add each iteration of the global array circpos to the results list, it is of course a shallow copy. Meaning it's just a reference to the original circpos array. So I end up with a list full of references to the same object. The print out was just me misinterpreting what I was looking at.
Instead I now add circpos.copy() to my list to get new copies of the array at each step.
This has tripped me up in Python before I realize. Still learning!

Returning matplotlib plot/figure from a function and saving it later

As the title states I want to return a plt or figure (still not sure what the difference between the two things are) using matplotlib. The main idea behind it is so I can save the plt/figure later.
import seaborn as sns
from matplotlib import pyplot as plt
def graph(df, id):
# size of the graph
xlims = (-180, 180)
ylims = (-180, 180)
# dictate the colors of the scatter plot based on the grouping of hot or cold
color_dict = {'COLD': 'blue',
'HOT': 'red'}
title_name = f"{id}"
ax = sns.scatterplot(data=df, hue='GRP', x='X_GRID', y='Y_GRID',
legend=False, palette=color_dict)
ax.set_title(title_name)
ax.set(xlim=xlims)
ax.set(ylim=ylims)
if show_grid:
# pass in the prev graph so I can overlay grid
ax = self.__get_grid( ax)
circle1 = plt.Circle(xy=(0, 0), radius=150, color='black', fill=False, zorder=3)
ax.add_patch(circle1)
ax.set_aspect('equal')
plt.axis('off')
plt.savefig(title_name + '_in_ftn.png')
fig = plt.figure()
plt.clf()
return (fig, title_name + '.png')
plots = []
# dfs is just a tuple of df, id for example purposes
for df, id in dfs:
plots.append(graph(df, id))
for plot, file_name in plots:
plot.savefig(file_name)
plot.clf()
When using plot.savefig(filename) it saves, but the saved file is blank which is wrong. Am I not properly returning the object I want to save? If not what should I return to be able to save it?
I kind of having it work, but not really. I am currently saving two figures for testing purposes. For some reason when I use the fig=plt.figure() and saving it outside the function the title of the figure and the filename are different (even though they should be the same since the only difference is .png)
However, when saving it inside the function the title name of the figure and the filename name are the same.
You code has multiple issues that I'll try to discuss here:
Your confusion around plt
First of all, there is no such thing as "a plt". plt is the custom name you are giving to the matplotlib.pyplot module when you are importing it with the line import matplotlib.pyplot as plt. You are basically just renaming the module with an easy to type abbreviation. If you had just written import matplotlib, you would have to write matplotlib.pyplot.axis('off') instead of plt.axis('off').
Mix of procedural and object oriented approach
You are using a mix of the procedural and object oriented approach for matplotlib.
Either you call your methods on the axis object (ax) or you can call functions that implicitly handle the axis and figure. For example you could either create and axis and then call ax.plot(...) or instead use plt.plot(...), which implicitly creates the figure and axis. In your case, you mainly use the object oriented approach on the axis object that is returned by the seaborn function. However, you should use ax.axis('off') instead of plt.axis('off').
You create a new blank figure
When you are calling the seaborn function sns.scatterplot, you are implicitly creating a matplotlib figure and axis object. You catch that axis object in the variable ax. You then use plt.savefig to save your image in the function, which works by implicitly getting the figure corresponding to the currently used axis object. However, you are then creating a new figure by calling fig = plt.figure(), which is of course blank, and then returning it. What you should do, is getting the figure currently used by the axis object you are working with. You can get it by calling fig = plt.gcf() (which stands for "get current figure") and would be the procedural approach, or better use fig = ax.get_figure()
What you should do instead is something like this:
import seaborn as sns
from matplotlib import pyplot as plt
def graph(df, id):
# size of the graph
xlims = (-180, 180)
ylims = (-180, 180)
# dictate the colors of the scatter plot based on the grouping of hot or cold
color_dict = {'COLD': 'blue',
'HOT': 'red'}
title_name = f"{id}"
ax = sns.scatterplot(data=df, hue='GRP', x='X_GRID', y='Y_GRID',
legend=False, palette=color_dict)
ax.set_title(title_name)
ax.set(xlim=xlims)
ax.set(ylim=ylims)
if show_grid:
# pass in the prev graph so I can overlay grid
ax = self.__get_grid( ax)
circle1 = plt.Circle(xy=(0, 0), radius=150, color='black', fill=False, zorder=3)
ax.add_patch(circle1)
ax.set_aspect('equal')
ax.axis('off')
fig = ax.get_figure()
fig.savefig(title_name + '_in_ftn.png')
return (fig, title_name + '.png')

Retrieve all colorbars in figure

I'm trying to manipulate all colorbar instances contained in a figure. There is fig.get_axes() to obtain a list of axes, but I cannot find anything similar for colorbars.
This answer, https://stackoverflow.com/a/19817573/7042795, only applies to special situations, but not the general case.
Consider this MWE:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random((10,10)) # Generate some random data to plot
fig, axs = plt.subplots(1,2)
im1 = axs[0].imshow(data)
cbar1 = fig.colorbar(im1)
im2 = axs[1].imshow(2*data)
cbar2 = fig.colorbar(im2)
fig.show()
How can I get cbar1 and cbar2 from fig?
What I need is a function like:
def get_colorbars(fig):
cbars = fig.get_colorbars()
return cbars
cbars = get_colorbars(fig)
You would have no choice but to check each object present in the figure whether it has a colorbar or not. This could look as follows:
def get_colorbars(fig):
cbs = []
for ax in fig.axes:
cbs.extend(ax.findobj(lambda obj: hasattr(obj, "colorbar") and obj.colorbar))
return [a.colorbar for a in cbs]
This will give you all the colorbars that are tied to an artist. There may be more colorbars in the figure though, e.g. created directly from a ScalarMappble or multiple colorbars for the same object; those cannot be found.
Since the only place I'm reasonably sure that colorbar references are retained is as an attribute of the artist they are tied to, the best solution I could think of is to search all artists in a figure. This is best done recursively:
def get_colorbars(fig):
def check_kids(obj, bars):
for child in obj.get_children():
if isinstance(getattr(child, 'colorbar', None), Colorbar):
bars.append(child.colorbar)
check_kids(child, bars)
return bars
return check_kids(fig, [])
I have not had a chance to test this code, but it should at least point you in the right direction.

Can I pass a list of colors for points to matplotlib's 'Axes.plot()'?

I've got a lot of points to plot and am noticing that plotting them individually in matplotlib takes much longer (more than 100 times longer, according to cProfile) than plotting them all at once.
However, I need to color code the points (based on data associated with each one) and can't figure out how to plot more than one color for a given call to Axes.plot(). For example, I can get a result similar to the one I want with something like
fig, ax = matplotlib.pyplot.subplots()
rands = numpy.random.random_sample((10000,))
for x in range(10000):
ax.plot(x, rands[x], 'o', color=str(rands[x]))
matplotlib.pyplot.show()
but would rather do something much faster like
fig, ax = matplotlib.pyplot.subplots()
rands = numpy.random.random_sample((10000,))
# List of colors doesn't work
ax.plot(range(10000), rands, 'o', color=[str(y) for y in rands])
matplotlib.pyplot.show()
but providing a list as the value for color doesn't work in this way.
Is there a way to provide a list of colors (and for that matter, edge colors, face colors , shapes, z-order, etc.) to Axes.plot() so that each point can potentially be customized, but all points can be plotted at once?
Using Axes.scatter() seems to get part way there, since it allows for individual setting of point color; but color is as far as that seems to go. (Axes.scatter() also lays out the figure completely differently.)
It is about 5 times faster for me to create the objects (patches) directly. To illustrate the example, I have changed the limits (which have to be set manually with this method). The circle themselves are draw with matplotlib.path.Path.circle. Minimal working example:
import numpy as np
import pylab as plt
from matplotlib.patches import Circle
from matplotlib.collections import PatchCollection
fig, ax = plt.subplots(figsize=(10,10))
rands = np.random.random_sample((N,))
patches = []
colors = []
for x in range(N):
C = Circle((x/float(N), rands[x]), .01)
colors.append([rands[x],rands[x],rands[x]])
patches.append(C)
plt.axis('equal')
ax.set_xlim(0,1)
ax.set_ylim(0,1)
collection = PatchCollection(patches)
collection.set_facecolor(colors)
ax.add_collection(collection)
plt.show()

Matplotlib animate fill_between shape

I am trying to animate a fill_between shape inside matplotlib and I don't know how to update the data of the PolyCollection. Take this simple example: I have two lines and I am always filling between them. Of course, the lines change and are animated.
Here is a dummy example:
import matplotlib.pyplot as plt
# Init plot:
f_dummy = plt.figure(num=None, figsize=(6, 6));
axes_dummy = f_dummy.add_subplot(111);
# Plotting:
line1, = axes_dummy.plot(X, line1_data, color = 'k', linestyle = '--', linewidth=2.0, animated=True);
line2, = axes_dummy.plot(X, line2_data, color = 'Grey', linestyle = '--', linewidth=2.0, animated=True);
fill_lines = axes_dummy.fill_between(X, line1_data, line2_data, color = '0.2', alpha = 0.5, animated=True);
f_dummy.show();
f_dummy.canvas.draw();
dummy_background = f_dummy.canvas.copy_from_bbox(axes_dummy.bbox);
# [...]
# Update plot data:
def update_data():
line1_data = # Do something with data
line2_data = # Do something with data
f_dummy.canvas.restore_region( dummy_background );
line1.set_ydata(line1_data);
line2.set_ydata(line2_data);
# Update fill data too
axes_dummy.draw_artist(line1);
axes_dummy.draw_artist(line2);
# Draw fill too
f_dummy.canvas.blit( axes_dummy.bbox );
The question is how to update the fill_between Poly data based on line1_data and line2_data each time update_data() is called and draw them before blit ("# Update fill data too" & "# Draw fill too"). I tried fill_lines.set_verts() without success and could not find an example.
Ok, as someone pointed out, we are dealing with a collection here, so we will have to delete and redraw. So somewhere in the update_data function, delete all collections associated with it:
axes_dummy.collections.clear()
and draw the new "fill_between" PolyCollection:
axes_dummy.fill_between(x, y-sigma, y+sigma, facecolor='yellow', alpha=0.5)
A similar trick is required to overlay an unfilled contour plot on top of a filled one, since an unfilled contour plot is a Collection as well (of lines I suppose?).
this is not my answer, but I found it most useful:
http://matplotlib.1069221.n5.nabble.com/animation-of-a-fill-between-region-td42814.html
Hi Mauricio,
Patch objects are a bit more difficult to work with than line objects, because unlike line objects are a step removed from the input data supplied by the user. There is an example similar to what you want to do here: http://matplotlib.org/examples/animation/histogram.html
Basically, you need to modify the vertices of the path at each frame. It might look something like this:
from matplotlib import animation
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set_xlim([0,10000])
x = np.linspace(6000.,7000., 5)
y = np.ones_like(x)
collection = plt.fill_between(x, y)
def animate(i):
path = collection.get_paths()[0]
path.vertices[:, 1] *= 0.9
animation.FuncAnimation(fig, animate,
frames=25, interval=30)
Take a look at path.vertices to see how they're laid out.
Hope that helps,
Jake
If you don't want to use anitmation, or to remove everything from your figure to update only filling, you could use this way :
call fill_lines.remove() and then call again axes_dummy.fill_between() to draw new ones. It worked in my case.
initialize pyplot interactive mode
import matplotlib.pyplot as plt
plt.ion()
use the optional label argument when plotting the fill:
plt.fill_between(
x,
y1,
y2,
color="yellow",
label="cone"
)
plt.pause(0.001) # refresh the animation
later in our script we can select by label to delete that specific fill or a list of fills, thus animating on a object by object basis.
axis = plt.gca()
fills = ["cone", "sideways", "market"]
for collection in axis.collections:
if str(collection.get_label()) in fills:
collection.remove()
del collection
plt.pause(0.001)
you can use the same label for groups of objects you would like to delete; or otherwise encode the labels with tags as needed to suit needs
for example if we had fills labelled:
"cone1" "cone2" "sideways1"
if "cone" in str(collection.get_label()):
would sort to delete both those prefixed with "cone".
You can also animate lines in the same manner
for line in axis.lines:
another idiom which will work is too keep a list of your plotted objects; this method seems to work with any type of plotted object.
# plot interactive mode on
plt.ion()
# create a dict to store "fills"
# perhaps some other subclass of plots
# "yellow lines" etc.
plots = {"fills":[]}
# begin the animation
while 1:
# cycle through previously plotted objects
# attempt to kill them; else remember they exist
fills = []
for fill in plots["fills"]:
try:
# remove and destroy reference
fill.remove()
del fill
except:
# and if not try again next time
fills.append(fill)
pass
plots["fills"] = fills
# transformation of data for next frame
x, y1, y2 = your_function(x, y1, y2)
# fill between plot is appended to stored fills list
plots["fills"].append(
plt.fill_between(
x,
y1,
y2,
color="red",
)
)
# frame rate
plt.pause(1)
In contrast to what most answers here stated, it is not necessary to remove and redraw a PolyCollection returned by fill_between each time you want to update its data. Instead, you can modify the vertices and codes attribute of the underlying Path object. Let's assume you've created a PolyCollection via
import numpy as np
import matplotlib.pyplot as plt
#dummy data
x = np.arange(10)
y0 = x-1
y1 = x+1
fig = plt.figure()
ax = fig.add_subplot()
p = ax.fill_between(x,y0,y1)
and now you want to update p with new data xnew, y0new and y1new. Then what you could do is
v_x = np.hstack([xnew[0],xnew,xnew[-1],xnew[::-1],xnew[0]])
v_y = np.hstack([y1new[0],y0new,y0new[-1],y1new[::-1],y1new[0]])
vertices = np.vstack([v_x,v_y]).T
codes = np.array([1]+(2*len(xnew)+1)*[2]+[79]).astype('uint8')
path = p.get_paths()[0]
path.vertices = vertices
path.codes = codes
Explanation: path.vertices contains the vertices of the patch drawn by fill_between including additional start and end positions, path.codes contains instructions on how to use them (1=MOVE POINTER TO, 2=DRAW LINE TO, 79=CLOSE POLY).

Categories