From 9270adddc8af514dba06944b0676021537755e1e Mon Sep 17 00:00:00 2001 From: Jae-Joon Lee Date: Tue, 3 Jan 2012 11:38:18 +0900 Subject: [PATCH 1/3] implement path_effects for Line2D objects --- lib/matplotlib/lines.py | 50 +++++++++++++++++++++++++++----- lib/matplotlib/patheffects.py | 54 +++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 2c7fa9bb49f0..2fd45dc2ad7b 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -144,6 +144,7 @@ def __init__(self, xdata, ydata, pickradius = 5, drawstyle = None, markevery = None, + path_effects = None, **kwargs ): """ @@ -222,6 +223,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 @@ -504,7 +507,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() @@ -552,16 +562,31 @@ def draw(self, renderer): marker_trans = marker_trans.scale(w) else: gc.set_linewidth(0) - renderer.draw_markers( - gc, marker_path, marker_trans, subsampled, affine.frozen(), - rgbFace) + + 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, rgbFace) + else: + renderer.draw_markers( + gc, marker_path, marker_trans, subsampled, affine.frozen(), + rgbFace) + alt_marker_path = marker.get_alt_path() if alt_marker_path: 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(), rgbFaceAlt) + 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, rgbFaceAlt) + else: + renderer.draw_markers( + gc, alt_marker_path, alt_marker_trans, subsampled, + affine.frozen(), rgbFaceAlt) gc.restore() @@ -1103,6 +1128,17 @@ 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: """ Manage the callbacks to maintain a list of selected vertices for diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py index 62707c5208da..35f80247bd96 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` @@ -71,6 +73,33 @@ 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. + + *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) # def draw_path_collection(self, renderer, # gc, master_transform, paths, all_transforms, @@ -88,6 +117,27 @@ def _draw_text_as_path(self, renderer, gc, x, y, s, prop, angle, ismath): # transform = transforms.Affine2D(transform.get_matrix()).translate(xo, yo) # self.draw_path(renderer, gc0, path, transform, 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) + class Normal(_Base): """ From fa5ad6e70da52f08ec09d2d3222c63288a7e6ccf Mon Sep 17 00:00:00 2001 From: Jae-Joon Lee Date: Mon, 5 Mar 2012 00:16:29 +0900 Subject: [PATCH 2/3] modify patheffect_demo.py to demonstrate patheffects for Line2D objects --- examples/pylab_examples/patheffect_demo.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/pylab_examples/patheffect_demo.py b/examples/pylab_examples/patheffect_demo.py index 665b9dc8fccf..58bfa08f00f1 100644 --- a/examples/pylab_examples/patheffect_demo.py +++ b/examples/pylab_examples/patheffect_demo.py @@ -17,6 +17,13 @@ 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) From adfc7b7437c2515c0a097f27474608961fe4e12a Mon Sep 17 00:00:00 2001 From: Jae-Joon Lee Date: Mon, 16 Jul 2012 14:19:45 +0900 Subject: [PATCH 3/3] initial implementation of patheffect for collections --- examples/pylab_examples/patheffect_demo.py | 6 ++ lib/matplotlib/collections.py | 33 +++++++++-- lib/matplotlib/patheffects.py | 66 +++++++++++++++++----- 3 files changed, 85 insertions(+), 20 deletions(-) diff --git a/examples/pylab_examples/patheffect_demo.py b/examples/pylab_examples/patheffect_demo.py index 58bfa08f00f1..13279533ec73 100644 --- a/examples/pylab_examples/patheffect_demo.py +++ b/examples/pylab_examples/patheffect_demo.py @@ -28,6 +28,12 @@ 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 de1d762fc1d0..68c7234ec75d 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -86,6 +86,7 @@ def __init__(self, hatch=None, urls = None, offset_position='screen', + path_effects=None, **kwargs ): """ @@ -119,6 +120,7 @@ def __init__(self, else: self._uniform_offsets = offsets + self._path_effects = None self.update(kwargs) self._paths = None @@ -245,11 +247,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__) @@ -627,6 +639,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/patheffects.py b/lib/matplotlib/patheffects.py index 35f80247bd96..ab82f9088cfe 100644 --- a/lib/matplotlib/patheffects.py +++ b/lib/matplotlib/patheffects.py @@ -53,6 +53,46 @@ 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. + """ + 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") @@ -101,21 +141,6 @@ def draw_markers(self, renderer, gc, marker_path, marker_trans, path, trans, rgb marker_trans + transforms.Affine2D().translate(x, y), rgbFace) -# 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) class ProxyRenderer(object): def __init__(self, path_effect, renderer): @@ -138,6 +163,17 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) 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): """