diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 06261a543f99..4b6e487a418d 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -46,3 +46,6 @@ matplotlib.spines.Spine._T # Parameter inconsistency due to 3.10 deprecation matplotlib.figure.FigureBase.get_figure + +# getitem method only exists for 3.10 deprecation backcompatability +matplotlib.inset.InsetIndicator.__getitem__ diff --git a/doc/api/index.rst b/doc/api/index.rst index 53f397a6817a..76b6cd5ffcef 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -104,6 +104,7 @@ Alphabetical list of modules: gridspec_api.rst hatch_api.rst image_api.rst + inset_api.rst layout_engine_api.rst legend_api.rst legend_handler_api.rst diff --git a/doc/api/inset_api.rst b/doc/api/inset_api.rst new file mode 100644 index 000000000000..d8b89a106a7a --- /dev/null +++ b/doc/api/inset_api.rst @@ -0,0 +1,8 @@ +******************** +``matplotlib.inset`` +******************** + +.. automodule:: matplotlib.inset + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/next_api_changes/behavior/27996-REC.rst b/doc/api/next_api_changes/behavior/27996-REC.rst new file mode 100644 index 000000000000..fe81a34073b8 --- /dev/null +++ b/doc/api/next_api_changes/behavior/27996-REC.rst @@ -0,0 +1,8 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator`. Use the +`~matplotlib.inset.InsetIndicator.rectangle` and +`~matplotlib.inset.InsetIndicator.connectors` properties of this artist to +access the objects that were previously returned directly. diff --git a/doc/users/next_whats_new/inset_indicator.rst b/doc/users/next_whats_new/inset_indicator.rst new file mode 100644 index 000000000000..614e830e016c --- /dev/null +++ b/doc/users/next_whats_new/inset_indicator.rst @@ -0,0 +1,18 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator` which contains the rectangle and +connector patches. These patches now update automatically so that + +.. code-block:: python + + ax.indicate_inset_zoom(ax_inset) + ax_inset.set_xlim(new_lim) + +now gives the same result as + +.. code-block:: python + + ax_inset.set_xlim(new_lim) + ax.indicate_inset_zoom(ax_inset) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 33fc42a4b860..d7b649ae437f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -16,6 +16,7 @@ import matplotlib.contour as mcontour import matplotlib.dates # noqa: F401, Register date unit converter as side effect. import matplotlib.image as mimage +import matplotlib.inset as minset import matplotlib.legend as mlegend import matplotlib.lines as mlines import matplotlib.markers as mmarkers @@ -419,9 +420,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax @_docstring.interpd - def indicate_inset(self, bounds, inset_ax=None, *, transform=None, + def indicate_inset(self, bounds=None, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, - zorder=4.99, **kwargs): + zorder=None, **kwargs): """ Add an inset indicator to the Axes. This is a rectangle on the plot at the position indicated by *bounds* that optionally has lines that @@ -433,18 +434,19 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Parameters ---------- - bounds : [x0, y0, width, height] + bounds : [x0, y0, width, height], optional Lower-left corner of rectangle to be marked, and its width - and height. + and height. If not set, the bounds will be calculated from the + data limits of *inset_ax*, which must be supplied. - inset_ax : `.Axes` + inset_ax : `.Axes`, optional An optional inset Axes to draw connecting lines to. Two lines are drawn connecting the indicator box to the inset Axes on corners chosen so as to not overlap with the indicator box. transform : `.Transform` Transform for the rectangle coordinates. Defaults to - `ax.transAxes`, i.e. the units of *rect* are in Axes-relative + ``ax.transAxes``, i.e. the units of *rect* are in Axes-relative coordinates. facecolor : :mpltype:`color`, default: 'none' @@ -469,15 +471,20 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Returns ------- - rectangle_patch : `.patches.Rectangle` - The indicator frame. + inset_indicator : `.inset.InsetIndicator` + An artist which contains - connector_lines : 4-tuple of `.patches.ConnectionPatch` - The four connector lines connecting to (lower_left, upper_left, - lower_right upper_right) corners of *inset_ax*. Two lines are - set with visibility to *False*, but the user can set the - visibility to True if the automatic choice is not deemed correct. + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ # to make the Axes connectors work, we need to apply the aspect to # the parent Axes. @@ -487,51 +494,13 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, transform = self.transData kwargs.setdefault('label', '_indicate_inset') - x, y, width, height = bounds - rectangle_patch = mpatches.Rectangle( - (x, y), width, height, + indicator_patch = minset.InsetIndicator( + bounds, inset_ax=inset_ax, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, zorder=zorder, transform=transform, **kwargs) - self.add_patch(rectangle_patch) - - connects = [] - - if inset_ax is not None: - # connect the inset_axes to the rectangle - for xy_inset_ax in [(0, 0), (0, 1), (1, 0), (1, 1)]: - # inset_ax positions are in axes coordinates - # The 0, 1 values define the four edges if the inset_ax - # lower_left, upper_left, lower_right upper_right. - ex, ey = xy_inset_ax - if self.xaxis.get_inverted(): - ex = 1 - ex - if self.yaxis.get_inverted(): - ey = 1 - ey - xy_data = x + ex * width, y + ey * height - p = mpatches.ConnectionPatch( - xyA=xy_inset_ax, coordsA=inset_ax.transAxes, - xyB=xy_data, coordsB=self.transData, - arrowstyle="-", zorder=zorder, - edgecolor=edgecolor, alpha=alpha) - connects.append(p) - self.add_patch(p) - - # decide which two of the lines to keep visible.... - pos = inset_ax.get_position() - bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) - rectbbox = mtransforms.Bbox.from_bounds( - *bounds - ).transformed(transform) - x0 = rectbbox.x0 < bboxins.x0 - x1 = rectbbox.x1 < bboxins.x1 - y0 = rectbbox.y0 < bboxins.y0 - y1 = rectbbox.y1 < bboxins.y1 - connects[0].set_visible(x0 ^ y0) - connects[1].set_visible(x0 == y1) - connects[2].set_visible(x1 == y0) - connects[3].set_visible(x1 ^ y1) - - return rectangle_patch, tuple(connects) if connects else None + self.add_artist(indicator_patch) + + return indicator_patch def indicate_inset_zoom(self, inset_ax, **kwargs): """ @@ -555,22 +524,23 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): Returns ------- - rectangle_patch : `.patches.Rectangle` - Rectangle artist. - - connector_lines : 4-tuple of `.patches.ConnectionPatch` - Each of four connector lines coming from the rectangle drawn on - this axis, in the order lower left, upper left, lower right, - upper right. - Two are set with visibility to *False*, but the user can - set the visibility to *True* if the automatic choice is not deemed - correct. + inset_indicator : `.inset.InsetIndicator` + An artist which contains + + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ - xlim = inset_ax.get_xlim() - ylim = inset_ax.get_ylim() - rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) - return self.indicate_inset(rect, inset_ax, **kwargs) + return self.indicate_inset(None, inset_ax, **kwargs) @_docstring.interpd def secondary_xaxis(self, location, functions=None, *, transform=None, **kwargs): diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 186177576067..de89990d7c13 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -15,6 +15,7 @@ from matplotlib.colors import Colormap, Normalize from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage +from matplotlib.inset import InsetIndicator from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase from matplotlib.lines import Line2D, AxLine @@ -74,17 +75,17 @@ class Axes(_AxesBase): ) -> Axes: ... def indicate_inset( self, - bounds: tuple[float, float, float, float], + bounds: tuple[float, float, float, float] | None = ..., inset_ax: Axes | None = ..., *, transform: Transform | None = ..., facecolor: ColorType = ..., edgecolor: ColorType = ..., alpha: float = ..., - zorder: float = ..., + zorder: float | None = ..., **kwargs - ) -> Rectangle: ... - def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ... + ) -> InsetIndicator: ... + def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> InsetIndicator: ... def secondary_xaxis( self, location: Literal["top", "bottom"] | float, diff --git a/lib/matplotlib/inset.py b/lib/matplotlib/inset.py new file mode 100644 index 000000000000..bab69491303e --- /dev/null +++ b/lib/matplotlib/inset.py @@ -0,0 +1,269 @@ +""" +The inset module defines the InsetIndicator class, which draws the rectangle and +connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`. +""" + +from . import _api, artist, transforms +from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle +from matplotlib.path import Path + + +_shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth') + + +class InsetIndicator(artist.Artist): + """ + An artist to highlight an area of interest. + + An inset indicator is a rectangle on the plot at the position indicated by + *bounds* that optionally has lines that connect the rectangle to an inset + Axes (`.Axes.inset_axes`). + + .. versionadded:: 3.10 + """ + zorder = 4.99 + + def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs): + """ + Parameters + ---------- + bounds : [x0, y0, width, height], optional + Lower-left corner of rectangle to be marked, and its width + and height. If not set, the bounds will be calculated from the + data limits of inset_ax, which must be supplied. + + inset_ax : `~.axes.Axes`, optional + An optional inset Axes to draw connecting lines to. Two lines are + drawn connecting the indicator box to the inset Axes on corners + chosen so as to not overlap with the indicator box. + + zorder : float, default: 4.99 + Drawing order of the rectangle and connector lines. The default, + 4.99, is just below the default level of inset Axes. + + **kwargs + Other keyword arguments are passed on to the `.Rectangle` patch. + """ + if bounds is None and inset_ax is None: + raise ValueError("At least one of bounds or inset_ax must be supplied") + + self._inset_ax = inset_ax + + if bounds is None: + # Work out bounds from inset_ax + self._auto_update_bounds = True + bounds = self._bounds_from_inset_ax() + else: + self._auto_update_bounds = False + + x, y, width, height = bounds + + self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs) + + # Connector positions cannot be calculated till the artist has been added + # to an axes, so just make an empty list for now. + self._connectors = [] + + super().__init__() + self.set_zorder(zorder) + + # Initial style properties for the artist should match the rectangle. + for prop in _shared_properties: + setattr(self, f'_{prop}', artist.getp(self._rectangle, prop)) + + def _shared_setter(self, prop, val): + """ + Helper function to set the same style property on the artist and its children. + """ + setattr(self, f'_{prop}', val) + + artist.setp([self._rectangle, *self._connectors], prop, val) + + def set_alpha(self, alpha): + # docstring inherited + self._shared_setter('alpha', alpha) + + def set_edgecolor(self, color): + """ + Set the edge color of the rectangle and the connectors. + + Parameters + ---------- + color : :mpltype:`color` or None + """ + self._shared_setter('edgecolor', color) + + def set_color(self, c): + """ + Set the edgecolor of the rectangle and the connectors, and the + facecolor for the rectangle. + + Parameters + ---------- + c : :mpltype:`color` + """ + self._shared_setter('edgecolor', c) + self._shared_setter('facecolor', c) + + def set_linewidth(self, w): + """ + Set the linewidth in points of the rectangle and the connectors. + + Parameters + ---------- + w : float or None + """ + self._shared_setter('linewidth', w) + + def set_linestyle(self, ls): + """ + Set the linestyle of the rectangle and the connectors. + + ========================================== ================= + linestyle description + ========================================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= + + Alternatively a dash tuple of the following form can be provided:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple of on and off ink in points. + + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + The line style. + """ + self._shared_setter('linestyle', ls) + + def _bounds_from_inset_ax(self): + xlim = self._inset_ax.get_xlim() + ylim = self._inset_ax.get_ylim() + return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) + + def _update_connectors(self): + (x, y) = self._rectangle.get_xy() + width = self._rectangle.get_width() + height = self._rectangle.get_height() + + existing_connectors = self._connectors or [None] * 4 + + # connect the inset_axes to the rectangle + for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)], + existing_connectors): + # inset_ax positions are in axes coordinates + # The 0, 1 values define the four edges if the inset_ax + # lower_left, upper_left, lower_right upper_right. + ex, ey = xy_inset_ax + if self.axes.xaxis.get_inverted(): + ex = 1 - ex + if self.axes.yaxis.get_inverted(): + ey = 1 - ey + xy_data = x + ex * width, y + ey * height + if existing is None: + # Create new connection patch with styles inherited from the + # parent artist. + p = ConnectionPatch( + xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes, + xyB=xy_data, coordsB=self.axes.transData, + arrowstyle="-", + edgecolor=self._edgecolor, alpha=self.get_alpha(), + linestyle=self._linestyle, linewidth=self._linewidth) + self._connectors.append(p) + else: + # Only update positioning of existing connection patch. We + # do not want to override any style settings made by the user. + existing.xy1 = xy_inset_ax + existing.xy2 = xy_data + existing.coords1 = self._inset_ax.transAxes + existing.coords2 = self.axes.transData + + if existing is None: + # decide which two of the lines to keep visible.... + pos = self._inset_ax.get_position() + bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) + rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed( + self._rectangle.get_transform()) + x0 = rectbbox.x0 < bboxins.x0 + x1 = rectbbox.x1 < bboxins.x1 + y0 = rectbbox.y0 < bboxins.y0 + y1 = rectbbox.y1 < bboxins.y1 + self._connectors[0].set_visible(x0 ^ y0) + self._connectors[1].set_visible(x0 == y1) + self._connectors[2].set_visible(x1 == y0) + self._connectors[3].set_visible(x1 ^ y1) + + @property + def rectangle(self): + """`.Rectangle`: the indicator frame.""" + return self._rectangle + + @property + def connectors(self): + """ + 4-tuple of `.patches.ConnectionPatch` or None + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + """ + if self._inset_ax is None: + return + + if self._auto_update_bounds: + self._rectangle.set_bounds(self._bounds_from_inset_ax()) + self._update_connectors() + return tuple(self._connectors) + + def draw(self, renderer): + # docstring inherited + conn_same_style = [] + + # Figure out which connectors have the same style as the box, so should + # be drawn as a single path. + for conn in self.connectors or []: + if conn.get_visible(): + drawn = False + for s in _shared_properties: + if artist.getp(self._rectangle, s) != artist.getp(conn, s): + # Draw this connector by itself + conn.draw(renderer) + drawn = True + break + + if not drawn: + # Connector has same style as box. + conn_same_style.append(conn) + + if conn_same_style: + # Since at least one connector has the same style as the rectangle, draw + # them as a compound path. + artists = [self._rectangle] + conn_same_style + paths = [a.get_transform().transform_path(a.get_path()) for a in artists] + path = Path.make_compound_path(*paths) + + # Create a temporary patch to draw the path. + p = PathPatch(path) + p.update_from(self._rectangle) + p.set_transform(transforms.IdentityTransform()) + p.draw(renderer) + + return + + # Just draw the rectangle + self._rectangle.draw(renderer) + + @_api.deprecated( + '3.10', + message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single ' + 'InsetIndicator artist with a rectangle property and a connectors ' + 'property. From 3.12 it will no longer be possible to unpack the ' + 'return value into two elements.')) + def __getitem__(self, key): + return [self._rectangle, self.connectors][key] diff --git a/lib/matplotlib/inset.pyi b/lib/matplotlib/inset.pyi new file mode 100644 index 000000000000..e895fd7be27c --- /dev/null +++ b/lib/matplotlib/inset.pyi @@ -0,0 +1,25 @@ +from . import artist +from .axes import Axes +from .backend_bases import RendererBase +from .patches import ConnectionPatch, Rectangle + +from .typing import ColorType, LineStyleType + +class InsetIndicator(artist.Artist): + def __init__( + self, + bounds: tuple[float, float, float, float] | None = ..., + inset_ax: Axes | None = ..., + zorder: float | None = ..., + **kwargs + ) -> None: ... + def set_alpha(self, alpha: float | None) -> None: ... + def set_edgecolor(self, color: ColorType | None) -> None: ... + def set_color(self, c: ColorType | None) -> None: ... + def set_linewidth(self, w: float | None) -> None: ... + def set_linestyle(self, ls: LineStyleType | None) -> None: ... + @property + def rectangle(self) -> Rectangle: ... + @property + def connectors(self) -> tuple[ConnectionPatch, ConnectionPatch, ConnectionPatch, ConnectionPatch] | None: ... + def draw(self, renderer: RendererBase) -> None: ... diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index 657adfd4e835..e8cf4d129f8d 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -43,6 +43,7 @@ python_sources = [ 'gridspec.py', 'hatch.py', 'image.py', + 'inset.py', 'layout_engine.py', 'legend_handler.py', 'legend.py', @@ -110,6 +111,7 @@ typing_sources = [ 'gridspec.pyi', 'hatch.pyi', 'image.pyi', + 'inset.pyi', 'layout_engine.pyi', 'legend_handler.pyi', 'legend.pyi', diff --git a/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png new file mode 100644 index 000000000000..df8b768725d7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6cb761ea02e1..33c81c44abaf 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7513,12 +7513,12 @@ def test_inset(): rect = [xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]] - rec, connectors = ax.indicate_inset(bounds=rect) - assert connectors is None + inset = ax.indicate_inset(bounds=rect) + assert inset.connectors is None fig.canvas.draw() xx = np.array([[1.5, 2.], [2.15, 2.5]]) - assert np.all(rec.get_bbox().get_points() == xx) + assert np.all(inset.rectangle.get_bbox().get_points() == xx) def test_zoom_inset(): @@ -7542,9 +7542,10 @@ def test_zoom_inset(): axin1.set_ylim([2, 2.5]) axin1.set_aspect(ax.get_aspect()) - rec, connectors = ax.indicate_inset_zoom(axin1) - assert len(connectors) == 4 + with pytest.warns(mpl.MatplotlibDeprecationWarning): + rec, connectors = ax.indicate_inset_zoom(axin1) fig.canvas.draw() + assert len(connectors) == 4 xx = np.array([[1.5, 2.], [2.15, 2.5]]) assert np.all(rec.get_bbox().get_points() == xx) @@ -7594,8 +7595,8 @@ def test_indicate_inset_inverted(x_inverted, y_inverted): if y_inverted: ax1.invert_yaxis() - rect, bounds = ax1.indicate_inset([2, 2, 5, 4], ax2) - lower_left, upper_left, lower_right, upper_right = bounds + inset = ax1.indicate_inset([2, 2, 5, 4], ax2) + lower_left, upper_left, lower_right, upper_right = inset.connectors sign_x = -1 if x_inverted else 1 sign_y = -1 if y_inverted else 1 diff --git a/lib/matplotlib/tests/test_inset.py b/lib/matplotlib/tests/test_inset.py new file mode 100644 index 000000000000..c25580214c80 --- /dev/null +++ b/lib/matplotlib/tests/test_inset.py @@ -0,0 +1,106 @@ +import platform + +import pytest + +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +from matplotlib.testing.decorators import image_comparison, check_figures_equal + + +def test_indicate_inset_no_args(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='At least one of bounds or inset_ax'): + ax.indicate_inset() + + +@check_figures_equal(extensions=["png"]) +def test_zoom_inset_update_limits(fig_test, fig_ref): + # Updating the inset axes limits should also update the indicator #19768 + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + for ax in ax_ref, ax_test: + ax.set_xlim([0, 5]) + ax.set_ylim([0, 5]) + + inset_ref = ax_ref.inset_axes([0.6, 0.6, 0.3, 0.3]) + inset_test = ax_test.inset_axes([0.6, 0.6, 0.3, 0.3]) + + inset_ref.set_xlim([1, 2]) + inset_ref.set_ylim([3, 4]) + ax_ref.indicate_inset_zoom(inset_ref) + + ax_test.indicate_inset_zoom(inset_test) + inset_test.set_xlim([1, 2]) + inset_test.set_ylim([3, 4]) + + +def test_inset_indicator_update_styles(): + fig, ax = plt.subplots() + inset = ax.inset_axes([0.6, 0.6, 0.3, 0.3]) + inset.set_xlim([0.2, 0.4]) + inset.set_ylim([0.2, 0.4]) + + indicator = ax.indicate_inset_zoom( + inset, edgecolor='red', alpha=0.5, linewidth=2, linestyle='solid') + + # Changing the rectangle styles should not affect the connectors. + indicator.rectangle.set(color='blue', linestyle='dashed', linewidth=42, alpha=0.2) + for conn in indicator.connectors: + assert mcolors.same_color(conn.get_edgecolor()[:3], 'red') + assert conn.get_alpha() == 0.5 + assert conn.get_linestyle() == 'solid' + assert conn.get_linewidth() == 2 + + # Changing the indicator styles should affect both rectangle and connectors. + indicator.set(color='green', linestyle='dotted', linewidth=7, alpha=0.8) + assert mcolors.same_color(indicator.rectangle.get_facecolor()[:3], 'green') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'green') + assert patch.get_alpha() == 0.8 + assert patch.get_linestyle() == 'dotted' + assert patch.get_linewidth() == 7 + + indicator.set_edgecolor('purple') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'purple') + + # This should also be true if connectors weren't created yet. + indicator._connectors = [] + indicator.set(color='burlywood', linestyle='dashdot', linewidth=4, alpha=0.4) + assert mcolors.same_color(indicator.rectangle.get_facecolor()[:3], 'burlywood') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'burlywood') + assert patch.get_alpha() == 0.4 + assert patch.get_linestyle() == 'dashdot' + assert patch.get_linewidth() == 4 + + indicator._connectors = [] + indicator.set_edgecolor('thistle') + for patch in (*indicator.connectors, indicator.rectangle): + assert mcolors.same_color(patch.get_edgecolor()[:3], 'thistle') + + +def test_inset_indicator_zorder(): + fig, ax = plt.subplots() + rect = [0.2, 0.2, 0.3, 0.4] + + inset = ax.indicate_inset(rect) + assert inset.get_zorder() == 4.99 + + inset = ax.indicate_inset(rect, zorder=42) + assert inset.get_zorder() == 42 + + +@image_comparison(['zoom_inset_connector_styles.png'], remove_text=True, style='mpl20', + tol=0.024 if platform.machine() == 'arm64' else 0) +def test_zoom_inset_connector_styles(): + fig, axs = plt.subplots(2) + for ax in axs: + ax.plot([1, 2, 3]) + + axs[1].set_xlim(0.5, 1.5) + indicator = axs[0].indicate_inset_zoom(axs[1], linewidth=5) + # Make one visible connector a different style + indicator.connectors[1].set_linestyle('dashed') + indicator.connectors[1].set_color('blue')