BodyBalanceEvaluation/backend/venv/Lib/site-packages/matplotlib/inset.py

270 lines
9.9 KiB
Python
Raw Normal View History

2025-07-31 17:23:05 +08:00
"""
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]