matplotlib duplicate figure and apply changes - matplotlib

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')

Related

How to group legend handles in matplotlib?

I was wondering how I can produce such fancy legends in matplotlib. In particular, I'd like to know how to group, for example, both solid and dashed blue lines which correspond to $\tau=10$ side by side, or all dashed (or solid) lines together as in the lower part of the legend.
The image is taken from this arxiv paper.
Thanks to the comments on my original post, I could come up with a script that does the grouping, albeit as mentioned, not as straightforwardly as I thought. The script is essentially an adapted version of the other answer on SO.
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib import patches as mpatches
from matplotlib.collections import PatchCollection
class AnyObject(object):
pass
class AnyObjectHandler(object):
def legend_artist(self, legend, orig_handle, fontsize, handlebox):
x0, y0 = handlebox.xdescent, handlebox.ydescent
width, height = handlebox.width, handlebox.height
codes = [Path.MOVETO, Path.LINETO]
# the following lines unfortunately may not be refactored
verts1 = [(x0, y0+0.25*height),(x0 + width, y0+0.25*height)]
verts2 = [(x0, y0+0.75*height),(x0 + width, y0+0.75*height)]
path1 = Path(verts1,codes)
path2 = Path(verts2,codes)
patch1 = mpatches.PathPatch(path1)
patch2 = mpatches.PathPatch(path2,ls='--',)
patch = PatchCollection([patch1,patch2],match_original=True)
handlebox.add_artist(patch)
return patch
fig, ax = plt.subplots()
ax.legend([AnyObject()], ['My grouped handlers'],
handler_map={AnyObject: AnyObjectHandler()})
while leads to
Take-home messages
The legend docs specifies a more natural way to group the entries using HandlerTuple (example here). But since mpl places the markers horizontally, such an approach is orthogonal to what I wished :). If it doesn't bother you, go for that option first.
As far as I understood, custom legends are designed such that they don't exchange any information with the data you want to plots. For example, in my case, I cannot tell the AnyObjectHandler how many lines are going to be grouped, what are their linestyle, etc. It is a good decision for generality, with the expense of a (minimal) harm to the code refactoring.

Better ticks and tick labels with log scale

I am trying to get better looking log-log plots and I almost got what I want except for a minor problem.
The reason my example throws off the standard settings is that the x values are confined within less than one decade and I want to use decimal, not scientific notation.
Allow me to illustrate with an example:
import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib as mpl
import numpy as np
x = np.array([0.6,0.83,1.1,1.8,2])
y = np.array([1e-5,1e-4,1e-3,1e-2,0.1])
fig1,ax = plt.subplots()
ax.plot(x,y)
ax.set_xscale('log')
ax.set_yscale('log')
which produces:
There are two problems with the x axis:
The use of scientific notation, which in this case is counterproductive
The horrible "offset" at the lower right corner
After much reading, I added three lines of code:
ax.xaxis.set_major_formatter(mpl.ticker.ScalarFormatter())
ax.xaxis.set_minor_formatter(mpl.ticker.ScalarFormatter())
ax.ticklabel_format(style='plain',axis='x',useOffset=False)
This produces:
My understanding of this is that there are 5 minor ticks and 1 major one. It is much better, but still not perfect:
I would like some additional ticks between 1 and 2
Formatting of label at 1 is wrong. It should be "1.0"
So I inserted the following line before the formatter statement:
ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(0.2))
I finally get the ticks I want:
I now have 8 major and 2 minor ticks. Now, this almost looks right except for the fact that the tick labels at 0.6, 0.8 and 2.0 appear bolder than the others. What is the reason for this and how can I correct it?
The reason, some of the labels appear bold is that they are part of the major and minor ticklabels. If two texts perfectly overlap, they appear bolder due to the antialiasing.
You may decide to only use minor ticklabels and set the major ones with a NullLocator.
Since the locations of the ticklabels you wish to have is really specific there is no automatic locator that would provide them out of the box. For this special case it may be easiest to use a FixedLocator and specify the labels you wish to have as a list.
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
x = np.array([0.6,0.83,1.1,1.8,2])
y = np.array([1e-5,1e-4,1e-3,1e-2,0.1])
fig1,ax = plt.subplots(dpi=72, figsize=(6,4))
ax.plot(x,y)
ax.set_xscale('log')
ax.set_yscale('log')
locs = np.append( np.arange(0.1,1,0.1),np.arange(1,10,0.2))
ax.xaxis.set_minor_locator(ticker.FixedLocator(locs))
ax.xaxis.set_major_locator(ticker.NullLocator())
ax.xaxis.set_minor_formatter(ticker.ScalarFormatter())
plt.show()
For a more generic labeling, one could of course subclass a locator, but we would then need to know the logic to use to determine the ticklabels. (As I do not see a well defined logic for the desired ticks from the question, I feel it would be wasted effort to provide such a solution for now.)

matplotlib duplicate axes or figure?

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.

Change figsize in matplotlib polar contourf

I am using the following example Example to create two polar contour subplots. When I create as the pdf there is a lot of white space which I want to remove by changing figsize.
I know how to change figsize usually but I am having difficulty seeing where to put it in this code example. Any guidance or hint would be greatly appreciated.
Many thanks!
import numpy as np
import matplotlib.pyplot as plt
#-- Generate Data -----------------------------------------
# Using linspace so that the endpoint of 360 is included...
azimuths = np.radians(np.linspace(0, 360, 20))
zeniths = np.arange(0, 70, 10)
r, theta = np.meshgrid(zeniths, azimuths)
values = np.random.random((azimuths.size, zeniths.size))
#-- Plot... ------------------------------------------------
fig, ax = plt.subplots(subplot_kw=dict(projection='polar'))
ax.contourf(theta, r, values)
plt.show()
Another way to do this would be to use the figsize kwarg in your call to plt.subplots.
fig, ax = plt.subplots(figsize=(6,6), subplot_kw=dict(projection='polar')).
Those values are in inches, by the way.
You can easily just put plt.figsize(x,y) at the beginning of the code, and it will work. plt.figsize changes the size of all future plots, not just the current plot.
However, I think your problem is not what you think it is. There tends to be quite a bit of whitespace in generated PDFs unless you change options around. I usually use
plt.savefig( 'name.pdf', bbox_inches='tight', pad_inches=0 )
This gives as little whitespace as possible. bbox_inches='tight' tries to make the bounding box as small as possible, while pad_inches sets how many inches of whitespace there should be padding it. In my case I have no extra padding at all, as I add padding in whatever I'm using the figure for.

Multiplot with matplotlib without knowing the number of plots before running

I have a problem with Matplotlib's subplots. I do not know the number of subplots I want to plot beforehand, but I know that I want them in two rows. so I cannot use
plt.subplot(212)
because I don't know the number that I should provide.
It should look like this:
Right now, I plot all the plots into a folder and put them together with illustrator, but there has to be a better way with Matplotlib. I can provide my code if I was unclear somewhere.
My understanding is that you only know the number of plots at runtime and hence are struggling with the shorthand syntax, e.g.:
plt.subplot(121)
Thankfully, to save you having to do some awkward math to figure out this number programatically, there is another interface which allows you to use the form:
plt.subplot(n_cols, n_rows, plot_num)
So in your case, given you want n plots, you can do:
n_plots = 5 # (or however many you programatically figure out you need)
n_cols = 2
n_rows = (n_plots + 1) // n_cols
for plot_num in range(n_plots):
ax = plt.subplot(n_cols, n_rows, plot_num)
# ... do some plotting
Alternatively, there is also a slightly more pythonic interface which you may wish to be aware of:
fig, subplots = plt.subplots(n_cols, n_rows)
for ax in subplots:
# ... do some plotting
(Notice that this was subplots() not the plain subplot()). Although I must admit, I have never used this latter interface.
HTH