matplotlib duplicate axes or figure? - matplotlib

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.

Related

Matplotlib video creation

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.

Accessing backend specific functionality with Julia Plots

Plots is simple and powerful but sometimes I would like to have a little bit more control over individual elements of the plot to fine-tune its appearance.
Is it possible to update the plot object of the backend directly?
E.g., for the default pyplot backend, I tried
using Plots
p = plot(sin)
p.o[:axes][1][:xaxis][:set_ticks_position]("top")
but the plot does not change. Calling p.o[:show]() afterwards does not help, either.
In other words: Is there a way to use the PyPlot interface for a plot that was initially created with Plots?
Edit:
The changes to the PyPlot object become visible (also in the gui) when saving the figure:
using Plots
using PyPlot
p = Plots.plot(sin, top_margin=1cm)
gui() # not needed when using the REPL
gca()[:xaxis][:set_ticks_position]("top")
PyPlot.savefig("test.png")
Here, I used p.o[:axes][1] == gca(). One has to set top_margin=1cm because the plot area is not adjusted automatically (for my actual fine-tuning, this doesn't matter).
This also works for subsequent updates as long as only the PyPlot interface is used. E.g., after the following commands, the plot will have a red right border in addition to labels at the top:
gca()[:spines]["right"][:set_color]("red")
PyPlot.savefig("test.png")
However, when a Plots command like plot!(xlabel="foo") is used, all previous changes made with PyPlot are overwritten (which is not suprising).
The remaining question is how to update the gui interactively without having to call PyPlot.savefig explicitly.
No - the plot is a Plots object, not a PyPlot object. In your specific example you can do plot(sin, xmirror = true).
I'm trying to do the same but didn't find a solution to update an existing plot. But here is a partial answer: you can query information from the PyPlot axes object
julia> Plots.plot(sin, 1:4)
julia> Plots.PyPlot.plt[:xlim]()
(1.0,4.0)
julia> Plots.plot(sin, 20:24)
julia> ax = Plots.PyPlot.plt[:xlim]()
(20.0,24.0)
and it gets updated.

matplotlib duplicate figure and apply changes

I'm making a series of figures with the same layout. As the layout is taking many lines of codes, I'm trying to duplicate the first once done and change only a few things, such as markers, and if possible the data though I know it might complicate things as it would need probably rescaling and so that at the end at 'plt.show()' command, the original figure plus the duplicated get displayed.
I'm not sure that "duplicating settings" in the way you describe is really feasible ... it probably requires lots of low-level access of all the objects involved in the layout (the axes, the axis objects, the line objects, patch objects, etc.) I could be totally wrong about that, but that's my instinct. I've put here something which may do close to what you want, though, as in not having to duplicate a lot of layout specifications.
import matplotlib.pyplot as plt
import numpy as np
def make_layout(data):
fig = plt.figure()
ax = fig.add_subplot(111)
p, = ax.plot(data,'o')
p.set_markerfacecolor('green')
# Presumably lots of complicated settings here
return fig, ax, p
data = np.linspace(0,1)
f1, a1, p1 = make_layout(data)
f2, a2, p2 = make_layout(data**2)
# Make the tweaks you want
a2.set(axis_bgcolor='m')
p2.set_markerfacecolor('yellow')

Matplotlib annotate doesn't work on log scale?

I am making log-log plots for different data sets and need to include the best fit line equation. I know where in the plot I should place the equation, but since the data sets have very different values, I'd like to use relative coordinates in the annotation. (Otherwise, the annotation would move for every data set.)
I am aware of the annotate() function of matplotlib, and I know that I can use textcoords='axes fraction' to enable relative coordinates. When I plot my data on the regular scale, it works. But then I change at least one of the scales to log and the annotation disappears. I get no error message.
Here's my code:
plt.clf()
samplevalues = [100,1000,5000,10^4]
ax = plt.subplot(111)
ax.plot(samplevalues,samplevalues,'o',color='black')
ax.annotate('hi',(0.5,0.5), textcoords='axes fraction')
ax.set_xscale('log')
ax.set_yscale('log')
plt.show()
If I comment out ax.set_xcale('log') and ax.set_ycale('log'), the annotation appears right in the middle of the plot (where it should be). Otherwise, it doesn't appear.
Thanks in advance for your help!
It may really be a bug as pointed out by #tcaswell in the comment but a workaround is to use text() in axis coords:
plt.clf()
samplevalues = [100,1000,5000,10^4]
ax = plt.subplot(111)
ax.loglog(samplevalues,samplevalues,'o',color='black')
ax.text(0.5, 0.5,'hi',transform=ax.transAxes)
plt.show()
Another approach is to use figtext() but that is more cumbersome to use if there are already several plots (panels).
By the way, in the code above, I plotted the data using log-log scale directly. That is, instead of:
ax.plot(samplevalues,samplevalues,'o',color='black')
ax.set_xscale('log')
ax.set_yscale('log')
I did:
ax.loglog(samplevalues,samplevalues,'o',color='black')

Adding a collection to multiple axis?

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.