I want to create a set of axes to form an inset at a specific location in the parent set of axes. It is therefore not appropriate to just use the parameter loc=1,2,3 in the inset_axes as shown here:
inset_axes = inset_axes(parent_axes,
width="30%", # width = 30% of parent_bbox
height=1., # height : 1 inch
loc=3)
However, I would like something close to this. And the answers here and here seem to be answers to questions slightly more complicated than mine.
So, the question is is there a parameter that I can replace in the above code that will allow custom locations of the inset axes within the parent axes? I've tried to use the bbox_to_anchor but do not understand it's specification or behavior from the documentation. Specifically I've tried:
inset_axes = inset_axes(parent_axes,
width="30%", # width = 30% of parent_bbox
height=1., # height : 1 inch
bbox_to_anchor=(0.4,0.1))
to try to get the anchor for the left and bottom of the inset to be at 40% and 10% of the x and y axis respectively. Or, I tried to put it in absolute coordinates:
inset_axes = inset_axes(parent_axes,
width="30%", # width = 30% of parent_bbox
height=1., # height : 1 inch
bbox_to_anchor=(-4,-100))
Neither of these worked correctly and gave me a warning that I couldn't interpret.
More generally, it seems like loc is a pretty standard parameter in many functions belonging to matplotlib, so, is there a general solution to this problem that can be used anywhere? It seems like that's what bbox_to_anchor is but again, I can't figure out how to use it correctly.
The approach you took is in principle correct. However, just like when placing a legend with bbox_to_anchor, the location is determined as an interplay between bbox_to_anchor and loc. Most of the explanation in the above linked answer applies here as well.
The default loc for inset_axes is loc=1 ("upper right"). This means that if you you specify bbox_to_anchor=(0.4,0.1), those will be the coordinates of the upper right corner, not the lower left one.
You would therefore need to specify loc=3 to have the lower left corner of the inset positionned at (0.4,0.1).
However, specifying a bounding as a 2-tuple only makes sense if not specifying the width and height in relative units ("30%"). Or in other words, in order to use relative units you need to use a 4-tuple notation for the bbox_to_anchor.
In case of specifying the bbox_to_anchor in axes units one needs to use the bbox_transform argument, again, just as with legends explained here, and set it to ax.transAxes.
plt.figure(figsize=(6,3))
ax = plt.subplot(221)
ax.set_title("100%, (0.5,1-0.3,.3,.3)")
ax.plot(xdata, ydata)
axins = inset_axes(ax, width="100%", height="100%", loc='upper left',
bbox_to_anchor=(0.5,1-0.3,.3,.3), bbox_transform=ax.transAxes)
ax = plt.subplot(222)
ax.set_title("30%, (0.5,0,1,1)")
ax.plot(xdata, ydata)
axins = inset_axes(ax, width="30%", height="30%", loc='upper left',
bbox_to_anchor=(0.5,0,1,1), bbox_transform=ax.transAxes)
Find a complete example on the matplotlib page: Inset Locator Demo
Another option is to use InsetPosition instead of inset_axes and to give an existing axes a new position. InsetPosition takes the x and y coordinates of the lower left corner of the axes in normalized axes coordinates, as well as the width and height as input.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
fig, ax= plt.subplots()
iax = plt.axes([0, 0, 1, 1])
ip = InsetPosition(ax, [0.4, 0.1, 0.3, 0.7]) #posx, posy, width, height
iax.set_axes_locator(ip)
iax.plot([1,2,4])
plt.show()
Finally one should mention that from matplotlib 3.0 on, you can use matplotlib.axes.Axes.inset_axes
import matplotlib.pyplot as plt
plt.figure(figsize=(6,3))
ax = plt.subplot(221)
ax.set_title("ax.inset_axes, (0.5,1-0.3,.3,.3)")
ax.plot([0,4], [0,10])
axins = ax.inset_axes((0.5,1-0.3,.3,.3))
plt.show()
The result is roughly the same, except that mpl_toolkits.axes_grid1.inset_locator.inset_axes allows for a padding around the axes (and applies it by default), while Axes.inset_axes does not have this kind of padding.
Using the answer from ImportanceOfBeingErnest and several of the suggested links from the unreleased matplotlib documentation like the locator demo and the inset_axes docs, it still took me some time to figure out how all the parameters behaved. So, I will repeat my understanding here for clarity. I ended up using:
bbox_ll_x = 0.2
bbox_ll_y = 0
bbox_w = 1
bbox_h = 1
eps = 0.01
inset_axes = inset_axes(parent_axes,
height="30%", #height of inset axes as frac of bounding box
width="70%", #width of inset axes as frac of bounding box
bbox_to_anchor=(bbox_ll_x,bbox_ll_y,bbox_w-bbox_ll_x,bbox_h),
loc='upper left',
bbox_transform=parent_axes.transAxes)
parent_axes.add_patch(plt.Rectangle((bbox_ll_x, bbox_ll_y+eps),
bbox_w-eps-bbox_ll_x,
bbox_h-eps,
ls="--",
ec="c",
fc="None",
transform=parent_axes.transAxes))
bbox_ll_x is the x location of the lower left corner of the bounding box in the parent axis coordinates (that is the meaning of the bbox_transform input)
bbox_ll_y is the y location of the lower left corner of the bounding box in the parent axis coordinates
bbox_w is the width of the bounding box in parent axis coordinates
bbox_h is the height of the bounding box in parent axis coordinates
eps is a small number to get the rectangles to show up from under axes when drawing the rectangular bounding box.
I used the add_patch call in order to put a cyan dashed line that represents the inner edge of the bounding box that is drawn.
The trickiest part for me was realizing that the height and width inputs (when specified as percents) are relative to the bounding box size. That's why (as noted in the links and the answer below) you must specify a 4-tuple for the bbox_to_anchor parameter if you specify the size of the inset axes in percents. If you specify the size of the inset axes as percents and don't supply bbox_w or bbox_h how can matplotlib get the absolute size of the inset?
Another thing was that the loc parameter specifies where to anchor the inset axes within the bounding box. As far as I can tell that's the only function of that parameter.
Related
I'm generating the following contour plot + colorbar in matplotlib:
I extract the relative bounds of the resulting axes using the following loop:
fig = plt.gcf()
for ax in fig.get_axes():
print(ax.get_position().bounds)
and obtain
(0.125, 0.10999999999999999, 0.62, 0.77)
(0.78375, 0.10999999999999999, 0.11624999999999996, 0.77)
According to a previous question of mine there denote the [left, bottom, width, height] bounds in relative coordinates for each axes. I actually measured the relative bounds and found them to be incorrect. One easy way to spot it is the last values of the height for each axes object. How can they both be 0.77 when the color bar clearly has a greater height than the contourplot?
I would like to have full control of the size of the contour plot and axes with respect to the figure.
The position of the axes is determined at draw time. That means that before actually drawing the figure the axes' position is the would-be position or the boundary of the space the axes is free to take.
Usually this would be the same position the axes will take in the final plot, in case you let it expand freely. However, here it seems there is some constraint about the aspect in the game.
To obtain the axes position as it will appear in the drawn figure one should draw the figure manually; then ask for its position.
fig = plt.gcf()
fig.canvas.draw()
for ax in fig.get_axes():
print(ax.get_position().bounds)
subplots by hand.
I am referring following link
http://nbviewer.jupyter.org/github/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/04.08-Multiple-Subplots.ipynb
The most basic method of creating an axes is to use the plt.axes function. As we've seen previously, by default this creates a standard axes object that fills the entire figure. plt.axes also takes an optional argument that is a list of four numbers in the figure coordinate system. These numbers represent [left, bottom, width, height] in the figure coordinate system, which ranges from 0 at the bottom left of the figure to 1 at the top right of the figure.
For example, we might create an inset axes at the top-right corner of another axes by setting the x and y position to 0.65 (that is, starting at 65% of the width and 65% of the height of the figure) and the x and y extents to 0.2 (that is, the size of the axes is 20% of the width and 20% of the height of the figure):
ax1 = plt.axes() # standard axes
ax2 = plt.axes([0.65, 0.65, 0.2, 0.2])
Here above example I am expecting ax2 at location starting (0.65,0.65) as we have bottom and left at 0.65 and 0.65 but i am observing (0.65, 0.7) and lenght and height is 0.2 i.e, right vertical line at location 0.85 but i am observing at 0.9? Why is this differnces. Kindly explain.
The numbers given to plt.axes are in figure units, where the figure is 1 unit wide and 1 unit heigh.
Let me just highlight the important part:
For example, we might create an inset axes at the top-right corner of another axes by setting the x and y position to 0.65 (that is, starting at 65% of the width and 65% of the height of the figure) and the x and y extents to 0.2 (that is, the size of the axes is 20% of the width and 20% of the height of the figure)
Maybe an image helps more to understand that
That said there is a little subtlety when this code is being used with the inline backend in IPython or jupyter. In that case the size of the figure shown in the output may slightly differ from the original figure as it is cropped or expanded to fit nicely to everything that is drawn inside. This is equivalent to the bbox_to_inched="tight" option of savefig. Hence, if you want to verify that the subplot is indeed placed at 65% of figure size and is 20% large, you would need to run the code as a script outside the notebook, or save the figure plt.savefig("test.png") (without using the bbox_to_inched="tight" option).
A final note: While add_axes may be an easy option to add an axes in figure coordintes, it is often desireable to add an inset in axes coordinates, i.e. in percentage of the axes width and height instead of the figure width and height. This can be done as follows:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
fig, ax1 = plt.subplots()
ax2 = plt.axes([0, 0, 1, 1])
ip = InsetPosition(ax1, [0.4, 0.1, 0.3, 0.7])
#posx, posy, width, height in coordinates of ax1
ax2.set_axes_locator(ip)
ax2.plot([1,2,3,4])
plt.show()
I'm trying to add in some annotations on polar projected matplotlib ax objects. Most of the annotations are pretty long (40 +/- 3 characters).
Is there a way to have more control of these longer annotations on a polar projection? By more control, I mean closer to the line.
# This annotation works alright
fig = plt.figure()
ax = plt.subplot(111, projection="polar")
ax.axvline(x=np.pi/3, color="black", alpha=0.618)
ax.annotate("this ok", xy=[np.pi/3,0.75], rotation=np.rad2deg(np.pi/3))
# This one doesn't work
fig = plt.figure()
ax = plt.subplot(111, projection="polar")
ax.axvline(x=np.pi/3, color="black", alpha=0.618)
ax.annotate("this doesn't work b/c it's pretty long", xy=[np.pi/3,0.75], rotation=np.rad2deg(np.pi/3))
The problem is that the annotating text is rotated. Thereby the upper left corner of the rectangle that surrounds the complete text (called bounding box) is far away from the text. This upper left corner however is the point which sits by default at the coordinates that are specified with the xy argument to ax.annotate(). This can be seen in the left plot below.
The solution is to use the lower left point of the surrounding rectangle instead and move it somewhere close to the origin. Using the lower left corner can be done by specifying ha="left" (horizontal alignment), va="bottom".
ax2.annotate("this does work b/c we specifiy alignment",
xy=[0,0.07], rotation=np.rad2deg(np.pi/3),
ha="left", va="bottom")
try setting the font sizes smaller so it fits
SIZE = 8
MED_SIZE = 10
BIG_SIZE = 12
plt.rc('axes',titlesize=SIZE) #fontsize of the axes titles is set to 8
I'm creating a figure with multiple subplots. One of these subplots is giving me some trouble, as none of the axes corners or centers are free (or can be freed up) for placing the legend. What I'd like to do is to have the legend placed somewhere in between the 'upper left' and 'center left' locations, while keeping the padding between it and the y-axis equal to the legends in the other subplots (that are placed using one of the predefined legend location keywords).
I know I can specify a custom position by using loc=(x,y), but then I can't figure out how to get the padding between the legend and the y-axis to be equal to that used by the other legends. Would it be possible to somehow use the borderaxespad property of the first legend? Though I'm not succeeding at getting that to work.
Any suggestions would be most welcome!
Edit: Here is a (very simplified) illustration of the problem:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, sharex=False, sharey=False)
ax[0].axhline(y=1, label='one')
ax[0].axhline(y=2, label='two')
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
ax[1].axhline(y=1, label='one')
ax[1].axhline(y=2, label='two')
ax[1].axhline(y=3, label='three')
ax[1].set_ylim([0.8,3.2])
ax[1].legend(loc=2)
plt.show()
What I'd like is that the legend in the right plot is moved down somewhat so it no longer overlaps with the line.
As a last resort I could change the axis limits, but I would very much like to avoid that.
I saw the answer you posted and tried it out. The problem however is that it is also depended on the figure size.
Here's a new try:
import numpy
import matplotlib.pyplot as plt
x = numpy.linspace(0, 10, 10000)
y = numpy.cos(x) + 2.
x_value = .014 #Offset by eye
y_value = .55
fig, ax = plt.subplots(1, 2, sharex = False, sharey = False)
fig.set_size_inches(50,30)
ax[0].plot(x, y, label = "cos")
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
line1 ,= ax[1].plot(x,y)
ax[1].set_ylim([0.8,3.2])
axbox = ax[1].get_position()
fig.legend([line1], ["cos"], loc = (axbox.x0 + x_value, axbox.y0 + y_value))
plt.show()
So what I am now doing is basically getting the coordinates from the subplot. I then create the legend based on the dimensions of the entire figure. Hence, the figure size does not change anything to the legend positioning anymore.
With the values for x_value and y_value the legend can be positioned in the subplot. x_value has been eyeballed for a good correspondence with the "normal" legend. This value can be changed at your desire. y_value determines the height of the legend.
Good luck!
After spending way too much time on this, I've come up with the following satisfactory solution (the Transformations Tutorial definitely helped):
bapad = plt.rcParams['legend.borderaxespad']
fontsize = plt.rcParams['font.size']
axline = plt.rcParams['axes.linewidth'] #need this, otherwise the result will be off by a few pixels
pad_points = bapad*fontsize + axline #padding is defined in relative to font size
pad_inches = pad_points/72.0 #convert from points to inches
pad_pixels = pad_inches*fig.dpi #convert from inches to pixels using the figure's dpi
Then, I found that both of the following work and give the same value for the padding:
# Define inverse transform, transforms display coordinates (pixels) to axes coordinates
inv = ax[1].transAxes.inverted()
# Inverse transform two points on the display and find the relative distance
pad_axes = inv.transform((pad_pixels, 0)) - inv.transform((0,0))
pad_xaxis = pad_axes[0]
or
# Find how may pixels there are on the x-axis
x_pixels = ax[1].transAxes.transform((1,0)) - ax[1].transAxes.transform((0,0))
# Compute the ratio between the pixel offset and the total amount of pixels
pad_xaxis = pad_pixels/x_pixels[0]
And then set the legend with:
ax[1].legend(loc=(pad_xaxis,0.6))
Plot:
When you set bbox_inches = 'tight' in Matplotlib's savefig() function, it tries to find the tightest bounding box that encapsulates all the content in your figure window. Unfortunately, the tightest bounding box appears to include invisible axes.
For example, here is a snippet where setting bbox_inches = 'tight' works as desired:
import matplotlib.pylab as plt
fig = plt.figure(figsize = (5,5))
data_ax = fig.add_axes([0.2, 0.2, 0.6, 0.6])
data_ax.plot([1,2], [1,2])
plt.savefig('Test1.pdf', bbox_inches = 'tight', pad_inches = 0)
which produces:
The bounds of the saved pdf correspond to the bounds of the content. This is great, except that I like to use a set of invisible figure axes to place annotations in. If the invisible axes extend beyond the bounds of the visible content, then the pdf bounds are larger than the visible content. For example:
import matplotlib.pylab as plt
fig = plt.figure(figsize = (5,5))
fig_ax = fig.add_axes([0, 0, 1, 1], frame_on = False)
fig_ax.xaxis.set_visible(False)
fig_ax.yaxis.set_visible(False)
data_ax = fig.add_axes([0.2, 0.2, 0.6, 0.6])
data_ax.plot([1,2], [1,2])
plt.savefig('Test2.pdf', bbox_inches = 'tight', pad_inches = 0)
producing
How can I force savefig() to ignore invisible items in the figure window? The only solution I have come up with is to calculate the bounding box myself and explicitly specify the bbox to savefig().
In case it matters, I am running Matplotlib 1.2.1 under Python 2.7.3 on Mac OS X 10.8.5.
The relevant function (called by canvas.print_figure which is called by figure.savefig to generate the bounding box) in backend_bases.py:
def get_tightbbox(self, renderer):
"""
Return a (tight) bounding box of the figure in inches.
It only accounts axes title, axis labels, and axis
ticklabels. Needs improvement.
"""
bb = []
for ax in self.axes:
if ax.get_visible():
bb.append(ax.get_tightbbox(renderer))
_bbox = Bbox.union([b for b in bb if b.width != 0 or b.height != 0])
bbox_inches = TransformedBbox(_bbox,
Affine2D().scale(1. / self.dpi))
return bbox_inches
The only consideration that goes into deciding if an axes is 'visible' is if ax.get_visible() returns true, even if you have no visible (either artist.get_visible() == False or simple transparent) artists in the axes.
The bounding box behavior you observe is the correct behavior.
tcaswell, thanks for your help. My original question was, "How can I force savefig() to ignore invisible items in the figure window?" When I put fig_ax.set_visible(False) then savefig() ignores the invisible axes. Unfortunately, when I set fig_ax.set_visible(False) then any artist placed in fig_ax is also invisible. I am back to the original plot I posted, where fig_ax does not exist.
As you intimated in your comment, tcaswell, I think the proper solution is avoid creating fig_ax. I am currently working on placing my annotations and data axis labels in the default figure object fig. It's a bit annoying since fig uses normalized figure units instead of mm units, but I can deal with it.