patheffects.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. """
  2. Defines classes for path effects. The path effects are supported in `.Text`,
  3. `.Line2D` and `.Patch`.
  4. .. seealso::
  5. :ref:`patheffects_guide`
  6. """
  7. from matplotlib.backend_bases import RendererBase
  8. from matplotlib import colors as mcolors
  9. from matplotlib import patches as mpatches
  10. from matplotlib import transforms as mtransforms
  11. from matplotlib.path import Path
  12. import numpy as np
  13. class AbstractPathEffect:
  14. """
  15. A base class for path effects.
  16. Subclasses should override the ``draw_path`` method to add effect
  17. functionality.
  18. """
  19. def __init__(self, offset=(0., 0.)):
  20. """
  21. Parameters
  22. ----------
  23. offset : (float, float), default: (0, 0)
  24. The (x, y) offset to apply to the path, measured in points.
  25. """
  26. self._offset = offset
  27. def _offset_transform(self, renderer):
  28. """Apply the offset to the given transform."""
  29. return mtransforms.Affine2D().translate(
  30. *map(renderer.points_to_pixels, self._offset))
  31. def _update_gc(self, gc, new_gc_dict):
  32. """
  33. Update the given GraphicsContext with the given dict of properties.
  34. The keys in the dictionary are used to identify the appropriate
  35. ``set_`` method on the *gc*.
  36. """
  37. new_gc_dict = new_gc_dict.copy()
  38. dashes = new_gc_dict.pop("dashes", None)
  39. if dashes:
  40. gc.set_dashes(**dashes)
  41. for k, v in new_gc_dict.items():
  42. set_method = getattr(gc, 'set_' + k, None)
  43. if not callable(set_method):
  44. raise AttributeError(f'Unknown property {k}')
  45. set_method(v)
  46. return gc
  47. def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
  48. """
  49. Derived should override this method. The arguments are the same
  50. as :meth:`matplotlib.backend_bases.RendererBase.draw_path`
  51. except the first argument is a renderer.
  52. """
  53. # Get the real renderer, not a PathEffectRenderer.
  54. if isinstance(renderer, PathEffectRenderer):
  55. renderer = renderer._renderer
  56. return renderer.draw_path(gc, tpath, affine, rgbFace)
  57. class PathEffectRenderer(RendererBase):
  58. """
  59. Implements a Renderer which contains another renderer.
  60. This proxy then intercepts draw calls, calling the appropriate
  61. :class:`AbstractPathEffect` draw method.
  62. .. note::
  63. Not all methods have been overridden on this RendererBase subclass.
  64. It may be necessary to add further methods to extend the PathEffects
  65. capabilities further.
  66. """
  67. def __init__(self, path_effects, renderer):
  68. """
  69. Parameters
  70. ----------
  71. path_effects : iterable of :class:`AbstractPathEffect`
  72. The path effects which this renderer represents.
  73. renderer : `~matplotlib.backend_bases.RendererBase` subclass
  74. """
  75. self._path_effects = path_effects
  76. self._renderer = renderer
  77. def copy_with_path_effect(self, path_effects):
  78. return self.__class__(path_effects, self._renderer)
  79. def __getattribute__(self, name):
  80. if name in ['flipy', 'get_canvas_width_height', 'new_gc',
  81. 'points_to_pixels', '_text2path', 'height', 'width']:
  82. return getattr(self._renderer, name)
  83. else:
  84. return object.__getattribute__(self, name)
  85. def draw_path(self, gc, tpath, affine, rgbFace=None):
  86. for path_effect in self._path_effects:
  87. path_effect.draw_path(self._renderer, gc, tpath, affine,
  88. rgbFace)
  89. def draw_markers(
  90. self, gc, marker_path, marker_trans, path, *args, **kwargs):
  91. # We do a little shimmy so that all markers are drawn for each path
  92. # effect in turn. Essentially, we induce recursion (depth 1) which is
  93. # terminated once we have just a single path effect to work with.
  94. if len(self._path_effects) == 1:
  95. # Call the base path effect function - this uses the unoptimised
  96. # approach of calling "draw_path" multiple times.
  97. return super().draw_markers(gc, marker_path, marker_trans, path,
  98. *args, **kwargs)
  99. for path_effect in self._path_effects:
  100. renderer = self.copy_with_path_effect([path_effect])
  101. # Recursively call this method, only next time we will only have
  102. # one path effect.
  103. renderer.draw_markers(gc, marker_path, marker_trans, path,
  104. *args, **kwargs)
  105. def draw_path_collection(self, gc, master_transform, paths, *args,
  106. **kwargs):
  107. # We do a little shimmy so that all paths are drawn for each path
  108. # effect in turn. Essentially, we induce recursion (depth 1) which is
  109. # terminated once we have just a single path effect to work with.
  110. if len(self._path_effects) == 1:
  111. # Call the base path effect function - this uses the unoptimised
  112. # approach of calling "draw_path" multiple times.
  113. return super().draw_path_collection(gc, master_transform, paths,
  114. *args, **kwargs)
  115. for path_effect in self._path_effects:
  116. renderer = self.copy_with_path_effect([path_effect])
  117. # Recursively call this method, only next time we will only have
  118. # one path effect.
  119. renderer.draw_path_collection(gc, master_transform, paths,
  120. *args, **kwargs)
  121. def open_group(self, s, gid=None):
  122. return self._renderer.open_group(s, gid)
  123. def close_group(self, s):
  124. return self._renderer.close_group(s)
  125. class Normal(AbstractPathEffect):
  126. """
  127. The "identity" PathEffect.
  128. The Normal PathEffect's sole purpose is to draw the original artist with
  129. no special path effect.
  130. """
  131. def _subclass_with_normal(effect_class):
  132. """
  133. Create a PathEffect class combining *effect_class* and a normal draw.
  134. """
  135. class withEffect(effect_class):
  136. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  137. super().draw_path(renderer, gc, tpath, affine, rgbFace)
  138. renderer.draw_path(gc, tpath, affine, rgbFace)
  139. withEffect.__name__ = f"with{effect_class.__name__}"
  140. withEffect.__qualname__ = f"with{effect_class.__name__}"
  141. withEffect.__doc__ = f"""
  142. A shortcut PathEffect for applying `.{effect_class.__name__}` and then
  143. drawing the original Artist.
  144. With this class you can use ::
  145. artist.set_path_effects([patheffects.with{effect_class.__name__}()])
  146. as a shortcut for ::
  147. artist.set_path_effects([patheffects.{effect_class.__name__}(),
  148. patheffects.Normal()])
  149. """
  150. # Docstring inheritance doesn't work for locally-defined subclasses.
  151. withEffect.draw_path.__doc__ = effect_class.draw_path.__doc__
  152. return withEffect
  153. class Stroke(AbstractPathEffect):
  154. """A line based PathEffect which re-draws a stroke."""
  155. def __init__(self, offset=(0, 0), **kwargs):
  156. """
  157. The path will be stroked with its gc updated with the given
  158. keyword arguments, i.e., the keyword arguments should be valid
  159. gc parameter values.
  160. """
  161. super().__init__(offset)
  162. self._gc = kwargs
  163. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  164. """Draw the path with updated gc."""
  165. gc0 = renderer.new_gc() # Don't modify gc, but a copy!
  166. gc0.copy_properties(gc)
  167. gc0 = self._update_gc(gc0, self._gc)
  168. renderer.draw_path(
  169. gc0, tpath, affine + self._offset_transform(renderer), rgbFace)
  170. gc0.restore()
  171. withStroke = _subclass_with_normal(effect_class=Stroke)
  172. class SimplePatchShadow(AbstractPathEffect):
  173. """A simple shadow via a filled patch."""
  174. def __init__(self, offset=(2, -2),
  175. shadow_rgbFace=None, alpha=None,
  176. rho=0.3, **kwargs):
  177. """
  178. Parameters
  179. ----------
  180. offset : (float, float), default: (2, -2)
  181. The (x, y) offset of the shadow in points.
  182. shadow_rgbFace : :mpltype:`color`
  183. The shadow color.
  184. alpha : float, default: 0.3
  185. The alpha transparency of the created shadow patch.
  186. rho : float, default: 0.3
  187. A scale factor to apply to the rgbFace color if *shadow_rgbFace*
  188. is not specified.
  189. **kwargs
  190. Extra keywords are stored and passed through to
  191. :meth:`AbstractPathEffect._update_gc`.
  192. """
  193. super().__init__(offset)
  194. if shadow_rgbFace is None:
  195. self._shadow_rgbFace = shadow_rgbFace
  196. else:
  197. self._shadow_rgbFace = mcolors.to_rgba(shadow_rgbFace)
  198. if alpha is None:
  199. alpha = 0.3
  200. self._alpha = alpha
  201. self._rho = rho
  202. #: The dictionary of keywords to update the graphics collection with.
  203. self._gc = kwargs
  204. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  205. """
  206. Overrides the standard draw_path to add the shadow offset and
  207. necessary color changes for the shadow.
  208. """
  209. gc0 = renderer.new_gc() # Don't modify gc, but a copy!
  210. gc0.copy_properties(gc)
  211. if self._shadow_rgbFace is None:
  212. r, g, b = (rgbFace or (1., 1., 1.))[:3]
  213. # Scale the colors by a factor to improve the shadow effect.
  214. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho)
  215. else:
  216. shadow_rgbFace = self._shadow_rgbFace
  217. gc0.set_foreground("none")
  218. gc0.set_alpha(self._alpha)
  219. gc0.set_linewidth(0)
  220. gc0 = self._update_gc(gc0, self._gc)
  221. renderer.draw_path(
  222. gc0, tpath, affine + self._offset_transform(renderer),
  223. shadow_rgbFace)
  224. gc0.restore()
  225. withSimplePatchShadow = _subclass_with_normal(effect_class=SimplePatchShadow)
  226. class SimpleLineShadow(AbstractPathEffect):
  227. """A simple shadow via a line."""
  228. def __init__(self, offset=(2, -2),
  229. shadow_color='k', alpha=0.3, rho=0.3, **kwargs):
  230. """
  231. Parameters
  232. ----------
  233. offset : (float, float), default: (2, -2)
  234. The (x, y) offset to apply to the path, in points.
  235. shadow_color : :mpltype:`color`, default: 'black'
  236. The shadow color.
  237. A value of ``None`` takes the original artist's color
  238. with a scale factor of *rho*.
  239. alpha : float, default: 0.3
  240. The alpha transparency of the created shadow patch.
  241. rho : float, default: 0.3
  242. A scale factor to apply to the rgbFace color if *shadow_color*
  243. is ``None``.
  244. **kwargs
  245. Extra keywords are stored and passed through to
  246. :meth:`AbstractPathEffect._update_gc`.
  247. """
  248. super().__init__(offset)
  249. if shadow_color is None:
  250. self._shadow_color = shadow_color
  251. else:
  252. self._shadow_color = mcolors.to_rgba(shadow_color)
  253. self._alpha = alpha
  254. self._rho = rho
  255. #: The dictionary of keywords to update the graphics collection with.
  256. self._gc = kwargs
  257. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  258. """
  259. Overrides the standard draw_path to add the shadow offset and
  260. necessary color changes for the shadow.
  261. """
  262. gc0 = renderer.new_gc() # Don't modify gc, but a copy!
  263. gc0.copy_properties(gc)
  264. if self._shadow_color is None:
  265. r, g, b = (gc0.get_foreground() or (1., 1., 1.))[:3]
  266. # Scale the colors by a factor to improve the shadow effect.
  267. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho)
  268. else:
  269. shadow_rgbFace = self._shadow_color
  270. gc0.set_foreground(shadow_rgbFace)
  271. gc0.set_alpha(self._alpha)
  272. gc0 = self._update_gc(gc0, self._gc)
  273. renderer.draw_path(
  274. gc0, tpath, affine + self._offset_transform(renderer))
  275. gc0.restore()
  276. class PathPatchEffect(AbstractPathEffect):
  277. """
  278. Draws a `.PathPatch` instance whose Path comes from the original
  279. PathEffect artist.
  280. """
  281. def __init__(self, offset=(0, 0), **kwargs):
  282. """
  283. Parameters
  284. ----------
  285. offset : (float, float), default: (0, 0)
  286. The (x, y) offset to apply to the path, in points.
  287. **kwargs
  288. All keyword arguments are passed through to the
  289. :class:`~matplotlib.patches.PathPatch` constructor. The
  290. properties which cannot be overridden are "path", "clip_box"
  291. "transform" and "clip_path".
  292. """
  293. super().__init__(offset=offset)
  294. self.patch = mpatches.PathPatch([], **kwargs)
  295. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  296. self.patch._path = tpath
  297. self.patch.set_transform(affine + self._offset_transform(renderer))
  298. self.patch.set_clip_box(gc.get_clip_rectangle())
  299. clip_path = gc.get_clip_path()
  300. if clip_path and self.patch.get_clip_path() is None:
  301. self.patch.set_clip_path(*clip_path)
  302. self.patch.draw(renderer)
  303. class TickedStroke(AbstractPathEffect):
  304. """
  305. A line-based PathEffect which draws a path with a ticked style.
  306. This line style is frequently used to represent constraints in
  307. optimization. The ticks may be used to indicate that one side
  308. of the line is invalid or to represent a closed boundary of a
  309. domain (i.e. a wall or the edge of a pipe).
  310. The spacing, length, and angle of ticks can be controlled.
  311. This line style is sometimes referred to as a hatched line.
  312. See also the :doc:`/gallery/misc/tickedstroke_demo` example.
  313. """
  314. def __init__(self, offset=(0, 0),
  315. spacing=10.0, angle=45.0, length=np.sqrt(2),
  316. **kwargs):
  317. """
  318. Parameters
  319. ----------
  320. offset : (float, float), default: (0, 0)
  321. The (x, y) offset to apply to the path, in points.
  322. spacing : float, default: 10.0
  323. The spacing between ticks in points.
  324. angle : float, default: 45.0
  325. The angle between the path and the tick in degrees. The angle
  326. is measured as if you were an ant walking along the curve, with
  327. zero degrees pointing directly ahead, 90 to your left, -90
  328. to your right, and 180 behind you. To change side of the ticks,
  329. change sign of the angle.
  330. length : float, default: 1.414
  331. The length of the tick relative to spacing.
  332. Recommended length = 1.414 (sqrt(2)) when angle=45, length=1.0
  333. when angle=90 and length=2.0 when angle=60.
  334. **kwargs
  335. Extra keywords are stored and passed through to
  336. :meth:`AbstractPathEffect._update_gc`.
  337. Examples
  338. --------
  339. See :doc:`/gallery/misc/tickedstroke_demo`.
  340. """
  341. super().__init__(offset)
  342. self._spacing = spacing
  343. self._angle = angle
  344. self._length = length
  345. self._gc = kwargs
  346. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  347. """Draw the path with updated gc."""
  348. # Do not modify the input! Use copy instead.
  349. gc0 = renderer.new_gc()
  350. gc0.copy_properties(gc)
  351. gc0 = self._update_gc(gc0, self._gc)
  352. trans = affine + self._offset_transform(renderer)
  353. theta = -np.radians(self._angle)
  354. trans_matrix = np.array([[np.cos(theta), -np.sin(theta)],
  355. [np.sin(theta), np.cos(theta)]])
  356. # Convert spacing parameter to pixels.
  357. spacing_px = renderer.points_to_pixels(self._spacing)
  358. # Transform before evaluation because to_polygons works at resolution
  359. # of one -- assuming it is working in pixel space.
  360. transpath = affine.transform_path(tpath)
  361. # Evaluate path to straight line segments that can be used to
  362. # construct line ticks.
  363. polys = transpath.to_polygons(closed_only=False)
  364. for p in polys:
  365. x = p[:, 0]
  366. y = p[:, 1]
  367. # Can not interpolate points or draw line if only one point in
  368. # polyline.
  369. if x.size < 2:
  370. continue
  371. # Find distance between points on the line
  372. ds = np.hypot(x[1:] - x[:-1], y[1:] - y[:-1])
  373. # Build parametric coordinate along curve
  374. s = np.concatenate(([0.0], np.cumsum(ds)))
  375. s_total = s[-1]
  376. num = int(np.ceil(s_total / spacing_px)) - 1
  377. # Pick parameter values for ticks.
  378. s_tick = np.linspace(spacing_px/2, s_total - spacing_px/2, num)
  379. # Find points along the parameterized curve
  380. x_tick = np.interp(s_tick, s, x)
  381. y_tick = np.interp(s_tick, s, y)
  382. # Find unit vectors in local direction of curve
  383. delta_s = self._spacing * .001
  384. u = (np.interp(s_tick + delta_s, s, x) - x_tick) / delta_s
  385. v = (np.interp(s_tick + delta_s, s, y) - y_tick) / delta_s
  386. # Normalize slope into unit slope vector.
  387. n = np.hypot(u, v)
  388. mask = n == 0
  389. n[mask] = 1.0
  390. uv = np.array([u / n, v / n]).T
  391. uv[mask] = np.array([0, 0]).T
  392. # Rotate and scale unit vector into tick vector
  393. dxy = np.dot(uv, trans_matrix) * self._length * spacing_px
  394. # Build tick endpoints
  395. x_end = x_tick + dxy[:, 0]
  396. y_end = y_tick + dxy[:, 1]
  397. # Interleave ticks to form Path vertices
  398. xyt = np.empty((2 * num, 2), dtype=x_tick.dtype)
  399. xyt[0::2, 0] = x_tick
  400. xyt[1::2, 0] = x_end
  401. xyt[0::2, 1] = y_tick
  402. xyt[1::2, 1] = y_end
  403. # Build up vector of Path codes
  404. codes = np.tile([Path.MOVETO, Path.LINETO], num)
  405. # Construct and draw resulting path
  406. h = Path(xyt, codes)
  407. # Transform back to data space during render
  408. renderer.draw_path(gc0, h, affine.inverted() + trans, rgbFace)
  409. gc0.restore()
  410. withTickedStroke = _subclass_with_normal(effect_class=TickedStroke)