diff --git a/CHANGELOG b/CHANGELOG
index b266df9123d0..ae7f142ca84c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+2013-04-16 Added patheffect support for Line2D objects. -JJL
+
2013-03-19 Added support for passing `linestyle` kwarg to `step` so all `plot`
kwargs are passed to the underlying `plot` call. -TAC
diff --git a/examples/pylab_examples/patheffect_demo.py b/examples/pylab_examples/patheffect_demo.py
index 665b9dc8fccf..13279533ec73 100644
--- a/examples/pylab_examples/patheffect_demo.py
+++ b/examples/pylab_examples/patheffect_demo.py
@@ -17,10 +17,23 @@
foreground="w"),
PathEffects.Normal()])
+ ax1.grid(True, linestyle="-")
+
+ pe = [PathEffects.withStroke(linewidth=3,
+ foreground="w")]
+ for l in ax1.get_xgridlines() + ax1.get_ygridlines():
+ l.set_path_effects(pe)
+
ax2 = plt.subplot(132)
arr = np.arange(25).reshape((5,5))
ax2.imshow(arr)
cntr = ax2.contour(arr, colors="k")
+
+ plt.setp(cntr.collections,
+ path_effects=[PathEffects.withStroke(linewidth=3,
+ foreground="w")])
+
+
clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
plt.setp(clbls,
path_effects=[PathEffects.withStroke(linewidth=3,
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index 867306a4c873..e8d9cee61539 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -91,6 +91,7 @@ def __init__(self,
urls=None,
offset_position='screen',
zorder=1,
+ path_effects=None,
**kwargs
):
"""
@@ -123,6 +124,7 @@ def __init__(self,
else:
self._uniform_offsets = offsets
+ self._path_effects = None
self.update(kwargs)
self._paths = None
@@ -255,11 +257,21 @@ def draw(self, renderer):
if self._hatch:
gc.set_hatch(self._hatch)
- renderer.draw_path_collection(
- gc, transform.frozen(), paths, self.get_transforms(),
- offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
- self._linewidths, self._linestyles, self._antialiaseds, self._urls,
- self._offset_position)
+ if self.get_path_effects():
+ #from patheffects import PathEffectsRenderer
+ for pe in self.get_path_effects():
+ pe.draw_path_collection(renderer,
+ gc, transform.frozen(), paths, self.get_transforms(),
+ offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
+ self._linewidths, self._linestyles, self._antialiaseds, self._urls,
+ self._offset_position)
+ else:
+
+ renderer.draw_path_collection(
+ gc, transform.frozen(), paths, self.get_transforms(),
+ offsets, transOffset, self.get_facecolor(), self.get_edgecolor(),
+ self._linewidths, self._linestyles, self._antialiaseds, self._urls,
+ self._offset_position)
gc.restore()
renderer.close_group(self.__class__.__name__)
@@ -638,6 +650,17 @@ def update_from(self, other):
self.cmap = other.cmap
# self.update_dict = other.update_dict # do we need to copy this? -JJL
+ def set_path_effects(self, path_effects):
+ """
+ set path_effects, which should be a list of instances of
+ matplotlib.patheffect._Base class or its derivatives.
+ """
+ self._path_effects = path_effects
+
+ def get_path_effects(self):
+ return self._path_effects
+
+
# these are not available for the object inspector until after the
# class is built so we define an initial set here for the init
# function and they will be overridden after object defn
diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py
index 7d4121dcdc62..fc2db3e4d4d3 100644
--- a/lib/matplotlib/lines.py
+++ b/lib/matplotlib/lines.py
@@ -149,6 +149,7 @@ def __init__(self, xdata, ydata,
pickradius=5,
drawstyle=None,
markevery=None,
+ path_effects = None,
**kwargs
):
"""
@@ -236,6 +237,8 @@ def __init__(self, xdata, ydata,
self._invalidy = True
self.set_data(xdata, ydata)
+ self.set_path_effects(path_effects)
+
def contains(self, mouseevent):
"""
Test whether the mouse event occurred on the line. The pick
@@ -550,7 +553,14 @@ def draw(self, renderer):
self._lineFunc = getattr(self, funcname)
funcname = self.drawStyles.get(self._drawstyle, '_draw_lines')
drawFunc = getattr(self, funcname)
- drawFunc(renderer, gc, tpath, affine.frozen())
+
+ if self.get_path_effects():
+ affine_frozen = affine.frozen()
+ for pe in self.get_path_effects():
+ pe_renderer = pe.get_proxy_renderer(renderer)
+ drawFunc(pe_renderer, gc, tpath, affine_frozen)
+ else:
+ drawFunc(renderer, gc, tpath, affine.frozen())
if self._marker:
gc = renderer.new_gc()
@@ -600,18 +610,34 @@ def draw(self, renderer):
gc.set_linewidth(0)
if rgbaFace is not None:
gc.set_alpha(rgbaFace[3])
- renderer.draw_markers(
- gc, marker_path, marker_trans, subsampled, affine.frozen(),
- rgbaFace)
+
+ if self.get_path_effects():
+ affine_frozen = affine.frozen()
+ for pe in self.get_path_effects():
+ pe.draw_markers(renderer, gc, marker_path, marker_trans,
+ subsampled, affine_frozen, rgbaFace)
+ else:
+ renderer.draw_markers(
+ gc, marker_path, marker_trans, subsampled, affine.frozen(),
+ rgbaFace)
+
alt_marker_path = marker.get_alt_path()
if alt_marker_path:
if rgbaFaceAlt is not None:
gc.set_alpha(rgbaFaceAlt[3])
alt_marker_trans = marker.get_alt_transform()
alt_marker_trans = alt_marker_trans.scale(w)
- renderer.draw_markers(
- gc, alt_marker_path, alt_marker_trans, subsampled,
- affine.frozen(), rgbaFaceAlt)
+
+ if self.get_path_effects():
+ affine_frozen = affine.frozen()
+ for pe in self.get_path_effects():
+ pe.draw_markers(renderer, gc, alt_marker_path,
+ alt_marker_trans, subsampled,
+ affine_frozen, rgbaFaceAlt)
+ else:
+ renderer.draw_markers(
+ gc, alt_marker_path, alt_marker_trans, subsampled,
+ affine.frozen(), rgbaFaceAlt)
gc.restore()
@@ -1161,6 +1187,16 @@ def is_dashed(self):
'return True if line is dashstyle'
return self._linestyle in ('--', '-.', ':')
+ def set_path_effects(self, path_effects):
+ """
+ set path_effects, which should be a list of instances of
+ matplotlib.patheffect._Base class or its derivatives.
+ """
+ self._path_effects = path_effects
+
+ def get_path_effects(self):
+ return self._path_effects
+
class VertexSelector:
"""
diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py
index 62707c5208da..77200188ce62 100644
--- a/lib/matplotlib/patheffects.py
+++ b/lib/matplotlib/patheffects.py
@@ -10,7 +10,6 @@
import matplotlib.transforms as transforms
-
class _Base(object):
"""
A base class for PathEffect. Derived must override draw_path method.
@@ -22,6 +21,9 @@ def __init__(self):
"""
super(_Base, self).__init__()
+ def get_proxy_renderer(self, renderer):
+ pe_renderer = ProxyRenderer(self, renderer)
+ return pe_renderer
def _update_gc(self, gc, new_gc_dict):
new_gc_dict = new_gc_dict.copy()
@@ -39,7 +41,7 @@ def _update_gc(self, gc, new_gc_dict):
return gc
- def draw_path(self, renderer, gc, tpath, affine, rgbFace):
+ def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
"""
Derived should override this method. The argument is same
as *draw_path* method of :class:`matplotlib.backend_bases.RendererBase`
@@ -51,6 +53,50 @@ def draw_path(self, renderer, gc, tpath, affine, rgbFace):
"""
renderer.draw_path(gc, tpath, affine, rgbFace)
+ def draw_path_collection(self, renderer,
+ gc, master_transform, paths, all_transforms,
+ offsets, offsetTrans, facecolors, edgecolors,
+ linewidths, linestyles, antialiaseds, urls,
+ offset_position):
+ """
+ Draws a collection of paths selecting drawing properties from
+ the lists *facecolors*, *edgecolors*, *linewidths*,
+ *linestyles* and *antialiaseds*. *offsets* is a list of
+ offsets to apply to each of the paths. The offsets in
+ *offsets* are first transformed by *offsetTrans* before being
+ applied. *offset_position* may be either "screen" or "data"
+ depending on the space that the offsets are in.
+
+ This provides a fallback implementation of
+ :meth:`draw_path_collection` that makes multiple calls to
+ :meth:`draw_path`. Some backends may want to override this in
+ order to render each set of path data only once, and then
+ reference that path multiple times with the different offsets,
+ colors, styles etc. The generator methods
+ :meth:`_iter_collection_raw_paths` and
+ :meth:`_iter_collection` are provided to help with (and
+ standardize) the implementation across backends. It is highly
+ recommended to use those generators, so that changes to the
+ behavior of :meth:`draw_path_collection` can be made globally.
+ """
+
+ if isinstance(renderer, MixedModeRenderer):
+ renderer = renderer._renderer
+
+ path_ids = []
+ for path, transform in renderer._iter_collection_raw_paths(
+ master_transform, paths, all_transforms):
+ path_ids.append((path, transform))
+
+ for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
+ gc, master_transform, all_transforms, path_ids, offsets,
+ offsetTrans, facecolors, edgecolors, linewidths, linestyles,
+ antialiaseds, urls, offset_position):
+ path, transform = path_id
+ transform = transforms.Affine2D(transform.get_matrix()).translate(xo, yo)
+ self.draw_path(renderer, gc0, path, transform, rgbFace)
+
+
def draw_tex(self, renderer, gc, x, y, s, prop, angle, ismath='TeX!'):
self._draw_text_as_path(renderer, gc, x, y, s, prop, angle, ismath="TeX")
@@ -71,22 +117,66 @@ def _draw_text_as_path(self, renderer, gc, x, y, s, prop, angle, ismath):
gc.set_linewidth(0.0)
self.draw_path(renderer, gc, path, transform, rgbFace=color)
+ def draw_markers(self, renderer, gc, marker_path, marker_trans, path, trans, rgbFace=None):
+ """
+ Draws a marker at each of the vertices in path. This includes
+ all vertices, including control points on curves. To avoid
+ that behavior, those vertices should be removed before calling
+ this function.
+
+ *gc*
+ the :class:`GraphicsContextBase` instance
+
+ *marker_trans*
+ is an affine transform applied to the marker.
-# def draw_path_collection(self, renderer,
-# gc, master_transform, paths, all_transforms,
-# offsets, offsetTrans, facecolors, edgecolors,
-# linewidths, linestyles, antialiaseds, urls):
-# path_ids = []
-# for path, transform in renderer._iter_collection_raw_paths(
-# master_transform, paths, all_transforms):
-# path_ids.append((path, transform))
-
-# for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection(
-# gc, path_ids, offsets, offsetTrans, facecolors, edgecolors,
-# linewidths, linestyles, antialiaseds, urls):
-# path, transform = path_id
-# transform = transforms.Affine2D(transform.get_matrix()).translate(xo, yo)
-# self.draw_path(renderer, gc0, path, transform, rgbFace)
+ *trans*
+ is an affine transform applied to the path.
+
+ This provides a fallback implementation of draw_markers that
+ makes multiple calls to :meth:`draw_path`. Some backends may
+ want to override this method in order to draw the marker only
+ once and reuse it multiple times.
+ """
+ for vertices, codes in path.iter_segments(trans, simplify=False):
+ if len(vertices):
+ x,y = vertices[-2:]
+ self.draw_path(renderer, gc, marker_path,
+ marker_trans + transforms.Affine2D().translate(x, y),
+ rgbFace)
+
+
+class ProxyRenderer(object):
+ def __init__(self, path_effect, renderer):
+ self._path_effect = path_effect
+ self._renderer = renderer
+
+ def draw_path(self, gc, tpath, affine, rgbFace=None):
+ self._path_effect.draw_path(self._renderer, gc, tpath, affine, rgbFace)
+
+ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'):
+ self._path_effect._draw_text_as_path(self._renderer,
+ gc, x, y, s, prop, angle, ismath="TeX")
+
+ def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
+ self._path_effect._draw_text(self.renderer,
+ gc, x, y, s, prop, angle, ismath)
+
+ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
+ self._path_effect.draw_markers(self._renderer,
+ gc, marker_path, marker_trans, path, trans,
+ rgbFace=rgbFace)
+
+ def draw_path_collection(self, gc, master_transform, paths, all_transforms,
+ offsets, offsetTrans, facecolors, edgecolors,
+ linewidths, linestyles, antialiaseds, urls,
+ offset_position):
+ pe = self._path_effect
+ pe.draw_path_collection(self._renderer,
+ gc, master_transform, paths, all_transforms,
+ offsets, offsetTrans, facecolors, edgecolors,
+ linewidths, linestyles, antialiaseds, urls,
+ offset_position)
class Normal(_Base):
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf
new file mode 100644
index 000000000000..02b2fe9141ae
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.png b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.png
new file mode 100644
index 000000000000..9c572860ad24
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.png differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.svg b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.svg
new file mode 100644
index 000000000000..d1f31166e652
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.svg
@@ -0,0 +1,450 @@
+
+
+
+
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf
new file mode 100644
index 000000000000..4ddf9f7ad9d8
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png
new file mode 100644
index 000000000000..8b39ed66bf96
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg
new file mode 100644
index 000000000000..e47a1b221b86
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg
@@ -0,0 +1,842 @@
+
+
+
+
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.pdf b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.pdf
new file mode 100644
index 000000000000..1d400a17dee9
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.pdf differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.png b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.png
new file mode 100644
index 000000000000..709e17dbd6c8
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.png differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.svg b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.svg
new file mode 100644
index 000000000000..8408f55b6790
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect3.svg
@@ -0,0 +1,357 @@
+
+
+
+
diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py
new file mode 100644
index 000000000000..7a85e0037b40
--- /dev/null
+++ b/lib/matplotlib/tests/test_patheffects.py
@@ -0,0 +1,54 @@
+import numpy as np
+
+from matplotlib.testing.decorators import image_comparison
+import matplotlib.pyplot as plt
+from matplotlib.patheffects import (Normal, Stroke, withStroke,
+ withSimplePatchShadow)
+
+
+@image_comparison(baseline_images=['patheffect1'], remove_text=True)
+def test_patheffect1():
+ ax1 = plt.subplot(111)
+ ax1.imshow([[1,2],[2,3]])
+ txt = ax1.annotate("test", (1., 1.), (0., 0),
+ arrowprops=dict(arrowstyle="->",
+ connectionstyle="angle3", lw=2),
+ size=20, ha="center")
+
+ txt.set_path_effects([withStroke(linewidth=3,
+ foreground="w")])
+ txt.arrow_patch.set_path_effects([Stroke(linewidth=5,
+ foreground="w"),
+ Normal()])
+
+ ax1.grid(True, linestyle="-")
+
+ pe = [withStroke(linewidth=3, foreground="w")]
+ for l in ax1.get_xgridlines() + ax1.get_ygridlines():
+ l.set_path_effects(pe)
+
+
+@image_comparison(baseline_images=['patheffect2'], remove_text=True)
+def test_patheffect2():
+
+ ax2 = plt.subplot(111)
+ arr = np.arange(25).reshape((5,5))
+ ax2.imshow(arr)
+ cntr = ax2.contour(arr, colors="k")
+
+ plt.setp(cntr.collections,
+ path_effects=[withStroke(linewidth=3, foreground="w")])
+
+
+ clbls = ax2.clabel(cntr, fmt="%2.0f", use_clabeltext=True)
+ plt.setp(clbls,
+ path_effects=[withStroke(linewidth=3, foreground="w")])
+
+
+@image_comparison(baseline_images=['patheffect3'], remove_text=True)
+def test_patheffect3():
+
+ ax3 = plt.subplot(111)
+ p1, = ax3.plot([0, 1], [0, 1])
+ leg = ax3.legend([p1], ["Line 1"], fancybox=True, loc=2)
+ leg.legendPatch.set_path_effects([withSimplePatchShadow()])