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)
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.
The objective is to insert a sub_figure in a simple plot as follows:
import numpy as np
from matplotlib import pyplot as plt
X = np.linspace(-6, 6, 1024)
Y = np.sinc(X)
X_detail = np.linspace(-3, 3, 1024)
Y_detail = np.sinc(X_detail)
plt.plot(X, Y, c = 'k')
sub_axes = plt.axes([0.6,0.6,0.25,0.25])
sub_axes.plot(X_detail, Y_detail, c = 'k')
plt.setp(sub_axes)
plt.show()
The code above gives the following output:
The matplotlib documentation says the argument the matplotlib.pyplot.axes() function takes is a list defined as rect=[left, bottom, width, height] where the coordinates left, bottom, width, height are added as normalized (0,1) values.
Can anyone explain that to me ?
The last two co-ordinates are for the size of the sub_figure, that much I get, now what is the deal with the first two ?
The confusion appears to be coming from the different coordinate systems that matplotlib uses. Here is a link to the (fairly exhaustive) tutorial on the subject: https://matplotlib.org/users/transforms_tutorial.html. I will summarize the key point that affect you directly here.
The coordinates you see on your axes are called the data space or data coordinates. This is basically the xlim and ylim of the plots. Note that these are totally independent for the two plots and are not affected by the size or position of your figure.
When you say sub_axes = plt.axes([0.6,0.6,0.25,0.25]), you are specifying the coordinates in figure space or figure coordinates. This is very similar conceptually to axis space or axis coordinates, except that it applies to the whole figure rather than just an individual set of axes.
In this case, the origin of your sub-axes is at (0.6, 0.6) relative to the bottom left corner of the figure. Where the upper-right corner of the figure is (1, 1). As expected, the sub-axes start just a bit above and to the right of the middle of the figure window.
Similarly, the width is (0.25, 0.25), meaning that the sub-axes are 1/4 the size of your figure in each dimension. This can also be interpreted to mean that the upper right-hand corner of the sub-axes is at (0.85, 0.85) in figure space, which looks about right.
You can do some tests. No matter how you pan or zoom on the main axes, the sub-axes are not affected. However, if you resize your figure, both sets of axes will change size to compensate. The sub-axes should always have the same aspect ratio as the figure itself because of how you sized them.
I want to create square subplots which are publication quality using Matplotlib. Currently, I have 2 subplots which are made in a figure of size 8,5 and the X limits for both plots are different.
I would want both the subplots to be of square size rather than the Y axis being taller. Any suggestions ?
Alternatively, is there a way where I can explicitly control the ratio of width and height of a subplot in matplotlib?
Below is the sample image which I have right now.
You can explicitly control the figure size, and you can explicitly set the axes' positions within a figure, as a fraction of the figure size.
fig = plt.figure(figsize=(8,4))
ax1 = fig.add_axes([0.1, 0.1, 0.4, 0.8])
ax1.plot(...)
ax2 = fig.add_axes([0.5, 0.1, 0.4, 0.8])
ax2.plot(...)
In addition, you can supply aspect='equal' to functions that create axes (add_axes, add_subplot) to force the axes shape to match the axes scales (not relevant for your linear-log plot).
The main problem is that after setting the xlim of the axis (in particular decreasing xmax from 512 to 50), the figure doesn't rescale to focus on the region of the new xlim (0 to 50). Instead, the plot in the figure is scaled smaller with big right margin after xmax although nothing gets plotted. Strangely this only happens when I save the figures in pdfs.
Here are screenshots of the two pdf images I generated. The first one is the original image without changing the xlim.
The second one here is the image by setting xmax to 50 from 512.
Here is part of my code:
fig = plt.figure(facecolor='white')
ax = plt.axes()
...
lines=ax.plot(*a, clip_on=False)
ax.set_xlim([0, 50])
...
pp = PdfPages(outfile + '.pdf')
pp.savefig(bbox_inches='tight')
What would be the right way to remove the extra right margin space after xlim?