inset.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. """
  2. The inset module defines the InsetIndicator class, which draws the rectangle and
  3. connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`.
  4. """
  5. from . import _api, artist, transforms
  6. from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle
  7. from matplotlib.path import Path
  8. _shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth')
  9. class InsetIndicator(artist.Artist):
  10. """
  11. An artist to highlight an area of interest.
  12. An inset indicator is a rectangle on the plot at the position indicated by
  13. *bounds* that optionally has lines that connect the rectangle to an inset
  14. Axes (`.Axes.inset_axes`).
  15. .. versionadded:: 3.10
  16. """
  17. zorder = 4.99
  18. def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs):
  19. """
  20. Parameters
  21. ----------
  22. bounds : [x0, y0, width, height], optional
  23. Lower-left corner of rectangle to be marked, and its width
  24. and height. If not set, the bounds will be calculated from the
  25. data limits of inset_ax, which must be supplied.
  26. inset_ax : `~.axes.Axes`, optional
  27. An optional inset Axes to draw connecting lines to. Two lines are
  28. drawn connecting the indicator box to the inset Axes on corners
  29. chosen so as to not overlap with the indicator box.
  30. zorder : float, default: 4.99
  31. Drawing order of the rectangle and connector lines. The default,
  32. 4.99, is just below the default level of inset Axes.
  33. **kwargs
  34. Other keyword arguments are passed on to the `.Rectangle` patch.
  35. """
  36. if bounds is None and inset_ax is None:
  37. raise ValueError("At least one of bounds or inset_ax must be supplied")
  38. self._inset_ax = inset_ax
  39. if bounds is None:
  40. # Work out bounds from inset_ax
  41. self._auto_update_bounds = True
  42. bounds = self._bounds_from_inset_ax()
  43. else:
  44. self._auto_update_bounds = False
  45. x, y, width, height = bounds
  46. self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs)
  47. # Connector positions cannot be calculated till the artist has been added
  48. # to an axes, so just make an empty list for now.
  49. self._connectors = []
  50. super().__init__()
  51. self.set_zorder(zorder)
  52. # Initial style properties for the artist should match the rectangle.
  53. for prop in _shared_properties:
  54. setattr(self, f'_{prop}', artist.getp(self._rectangle, prop))
  55. def _shared_setter(self, prop, val):
  56. """
  57. Helper function to set the same style property on the artist and its children.
  58. """
  59. setattr(self, f'_{prop}', val)
  60. artist.setp([self._rectangle, *self._connectors], prop, val)
  61. def set_alpha(self, alpha):
  62. # docstring inherited
  63. self._shared_setter('alpha', alpha)
  64. def set_edgecolor(self, color):
  65. """
  66. Set the edge color of the rectangle and the connectors.
  67. Parameters
  68. ----------
  69. color : :mpltype:`color` or None
  70. """
  71. self._shared_setter('edgecolor', color)
  72. def set_color(self, c):
  73. """
  74. Set the edgecolor of the rectangle and the connectors, and the
  75. facecolor for the rectangle.
  76. Parameters
  77. ----------
  78. c : :mpltype:`color`
  79. """
  80. self._shared_setter('edgecolor', c)
  81. self._shared_setter('facecolor', c)
  82. def set_linewidth(self, w):
  83. """
  84. Set the linewidth in points of the rectangle and the connectors.
  85. Parameters
  86. ----------
  87. w : float or None
  88. """
  89. self._shared_setter('linewidth', w)
  90. def set_linestyle(self, ls):
  91. """
  92. Set the linestyle of the rectangle and the connectors.
  93. ========================================== =================
  94. linestyle description
  95. ========================================== =================
  96. ``'-'`` or ``'solid'`` solid line
  97. ``'--'`` or ``'dashed'`` dashed line
  98. ``'-.'`` or ``'dashdot'`` dash-dotted line
  99. ``':'`` or ``'dotted'`` dotted line
  100. ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing
  101. ========================================== =================
  102. Alternatively a dash tuple of the following form can be provided::
  103. (offset, onoffseq)
  104. where ``onoffseq`` is an even length tuple of on and off ink in points.
  105. Parameters
  106. ----------
  107. ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
  108. The line style.
  109. """
  110. self._shared_setter('linestyle', ls)
  111. def _bounds_from_inset_ax(self):
  112. xlim = self._inset_ax.get_xlim()
  113. ylim = self._inset_ax.get_ylim()
  114. return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0])
  115. def _update_connectors(self):
  116. (x, y) = self._rectangle.get_xy()
  117. width = self._rectangle.get_width()
  118. height = self._rectangle.get_height()
  119. existing_connectors = self._connectors or [None] * 4
  120. # connect the inset_axes to the rectangle
  121. for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)],
  122. existing_connectors):
  123. # inset_ax positions are in axes coordinates
  124. # The 0, 1 values define the four edges if the inset_ax
  125. # lower_left, upper_left, lower_right upper_right.
  126. ex, ey = xy_inset_ax
  127. if self.axes.xaxis.get_inverted():
  128. ex = 1 - ex
  129. if self.axes.yaxis.get_inverted():
  130. ey = 1 - ey
  131. xy_data = x + ex * width, y + ey * height
  132. if existing is None:
  133. # Create new connection patch with styles inherited from the
  134. # parent artist.
  135. p = ConnectionPatch(
  136. xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes,
  137. xyB=xy_data, coordsB=self.axes.transData,
  138. arrowstyle="-",
  139. edgecolor=self._edgecolor, alpha=self.get_alpha(),
  140. linestyle=self._linestyle, linewidth=self._linewidth)
  141. self._connectors.append(p)
  142. else:
  143. # Only update positioning of existing connection patch. We
  144. # do not want to override any style settings made by the user.
  145. existing.xy1 = xy_inset_ax
  146. existing.xy2 = xy_data
  147. existing.coords1 = self._inset_ax.transAxes
  148. existing.coords2 = self.axes.transData
  149. if existing is None:
  150. # decide which two of the lines to keep visible....
  151. pos = self._inset_ax.get_position()
  152. bboxins = pos.transformed(self.get_figure(root=False).transSubfigure)
  153. rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed(
  154. self._rectangle.get_transform())
  155. x0 = rectbbox.x0 < bboxins.x0
  156. x1 = rectbbox.x1 < bboxins.x1
  157. y0 = rectbbox.y0 < bboxins.y0
  158. y1 = rectbbox.y1 < bboxins.y1
  159. self._connectors[0].set_visible(x0 ^ y0)
  160. self._connectors[1].set_visible(x0 == y1)
  161. self._connectors[2].set_visible(x1 == y0)
  162. self._connectors[3].set_visible(x1 ^ y1)
  163. @property
  164. def rectangle(self):
  165. """`.Rectangle`: the indicator frame."""
  166. return self._rectangle
  167. @property
  168. def connectors(self):
  169. """
  170. 4-tuple of `.patches.ConnectionPatch` or None
  171. The four connector lines connecting to (lower_left, upper_left,
  172. lower_right upper_right) corners of *inset_ax*. Two lines are
  173. set with visibility to *False*, but the user can set the
  174. visibility to True if the automatic choice is not deemed correct.
  175. """
  176. if self._inset_ax is None:
  177. return
  178. if self._auto_update_bounds:
  179. self._rectangle.set_bounds(self._bounds_from_inset_ax())
  180. self._update_connectors()
  181. return tuple(self._connectors)
  182. def draw(self, renderer):
  183. # docstring inherited
  184. conn_same_style = []
  185. # Figure out which connectors have the same style as the box, so should
  186. # be drawn as a single path.
  187. for conn in self.connectors or []:
  188. if conn.get_visible():
  189. drawn = False
  190. for s in _shared_properties:
  191. if artist.getp(self._rectangle, s) != artist.getp(conn, s):
  192. # Draw this connector by itself
  193. conn.draw(renderer)
  194. drawn = True
  195. break
  196. if not drawn:
  197. # Connector has same style as box.
  198. conn_same_style.append(conn)
  199. if conn_same_style:
  200. # Since at least one connector has the same style as the rectangle, draw
  201. # them as a compound path.
  202. artists = [self._rectangle] + conn_same_style
  203. paths = [a.get_transform().transform_path(a.get_path()) for a in artists]
  204. path = Path.make_compound_path(*paths)
  205. # Create a temporary patch to draw the path.
  206. p = PathPatch(path)
  207. p.update_from(self._rectangle)
  208. p.set_transform(transforms.IdentityTransform())
  209. p.draw(renderer)
  210. return
  211. # Just draw the rectangle
  212. self._rectangle.draw(renderer)
  213. @_api.deprecated(
  214. '3.10',
  215. message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single '
  216. 'InsetIndicator artist with a rectangle property and a connectors '
  217. 'property. From 3.12 it will no longer be possible to unpack the '
  218. 'return value into two elements.'))
  219. def __getitem__(self, key):
  220. return [self._rectangle, self.connectors][key]