Skip to content

Create InsetIndicator artist #27996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ci/mypy-stubtest-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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__
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions doc/api/inset_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
********************
``matplotlib.inset``
********************

.. automodule:: matplotlib.inset
:members:
:undoc-members:
:show-inheritance:
8 changes: 8 additions & 0 deletions doc/api/next_api_changes/behavior/27996-REC.rst
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions doc/users/next_whats_new/inset_indicator.rst
Original file line number Diff line number Diff line change
@@ -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)
110 changes: 40 additions & 70 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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.

Comment on lines +477 to +485
Copy link
Member

@story645 story645 Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to make this an interpolated doc, even if it's only used in like two places? That way it can be defined in the module? Only thinking about this b/c of how it's being done for colorizer:

mpl._docstring.interpd.update(
colorizer_doc="""\
colorizer : `~matplotlib.colorizer.Colorizer` or None, default: None
The Colorizer object used to map color to data. If None, a Colorizer
object is created base on *norm* and *cmap*.""",
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly can do something even more direct but I do not follow this example at all https://matplotlib.org/devdocs/devel/document.html#keyword-arguments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any general guidance about when it makes sense to do this rather than copy/paste? As you say it's only in two places and I am keenly aware that I have a lot more copy/paste going on in the artist's set_* docstrings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to figure that out w/ #28746 and probably a @timhoffm question?

.. 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.
Expand All @@ -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):
"""
Expand All @@ -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):
Expand Down
9 changes: 5 additions & 4 deletions lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading