patheffects.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. """
  2. Defines classes for path effects. The path effects are supported in
  3. :class:`~matplotlib.text.Text`, :class:`~matplotlib.lines.Line2D`
  4. and :class:`~matplotlib.patches.Patch`.
  5. """
  6. from matplotlib.backend_bases import RendererBase
  7. from matplotlib import colors as mcolors
  8. from matplotlib import patches as mpatches
  9. from matplotlib import transforms as mtransforms
  10. class AbstractPathEffect:
  11. """
  12. A base class for path effects.
  13. Subclasses should override the ``draw_path`` method to add effect
  14. functionality.
  15. """
  16. def __init__(self, offset=(0., 0.)):
  17. """
  18. Parameters
  19. ----------
  20. offset : pair of floats
  21. The offset to apply to the path, measured in points.
  22. """
  23. self._offset = offset
  24. def _offset_transform(self, renderer):
  25. """Apply the offset to the given transform."""
  26. return mtransforms.Affine2D().translate(
  27. *map(renderer.points_to_pixels, self._offset))
  28. def _update_gc(self, gc, new_gc_dict):
  29. """
  30. Update the given GraphicsCollection with the given
  31. dictionary of properties. The keys in the dictionary are used to
  32. identify the appropriate set_ method on the gc.
  33. """
  34. new_gc_dict = new_gc_dict.copy()
  35. dashes = new_gc_dict.pop("dashes", None)
  36. if dashes:
  37. gc.set_dashes(**dashes)
  38. for k, v in new_gc_dict.items():
  39. set_method = getattr(gc, 'set_' + k, None)
  40. if not callable(set_method):
  41. raise AttributeError('Unknown property {0}'.format(k))
  42. set_method(v)
  43. return gc
  44. def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
  45. """
  46. Derived should override this method. The arguments are the same
  47. as :meth:`matplotlib.backend_bases.RendererBase.draw_path`
  48. except the first argument is a renderer.
  49. """
  50. # Get the real renderer, not a PathEffectRenderer.
  51. if isinstance(renderer, PathEffectRenderer):
  52. renderer = renderer._renderer
  53. return renderer.draw_path(gc, tpath, affine, rgbFace)
  54. class PathEffectRenderer(RendererBase):
  55. """
  56. Implements a Renderer which contains another renderer.
  57. This proxy then intercepts draw calls, calling the appropriate
  58. :class:`AbstractPathEffect` draw method.
  59. .. note::
  60. Not all methods have been overridden on this RendererBase subclass.
  61. It may be necessary to add further methods to extend the PathEffects
  62. capabilities further.
  63. """
  64. def __init__(self, path_effects, renderer):
  65. """
  66. Parameters
  67. ----------
  68. path_effects : iterable of :class:`AbstractPathEffect`
  69. The path effects which this renderer represents.
  70. renderer : :class:`matplotlib.backend_bases.RendererBase` instance
  71. """
  72. self._path_effects = path_effects
  73. self._renderer = renderer
  74. def copy_with_path_effect(self, path_effects):
  75. return self.__class__(path_effects, self._renderer)
  76. def draw_path(self, gc, tpath, affine, rgbFace=None):
  77. for path_effect in self._path_effects:
  78. path_effect.draw_path(self._renderer, gc, tpath, affine,
  79. rgbFace)
  80. def draw_markers(
  81. self, gc, marker_path, marker_trans, path, *args, **kwargs):
  82. # We do a little shimmy so that all markers are drawn for each path
  83. # effect in turn. Essentially, we induce recursion (depth 1) which is
  84. # terminated once we have just a single path effect to work with.
  85. if len(self._path_effects) == 1:
  86. # Call the base path effect function - this uses the unoptimised
  87. # approach of calling "draw_path" multiple times.
  88. return RendererBase.draw_markers(self, gc, marker_path,
  89. marker_trans, path, *args,
  90. **kwargs)
  91. for path_effect in self._path_effects:
  92. renderer = self.copy_with_path_effect([path_effect])
  93. # Recursively call this method, only next time we will only have
  94. # one path effect.
  95. renderer.draw_markers(gc, marker_path, marker_trans, path,
  96. *args, **kwargs)
  97. def draw_path_collection(self, gc, master_transform, paths, *args,
  98. **kwargs):
  99. # We do a little shimmy so that all paths are drawn for each path
  100. # effect in turn. Essentially, we induce recursion (depth 1) which is
  101. # terminated once we have just a single path effect to work with.
  102. if len(self._path_effects) == 1:
  103. # Call the base path effect function - this uses the unoptimised
  104. # approach of calling "draw_path" multiple times.
  105. return RendererBase.draw_path_collection(self, gc,
  106. master_transform, paths,
  107. *args, **kwargs)
  108. for path_effect in self._path_effects:
  109. renderer = self.copy_with_path_effect([path_effect])
  110. # Recursively call this method, only next time we will only have
  111. # one path effect.
  112. renderer.draw_path_collection(gc, master_transform, paths,
  113. *args, **kwargs)
  114. def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
  115. # Implements the naive text drawing as is found in RendererBase.
  116. path, transform = self._get_text_path_transform(x, y, s, prop,
  117. angle, ismath)
  118. color = gc.get_rgb()
  119. gc.set_linewidth(0.0)
  120. self.draw_path(gc, path, transform, rgbFace=color)
  121. def __getattribute__(self, name):
  122. if name in ['flipy', 'get_canvas_width_height', 'new_gc',
  123. 'points_to_pixels', '_text2path', 'height', 'width']:
  124. return getattr(self._renderer, name)
  125. else:
  126. return object.__getattribute__(self, name)
  127. class Normal(AbstractPathEffect):
  128. """
  129. The "identity" PathEffect.
  130. The Normal PathEffect's sole purpose is to draw the original artist with
  131. no special path effect.
  132. """
  133. pass
  134. class Stroke(AbstractPathEffect):
  135. """A line based PathEffect which re-draws a stroke."""
  136. def __init__(self, offset=(0, 0), **kwargs):
  137. """
  138. The path will be stroked with its gc updated with the given
  139. keyword arguments, i.e., the keyword arguments should be valid
  140. gc parameter values.
  141. """
  142. super().__init__(offset)
  143. self._gc = kwargs
  144. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  145. """
  146. Draw the path with updated gc.
  147. """
  148. gc0 = renderer.new_gc() # Don't modify gc, but a copy!
  149. gc0.copy_properties(gc)
  150. gc0 = self._update_gc(gc0, self._gc)
  151. renderer.draw_path(
  152. gc0, tpath, affine + self._offset_transform(renderer), rgbFace)
  153. gc0.restore()
  154. class withStroke(Stroke):
  155. """
  156. Adds a simple :class:`Stroke` and then draws the
  157. original Artist to avoid needing to call :class:`Normal`.
  158. """
  159. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  160. Stroke.draw_path(self, renderer, gc, tpath, affine, rgbFace)
  161. renderer.draw_path(gc, tpath, affine, rgbFace)
  162. class SimplePatchShadow(AbstractPathEffect):
  163. """A simple shadow via a filled patch."""
  164. def __init__(self, offset=(2, -2),
  165. shadow_rgbFace=None, alpha=None,
  166. rho=0.3, **kwargs):
  167. """
  168. Parameters
  169. ----------
  170. offset : pair of floats
  171. The offset of the shadow in points.
  172. shadow_rgbFace : color
  173. The shadow color.
  174. alpha : float
  175. The alpha transparency of the created shadow patch.
  176. Default is 0.3.
  177. http://matplotlib.1069221.n5.nabble.com/path-effects-question-td27630.html
  178. rho : float
  179. A scale factor to apply to the rgbFace color if `shadow_rgbFace`
  180. is not specified. Default is 0.3.
  181. **kwargs
  182. Extra keywords are stored and passed through to
  183. :meth:`AbstractPathEffect._update_gc`.
  184. """
  185. super().__init__(offset)
  186. if shadow_rgbFace is None:
  187. self._shadow_rgbFace = shadow_rgbFace
  188. else:
  189. self._shadow_rgbFace = mcolors.to_rgba(shadow_rgbFace)
  190. if alpha is None:
  191. alpha = 0.3
  192. self._alpha = alpha
  193. self._rho = rho
  194. #: The dictionary of keywords to update the graphics collection with.
  195. self._gc = kwargs
  196. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  197. """
  198. Overrides the standard draw_path to add the shadow offset and
  199. necessary color changes for the shadow.
  200. """
  201. gc0 = renderer.new_gc() # Don't modify gc, but a copy!
  202. gc0.copy_properties(gc)
  203. if self._shadow_rgbFace is None:
  204. r, g, b = (rgbFace or (1., 1., 1.))[:3]
  205. # Scale the colors by a factor to improve the shadow effect.
  206. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho)
  207. else:
  208. shadow_rgbFace = self._shadow_rgbFace
  209. gc0.set_foreground("none")
  210. gc0.set_alpha(self._alpha)
  211. gc0.set_linewidth(0)
  212. gc0 = self._update_gc(gc0, self._gc)
  213. renderer.draw_path(
  214. gc0, tpath, affine + self._offset_transform(renderer),
  215. shadow_rgbFace)
  216. gc0.restore()
  217. class withSimplePatchShadow(SimplePatchShadow):
  218. """
  219. Adds a simple :class:`SimplePatchShadow` and then draws the
  220. original Artist to avoid needing to call :class:`Normal`.
  221. """
  222. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  223. SimplePatchShadow.draw_path(self, renderer, gc, tpath, affine, rgbFace)
  224. renderer.draw_path(gc, tpath, affine, rgbFace)
  225. class SimpleLineShadow(AbstractPathEffect):
  226. """A simple shadow via a line."""
  227. def __init__(self, offset=(2, -2),
  228. shadow_color='k', alpha=0.3, rho=0.3, **kwargs):
  229. """
  230. Parameters
  231. ----------
  232. offset : pair of floats
  233. The offset to apply to the path, in points.
  234. shadow_color : color
  235. The shadow color. Default is black.
  236. A value of ``None`` takes the original artist's color
  237. with a scale factor of *rho*.
  238. alpha : float
  239. The alpha transparency of the created shadow patch.
  240. Default is 0.3.
  241. rho : float
  242. A scale factor to apply to the rgbFace color if `shadow_rgbFace`
  243. is ``None``. Default is 0.3.
  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 :class:`~matplotlib.patches.PathPatch` instance whose Path
  279. comes from the original PathEffect artist.
  280. """
  281. def __init__(self, offset=(0, 0), **kwargs):
  282. """
  283. Parameters
  284. ----------
  285. offset : pair of floats
  286. The 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:
  301. self.patch.set_clip_path(*clip_path)
  302. self.patch.draw(renderer)