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:

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:

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?
    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])

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