From 07f5ef1985069ca1f3d08fe524e16019ce5ea56f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Jul 2026 21:30:38 +1000 Subject: [PATCH] Revert misplaced hawkeye commits from main Remove the circular inset implementation and its follow-up documentation simplification from main because both changes belong on the feature/hawkeye branch. --- docs/insets_panels.py | 73 +----- ultraplot/axes/base.py | 19 +- ultraplot/axes/geo.py | 438 ---------------------------------- ultraplot/tests/test_inset.py | 147 +----------- 4 files changed, 3 insertions(+), 674 deletions(-) diff --git a/docs/insets_panels.py b/docs/insets_panels.py index 7ce445a16..03f9d6cbd 100644 --- a/docs/insets_panels.py +++ b/docs/insets_panels.py @@ -88,8 +88,8 @@ ) # %% -import numpy as np import ultraplot as uplt +import numpy as np state = np.random.RandomState(51423) data = (state.rand(20, 20) - 0.48).cumsum(axis=1).cumsum(axis=0) @@ -192,74 +192,3 @@ ) 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 `__ -# 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", - connectors="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", -) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 8da6e6409..340a2960d 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -750,20 +750,6 @@ 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.""" @@ -1072,9 +1058,8 @@ 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_input, transform) + locator = self._make_inset_locator(bounds, transform) bounds = locator(self, None).bounds label = kwargs.pop("label", "inset_axes") zorder = _not_none(zorder, 4) @@ -1092,8 +1077,6 @@ 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 diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 308ac446a..4df8ff08f 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -8,7 +8,6 @@ import copy import inspect from functools import partial -from numbers import Real try: # From python 3.12 @@ -20,7 +19,6 @@ from typing import Any, Optional, Protocol import matplotlib.axis as maxis -import matplotlib.axes as maxes import matplotlib.collections as mcollections import matplotlib.patches as mpatches import matplotlib.path as mpath @@ -73,232 +71,6 @@ _BASEMAP_LABEL_X_SCALE = 0.25 # empirical spacing to mimic cartopy _CARTOPY_LABEL_SIDES = ("labelleft", "labelright", "labelbottom", "labeltop", "geo") _BASEMAP_LABEL_SIDES = ("labelleft", "labelright", "labelbottom", "labeltop", "geo") -_HAWKEYE_ANCHORS = { - "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), -} - - -class _AnchoredInsetLocator: - """Locate an inset by anchoring one of its points to a parent coordinate.""" - - def __init__(self, parent, xy, size, transform, anchor, square=False): - self._parent = parent - self._xy = tuple(xy) - self._size = tuple(size) - self._transform = transform - self._anchor = anchor - self._square = square - - def __call__(self, ax, renderer): # noqa: U100 - parent = self._parent - transform = self._transform - if ccrs is not None and isinstance(transform, ccrs.CRS): - transform = transform._as_mpl_transform(parent) - xy = transform.transform(self._xy) - transfig = getattr(parent.figure, "transSubfigure", parent.figure.transFigure) - x, y = transfig.inverted().transform(xy) - parent_bbox = parent.get_position() - width = self._size[0] * parent_bbox.width - height = self._size[1] * parent_bbox.height - if self._square: - figure_bbox = parent.figure.bbox - side = min(width * figure_bbox.width, height * figure_bbox.height) - width = side / figure_bbox.width - height = side / figure_bbox.height - return mtransforms.Bbox.from_bounds( - x - self._anchor[0] * width, - y - self._anchor[1] * height, - width, - height, - ) - - -def _parse_hawkeye_anchor(anchor): - """Translate a named hawkeye anchor to normalized axes coordinates.""" - if isinstance(anchor, str): - try: - return _HAWKEYE_ANCHORS[anchor] - except KeyError as exc: - options = ", ".join(map(repr, _HAWKEYE_ANCHORS)) - raise ValueError( - f"Invalid anchor {anchor!r}. Must be one of {options}." - ) from exc - if len(anchor) != 2: - raise ValueError(f"anchor must have length 2, got {anchor!r}.") - return tuple(anchor) - - -def _parse_hawkeye_size(size): - """Normalize scalar hawkeye sizes to square inset dimensions.""" - if isinstance(size, Real): - size = (size, size) - if len(size) != 2 or any(value <= 0 for value in size): - raise ValueError(f"size must contain two positive values, got {size!r}.") - return tuple(size) - - -def _parse_hawkeye_extent_transform(transform): - """Translate hawkeye extent transforms to cartopy coordinate systems.""" - if transform == "map": - return ccrs.PlateCarree() - if isinstance(transform, ccrs.CRS): - return transform - raise ValueError("extent_transform must be 'map' or a cartopy CRS.") - - -def _parse_hawkeye_connectors(connectors): - """Normalize connector shorthand to a named presentation mode.""" - if connectors is False: - return None - if connectors is True: - return "corners" - if connectors in ("corners", "line"): - return connectors - raise ValueError("connectors must be False, True, 'corners', or 'line'.") - - -def _parse_hawkeye_shape(value, name): - """Validate a hawkeye inset or target shape.""" - if value not in ("box", "circle"): - raise ValueError(f"{name} must be 'box' or 'circle'.") - return value - - -def _square_hawkeye_view(inset): - """Expand the shorter projected dimension to make a square map viewport.""" - x0, x1 = inset.get_xlim() - y0, y1 = inset.get_ylim() - width, height = x1 - x0, y1 - y0 - if width > height: - center = (y0 + y1) / 2 - inset.set_ylim(center - width / 2, center + width / 2) - elif height > width: - center = (x0 + x1) / 2 - inset.set_xlim(center - height / 2, center + height / 2) - - -def _infer_hawkeye_relation(parent_extent, inset_extent): - """Infer whether an inset is a geographic detail or overview.""" - parent_west, parent_east, parent_south, parent_north = parent_extent - inset_west, inset_east, inset_south, inset_north = inset_extent - parent_area = abs((parent_east - parent_west) * (parent_north - parent_south)) - inset_area = abs((inset_east - inset_west) * (inset_north - inset_south)) - return "overview" if inset_area > parent_area else "detail" - - -def _segments_intersect(start1, end1, start2, end2): - """Return whether two display-coordinate line segments intersect.""" - - def _cross(origin, point1, point2): - return np.cross(point1 - origin, point2 - origin) - - cross1 = _cross(start1, end1, start2) - cross2 = _cross(start1, end1, end2) - cross3 = _cross(start2, end2, start1) - cross4 = _cross(start2, end2, end1) - return cross1 * cross2 < 0 and cross3 * cross4 < 0 - - -def _select_hawkeye_connector_pairs(extent_display, frame_display): - """Select the shortest pair of non-crossing overview connectors.""" - candidates = [ - (extent_index, frame_index) - for extent_index in range(len(extent_display)) - for frame_index in range(len(frame_display)) - ] - best = None - for first_index, first in enumerate(candidates): - for second in candidates[first_index + 1 :]: - if first[0] == second[0] or first[1] == second[1]: - continue - first_start, first_end = extent_display[first[0]], frame_display[first[1]] - second_start, second_end = ( - extent_display[second[0]], - frame_display[second[1]], - ) - if _segments_intersect(first_start, first_end, second_start, second_end): - continue - distance = np.linalg.norm(first_end - first_start) + np.linalg.norm( - second_end - second_start - ) - if best is None or distance < best[0]: - best = (distance, (first, second)) - return best[1] if best is not None else () - - -def _add_hawkeye_overview_connectors(parent, inset, extent, transform, **kwargs): - """Connect the parent frame to its geographic extent on an overview inset.""" - west, east, south, north = extent - extent_corners = np.array( - ((west, south), (west, north), (east, south), (east, north)) - ) - frame_corners = np.array(((0, 0), (0, 1), (1, 0), (1, 1))) - extent_transform = transform._as_mpl_transform(inset) - pairs = _select_hawkeye_connector_pairs( - extent_transform.transform(extent_corners), - parent.transAxes.transform(frame_corners), - ) - - kwargs = dict(kwargs) - kwargs.pop("facecolor", None) - kwargs["color"] = kwargs.pop("edgecolor", kwargs.get("color", None)) - kwargs.setdefault("clip_on", False) - connectors = [] - for extent_index, frame_index in pairs: - connector = mpatches.ConnectionPatch( - extent_corners[extent_index], - frame_corners[frame_index], - coordsA=extent_transform, - coordsB=parent.transAxes, - axesA=inset, - axesB=parent, - **kwargs, - ) - parent.figure.add_artist(connector) - connectors.append(connector) - return tuple(connectors) - - -def _add_hawkeye_leader( - inset, target_axes, target_xy, transform, target_patch=None, **kwargs -): - """Draw a leader from an inset edge to a geographic target point.""" - kwargs = dict(kwargs) - kwargs.pop("facecolor", None) - kwargs["color"] = kwargs.pop("edgecolor", kwargs.get("color", None)) - kwargs.setdefault("clip_on", False) - connector = mpatches.ConnectionPatch( - (0.5, 0.5), - target_xy, - coordsA=inset.transAxes, - coordsB=transform._as_mpl_transform(target_axes), - axesA=inset, - axesB=target_axes, - patchA=inset.patch, - patchB=target_patch, - **kwargs, - ) - # The parent draws after its map artists but before the inset axes. Clipping the - # source point to ``inset.patch`` stops the leader at any custom inset boundary. - target_axes.add_artist(connector) - return connector # Format docstring @@ -465,60 +237,6 @@ def _add_hawkeye_leader( """ docstring._snippet_manager["geo.format"] = _format_docstring -_hawkeye_docstring = """ -Add a transform-anchored geographic callout inset. - -Parameters ----------- -xy : 2-tuple of float - The parent-axes coordinate at which to anchor the inset. -size : float or 2-tuple of float - The requested inset width and height as fractions of the parent axes box. A scalar - requests equal width and height before geographic aspect adjustment. -transform : {'axes', 'data', 'figure', 'subfigure', 'map'} or transform, default: 'axes' - Coordinate system for *xy*. ``'map'`` uses `cartopy.crs.PlateCarree`. -anchor : str or 2-tuple of float, default: 'upper right' - The inset point placed at *xy*. String aliases include ``'ul'``, ``'ur'``, - ``'ll'``, ``'lr'``, and ``'c'``. -aspect : {'auto', 'projection'} or float, default: 'projection' - The inset aspect. ``'projection'`` preserves the geographic projection aspect - inside the requested box, while ``'auto'`` stretches the map to fill that box. - Circular insets expand the shorter projected dimension to avoid distorting the - projection. -grid : bool, default: False - Whether to draw gridlines in the inset. -extent : 4-tuple of float, optional - The geographic scope ``(west, east, south, north)`` displayed by the inset. -extent_transform : {'map'} or cartopy CRS, default: 'map' - Coordinate system for *extent*. ``'map'`` uses `cartopy.crs.PlateCarree`. -relation : {'auto', 'detail', 'overview'}, default: 'auto' - Whether the inset is a zoomed detail of the parent or an overview containing the - parent extent. ``'auto'`` compares the rectangular extent sizes. This determines - where the extent outline and connectors are drawn. -indicator : bool, default: True - Whether to outline *extent* on the parent map when an extent is supplied. -connectors : {False, True, 'corners', 'line'}, default: False - The connector presentation. ``True`` and ``'corners'`` draw corner links between - the extent outline and inset. ``'line'`` draws one leader from the inset boundary - to the target centre. Requires *extent*. -shape, target : {'box', 'circle'}, default: 'box' - The inset clipping shape and target marker shape. Circular targets require - ``connectors='line'`` or no connectors. -indicator_kw : dict-like, optional - Patch properties for the extent outline and connector lines. - -Other parameters ----------------- -**kwargs - Passed to `~Axes.inset_axes`. - -Returns -------- -GeoAxes - The geographic inset axes. -""" -docstring._snippet_manager["geo.hawkeye"] = _hawkeye_docstring - _choropleth_docstring = """ Draw polygon geometries colored by numeric values. @@ -1369,162 +1087,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._edge_lat_labels: list[mtext.Text] = [] super().__init__(*args, **kwargs) - @docstring._snippet_manager - def hawkeye( - self, - xy: Sequence[float], - size: float | Sequence[float], - *, - transform: Any = "axes", - anchor: str | Sequence[float] = "upper right", - aspect: str | float = "projection", - extent: Optional[Sequence[float]] = None, - extent_transform: Any = "map", - relation: str = "auto", - indicator: bool = True, - connectors: bool | str = False, - shape: str = "box", - target: str = "box", - indicator_kw: Optional[Mapping[str, Any]] = None, - **kwargs: Any, - ) -> "GeoAxes": - """ - %(geo.hawkeye)s - """ - if len(xy) != 2: - raise ValueError(f"xy must have length 2, got {xy!r}.") - size = _parse_hawkeye_size(size) - anchor = _parse_hawkeye_anchor(anchor) - transform = self._get_transform(transform, default="axes") - connectors = _parse_hawkeye_connectors(connectors) - shape = _parse_hawkeye_shape(shape, "shape") - target = _parse_hawkeye_shape(target, "target") - if connectors and extent is None: - raise ValueError("connectors requires extent.") - if connectors == "corners" and target == "circle": - raise ValueError("target='circle' requires connectors='line' or False.") - if relation not in ("auto", "detail", "overview"): - raise ValueError("relation must be 'auto', 'detail', or 'overview'.") - if extent is not None: - if self._name != "cartopy": - raise NotImplementedError( - "hawkeye(extent=...) currently requires the cartopy backend." - ) - if len(extent) != 4: - raise ValueError(f"extent must have length 4, got {extent!r}.") - west, east, south, north = extent - if west >= east or south >= north: - raise ValueError( - "extent must be ordered as (west, east, south, north) with " - "west < east and south < north." - ) - extent_transform = _parse_hawkeye_extent_transform(extent_transform) - if relation == "auto": - relation = _infer_hawkeye_relation( - self.get_extent(crs=extent_transform), extent - ) - kwargs.setdefault("grid", False) - inset = self._add_inset_axes((0, 0, *size), transform="axes", **kwargs) - # Hawkeyes may intentionally extend outside their parent axes. They must not - # claim that space when the figure performs automatic layout. - inset.set_in_layout(False) - if extent is not None: - if isinstance(extent_transform, ccrs.PlateCarree) and np.allclose( - extent, (-180, 180, -90, 90) - ): - inset.set_global() - else: - inset.set_extent(extent, crs=extent_transform) - if aspect == "projection": - aspect = inset.get_aspect() - inset.set_aspect(aspect) - inset.set_anchor(anchor) - inset.set_axes_locator( - _AnchoredInsetLocator( - self, - xy, - size, - transform, - anchor, - square=shape == "circle", - ) - ) - if shape == "circle": - if aspect != "auto": - _square_hawkeye_view(inset) - x0, x1 = inset.get_xlim() - y0, y1 = inset.get_ylim() - radius_x = (x1 - x0) / 2 - radius_y = (y1 - y0) / 2 - theta = np.linspace(0, 2 * np.pi, 256) - circle_path = mpath.Path( - np.column_stack( - ( - (x0 + x1) / 2 + radius_x * np.cos(theta), - (y0 + y1) / 2 + radius_y * np.sin(theta), - ) - ) - ) - inset.set_boundary(circle_path, transform=inset.transData) - inset._hawkeye_relation = relation - if indicator and extent is not None: - indicator_kw = dict(indicator_kw or {}) - indicator_kw.setdefault( - "edgecolor", kwargs.get("color", rc["axes.edgecolor"]) - ) - indicator_kw.setdefault("facecolor", "none") - indicator_kw.setdefault("linewidth", rc["axes.linewidth"]) - indicator_kw.setdefault("zorder", 3.5) - if connectors == "corners" and relation == "detail": - inset._hawkeye_indicator = maxes.Axes.indicate_inset_zoom( - self, inset, **indicator_kw - ) - else: - outline_axes = self if relation == "detail" else inset - outline_extent = ( - extent - if relation == "detail" - else self.get_extent(crs=extent_transform) - ) - west, east, south, north = outline_extent - if target == "circle": - patch = mpatches.Circle( - ((west + east) / 2, (south + north) / 2), - min(east - west, north - south) / 2, - transform=extent_transform, - **indicator_kw, - ) - else: - patch = mpatches.Rectangle( - (west, south), - east - west, - north - south, - transform=extent_transform, - **indicator_kw, - ) - outline_axes.add_patch(patch) - inset._hawkeye_indicator = patch - if connectors == "corners": - inset._hawkeye_connectors = _add_hawkeye_overview_connectors( - self, - inset, - outline_extent, - extent_transform, - **indicator_kw, - ) - elif connectors == "line": - inset._hawkeye_connectors = ( - _add_hawkeye_leader( - inset, - outline_axes, - ((west + east) / 2, (south + north) / 2), - extent_transform, - target_patch=patch, - **indicator_kw, - ), - ) - return inset - @override def _sharey_limits(self, sharey: "GeoAxes") -> None: return self._share_limits_with(sharey, which="y") diff --git a/ultraplot/tests/test_inset.py b/ultraplot/tests/test_inset.py index 96ae54626..9a1dfc611 100644 --- a/ultraplot/tests/test_inset.py +++ b/ultraplot/tests/test_inset.py @@ -1,8 +1,4 @@ -import numpy as np -import pytest -from matplotlib import patches as mpatches - -import ultraplot as uplt +import ultraplot as uplt, numpy as np, pytest @pytest.mark.mpl_image_compare @@ -31,144 +27,3 @@ def test_inset_basic(): titleabove=False, ) return fig - - -def test_geo_inset_preserves_bounds_anchor(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - parent = ax[0] - inset = parent.inset((0.1, 0.2, 0.4, 0.2), transform="axes") - fig.canvas.draw() - - expected = fig.transFigure.inverted().transform( - parent.transAxes.transform((0.1, 0.2)) - ) - actual = inset.get_position() - np.testing.assert_allclose((actual.x0, actual.y0), expected) - assert actual.width < 0.4 * parent.get_position().width - uplt.close(fig) - - -def test_hawkeye_projection_and_auto_aspects(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - parent = ax[0] - projected = parent.hawkeye((0.9, 0.9), size=(0.2, 0.2)) - stretched = parent.hawkeye((0.9, 0.6), size=0.2, aspect="auto") - fig.canvas.draw() - - projected_bbox = projected.get_position() - stretched_bbox = stretched.get_position() - np.testing.assert_allclose( - (stretched_bbox.width, stretched_bbox.height), - (0.2 * parent.get_position().width, 0.2 * parent.get_position().height), - ) - assert projected_bbox.width != pytest.approx(projected_bbox.height) - uplt.close(fig) - - -def test_hawkeye_extent_and_connectors(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - inset = ax[0].hawkeye( - (0.9, 0.9), - size=0.2, - extent=(120, 180, 10, 30), - connectors=True, - ) - fig.canvas.draw() - - assert inset._hawkeye_relation == "detail" - assert len(inset._hawkeye_indicator.connectors) == 4 - uplt.close(fig) - - -def test_hawkeye_auto_relation_handles_disjoint_detail_extent(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - ax[0].set_extent((30, 100, -20, 30)) - inset = ax[0].hawkeye((0.9, 0.9), size=0.2, extent=(120, 180, 10, 30)) - - assert inset._hawkeye_relation == "detail" - uplt.close(fig) - - -def test_hawkeye_connectors_require_extent(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - with pytest.raises(ValueError, match="requires extent"): - ax[0].hawkeye((0.9, 0.9), size=0.2, connectors=True) - uplt.close(fig) - - -def test_hawkeye_circular_cutout_and_leader(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - inset = ax[0].hawkeye( - (0.9, 0.9), - size=0.2, - extent=(120, 180, 10, 30), - connectors="line", - shape="circle", - target="circle", - ) - fig.canvas.draw() - - assert isinstance(inset._hawkeye_indicator, mpatches.Circle) - assert len(inset._hawkeye_connectors) == 1 - connector = inset._hawkeye_connectors[0] - assert connector.patchA is inset.patch - assert connector.patchB is inset._hawkeye_indicator - assert connector.axes is ax[0] - assert not inset.get_in_layout() - bbox = inset.get_position() - figure_bbox = fig.bbox - assert bbox.width * figure_bbox.width == pytest.approx( - bbox.height * figure_bbox.height - ) - assert inset.get_aspect() == 1 - assert not inset._gridlines_major.xline_artists - assert not inset._gridlines_major.yline_artists - uplt.close(fig) - - -def test_hawkeye_overview_connectors(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - parent = ax[0] - parent.set_extent((145, 155, -38, -32)) - inset = parent.hawkeye( - (0.9, 0.9), - size=0.2, - extent=(110, 180, -50, 0), - connectors=True, - ) - fig.canvas.draw() - - assert inset._hawkeye_relation == "overview" - assert inset._hawkeye_indicator in inset.patches - assert len(inset._hawkeye_connectors) == 2 - uplt.close(fig) - - -def test_hawkeye_overview_indicator(): - pytest.importorskip("cartopy.crs") - - fig, ax = uplt.subplots(proj="cyl") - parent = ax[0] - parent.set_extent((145, 155, -38, -32)) - inset = parent.hawkeye( - (0.9, 0.9), size=0.2, extent=(110, 180, -50, 0), relation="overview" - ) - fig.canvas.draw() - - assert inset._hawkeye_indicator in inset.patches - assert inset._hawkeye_indicator not in parent.patches - uplt.close(fig)