diff --git a/examples/pylab_examples/contourf_hatching.py b/examples/pylab_examples/contourf_hatching.py new file mode 100755 index 000000000000..d9760981bf51 --- /dev/null +++ b/examples/pylab_examples/contourf_hatching.py @@ -0,0 +1,45 @@ +import matplotlib.pyplot as plt +import numpy + + +# invent some numbers, turning the x and y arrays into simple +# 2d arrays, which make combining them together easier. +x = numpy.linspace(-3, 5, 150).reshape(1, -1) +y = numpy.linspace(-3, 5, 120).reshape(-1, 1) +z = numpy.cos(x) + numpy.sin(y) + +# we no longer need x and y to be 2 dimensional, so flatten them. +x, y = x.flatten(), y.flatten() + + +# --------------------------------------------- +# | Plot #1 | +# --------------------------------------------- +# the simplest hatched plot with a colorbar +fig = plt.figure() +cs = plt.contourf(x, y, z, hatches=['-', '/', '\\', '//'], + cmap=plt.get_cmap('gray'), + extend='both', alpha=0.5 + ) +plt.colorbar() + + +# --------------------------------------------- +# | Plot #2 | +# --------------------------------------------- +# a plot of hatches without color with a legend +plt.figure() +n_levels = 6 +plt.contour(x, y, z, n_levels, colors='black', linestyles='-') +cs = plt.contourf(x, y, z, n_levels, colors='none', + hatches=['.', '/', '\\', None, '\\\\', '*'], + extend='lower' + ) + +# create a legend for the contour set +artists, labels = cs.legend_elements() +plt.legend(artists, labels, handleheight=2) + + + +plt.show() \ No newline at end of file diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 79b11bd87cd1..5d6b7dd71d94 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -46,6 +46,7 @@ class Collection(artist.Artist, cm.ScalarMappable): :class:`matplotlib.cm.ScalarMappable`) * *cmap*: None (optional for :class:`matplotlib.cm.ScalarMappable`) + * *hatch*: None *offsets* and *transOffset* are used to translate the patch after rendering (default no offsets). @@ -77,6 +78,7 @@ def __init__(self, norm = None, # optional for ScalarMappable cmap = None, # ditto pickradius = 5.0, + hatch=None, urls = None, **kwargs ): @@ -95,6 +97,7 @@ def __init__(self, self.set_antialiased(antialiaseds) self.set_pickradius(pickradius) self.set_urls(urls) + self.set_hatch(hatch) self._uniform_offsets = None @@ -232,7 +235,10 @@ def draw(self, renderer): gc = renderer.new_gc() self._set_gc_clip(gc) gc.set_snap(self.get_snap()) - + + 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(), @@ -292,6 +298,38 @@ def set_urls(self, urls): def get_urls(self): return self._urls + def set_hatch(self, hatch): + """ + Set the hatching pattern + + *hatch* can be one of:: + + / - diagonal hatching + \ - back diagonal + | - vertical + - - horizontal + + - crossed + x - crossed diagonal + o - small circle + O - large circle + . - dots + * - stars + + Letters can be combined, in which case all the specified + hatchings are done. If same letter repeats, it increases the + density of hatching of that pattern. + + Hatching is supported in the PostScript, PDF, SVG and Agg + backends only. + + ACCEPTS: [ '/' | '\\\\' | '|' | '-' | '+' | 'x' | 'o' | 'O' | '.' | '*' ] + """ + self._hatch = hatch + + def get_hatch(self): + 'Return the current hatching pattern' + return self._hatch + def set_offsets(self, offsets): """ Set the offsets for the collection. *offsets* can be a scalar @@ -547,6 +585,7 @@ def update_from(self, other): self._linewidths = other._linewidths self._linestyles = other._linestyles self._pickradius = other._pickradius + self._hatch = other._hatch # update_from for scalarmappable self._A = other._A diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index e0758ce71cf4..aa1f436bf60c 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -22,20 +22,21 @@ import warnings import numpy as np + import matplotlib as mpl -import matplotlib.colors as colors -import matplotlib.cm as cm -from matplotlib import docstring -import matplotlib.ticker as ticker +import matplotlib.artist as martist import matplotlib.cbook as cbook -import matplotlib.lines as lines -import matplotlib.patches as patches import matplotlib.collections as collections +import matplotlib.colors as colors import matplotlib.contour as contour -import matplotlib.artist as martist - +import matplotlib.cm as cm import matplotlib.gridspec as gridspec +import matplotlib.lines as lines +import matplotlib.patches as mpatches +import matplotlib.path as mpath +import matplotlib.ticker as ticker +from matplotlib import docstring make_axes_kw_doc = ''' @@ -203,10 +204,10 @@ class ColorbarBase(cm.ScalarMappable): Useful public methods are :meth:`set_label` and :meth:`add_lines`. ''' - _slice_dict = {'neither': slice(0,1000000), - 'both': slice(1,-1), - 'min': slice(1,1000000), - 'max': slice(0,-1)} + _slice_dict = {'neither': slice(0, None), + 'both': slice(1, -1), + 'min': slice(1, None), + 'max': slice(0, -1)} def __init__(self, ax, cmap=None, norm=None, @@ -258,6 +259,14 @@ def __init__(self, ax, cmap=None, self.config_axis() self.draw_all() + def _extend_lower(self): + """Returns whether the lower limit is open ended.""" + return self.extend in ('both', 'min') + + def _extend_upper(self): + """Returns whether the uper limit is open ended.""" + return self.extend in ('both', 'max') + def _patch_ax(self): def _warn(*args, **kw): warnings.warn("Use the colorbar set_ticks() method instead.") @@ -273,7 +282,7 @@ def draw_all(self): self._process_values() self._find_range() X, Y = self._mesh() - C = self._values[:,np.newaxis] + C = self._values[:, np.newaxis] self._config_axes(X, Y) if self.filled: self._add_solids(X, Y, C) @@ -354,7 +363,7 @@ def _config_axes(self, X, Y): c = mpl.rcParams['axes.facecolor'] if self.patch is not None: self.patch.remove() - self.patch = patches.Polygon(xy, edgecolor=c, + self.patch = mpatches.Polygon(xy, edgecolor=c, facecolor=c, linewidth=0.01, zorder=-1) @@ -401,13 +410,13 @@ def _edges(self, X, Y): # Using the non-array form of these line segments is much # simpler than making them into arrays. if self.orientation == 'vertical': - return [zip(X[i], Y[i]) for i in range(1, N-1)] + return [zip(X[i], Y[i]) for i in xrange(1, N-1)] else: - return [zip(Y[i], X[i]) for i in range(1, N-1)] + return [zip(Y[i], X[i]) for i in xrange(1, N-1)] def _add_solids(self, X, Y, C): ''' - Draw the colors using :meth:`~matplotlib.axes.Axes.pcolor`; + Draw the colors using :meth:`~matplotlib.axes.Axes.pcolormesh`; optionally add separators. ''' if self.orientation == 'vertical': @@ -449,9 +458,9 @@ def add_lines(self, levels, colors, linewidths): x = np.array([0.0, 1.0]) X, Y = np.meshgrid(x,y) if self.orientation == 'vertical': - xy = [zip(X[i], Y[i]) for i in range(N)] + xy = [zip(X[i], Y[i]) for i in xrange(N)] else: - xy = [zip(Y[i], X[i]) for i in range(N)] + xy = [zip(Y[i], X[i]) for i in xrange(N)] col = collections.LineCollection(xy, linewidths=linewidths) if self.lines: @@ -540,26 +549,26 @@ def _process_values(self, b=None): b = self._uniform_y(self.cmap.N+1) * self.cmap.N - 0.5 v = np.zeros((len(b)-1,), dtype=np.int16) v[self._inside] = np.arange(self.cmap.N, dtype=np.int16) - if self.extend in ('both', 'min'): + if self._extend_lower(): v[0] = -1 - if self.extend in ('both', 'max'): + if self._extend_upper(): v[-1] = self.cmap.N self._boundaries = b self._values = v return elif isinstance(self.norm, colors.BoundaryNorm): b = list(self.norm.boundaries) - if self.extend in ('both', 'min'): + if self._extend_lower(): b = [b[0]-1] + b - if self.extend in ('both', 'max'): + if self._extend_upper(): b = b + [b[-1] + 1] b = np.array(b) v = np.zeros((len(b)-1,), dtype=float) bi = self.norm.boundaries v[self._inside] = 0.5*(bi[:-1] + bi[1:]) - if self.extend in ('both', 'min'): + if self._extend_lower(): v[0] = b[0] - 1 - if self.extend in ('both', 'max'): + if self._extend_upper(): v[-1] = b[-1] + 1 self._boundaries = b self._values = v @@ -569,9 +578,9 @@ def _process_values(self, b=None): self.norm.vmin = 0 self.norm.vmax = 1 b = self.norm.inverse(self._uniform_y(self.cmap.N+1)) - if self.extend in ('both', 'min'): + if self._extend_lower(): b[0] = b[0] - 1 - if self.extend in ('both', 'max'): + if self._extend_upper(): b[-1] = b[-1] + 1 self._process_values(b) @@ -589,7 +598,7 @@ def _central_N(self): nb = len(self._boundaries) if self.extend == 'both': nb -= 2 - elif self.extend in ('min', 'max'): + elif self._extend_lower(): nb -= 1 return nb @@ -637,9 +646,9 @@ def _proportional_y(self): y = y / (self._boundaries[-1] - self._boundaries[0]) else: y = self.norm(self._boundaries.copy()) - if self.extend in ('both', 'min'): + if self._extend_lower(): y[0] = -0.05 - if self.extend in ('both', 'max'): + if self._extend_upper(): y[-1] = 1.05 yi = y[self._inside] norm = colors.Normalize(yi[0], yi[-1]) @@ -660,10 +669,10 @@ def _mesh(self): y = self._proportional_y() self._y = y X, Y = np.meshgrid(x,y) - if self.extend in ('min', 'both'): - X[0,:] = 0.5 - if self.extend in ('max', 'both'): - X[-1,:] = 0.5 + if self._extend_lower(): + X[0, :] = 0.5 + if self._extend_upper(): + X[-1, :] = 0.5 return X, Y def _locate(self, x): @@ -703,6 +712,7 @@ def _locate(self, x): def set_alpha(self, alpha): self.alpha = alpha + class Colorbar(ColorbarBase): """ This class connects a :class:`ColorbarBase` to a @@ -743,6 +753,17 @@ def __init__(self, ax, mappable, **kw): ColorbarBase.__init__(self, ax, **kw) + def on_mappable_changed(self, mappable): + """ + Updates this colorbar to match the mappable's properties. + + Typically this is automatically registered as an event handler + by :func:`colorbar_factory` and should not be called manually. + + """ + self.set_cmap(mappable.get_cmap()) + self.set_clim(mappable.get_clim()) + self.update_normal(mappable) def add_lines(self, CS): ''' @@ -952,3 +973,102 @@ def make_axes_gridspec(parent, **kw): cax = fig.add_subplot(gs2[1]) cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw + + +class ColorbarPatch(Colorbar): + """ + A Colorbar which is created using :class:`~matplotlib.patches.Patch` + rather than the default :func:`~matplotlib.axes.pcolor`. + + """ + def __init__(self, ax, mappable, **kw): + # we do not want to override the behaviour of solids + # so add a new attribute which will be a list of the + # colored patches in the colorbar + self.solids_patches = [] + Colorbar.__init__(self, ax, mappable, **kw) + + def _add_solids(self, X, Y, C): + ''' + Draw the colors using :class:`~matplotlib.patches.Patch`; + optionally add separators. + ''' + # Save, set, and restore hold state to keep pcolor from + # clearing the axes. Ordinarily this will not be needed, + # since the axes object should already have hold set. + _hold = self.ax.ishold() + self.ax.hold(True) + + kw = {'alpha':self.alpha,} + + n_segments = len(C) + + # ensure there are sufficent hatches + hatches = self.mappable.hatches * n_segments + + patches = [] + for i in xrange(len(X)-1): + val = C[i][0] + hatch = hatches[i] + + xy = np.array([[X[i][0], Y[i][0]], [X[i][1], Y[i][0]], + [X[i+1][1], Y[i+1][0]], [X[i+1][0], Y[i+1][1]]]) + + if self.orientation == 'horizontal': + # if horizontal swap the xs and ys + xy = xy[..., ::-1] + + patch = mpatches.PathPatch(mpath.Path(xy), + facecolor=self.cmap(self.norm(val)), + hatch=hatch, + edgecolor='none', linewidth=0, + antialiased=False, **kw + ) + c = self.mappable.collections[i] + + self.ax.add_patch(patch) + patches.append(patch) + + if self.solids_patches: + for solid in self.solids_patches: + solid.remove() + + self.solids_patches = patches + + # for compatibility with Colorbar, we will implement edge drawing as a + # seperate line collection, even though we could have put a line on + # the patches in self.solids_patches. + if self.dividers is not None: + self.dividers.remove() + self.dividers = None + + if self.drawedges: + self.dividers = collections.LineCollection(self._edges(X,Y), + colors=(mpl.rcParams['axes.edgecolor'],), + linewidths=(0.5*mpl.rcParams['axes.linewidth'],) + ) + self.ax.add_collection(self.dividers) + + self.ax.hold(_hold) + + +def colorbar_factory(cax, mappable, **kwargs): + """ + Creates a colorbar on the given axes for the given mappable. + + Typically, for automatic colorbar placement given only a mappable use + :meth:`~matplotlib.figure.Figure.colorbar`. + + """ + # if the given mappable is a contourset with any hatching, use + # ColorbarPatch else use Colorbar + if (isinstance(mappable, contour.ContourSet) \ + and any([hatch is not None for hatch in mappable.hatches])): + cb = ColorbarPatch(cax, mappable, **kwargs) + else: + cb = Colorbar(cax, mappable, **kwargs) + + mappable.callbacksSM.connect('changed', cb.on_mappable_changed) + mappable.set_colorbar(cb, cax) + + return cb \ No newline at end of file diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 53f6b893c23b..6ee4c91fc003 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -18,6 +18,7 @@ import matplotlib.cbook as cbook import matplotlib.mlab as mlab import matplotlib.mathtext as mathtext +import matplotlib.patches as mpatches import matplotlib.texmanager as texmanager # Import needed for adding manual selection capability to clabel @@ -151,10 +152,9 @@ def clabel(self, *args, **kwargs): self.labelManual=kwargs.get('manual',False) self.rightside_up = kwargs.get('rightside_up', True) - if len(args) == 0: levels = self.levels - indices = range(len(levels)) + indices = range(len(self.cvalues)) elif len(args) == 1: levlabs = list(args[0]) indices, levels = [], [] @@ -731,6 +731,8 @@ def __init__(self, ax, *args, **kwargs): self.linewidths = kwargs.get('linewidths', None) self.linestyles = kwargs.get('linestyles', None) + self.hatches = kwargs.get('hatches', [None]) + self.alpha = kwargs.get('alpha', None) self.origin = kwargs.get('origin', None) self.extent = kwargs.get('extent', None) @@ -835,6 +837,56 @@ def __init__(self, ax, *args, **kwargs): self.collections.append(col) self.changed() # set the colors + def legend_elements(self, variable_name='x', str_format=str): + """ + Return a list of artist and labels suitable for passing through + to :func:`plt.legend` which represent this ContourSet. + + Args: + + *variable_name*: the string used inside the innequality used + on the labels + + *str_format*: function used to format the numbers in the labels + """ + artists = [] + labels = [] + + if self.filled: + lowers, uppers = self._get_lowers_and_uppers() + n_levels = len(self.collections) + + for i, (collection, lower, upper) in enumerate(zip(self.collections, + lowers, uppers)): + patch = mpatches.Rectangle((0, 0), 1, 1, + facecolor=collection.get_facecolor()[0], + hatch=collection.get_hatch(), + alpha=collection.get_alpha(), + ) + artists.append(patch) + + lower = str_format(lower) + upper = str_format(upper) + + if i == 0 and self.extend in ('lower', 'both'): + labels.append(r'$%s \leq %s$' % (variable_name, upper, )) + elif i == n_levels-1 and self.extend in ('upper', 'both'): + labels.append(r'$%s > %s$' % (variable_name, lower, )) + else: + labels.append(r'$%s < %s \leq %s$' % (lower, variable_name, upper)) + else: + for collection, level in zip(self.collections, self.levels): + + patch = mcoll.LineCollection(None) + patch.update_from(collection) + + artists.append(patch) + # format the level for insertion into the labels + level = str_format(level) + labels.append(r'$%s = %s$' % (variable_name, level)) + + return artists, labels + def _process_args(self, *args, **kwargs): """ Process args and kwargs; override in derived classes. @@ -909,9 +961,12 @@ def changed(self): tcolors = [ (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, alpha=self.alpha)] self.tcolors = tcolors - for color, collection in zip(tcolors, self.collections): + hatches = self.hatches * len(tcolors) + for color, hatch, collection in zip(tcolors, hatches, self.collections): if self.filled: collection.set_facecolor(color) + # update the collection's hatch (may be None) + collection.set_hatch(hatch) else: collection.set_color(color) for label, cv in zip(self.labelTexts, self.labelCValues): @@ -1481,6 +1536,11 @@ def _initialize_x_y(self, z): points. This may never actually be advantageous, so this option may be removed. Chunking introduces artifacts at the chunk boundaries unless *antialiased* is *False*. + + *hatches*: + A list of cross hatch patterns to use on the filled areas. + If None, no hatching will be added to the contour. + Currently only supported on a few backends. Note: contourf fills intervals that are closed at the top; that is, for boundaries *z1* and *z2*, the filled region is:: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 87d819c1260e..b71900c3b91c 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1194,16 +1194,8 @@ def colorbar(self, mappable, cax=None, ax=None, **kw): else: cax, kw = cbar.make_axes(ax, **kw) cax.hold(True) - cb = cbar.Colorbar(cax, mappable, **kw) + cb = cbar.colorbar_factory(cax, mappable, **kw) - def on_changed(m): - #print 'calling on changed', m.get_cmap().name - cb.set_cmap(m.get_cmap()) - cb.set_clim(m.get_clim()) - cb.update_normal(m) - - self.cbid = mappable.callbacksSM.connect('changed', on_changed) - mappable.set_colorbar(cb, cax) self.sca(ax) return cb diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 069b7290b518..6b7c66589701 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -505,6 +505,7 @@ def __str__(self): return self.__class__.__name__ \ + "(%g,%g;%gx%g)" % (self._x, self._y, self._width, self._height) + @docstring.dedent_interpd def __init__(self, xy, width, height, **kwargs): """ diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1261649e525a..0f12d8f3a033 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1684,7 +1684,10 @@ def colorbar(mappable=None, cax=None, ax=None, **kw): if mappable is None: mappable = gci() if mappable is None: - raise RuntimeError('You must first define an image, eg with imshow') + raise RuntimeError('No mappable was found to use for colorbar ' + 'creation. First define a mappable such as ' + 'an image (with imshow) or a contour set (' + 'with contourf).') if ax is None: ax = gca()