EDIT: ImportanceOfBeingErnest provided the answer, however I am still inviting you all to explain, why is savefig logic different from animation logic.
I want to make a video in matplotlib. I went through manuals and examples and I just don't get it. (regarding matplotlib, I always copy examples, because after five years of python and two years of mathplotlib I still understand 0.0% of matplotlib syntax)
After half a dozen hours here is what I came up to. Well, I get empty video. No idea why.
import os
import math
import matplotlib
matplotlib.use("Agg")
from matplotlib import pyplot as plt
import matplotlib.animation as animation
# Set up formatting for the movie files
Writer = animation.writers['ffmpeg']
writer = Writer(fps=15, metadata=dict(artist='Me'), bitrate=1800)
numb=100
temp=[0.0]*numb
cont=[0.0]*numb
for i in range(int(4*numb/10),int(6*numb/10)):
temp[i]=2
cont[i]=2
fig = plt.figure()
plts=fig.add_subplot(1,1,1)
plts.set_ylim([0,2.1])
plts.get_xaxis().set_visible(False)
plts.get_yaxis().set_visible(False)
ims = []
for i in range(1,10):
line1, = plts.plot(range(0,numb),temp, linewidth=1, color='black')
line2, = plts.plot(range(0,numb),cont, linewidth=1, color='red')
# savefig is here for testing, works perfectly!
# fig.savefig('test'+str(i)+'.png', bbox_inches='tight', dpi=300)
ims.append([line1,line2])
plts.lines.remove(line1)
plts.lines.remove(line2)
for j in range(1,10):
tempa=0
for k in range (1,numb-1):
tempb=temp[k]+0.51*(temp[k-1]-2*temp[k]+temp[k+1])
temp[k-1]=tempa
tempa=tempb
temp[numb-1]=0
for j in range(1,20):
conta=0
for k in range (1,numb-1):
contb=cont[k]+0.255*(cont[k-1]-2*cont[k]+cont[k+1])
cont[k-1]=conta
conta=contb
cont[numb-1]=0
im_ani = animation.ArtistAnimation(fig, ims, interval=50, repeat_delay=3000,blit=True)
im_ani.save('im.mp4', writer=writer)
Can someone help me with this?
If you want to have a plot which is not empty, the main idea would be not to remove the lines from the plot.
That is, delete the two lines
plts.lines.remove(line1)
plts.lines.remove(line2)
If you delete these two lines the output will look something like this
[Link to orginial size animation]
Now one might ask, why do I not need to remove the artist in each iteration step, as otherwise all of the lines would populate the canvas at once?
The answer is that the ArtistAnimation takes care of this. It will only show those artists in the supplied list that correspond to the given time step. So while at the end of the for loop you end up with all the lines drawn to the canvas, once the animation starts they will all be removed and only one set of artists is shown at a time.
In such a case it is of course not a good idea to use the loop for saving the individual images as the final image would contain all of the drawn line at once,
The solution is then either to make two runs of the script, one for the animation, and one where the lines are removes in each timestep. Or, maybe better, use the animation istself to create the images.
im_ani.save('im.png', writer="imagemagick")
will create the images as im-<nr>.png in the current folder. It will require to have imagemagick installed.
I'm trying here to answer the two questions from the comments:
1. I have appended line1 and line2 before deleting them. Still they disappeared in the final result. How come?
You have appended the lines to a list. After that you removed the lines from the axes. Now the lines are in the list but not part of the axes. When doing the animation, matplotlib finds the lines in the list and makes them visible. But they are not in the axes (because they have been removed) so the visibility of some Line2D object, which does not live in any axes but only somewhere in memory, is changed. But that isn't reflected in the plot because the plot doesn't know this line any more.
2. If I understand right, when you issue line1, = plts.plot... command then the line1 plot object is added to the plts graph object. However, if you change the line1 plot object by issuing line1, = plts.plot... command again, matplotlib does change line1 object but before that saves the old line1 to the plts graph object permanently. Is this what caused my problem?
No. The first step is correct, line1, = plts.plot(..) adds a Line2D object to the axes. However, in a later loop step line1, = plts.plot() creates another Line2D object and puts it to the canvas. The initial Line2D object is not changed and it doesn't know that there is now some other line next to it in the plot. Therefore, if you don't remove the lines they will all be visible in the static plot at the end.
Related
I have a list of data frames, and I want to make heatmaps of every data frame in the list. The first heatmap comes out perfectly, but the second one has two colorbars, one much larger than the other, which distorts the figure. The third has THREE colorbars, the last one being even larger, and this continues for as many heatmaps as I make.
This seems like a bug to me, as I have no idea why it's happening. Each heatmap should be stored as a separate element in the list of heatmaps, and even if I plot them individually, instead of using a loop or list comprehension, I get the same problem.
Here is my code:
# Set the seaborn font size.
sns.set(font_scale=0.5)
# Ensure that labels are not cut off.
plt.gcf().subplots_adjust(bottom=0.18)
plt.gcf().subplots_adjust(right=.3)
black_yellow = sns.dark_palette("yellow",10)
heatmap_list = [sns.heatmap(df, cmap=black_yellow, xticklabels=True, yticklabels=True) for df in df_list]
[heatmap_list[x].figure.savefig(file_names_list[x]+'.pdf', format='pdf') for x in range(0,len(heatmap_list))]
sns.heatmap() creates a problem while we are working in loop. To resolve this issue, the first iteration will be done individually and rest of the loop remains the same but we will add a parameter cbar=False to stop this recursion of colorbar in the loop portion.
# Set the seaborn font size.
sns.set(font_scale=0.5)
# Ensure that labels are not cut off.
plt.gcf().subplots_adjust(bottom=0.18)
plt.gcf().subplots_adjust(right=.3)
black_yellow = sns.dark_palette("yellow", 10)
hm = sns.heatmap(df_list[0], cmap=black_yellow, xticklabels=True, yticklabels=True)
hm.figure.savefig(file_names_list[0]+'.pdf', format='pdf')
heatmap_list = [sns.heatmap(df_list[i], cmap=black_yellow, xticklabels=True, yticklabels=True, cbar=False) for i in range(1, len(df_list))]
[heatmap_list[x].figure.savefig(file_names_list[x+1]+'.pdf', format='pdf') for x in range(0, len(heatmap_list))]
I've developed an gui with python pyqt. There I have a matplotlib figure with x,y-Data and vlines that needs to change dynamically with a QSlider.
Right now I change the data just with deleting everything and plot again but this is not effective
This is how I do it:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
if PlotNr == 2:
self.axes.cla()
self.axes.plot(xData, Data, color='b', linewidth=2)
self.axes.vlines(valueSlider1,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.vlines(valueSlider2,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.text(1,0.8*max(Data),str(np.round(width_wg,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.6*max(Data),"Pos1: "+str(round(valueSlider1,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.4*max(Data),"Pos2: "+str(round(valueSlider2,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.grid(True)
self.draw()
"vlines" are LineCollections in matplotlib. I searched in the documentation but could not find any hint to a function like 'set_xdata' How can I change the x value of vertical lines when they are already drawn and embedded into FigureCanvas?
I have the same problem with changing the x and y data. When trying the known functions of matplotlib like 'set_data', I get an error that AxisSubPlot does not have this attribute.
In the following is my code for the FigureCanvas Class. The def update_verticalLines should only contain commands for changing the x coord of the vlines and not complete redraw.
Edit: solution
Thanks #Craigular Joe
This was not exactly how it worked for me. I needed to change something:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
self.vLine1.remove()
self.vLine1 = self.axes.vlines(valueSlider1,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.vLine2.remove()
self.vLine2 = self.axes.vlines(valueSlider2,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.axes.draw_artist(self.vLine1)
self.axes.draw_artist(self.vLine2)
#self.update()
#self.flush_events()
self.draw()
update() did not work without draw(). (The old vlines stayed)
flush_events() did some crazy stuff. I have two instances of FigureCanvas. flush_events() caused that within the second instance call the vlines moved with the slider but moved then back to the start position.
When you create the vlines, save a reference to them, e.g.
self.my_vlines = self.axes.vlines(...)
so that when you want to change them, you can just remove and replace them, e.g.
self.my_vlines.remove()
self.my_vlines = self.axes.vlines(...)
# Redraw vline
self.axes.draw_artist(self.my_vlines)
# Add newly-rendered lines to drawing backend
self.update()
# Flush GUI events for figure
self.flush_events()
By the way, in the future you should try your best to pare down your code sample to just the essential parts. Having a lot of unnecessary sample code makes it hard to understand your question. :)
From all example code/demos I have seen in the VisPy library, I only see one way that people plot many lines, for example:
for i in range(N):
pos = pos.copy()
pos[:, 1] = np.random.normal(scale=5, loc=(i+1)*30, size=N)
line = scene.visuals.Line(pos=pos, color=color, parent=canvas.scene)
lines.append(line)
canvas.show()
My issue is that I have many lines to plot (each several hundred thousand points). Matplotlib proved too slow because of the total number of points plotted was in the millions, hence I switched to VisPy. But VisPy is even slower when you plot thousands of lines each with thousands of points (the speed-up comes when you have millions of points).
The root cause is in the way lines are drawn. When you create a plot widget and then plot a line, each line is rendered to the canvas. In matplotlib you can explicitly state to not show the canvas until all lines are drawn in memory, but there doesn't appear to be the same functionality in VisPy, making it useless.
Is there any way around this? I need to plot multiple lines so that I can change properties interactively, so flattening all the data points into one plot call won't work.
(I am using a PyQt4 to embed the plot in a GUI. I have also considered pyqtgraph.)
You should pass an array to the "connect" parameter of the Line() function.
xy = np.random.rand(5,2) # 2D positions
# Create an array of point connections :
toconnect = np.array([[0,1], [0,2], [1,4], [2,3], [2,4]])
# Point 0 in your xy will be connected with 1 and 2, point
# 1 with 4 and point 2 with 3 and 4.
line = scene.visuals.Line(pos=xy, connect=toconnect)
You only add one object to your canvas but the control pear line is more limited.
I have several lines, bars and artists in one figure/axes. I would like to add different sets of components onto the original figure/axes. Is that possible to duplicate all the objects of the original figure/axes into another figure/axes without redraw everything by code?
One way will be remove all the new added components before drawing another set of components. However, if I want to put several axes into one figure, this will not work. Some discussion has been done here. But it copy/add all objects one by one, which is not better than redraw everything when there are many objects.
#Greg, thanks a lot for replying. If I just plot out data, it will be simple, just replot the data, or even copy some lines. However, this figures contain lots of artists, which can also be added by user through GUI interface, or bot by on-the-fly script, their types I might not known at runtime. And the plot are generated on-the-fly. Of course, I can try to copy all the data, record all the artists type, properties and replot them again. But it just too much and involved in modifying the software which generates those figures. Maybe I can loop through all possible objects do copy and add_xxx. But I hoope there will be a better way.
Thanks to #Joe Kington and his post: "add an axes instance to another figure".
I wrapped up a way to duplicate the axes and insert the axes into a subplot:
def test_pickleAxes():
import pickle
import numpy as npy
x = npy.arange(0,4*npy.pi,0.2)
y = npy.sin(x)
fig, ax = plt.subplots()
p = pickle.dumps(ax)
ax2 = pickle.loads(p)
ax.change_geometry(2,1,1)
ax2.change_geometry(2,1,2)
fig._axstack.add(fig._make_key(ax2), ax2)
plt.show()
However, in most cases it is seems no better than blit so far. Why is that? Because the pickle of the axes is actually the pickle of the entire figure. When unpickle it, it will create a new figure and the loaded axes instance will associate with it. Even we managed to add the axes into the old figure. ax2 are still ONLY associate with the new figure. Thus when we try to interact with the old figure, the ax2 will not interact. Instead, if we scale/pan the new figure, ax2 in both figures will change. If we just save svg or pdf file, it seems a quite OK solution.
Still try to find a way to decouple the ax2 from new figure and make it couple with the old one.
I am trying to add a BrokenBarHCollection to multiple axis on my figure, as follows:
barcollection = collections.BrokenBarHCollection(...
ax1 = plt.subplot(211)
ax1.add_collection(barcollection)
ax2 = plt.subplot(212)
ax2.add_collection(barcollection)
plt.show()
As is, the figure only shows the collection in the second subplot. If I comment the ax2.add line out, it shows the collection only in the first subplot. Declaring the barcollection again between lines 3 and 4 makes it show up in both subplots. Why is this happening?
This is because the matplotlib objects know what plot they are attached to and will not attach to more than one.
If you use the copy module to make a shallow copy, then you can re-use most of the data structure across multiple axes.
import copy
bc2 = copy.copy(barcollection)
ax2.add_collection(bc2)
There was another question about this recently, but I am having trouble finding it.