patheffects.py 18 KB

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