You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
14 KiB

4 years ago
  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(object):
  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. self._offset_trans = mtransforms.Affine2D()
  25. def _offset_transform(self, renderer, transform):
  26. """Apply the offset to the given transform."""
  27. offset_x = renderer.points_to_pixels(self._offset[0])
  28. offset_y = renderer.points_to_pixels(self._offset[1])
  29. return transform + self._offset_trans.clear().translate(offset_x,
  30. offset_y)
  31. def _update_gc(self, gc, new_gc_dict):
  32. """
  33. Update the given GraphicsCollection with the given
  34. dictionary of properties. The keys in the dictionary are used to
  35. identify the appropriate 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('Unknown property {0}'.format(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 : :class:`matplotlib.backend_bases.RendererBase` instance
  74. """
  75. self._path_effects = path_effects
  76. self._renderer = renderer
  77. def new_gc(self):
  78. return self._renderer.new_gc()
  79. def copy_with_path_effect(self, path_effects):
  80. return self.__class__(path_effects, self._renderer)
  81. def draw_path(self, gc, tpath, affine, rgbFace=None):
  82. for path_effect in self._path_effects:
  83. path_effect.draw_path(self._renderer, gc, tpath, affine,
  84. rgbFace)
  85. def draw_markers(self, gc, marker_path, marker_trans, path, *args,
  86. **kwargs):
  87. # We do a little shimmy so that all markers are drawn for each path
  88. # effect in turn. Essentially, we induce recursion (depth 1) which is
  89. # terminated once we have just a single path effect to work with.
  90. if len(self._path_effects) == 1:
  91. # Call the base path effect function - this uses the unoptimised
  92. # approach of calling "draw_path" multiple times.
  93. return RendererBase.draw_markers(self, gc, marker_path,
  94. marker_trans, path, *args,
  95. **kwargs)
  96. for path_effect in self._path_effects:
  97. renderer = self.copy_with_path_effect([path_effect])
  98. # Recursively call this method, only next time we will only have
  99. # one path effect.
  100. renderer.draw_markers(gc, marker_path, marker_trans, path,
  101. *args, **kwargs)
  102. def draw_path_collection(self, gc, master_transform, paths, *args,
  103. **kwargs):
  104. # We do a little shimmy so that all paths are drawn for each path
  105. # effect in turn. Essentially, we induce recursion (depth 1) which is
  106. # terminated once we have just a single path effect to work with.
  107. if len(self._path_effects) == 1:
  108. # Call the base path effect function - this uses the unoptimised
  109. # approach of calling "draw_path" multiple times.
  110. return RendererBase.draw_path_collection(self, gc,
  111. master_transform, paths,
  112. *args, **kwargs)
  113. for path_effect in self._path_effects:
  114. renderer = self.copy_with_path_effect([path_effect])
  115. # Recursively call this method, only next time we will only have
  116. # one path effect.
  117. renderer.draw_path_collection(gc, master_transform, paths,
  118. *args, **kwargs)
  119. def points_to_pixels(self, points):
  120. return self._renderer.points_to_pixels(points)
  121. def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
  122. # Implements the naive text drawing as is found in RendererBase.
  123. path, transform = self._get_text_path_transform(x, y, s, prop,
  124. angle, ismath)
  125. color = gc.get_rgb()
  126. gc.set_linewidth(0.0)
  127. self.draw_path(gc, path, transform, rgbFace=color)
  128. def __getattribute__(self, name):
  129. if name in ['_text2path', 'flipy', 'height', 'width']:
  130. return getattr(self._renderer, name)
  131. else:
  132. return object.__getattribute__(self, name)
  133. class Normal(AbstractPathEffect):
  134. """
  135. The "identity" PathEffect.
  136. The Normal PathEffect's sole purpose is to draw the original artist with
  137. no special path effect.
  138. """
  139. pass
  140. class Stroke(AbstractPathEffect):
  141. """A line based PathEffect which re-draws a stroke."""
  142. def __init__(self, offset=(0, 0), **kwargs):
  143. """
  144. The path will be stroked with its gc updated with the given
  145. keyword arguments, i.e., the keyword arguments should be valid
  146. gc parameter values.
  147. """
  148. super().__init__(offset)
  149. self._gc = kwargs
  150. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  151. """
  152. draw the path with updated gc.
  153. """
  154. # Do not modify the input! Use copy instead.
  155. gc0 = renderer.new_gc()
  156. gc0.copy_properties(gc)
  157. gc0 = self._update_gc(gc0, self._gc)
  158. trans = self._offset_transform(renderer, affine)
  159. renderer.draw_path(gc0, tpath, trans, rgbFace)
  160. gc0.restore()
  161. class withStroke(Stroke):
  162. """
  163. Adds a simple :class:`Stroke` and then draws the
  164. original Artist to avoid needing to call :class:`Normal`.
  165. """
  166. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  167. Stroke.draw_path(self, renderer, gc, tpath, affine, rgbFace)
  168. renderer.draw_path(gc, tpath, affine, rgbFace)
  169. class SimplePatchShadow(AbstractPathEffect):
  170. """A simple shadow via a filled patch."""
  171. def __init__(self, offset=(2, -2),
  172. shadow_rgbFace=None, alpha=None,
  173. rho=0.3, **kwargs):
  174. """
  175. Parameters
  176. ----------
  177. offset : pair of floats
  178. The offset of the shadow in points.
  179. shadow_rgbFace : color
  180. The shadow color.
  181. alpha : float
  182. The alpha transparency of the created shadow patch.
  183. Default is 0.3.
  184. http://matplotlib.1069221.n5.nabble.com/path-effects-question-td27630.html
  185. rho : float
  186. A scale factor to apply to the rgbFace color if `shadow_rgbFace`
  187. is not specified. Default is 0.3.
  188. **kwargs
  189. Extra keywords are stored and passed through to
  190. :meth:`AbstractPathEffect._update_gc`.
  191. """
  192. super().__init__(offset)
  193. if shadow_rgbFace is None:
  194. self._shadow_rgbFace = shadow_rgbFace
  195. else:
  196. self._shadow_rgbFace = mcolors.to_rgba(shadow_rgbFace)
  197. if alpha is None:
  198. alpha = 0.3
  199. self._alpha = alpha
  200. self._rho = rho
  201. #: The dictionary of keywords to update the graphics collection with.
  202. self._gc = kwargs
  203. #: The offset transform object. The offset isn't calculated yet
  204. #: as we don't know how big the figure will be in pixels.
  205. self._offset_tran = mtransforms.Affine2D()
  206. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  207. """
  208. Overrides the standard draw_path to add the shadow offset and
  209. necessary color changes for the shadow.
  210. """
  211. # IMPORTANT: Do not modify the input - we copy everything instead.
  212. affine0 = self._offset_transform(renderer, affine)
  213. gc0 = renderer.new_gc()
  214. gc0.copy_properties(gc)
  215. if self._shadow_rgbFace is None:
  216. r,g,b = (rgbFace or (1., 1., 1.))[:3]
  217. # Scale the colors by a factor to improve the shadow effect.
  218. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho)
  219. else:
  220. shadow_rgbFace = self._shadow_rgbFace
  221. gc0.set_foreground("none")
  222. gc0.set_alpha(self._alpha)
  223. gc0.set_linewidth(0)
  224. gc0 = self._update_gc(gc0, self._gc)
  225. renderer.draw_path(gc0, tpath, affine0, shadow_rgbFace)
  226. gc0.restore()
  227. class withSimplePatchShadow(SimplePatchShadow):
  228. """
  229. Adds a simple :class:`SimplePatchShadow` and then draws the
  230. original Artist to avoid needing to call :class:`Normal`.
  231. """
  232. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  233. SimplePatchShadow.draw_path(self, renderer, gc, tpath, affine, rgbFace)
  234. renderer.draw_path(gc, tpath, affine, rgbFace)
  235. class SimpleLineShadow(AbstractPathEffect):
  236. """A simple shadow via a line."""
  237. def __init__(self, offset=(2,-2),
  238. shadow_color='k', alpha=0.3, rho=0.3, **kwargs):
  239. """
  240. Parameters
  241. ----------
  242. offset : pair of floats
  243. The offset to apply to the path, in points.
  244. shadow_color : color
  245. The shadow color. Default is black.
  246. A value of ``None`` takes the original artist's color
  247. with a scale factor of `rho`.
  248. alpha : float
  249. The alpha transparency of the created shadow patch.
  250. Default is 0.3.
  251. rho : float
  252. A scale factor to apply to the rgbFace color if `shadow_rgbFace`
  253. is ``None``. Default is 0.3.
  254. **kwargs
  255. Extra keywords are stored and passed through to
  256. :meth:`AbstractPathEffect._update_gc`.
  257. """
  258. super().__init__(offset)
  259. if shadow_color is None:
  260. self._shadow_color = shadow_color
  261. else:
  262. self._shadow_color = mcolors.to_rgba(shadow_color)
  263. self._alpha = alpha
  264. self._rho = rho
  265. #: The dictionary of keywords to update the graphics collection with.
  266. self._gc = kwargs
  267. #: The offset transform object. The offset isn't calculated yet
  268. #: as we don't know how big the figure will be in pixels.
  269. self._offset_tran = mtransforms.Affine2D()
  270. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  271. """
  272. Overrides the standard draw_path to add the shadow offset and
  273. necessary color changes for the shadow.
  274. """
  275. # IMPORTANT: Do not modify the input - we copy everything instead.
  276. affine0 = self._offset_transform(renderer, affine)
  277. gc0 = renderer.new_gc()
  278. gc0.copy_properties(gc)
  279. if self._shadow_color is None:
  280. r,g,b = (gc0.get_foreground() or (1., 1., 1.))[:3]
  281. # Scale the colors by a factor to improve the shadow effect.
  282. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho)
  283. else:
  284. shadow_rgbFace = self._shadow_color
  285. fill_color = None
  286. gc0.set_foreground(shadow_rgbFace)
  287. gc0.set_alpha(self._alpha)
  288. gc0 = self._update_gc(gc0, self._gc)
  289. renderer.draw_path(gc0, tpath, affine0, fill_color)
  290. gc0.restore()
  291. class PathPatchEffect(AbstractPathEffect):
  292. """
  293. Draws a :class:`~matplotlib.patches.PathPatch` instance whose Path
  294. comes from the original PathEffect artist.
  295. """
  296. def __init__(self, offset=(0, 0), **kwargs):
  297. """
  298. Parameters
  299. ----------
  300. offset : pair of floats
  301. The offset to apply to the path, in points.
  302. **kwargs :
  303. All keyword arguments are passed through to the
  304. :class:`~matplotlib.patches.PathPatch` constructor. The
  305. properties which cannot be overridden are "path", "clip_box"
  306. "transform" and "clip_path".
  307. """
  308. super().__init__(offset=offset)
  309. self.patch = mpatches.PathPatch([], **kwargs)
  310. def draw_path(self, renderer, gc, tpath, affine, rgbFace):
  311. affine = self._offset_transform(renderer, affine)
  312. self.patch._path = tpath
  313. self.patch.set_transform(affine)
  314. self.patch.set_clip_box(gc.get_clip_rectangle())
  315. clip_path = gc.get_clip_path()
  316. if clip_path:
  317. self.patch.set_clip_path(*clip_path)
  318. self.patch.draw(renderer)