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:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
fig, 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()) for i in range(10)]
# for the first 9 iax, use loc string as anchor:
for iax, pos, anchor in zip(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?
from ultraplot.internals import _not_none
from matplotlib import transforms as mtransforms
from cartopy.crs import CRS, PlateCarree
transform = _not_none(transform, default)
if isinstance(transform, mtransforms.Transform):
return transform
elif CRS is not object and isinstance(transform, CRS):
return transform
elif PlateCarree is not object and transform == "map":
return PlateCarree()
elif transform == "data":
return ax.transData
elif transform == "axes":
return ax.transAxes
elif transform == "figure":
return ax.figure.transFigure
elif transform == "subfigure":
return ax.figure.transSubfigure
else:
raise ValueError(f"Unknown transform {transform!r}.")
def update_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 iax
if isinstance(anchor, str):
if anchor not in anchor_position_loc:
raise ValueError(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' if anchor_transform == 'iax' else anchor_transform
anchor_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.width
y -= 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: