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()])