Source code for matplotlib.patheffects

"""
Defines classes for path effects. The path effects are supported in `~.Text`,
`~.Line2D` and `~.Patch`.

.. seealso::
   :doc:`/tutorials/advanced/patheffects_guide`
"""

from matplotlib.backend_bases import RendererBase
from matplotlib import colors as mcolors
from matplotlib import patches as mpatches
from matplotlib import transforms as mtransforms


[docs]class AbstractPathEffect: """ A base class for path effects. Subclasses should override the ``draw_path`` method to add effect functionality. """ def __init__(self, offset=(0., 0.)): """ Parameters ---------- offset : pair of floats The offset to apply to the path, measured in points. """ self._offset = offset def _offset_transform(self, renderer): """Apply the offset to the given transform.""" return mtransforms.Affine2D().translate( *map(renderer.points_to_pixels, self._offset)) def _update_gc(self, gc, new_gc_dict): """ Update the given GraphicsCollection with the given dictionary of properties. The keys in the dictionary are used to identify the appropriate set_ method on the gc. """ new_gc_dict = new_gc_dict.copy() dashes = new_gc_dict.pop("dashes", None) if dashes: gc.set_dashes(**dashes) for k, v in new_gc_dict.items(): set_method = getattr(gc, 'set_' + k, None) if not callable(set_method): raise AttributeError('Unknown property {0}'.format(k)) set_method(v) return gc
[docs] def draw_path(self, renderer, gc, tpath, affine, rgbFace=None): """ Derived should override this method. The arguments are the same as :meth:`matplotlib.backend_bases.RendererBase.draw_path` except the first argument is a renderer. """ # Get the real renderer, not a PathEffectRenderer. if isinstance(renderer, PathEffectRenderer): renderer = renderer._renderer return renderer.draw_path(gc, tpath, affine, rgbFace)
[docs]class PathEffectRenderer(RendererBase): """ Implements a Renderer which contains another renderer. This proxy then intercepts draw calls, calling the appropriate :class:`AbstractPathEffect` draw method. .. note:: Not all methods have been overridden on this RendererBase subclass. It may be necessary to add further methods to extend the PathEffects capabilities further. """ def __init__(self, path_effects, renderer): """ Parameters ---------- path_effects : iterable of :class:`AbstractPathEffect` The path effects which this renderer represents. renderer : `matplotlib.backend_bases.RendererBase` subclass """ self._path_effects = path_effects self._renderer = renderer
[docs] def copy_with_path_effect(self, path_effects): return self.__class__(path_effects, self._renderer)
[docs] def draw_path(self, gc, tpath, affine, rgbFace=None): for path_effect in self._path_effects: path_effect.draw_path(self._renderer, gc, tpath, affine, rgbFace)
[docs] def draw_markers( self, gc, marker_path, marker_trans, path, *args, **kwargs): # We do a little shimmy so that all markers are drawn for each path # effect in turn. Essentially, we induce recursion (depth 1) which is # terminated once we have just a single path effect to work with. if len(self._path_effects) == 1: # Call the base path effect function - this uses the unoptimised # approach of calling "draw_path" multiple times. return RendererBase.draw_markers(self, gc, marker_path, marker_trans, path, *args, **kwargs) for path_effect in self._path_effects: renderer = self.copy_with_path_effect([path_effect]) # Recursively call this method, only next time we will only have # one path effect. renderer.draw_markers(gc, marker_path, marker_trans, path, *args, **kwargs)
[docs] def draw_path_collection(self, gc, master_transform, paths, *args, **kwargs): # We do a little shimmy so that all paths are drawn for each path # effect in turn. Essentially, we induce recursion (depth 1) which is # terminated once we have just a single path effect to work with. if len(self._path_effects) == 1: # Call the base path effect function - this uses the unoptimised # approach of calling "draw_path" multiple times. return RendererBase.draw_path_collection(self, gc, master_transform, paths, *args, **kwargs) for path_effect in self._path_effects: renderer = self.copy_with_path_effect([path_effect]) # Recursively call this method, only next time we will only have # one path effect. renderer.draw_path_collection(gc, master_transform, paths, *args, **kwargs)
def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): # Implements the naive text drawing as is found in RendererBase. path, transform = self._get_text_path_transform(x, y, s, prop, angle, ismath) color = gc.get_rgb() gc.set_linewidth(0.0) self.draw_path(gc, path, transform, rgbFace=color) def __getattribute__(self, name): if name in ['flipy', 'get_canvas_width_height', 'new_gc', 'points_to_pixels', '_text2path', 'height', 'width']: return getattr(self._renderer, name) else: return object.__getattribute__(self, name)
[docs]class Normal(AbstractPathEffect): """ The "identity" PathEffect. The Normal PathEffect's sole purpose is to draw the original artist with no special path effect. """
def _subclass_with_normal(effect_class): """ Create a PathEffect class combining *effect_class* and a normal draw. """ class withEffect(effect_class): def draw_path(self, renderer, gc, tpath, affine, rgbFace): super().draw_path(renderer, gc, tpath, affine, rgbFace) renderer.draw_path(gc, tpath, affine, rgbFace) withEffect.__name__ = f"with{effect_class.__name__}" withEffect.__qualname__ = f"with{effect_class.__name__}" withEffect.__doc__ = f""" A shortcut PathEffect for applying `.{effect_class.__name__}` and then drawing the original Artist. With this class you can use :: artist.set_path_effects([path_effects.with{effect_class.__name__}()]) as a shortcut for :: artist.set_path_effects([path_effects.{effect_class.__name__}(), path_effects.Normal()]) """ # Docstring inheritance doesn't work for locally-defined subclasses. withEffect.draw_path.__doc__ = effect_class.draw_path.__doc__ return withEffect
[docs]class Stroke(AbstractPathEffect): """A line based PathEffect which re-draws a stroke.""" def __init__(self, offset=(0, 0), **kwargs): """ The path will be stroked with its gc updated with the given keyword arguments, i.e., the keyword arguments should be valid gc parameter values. """ super().__init__(offset) self._gc = kwargs
[docs] def draw_path(self, renderer, gc, tpath, affine, rgbFace): """Draw the path with updated gc.""" gc0 = renderer.new_gc() # Don't modify gc, but a copy! gc0.copy_properties(gc) gc0 = self._update_gc(gc0, self._gc) renderer.draw_path( gc0, tpath, affine + self._offset_transform(renderer), rgbFace) gc0.restore()
withStroke = _subclass_with_normal(effect_class=Stroke)
[docs]class SimplePatchShadow(AbstractPathEffect): """A simple shadow via a filled patch.""" def __init__(self, offset=(2, -2), shadow_rgbFace=None, alpha=None, rho=0.3, **kwargs): """ Parameters ---------- offset : pair of floats The offset of the shadow in points. shadow_rgbFace : color The shadow color. alpha : float, default: 0.3 The alpha transparency of the created shadow patch. http://matplotlib.1069221.n5.nabble.com/path-effects-question-td27630.html rho : float, default: 0.3 A scale factor to apply to the rgbFace color if `shadow_rgbFace` is not specified. **kwargs Extra keywords are stored and passed through to :meth:`AbstractPathEffect._update_gc`. """ super().__init__(offset) if shadow_rgbFace is None: self._shadow_rgbFace = shadow_rgbFace else: self._shadow_rgbFace = mcolors.to_rgba(shadow_rgbFace) if alpha is None: alpha = 0.3 self._alpha = alpha self._rho = rho #: The dictionary of keywords to update the graphics collection with. self._gc = kwargs
[docs] def draw_path(self, renderer, gc, tpath, affine, rgbFace): """ Overrides the standard draw_path to add the shadow offset and necessary color changes for the shadow. """ gc0 = renderer.new_gc() # Don't modify gc, but a copy! gc0.copy_properties(gc) if self._shadow_rgbFace is None: r, g, b = (rgbFace or (1., 1., 1.))[:3] # Scale the colors by a factor to improve the shadow effect. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho) else: shadow_rgbFace = self._shadow_rgbFace gc0.set_foreground("none") gc0.set_alpha(self._alpha) gc0.set_linewidth(0) gc0 = self._update_gc(gc0, self._gc) renderer.draw_path( gc0, tpath, affine + self._offset_transform(renderer), shadow_rgbFace) gc0.restore()
withSimplePatchShadow = _subclass_with_normal(effect_class=SimplePatchShadow)
[docs]class SimpleLineShadow(AbstractPathEffect): """A simple shadow via a line.""" def __init__(self, offset=(2, -2), shadow_color='k', alpha=0.3, rho=0.3, **kwargs): """ Parameters ---------- offset : pair of floats The offset to apply to the path, in points. shadow_color : color, default: 'black' The shadow color. A value of ``None`` takes the original artist's color with a scale factor of *rho*. alpha : float, default: 0.3 The alpha transparency of the created shadow patch. rho : float, default: 0.3 A scale factor to apply to the rgbFace color if `shadow_rgbFace` is ``None``. **kwargs Extra keywords are stored and passed through to :meth:`AbstractPathEffect._update_gc`. """ super().__init__(offset) if shadow_color is None: self._shadow_color = shadow_color else: self._shadow_color = mcolors.to_rgba(shadow_color) self._alpha = alpha self._rho = rho #: The dictionary of keywords to update the graphics collection with. self._gc = kwargs
[docs] def draw_path(self, renderer, gc, tpath, affine, rgbFace): """ Overrides the standard draw_path to add the shadow offset and necessary color changes for the shadow. """ gc0 = renderer.new_gc() # Don't modify gc, but a copy! gc0.copy_properties(gc) if self._shadow_color is None: r, g, b = (gc0.get_foreground() or (1., 1., 1.))[:3] # Scale the colors by a factor to improve the shadow effect. shadow_rgbFace = (r * self._rho, g * self._rho, b * self._rho) else: shadow_rgbFace = self._shadow_color gc0.set_foreground(shadow_rgbFace) gc0.set_alpha(self._alpha) gc0 = self._update_gc(gc0, self._gc) renderer.draw_path( gc0, tpath, affine + self._offset_transform(renderer)) gc0.restore()
[docs]class PathPatchEffect(AbstractPathEffect): """ Draws a `.PathPatch` instance whose Path comes from the original PathEffect artist. """ def __init__(self, offset=(0, 0), **kwargs): """ Parameters ---------- offset : pair of floats The offset to apply to the path, in points. **kwargs All keyword arguments are passed through to the :class:`~matplotlib.patches.PathPatch` constructor. The properties which cannot be overridden are "path", "clip_box" "transform" and "clip_path". """ super().__init__(offset=offset) self.patch = mpatches.PathPatch([], **kwargs)
[docs] def draw_path(self, renderer, gc, tpath, affine, rgbFace): self.patch._path = tpath self.patch.set_transform(affine + self._offset_transform(renderer)) self.patch.set_clip_box(gc.get_clip_rectangle()) clip_path = gc.get_clip_path() if clip_path: self.patch.set_clip_path(*clip_path) self.patch.draw(renderer)