Skip to content
Open
73 changes: 72 additions & 1 deletion docs/insets_panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
)

# %%
import ultraplot as uplt
import numpy as np
import ultraplot as uplt

state = np.random.RandomState(51423)
data = (state.rand(20, 20) - 0.48).cumsum(axis=1).cumsum(axis=0)
Expand Down Expand Up @@ -192,3 +192,74 @@
)
ix.format(xlim=(2, 4), ylim=(2, 4), color="red8", linewidth=1.5, ticklabelweight="bold")
ix.pcolormesh(data, cmap="Grays", levels=N, inbounds=False)

# %% [raw] raw_mimetype="text/restructuredtext"
# Hawkeye map insets
# ~~~~~~~~~~~~~~~~~~
#
# :class:`~ultraplot.axes.GeoAxes` adds :meth:`~ultraplot.axes.GeoAxes.hawkeye`
# for geographic callout maps. The inset is anchored at ``xy`` in a parent coordinate
# system, and its ``size`` is a fraction of the parent axes. Supply ``extent`` to set
# the inset map scope and draw a matching indicator and optional connectors on the
# parent map. Hawkeyes are excluded from automatic layout, so they can extend beyond
# the parent axes without reserving subplot space.
#
# Rectangular hawkeyes preserve both the requested extent and projection scale. A
# circular frame cannot simultaneously preserve projection scale and display an exact
# non-square extent. Circular hawkeyes therefore expand the shorter projected dimension
# to retain the projection; use a square extent when the exact circular map scope is
# important, or pass ``aspect='auto'`` to fit the exact extent with distortion.
#
# The following dependency-free example shows a Singapore city locator on a world map.
# The returned inset is an ordinary geographic axes, so optional geospatial libraries
# can draw external data inside it. For example, `OSMnx <https://osmnx.readthedocs.io/>`__
# can provide street geometry when it is installed separately:
#
# .. code-block:: python
#
# import osmnx as ox
# from cartopy import crs as ccrs
#
# network = ox.graph_from_point((1.3521, 103.8198), dist=8_000, network_type="drive")
# streets = ox.graph_to_gdfs(network, nodes=False)
# streets.plot(ax=inax, color="0.3", linewidth=0.4, transform=ccrs.PlateCarree())
#
# The data download and OpenStreetMap attribution are the responsibility of the
# application using that optional integration.

# %%
singapore = (103.8198, 1.3521)
fig, ax = uplt.subplots(proj="robin", refwidth=4)
ax.format(land=True, landcolor="gray8", oceancolor="blue9")
ax.plot(
*singapore,
marker="o",
color="red",
markeredgecolor="white",
markeredgewidth=0.8,
ms=5,
transform="cyl",
)
ax.text(106, 4, "Singapore", color="red", size=7, transform="map")
inax = ax.hawkeye(
(0.97, 0.97),
size=0.23,
anchor="ur",
proj="merc",
extent=(103.76, 103.90, 1.27, 1.41),
shape="circle",
target="circle",
connector="line",
color="red",
indicator_kw={"linewidth": 1.5},
)
inax.format(land=True, landcolor="gray9", oceancolor="blue9")
inax.plot(
*singapore,
marker="o",
color="red",
markeredgecolor="white",
markeredgewidth=0.8,
ms=5,
transform="cyl",
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ exclude = ["**/*.ipynb"]

[tool.pytest.ini_options]
filterwarnings = [
"ignore:'parseString' deprecated - use 'parse_string':DeprecationWarning:matplotlib._fontconfig_pattern",
"ignore:'resetCache' deprecated - use 'reset_cache':DeprecationWarning:matplotlib._fontconfig_pattern",
]
mpl-default-style = { axes.prop_cycle = "cycler('color', ['#4c72b0ff', '#55a868ff', '#c44e52ff', '#8172b2ff', '#ccb974ff', '#64b5cdff'])" }
Expand Down
19 changes: 18 additions & 1 deletion ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,20 @@ def __call__(self, ax, renderer): # noqa: U100
return bbox


class _AspectAwareTransformedBoundsLocator(_TransformedBoundsLocator):
"""Preserve an inset's lower-left anchor after box-aspect adjustment."""

def __call__(self, ax, renderer):
bbox = super().__call__(ax, renderer)
aspect = ax.get_aspect()
if aspect == "auto" or ax.get_adjustable() != "box":
return bbox
box_aspect = float(aspect) * ax.get_data_ratio()
fig_bbox = ax.figure.bbox
fig_aspect = fig_bbox.height / fig_bbox.width
return bbox.shrunk_to_aspect(box_aspect, bbox, fig_aspect)


class _SideColorbarLocator:
"""Position a side colorbar beyond its parent axes decorations."""

Expand Down Expand Up @@ -1058,8 +1072,9 @@ def _add_inset_axes(
Add an inset axes using arbitrary projection.
"""
# Converting transform to figure-relative coordinates
bounds_input = bounds
transform = self._get_transform(transform, "axes")
locator = self._make_inset_locator(bounds, transform)
locator = self._make_inset_locator(bounds_input, transform)
bounds = locator(self, None).bounds
label = kwargs.pop("label", "inset_axes")
zorder = _not_none(zorder, 4)
Expand All @@ -1077,6 +1092,8 @@ def _add_inset_axes(
# automatically if we used data coords. Called by ax.apply_aspect()
cls = mproj.get_projection_class(kwargs.pop("projection"))
ax = cls(self.figure, bounds, zorder=zorder, label=label, **kwargs)
if ax._name in ("cartopy", "basemap"):
locator = _AspectAwareTransformedBoundsLocator(bounds_input, transform)
ax.set_axes_locator(locator)
ax._inset_parent = self
ax._inset_bounds = bounds
Expand Down
Loading
Loading