Reputation: 1239
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.
Upvotes: 16
Views: 25271
Reputation: 91
You might want to take a look at the outset
library, which helps manage axes inset placement at different levels of abstraction --- including simple direct specification of inset size and placement.
outset provides the OutsetGrid
class to manage a main axes (OutsetGrid.source_axes
) and auxiliary axes (OutsetGrid.outset_axes
array).
Auxiliary axes can be inset over the main axes using the outset.inset_outsets
function.
import outset as otst
# create main/auxiliary axes manager object
grid = otst.OutsetGrid(1) # one inset axes
grid.source_axes.axline((0, 0), (1, 1)) # plot y = x
grid.source_axes.set_xlim(0, 1) # set nice unit ax limits
grid.source_axes.set_ylim(0, 1)
otst.inset_outsets( # position auxiliary axes over main axes
grid,
insets=[(0.0, 0.6, 0.3, 0.3)], # exact axes-relative coordinates
)
for spine in grid.outset_axes[0].spines.values(): # style inset axes
spine.set_color("red")
spine.set_linewidth(3)
Manual inset placement can be combined with automatic grid-based layout tools, as shown in this example.
import outset as otst
# create main/auxiliary axes manager object
grid = otst.OutsetGrid(4) # 4 inset axes
grid.source_axes.axline((0, 0), (1, 1)) # plot y = x
grid.source_axes.set_xlim(0, 1) # set nice unit ax limits
grid.source_axes.set_ylim(0, 1)
otst.inset_outsets( # position auxiliary axes over main axes
grid,
insets=[
(0, 0.6, 0.3, 0.3), # manual position, (x0, y0, width, height)
*otst.util.layout_corner_insets( # automatically positioned
3, "SE", # 3 axes in lower right corner
inset_margin_size=(0, 0.1), # customize inset grid geometry
inset_pad_ratio=0.3,
inset_grid_size=0.7,
),
],
)
# finishing touches styling inset axes
for ax in grid.outset_axes:
for spine in ax.spines.values():
spine.set_color("red")
spine.set_linewidth(3)
The original question asks how to create an inset axes that is 1 inch tall and 30% of axes width. Here's how to convert absolute units (inches) to axes-relative units to get the one inch height.
import outset as otst
# create main/auxiliary axes manager object
grid = otst.OutsetGrid(1) # one inset
grid.source_axes.axline((0, 0), (1, 1)) # plot y = 1 - x
grid.source_axes.set_xlim(0, 1) # set nice unit ax limits
grid.source_axes.set_ylim(0, 1)
# calc main axes dimensions in inches...
bb = grid.source_axes.get_window_extent().transformed(
grid.figure.dpi_scale_trans.inverted(),
)
# ... then calculate axes-relative height of one inch
inches_height = 1
relative_height = 1 / bb.height
otst.inset_outsets( # position auxiliary axes over main axes
grid,
insets=[(0.4, 0.1, 0.3, relative_height)], # (x0, y0, width, height)
strip_ticks=False, # keep axes ticks
equalize_aspect=False, # allow different aspects, main vs. insets
)
python3 -m pip install outset
The OutsetGrid
class is derived from seaborn's FacetGrid
class, which provides mechanisms to customize figure size and aspect ratio, among other things.
In addition to inset layout control, the library also provides convenient mechanisms to broadcast content over main/auxiliary axes (e.g., for zoom plots of the same content) and a seaborn-like data-oriented API to infer zoom inserts containing categorical subsets of a dataframe.
Refer to the outset quickstart guide and gallery for more info.
Disclosure: am library author
Upvotes: 0
Reputation: 339112
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.
Upvotes: 18
Reputation: 1239
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.
Upvotes: 2