270 lines
9.9 KiB
Python
270 lines
9.9 KiB
Python
![]() |
"""
|
||
|
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]
|