Skip to content

Suggestion: improve inset positioning for geoaxes #770

Description

@gepcel

Why this?

Inset axes are widely used in plots, both for Cartesian Axes and for GeoAxes. In map contexts, this is sometimes called a hawkeye map.

While ax.inset works well for Cartesian axes, it does not handle geoaxes correctly. The reason is that geoaxes enforce a fixed aspect ratio determined by the map projection. As a result, the standard inset positioning logic fails, as reported in issue #751.

I have a small helper function that works around this for personal use, but I'm not sure whether this approach fits UltraPlot's design philosophy. If it does, Could you take a look? And see how it might be able to integrated properly, following UltraPlot's code style.

How

  1. Ignore the explicitly set width and height for geoaxes (though maybe there is a way to enforce at least a minimum width that I am unaware of).
  2. Recalculate the position, width, and height from the desired anchor point, and then reset the inset position.

Here is a minimal example using the helper function:

importmatplotlib.pyplotaspltimportcartopy.crsasccrsfig, ax=plt.subplots(1, 1, subplot_kw={'projection': ccrs.PlateCarree()})
ax.coastlines()
# iax.set_position doesn't work for inset_exes. Don't know why.# iax = ax.inset_axes([0.8, 0.8, 0.2, 0.2], projection=ccrs.PlateCarree())iaxs= [fig.add_axes([0.8, 0.8, 0.1, 0.2], projection=ccrs.PlateCarree()) foriinrange(10)]
# for the first 9 iax, use loc string as anchor:foriax, pos, anchorinzip(iaxs[:-1],
[(0,1), (.5, 1), (1,1), (0,.5), (.5,.5), (1,.5), (0,0), (.5, 0), (1, 0)],
['ul', 'uc', 'ur', 'cl', 'c', 'cr', 'll', 'lc', 'lr']):
iax.set_extent([68, 70, 22, 24])
iax.coastlines()
iax.spines['geo'].set_edgecolor('red')
iax.text(.5, .5, anchor, ha='center', va='center', fontsize=24, color='r', transform=iax.transAxes)
update_inset_position(pos[0], pos[1], ax, iax, anchor=anchor, anchor_transform='data')
# For the last iax, use (x,y) as for anchor:iaxs[-1].set_extent([68, 70, 22, 24])
iaxs[-1].coastlines()
iaxs[-1].spines['geo'].set_edgecolor('blue')
iaxs[-1].spines['geo'].set_linewidth(2)
update_inset_position(68, 22, ax, iaxs[-1], transform='data', anchor=(68, 22), anchor_transform='data')
iaxs[-1].text(.5, .5, '(68,22)', ha='center', va='center', fontsize=16, color='b', transform=iaxs[-1].transAxes)
plt.show()

And here is the resulting image:

Image

Future enhancements which the helper func doesn't include.

  1. Indicator rectangle – automatically place a rectangle on the main axes (or the inset) to indicate the region that is magnified, depending on which of the two is zoomed in.
  2. Indicator lines – add connecting lines between the inset and the main axes, if necessary.

The helper function:

# Copy the method from ultraplot here, to work with matplotlib and cartopy.def_get_transform(ax, transform, default="data"):
""" Translates user input transform. Also used in an axes method. """# TODO: Can this support cartopy transforms? Seems not when this# is used for inset axes bounds but maybe in other places?fromultraplot.internalsimport_not_nonefrommatplotlibimporttransformsasmtransformsfromcartopy.crsimportCRS, PlateCarreetransform=_not_none(transform, default)
ifisinstance(transform, mtransforms.Transform):
returntransformelifCRSisnotobjectandisinstance(transform, CRS):
returntransformelifPlateCarreeisnotobjectandtransform=="map":
returnPlateCarree()
eliftransform=="data":
returnax.transDataeliftransform=="axes":
returnax.transAxeseliftransform=="figure":
returnax.figure.transFigureeliftransform=="subfigure":
returnax.figure.transSubfigureelse:
raiseValueError(f"Unknown transform {transform!r}.")
defupdate_inset_position(x, y, ax, iax, transform='axes', anchor='ur', anchor_transform='iax'):
''' Update the position of the inset axes based on position(x, y) and anchor. Place anchor point of iax in the position (x, y) of ax. Args: x, y: float, point position to put iax on ax. transform: str, 'axes' or 'data', default 'axes'. anchor: str or tuple of float. If str, must be one of: ul, ur, ll, lr, c, uc, lc, cl, cr, or "upper right", "upper left", "lower right", "lower left", "center", "center left", "center right", "upper center", "lower center". If tuple, must be (x, y) of iax. trnasform: str or matplotlib transform object, the transform of x, y, default to "axes", aka: ax.transAxes anchor_transform: str or matplotlib transform object, the transform of anchor point, default to "iax", aka: iax.transAxes '''plt.draw()
anchor_position_loc= {
'ul': (0, 1), 'upper left': (0, 1),
'ur': (1, 1), 'upper right': (1, 1),
'll': (0, 0), 'lower left': (0, 0),
'lr': (1, 0), 'lower right': (1, 0),
'c': (0.5, 0.5), 'center': (0.5, 0.5),
'uc': (0.5, 1), 'upper center': (0.5, 1),
'lc': (0.5, 0), 'lower center': (0.5, 0),
'cl': (0, 0.5), 'center left': (0, 0.5),
'cr': (1, 0.5), 'center right': (1, 0.5),
}
# get position of iaxifisinstance(anchor, str):
ifanchornotinanchor_position_loc:
raiseValueError(f'anchor must be one of {list(anchor_position_loc.keys())}, got {anchor}')
anchor=anchor_position_loc[anchor]
# if anhcor is string, ignore anchor_transform.else:
anchor=tuple(anchor) # expect (x, y)# for anchor_transform, The 'iax' shortcut means "use the inset axes' own transAxes".# Since _get_transform does not recognize 'iax', we map it to 'ax'# and pass the inset axes (iax) as the axes argument.# This yields iax.transAxes, which places the anchor in# the inset axes' normalized coordinate system (0-1).anchor_transform='axes'ifanchor_transform=='iax'elseanchor_transformanchor_transform=_get_transform(iax, anchor_transform)
anchor=iax.transAxes.inverted().transform(anchor_transform.transform(anchor))
position_ax=ax.get_position()
position_iax=iax.get_position()
invert_fig_trans=ax.figure.transFigure.inverted()
x, y=_get_transform(ax, transform).transform((x, y))
x, y=invert_fig_trans.transform((x, y))
x-=anchor[0] *position_iax.widthy-=anchor[1] *position_iax.height# trans_ax.transform(x,y)iax.set_position([x, y, position_iax.width, position_iax.height])

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions