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
- 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).
- 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:

Future enhancements which the helper func doesn't include.
- 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.
- 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])
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.insetworks 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
Here is a minimal example using the helper function:
And here is the resulting image:
Future enhancements which the helper func doesn't include.
The helper function: