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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions