specific location for inset axes
Asked Answered
L

3

16

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.

Liederkranz answered 28/7, 2017 at 17:5 Comment(1)
You should not be assigning the inset_axes function to a similarly named variable. Use something like inset = inset_axes(...).Grogram
M
18

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)

enter image description here

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.

enter image description here

Mexicali answered 28/7, 2017 at 17:41 Comment(17)
I've tried inset_axes(ax3, height="30%", width="70%", bbox_to_anchor=(0.4,0.6), loc=3) but it produced a warning: UserWarning: Unable to find pixel distance along axis for interval padding of ticks; assuming no interval padding needed. and the inset appears as a tiny axis well outside the parent axesLiederkranz
You are right. There is either a bug in matplotlib or an error in the documentation. I'm trying to find out what's going on.Mexicali
Does this mean I can stop constructing a Minimal, Complete, and Verifiable example? Or, can you share the one that you've apparently constructed?Liederkranz
Yep, I can easily reproduce the issue. You will find a mcve in the issue that I have just opened on GitHub about this. I have also updated the answer for a workaround. This is probably as easy to use as inset axes.Mexicali
Thanks, just read that post, when you say "Note that when explicitely supplying the bbox_to_anchor we at least...." did you mean bbox_transform ?Liederkranz
Is this workaround working for you or do you explicitely need one of width and height to be in percent and the other in inches?Mexicali
I am used to getting an axis object in order to plot things, so I am confused by the example when I try to "reset" the axis the original plots are deformed. I don't have time to produce a minimal, complete, and verifiable example for this so quickly: I have three plots (axis objects ax1,2,3) on a 3x1 grid. Only the last one has an inset.Liederkranz
What exactly do you mean by "reset"?Mexicali
Let us continue this discussion in chat.Liederkranz
I updated the answer to show an actual solution to the problem.Mexicali
I apparently used the InsetPosition workaround before. I now want to use the inset_axes method because it is conceptually clearer to me. I found it essential to read the pull request conversation in order to understand what you did here. I still can't figure out how the coordinates (0.5,1-0.3,.3,.3) and (0.5,0,1,1) produce the same location. I thought the first two numbers represent the left and bottom of the inset axis, but I don't understand how width and height modify their meaning.Liederkranz
The bbox_to_anchor produces a box. Inside of this box an inset of the specified width and height is created and is aligned according to the loc parameter. The PR is still not merged. A preview version of the docs is here. Together with this comes an example, which might be more helpful.Mexicali
Ok, I edited the question with my understanding of this documentation and what worked for me. Feel free to edit/comment if something is misunderstood there.Liederkranz
What you call "Edit" and everything below rather seems like an answer than a question. Would you mind cutting it from the question and providing it as answer? This makes it much more useful for future readers.Mexicali
Also since I wrote that new documentation, if you want to give some feedback on what was not so understandable it could help me improving the docs even further.Mexicali
I moved that portion of the question to a new answer. The hardest part I had with the documentation was the overall picture of things: i.e. figuring how why your different parameters sets gave identical results. A major advance I made was realizing that the width and height parameters had in a sense nothing to do with the interpretation of the bounding box parameters. Since the last two coordinates you gave for the 100% case was 0.3, and in the 30% case was 1 I thought somehow the size was the multiplication of those. Suggest a place I can write more and I'll try to be clearer.Liederkranz
A good place for feedback is of course the pull request itself.Mexicali
L
2

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.

Liederkranz answered 23/5, 2018 at 21:34 Comment(2)
Thank you for taking time to organise what you have found in this answer! It as very helpfull for meIgnatzia
@Ignatzia no problem, I'm happy it was time well spentLiederkranz
A
0

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.

Axes-relative Layout

axes-relative placement example

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)

Mix Manual and Automatic Layout

manual/automatic layout example

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)

Absolute Layout (e.g., inches)

absolute placement

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
)

Installation

python3 -m pip install outset

Additional Features

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

Acoustician answered 25/12, 2023 at 18:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.