| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- """
- 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]
|