123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- """
- Defines classes for path effects. The path effects are supported in
- :class:`~matplotlib.text.Text`, :class:`~matplotlib.lines.Line2D`
- and :class:`~matplotlib.patches.Patch`.
- """
- from matplotlib.backend_bases import RendererBase
- from matplotlib import colors as mcolors
- from matplotlib import patches as mpatches
- from matplotlib import transforms as mtransforms
- 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
- 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)
- 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 : :class:`matplotlib.backend_bases.RendererBase` instance
- """
- self._path_effects = path_effects
- self._renderer = renderer
- def copy_with_path_effect(self, path_effects):
- return self.__class__(path_effects, self._renderer)
- 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)
- 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)
- 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)
- class Normal(AbstractPathEffect):
- """
- The "identity" PathEffect.
- The Normal PathEffect's sole purpose is to draw the original artist with
- no special path effect.
- """
- pass
- 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
- 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()
- class withStroke(Stroke):
- """
- Adds a simple :class:`Stroke` and then draws the
- original Artist to avoid needing to call :class:`Normal`.
- """
- def draw_path(self, renderer, gc, tpath, affine, rgbFace):
- Stroke.draw_path(self, renderer, gc, tpath, affine, rgbFace)
- renderer.draw_path(gc, tpath, affine, rgbFace)
- 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
- The alpha transparency of the created shadow patch.
- Default is 0.3.
- http://matplotlib.1069221.n5.nabble.com/path-effects-question-td27630.html
- rho : float
- A scale factor to apply to the rgbFace color if `shadow_rgbFace`
- is not specified. Default is 0.3.
- **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
- 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()
- class withSimplePatchShadow(SimplePatchShadow):
- """
- Adds a simple :class:`SimplePatchShadow` and then draws the
- original Artist to avoid needing to call :class:`Normal`.
- """
- def draw_path(self, renderer, gc, tpath, affine, rgbFace):
- SimplePatchShadow.draw_path(self, renderer, gc, tpath, affine, rgbFace)
- renderer.draw_path(gc, tpath, affine, rgbFace)
- 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
- The shadow color. Default is black.
- A value of ``None`` takes the original artist's color
- with a scale factor of *rho*.
- alpha : float
- The alpha transparency of the created shadow patch.
- Default is 0.3.
- rho : float
- A scale factor to apply to the rgbFace color if `shadow_rgbFace`
- is ``None``. Default is 0.3.
- **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
- 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()
- class PathPatchEffect(AbstractPathEffect):
- """
- Draws a :class:`~matplotlib.patches.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)
- 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)
|