diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 4682b3152f3e..5a6678036b44 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -35,6 +35,7 @@ import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.ticker as ticker +import matplotlib.transforms as mtrans from matplotlib import docstring @@ -52,7 +53,8 @@ *anchor* (0.0, 0.5) if vertical; (0.5, 1.0) if horizontal; the anchor point of the colorbar axes *panchor* (1.0, 0.5) if vertical; (0.5, 0.0) if horizontal; - the anchor point of the colorbar parent axes + the anchor point of the colorbar parent axes. If + False, the parent axes' anchor will be unchanged ============= ==================================================== ''' @@ -149,8 +151,9 @@ *cax* None | axes object into which the colorbar will be drawn *ax* - None | parent axes object from which space for a new - colorbar axes will be stolen + None | parent axes object(s) from which space for a new + colorbar axes will be stolen. If a list of axes is given + they will all be resized to make room for the colorbar axes. *use_gridspec* False | If *cax* is None, a new *cax* is created as an instance of Axes. If *ax* is an instance of Subplot and *use_gridspec* is True, @@ -255,6 +258,7 @@ def __init__(self, ax, cmap=None, values=None, boundaries=None, orientation='vertical', + ticklocation='auto', extend='neither', spacing='uniform', # uniform or proportional ticks=None, @@ -263,6 +267,7 @@ def __init__(self, ax, cmap=None, filled=True, extendfrac=None, extendrect=False, + label='', ): self.ax = ax self._patch_ax() @@ -287,7 +292,12 @@ def __init__(self, ax, cmap=None, self.outline = None self.patch = None self.dividers = None - self.set_label('') + + if ticklocation == 'auto': + ticklocation = 'bottom' if orientation == 'horizontal' else 'right' + self.ticklocation = ticklocation + + self.set_label(label) if cbook.iterable(ticks): self.locator = ticker.FixedLocator(ticks, nbins=len(ticks)) else: @@ -336,11 +346,14 @@ def config_axis(self): ax = self.ax if self.orientation == 'vertical': ax.xaxis.set_ticks([]) - ax.yaxis.set_label_position('right') - ax.yaxis.set_ticks_position('right') + # location is either one of 'bottom' or 'top' + ax.yaxis.set_label_position(self.ticklocation) + ax.yaxis.set_ticks_position(self.ticklocation) else: ax.yaxis.set_ticks([]) - ax.xaxis.set_label_position('bottom') + # location is either one of 'left' or 'right' + ax.xaxis.set_label_position(self.ticklocation) + ax.xaxis.set_ticks_position(self.ticklocation) self._set_label() @@ -835,11 +848,10 @@ class Colorbar(ColorbarBase): """ def __init__(self, ax, mappable, **kw): - mappable.autoscale_None() # Ensure mappable.norm.vmin, vmax - # are set when colorbar is called, - # even if mappable.draw has not yet - # been called. This will not change - # vmin, vmax if they are already set. + # Ensure the given mappable's norm has appropriate vmin and vmax set + # even if mappable.draw has not yet been called. + mappable.autoscale_None() + self.mappable = mappable kw['cmap'] = mappable.cmap kw['norm'] = mappable.norm @@ -948,47 +960,118 @@ def update_bruteforce(self, mappable): @docstring.Substitution(make_axes_kw_doc) -def make_axes(parent, **kw): +def make_axes(parents, location=None, orientation=None, fraction=0.15, + shrink=1.0, aspect=20, **kw): ''' - Resize and reposition a parent axes, and return a child + Resize and reposition parent axes, and return a child axes suitable for a colorbar:: cax, kw = make_axes(parent, **kw) Keyword arguments may include the following (with defaults): - *orientation* - 'vertical' or 'horizontal' + *location*: [**None**|'left'|'right'|'top'|'bottom'] + The position, relative to **parents**, where the colorbar axes + should be created. If None, the value will either come from the + given **orientation**, else it will default to 'right'. - %s + *orientation*: [**None**|'vertical'|'horizontal'] + The orientation of the colorbar. Typically, this keyword shouldn't + be used, as it can be derived from the **location** keyword. - All but the first of these are stripped from the input kw set. + %s - Returns (cax, kw), the child axes and the reduced kw dictionary. + Returns (cax, kw), the child axes and the reduced kw dictionary to be + passed when creating the colorbar instance. ''' - orientation = kw.setdefault('orientation', 'vertical') - fraction = kw.pop('fraction', 0.15) - shrink = kw.pop('shrink', 1.0) - aspect = kw.pop('aspect', 20) - #pb = transforms.PBox(parent.get_position()) - pb = parent.get_position(original=True).frozen() - if orientation == 'vertical': - pad = kw.pop('pad', 0.05) - x1 = 1.0 - fraction - pb1, pbx, pbcb = pb.splitx(x1 - pad, x1) - pbcb = pbcb.shrunk(1.0, shrink).anchored('C', pbcb) - anchor = kw.pop('anchor', (0.0, 0.5)) - panchor = kw.pop('panchor', (1.0, 0.5)) + locations = ["left", "right", "top", "bottom"] + if orientation is not None and location is not None: + raise TypeError('position and orientation are mutually exclusive. Consider ' \ + 'setting the position to any of %s' % ', '.join(locations)) + + # provide a default location + if location is None and orientation is None: + location = 'right' + + # allow the user to not specify the location by specifying the orientation instead + if location is None: + location = 'right' if orientation == 'vertical' else 'bottom' + + if location not in locations: + raise ValueError('Invalid colorbar location. Must be one of %s' % ', '.join(locations)) + + default_location_settings = {'left': {'anchor': (1.0, 0.5), + 'panchor': (0.0, 0.5), + 'pad': 0.10, + 'orientation': 'vertical'}, + 'right': {'anchor': (0.0, 0.5), + 'panchor': (1.0, 0.5), + 'pad': 0.05, + 'orientation': 'vertical'}, + 'top': {'anchor': (0.5, 0.0), + 'panchor': (0.5, 1.0), + 'pad': 0.05, + 'orientation': 'horizontal'}, + 'bottom': {'anchor': (0.5, 1.0), + 'panchor': (0.5, 0.0), + 'pad': 0.15, # backwards compat + 'orientation': 'horizontal'}, + } + + loc_settings = default_location_settings[location] + + # put appropriate values into the kw dict for passing back to + # the Colorbar class + kw['orientation'] = loc_settings['orientation'] + kw['ticklocation'] = location + + anchor = kw.pop('anchor', loc_settings['anchor']) + parent_anchor = kw.pop('panchor', loc_settings['panchor']) + pad = kw.pop('pad', loc_settings['pad']) + + + # turn parents into a list if it is not already + if not isinstance(parents, (list, tuple)): + parents = [parents] + + fig = parents[0].get_figure() + if not all(fig is ax.get_figure() for ax in parents): + raise ValueError('Unable to create a colorbar axes as not all ' + \ + 'parents share the same figure.') + + # take a bounding box around all of the given axes + parents_bbox = mtrans.Bbox.union([ax.get_position(original=True).frozen() \ + for ax in parents]) + + pb = parents_bbox + if location in ('left', 'right'): + if location == 'left': + pbcb, _, pb1 = pb.splitx(fraction, fraction + pad) + else: + pb1, _, pbcb = pb.splitx(1 - fraction - pad, 1 - fraction) + pbcb = pbcb.shrunk(1.0, shrink).anchored(anchor, pbcb) else: - pad = kw.pop('pad', 0.15) - pbcb, pbx, pb1 = pb.splity(fraction, fraction + pad) - pbcb = pbcb.shrunk(shrink, 1.0).anchored('C', pbcb) - aspect = 1.0 / aspect - anchor = kw.pop('anchor', (0.5, 1.0)) - panchor = kw.pop('panchor', (0.5, 0.0)) - parent.set_position(pb1) - parent.set_anchor(panchor) - fig = parent.get_figure() + if location == 'bottom': + pbcb, _, pb1 = pb.splity(fraction, fraction + pad) + else: + pb1, _, pbcb = pb.splity(1 - fraction - pad, 1 - fraction) + pbcb = pbcb.shrunk(shrink, 1.0).anchored(anchor, pbcb) + + # define the aspect ratio in terms of y's per x rather than x's per y + aspect = 1.0/aspect + + # define a transform which takes us from old axes coordinates to + # new axes coordinates + shrinking_trans = mtrans.BboxTransform(parents_bbox, pb1) + + # transform each of the axes in parents using the new transform + for ax in parents: + new_posn = shrinking_trans.transform(ax.get_position()) + new_posn = mtrans.Bbox(new_posn) + ax.set_position(new_posn) + if parent_anchor is not False: + ax.set_anchor(parent_anchor) + cax = fig.add_axes(pbcb) cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw @@ -1001,6 +1084,9 @@ def make_axes_gridspec(parent, **kw): suitable for a colorbar. This function is similar to make_axes. Prmary differences are + * *make_axes_gridspec* only handles the *orientation* keyword + and cannot handle the "location" keyword. + * *make_axes_gridspec* should only be used with a subplot parent. * *make_axes* creates an instance of Axes. *make_axes_gridspec* @@ -1018,16 +1104,19 @@ def make_axes_gridspec(parent, **kw): Keyword arguments may include the following (with defaults): *orientation* - 'vertical' or 'horizontal' + 'vertical' or 'horizontal' %s All but the first of these are stripped from the input kw set. - Returns (cax, kw), the child axes and the reduced kw dictionary. + Returns (cax, kw), the child axes and the reduced kw dictionary to be + passed when creating the colorbar instance. ''' orientation = kw.setdefault('orientation', 'vertical') + kw['ticklocation'] = 'auto' + fraction = kw.pop('fraction', 0.15) shrink = kw.pop('shrink', 1.0) aspect = kw.pop('aspect', 20) @@ -1139,11 +1228,8 @@ def _add_solids(self, X, Y, C): patch = mpatches.PathPatch(mpath.Path(xy), facecolor=self.cmap(self.norm(val)), - hatch=hatch, - edgecolor='none', linewidth=0, - antialiased=False, **kw - ) - + hatch=hatch, linewidth=0, + antialiased=False, **kw) self.ax.add_patch(patch) patches.append(patch) @@ -1158,12 +1244,9 @@ def _add_solids(self, X, Y, C): 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.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) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0892d93898ff..31a53896c480 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1418,7 +1418,7 @@ def savefig(self, *args, **kwargs): ax.patch.set_edgecolor(cc[1]) @docstring.dedent_interpd - def colorbar(self, mappable, cax=None, ax=None, **kw): + def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): """ Create a colorbar for a ScalarMappable instance, *mappable*. @@ -1427,7 +1427,10 @@ def colorbar(self, mappable, cax=None, ax=None, **kw): """ if ax is None: ax = self.gca() - use_gridspec = kw.pop("use_gridspec", True) + + # Store the value of gca so that we can set it back later on. + current_ax = self.gca() + if cax is None: if use_gridspec and isinstance(ax, SubplotBase): cax, kw = cbar.make_axes_gridspec(ax, **kw) @@ -1436,7 +1439,7 @@ def colorbar(self, mappable, cax=None, ax=None, **kw): cax.hold(True) cb = cbar.colorbar_factory(cax, mappable, **kw) - self.sca(ax) + self.sca(current_ax) return cb def subplots_adjust(self, *args, **kwargs): @@ -1451,17 +1454,15 @@ def subplots_adjust(self, *args, **kwargs): """ self.subplotpars.update(*args, **kwargs) - import matplotlib.axes for ax in self.axes: - if not isinstance(ax, matplotlib.axes.SubplotBase): + if not isinstance(ax, SubplotBase): # Check if sharing a subplots axis if (ax._sharex is not None and - isinstance(ax._sharex, - matplotlib.axes.SubplotBase)): + isinstance(ax._sharex, SubplotBase)): ax._sharex.update_params() ax.set_position(ax._sharex.figbox) elif (ax._sharey is not None and - isinstance(ax._sharey, matplotlib.axes.SubplotBase)): + isinstance(ax._sharey, SubplotBase)): ax._sharey.update_params() ax.set_position(ax._sharey.figbox) else: diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e245b0b49e28..a15a0b1fc3b0 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -21,11 +21,13 @@ import warnings import matplotlib +import matplotlib.colorbar from matplotlib import _pylab_helpers, interactive from matplotlib.cbook import dedent, silent_list, is_string_like, is_numlike from matplotlib import docstring -from matplotlib.figure import Figure, figaspect from matplotlib.backend_bases import FigureCanvasBase +from matplotlib.figure import Figure, figaspect +from matplotlib.gridspec import GridSpec from matplotlib.image import imread as _imread from matplotlib.image import imsave as _imsave from matplotlib import rcParams, rcParamsDefault, get_backend @@ -212,11 +214,10 @@ def rcdefaults(): matplotlib.rcdefaults() draw_if_interactive() + # The current "image" (ScalarMappable) is retrieved or set # only via the pyplot interface using the following two # functions: - - def gci(): """ Get the current colorable artist. Specifically, returns the @@ -246,9 +247,7 @@ def sci(im): ## Any Artist ## - # (getp is simply imported) - @docstring.copy(_setp) def setp(*args, **kwargs): ret = _setp(*args, **kwargs) @@ -303,8 +302,8 @@ def xkcd(): raise return context -## Figures ## +## Figures ## def figure(num=None, # autoincrement if None, else integer from 1-N figsize=None, # defaults to rc figure.figsize @@ -1099,7 +1098,6 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, return ret -from gridspec import GridSpec def subplot2grid(shape, loc, rowspan=1, colspan=1, **kwargs): """ Create a subplot in a grid. The grid is specified by *shape*, at @@ -2091,7 +2089,6 @@ def pad(s, l): ## Plotting part 1: manually generated functions and wrappers ## -import matplotlib.colorbar def colorbar(mappable=None, cax=None, ax=None, **kw): if mappable is None: mappable = gci() diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 7f649e58befd..358d1dfc77f5 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -296,6 +296,10 @@ def compare_images( expected, actual, tol, in_decorator=False ): # Convert the image to png extension = expected.split('.')[-1] + + if not os.path.exists(expected): + raise IOError('Baseline image %r does not exist.' % expected) + if extension != 'png': actual = convert(actual, False) expected = convert(expected, True) diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_locationing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_locationing.png new file mode 100644 index 000000000000..8616feb549d1 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_locationing.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_sharing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_sharing.png new file mode 100644 index 000000000000..e8a6e6d5d70f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_sharing.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_orientation.png b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_orientation.png new file mode 100644 index 000000000000..f892209b44cc Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_orientation.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_subplots_adjust.png b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_subplots_adjust.png new file mode 100644 index 000000000000..84c683774ad3 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/cbar_with_subplots_adjust.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png b/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png new file mode 100644 index 000000000000..f32b14d9ee8c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png differ diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index d365deffe44b..75a2ca80e769 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1,6 +1,9 @@ -from matplotlib import rcParams, rcParamsDefault -from matplotlib.testing.decorators import image_comparison +import numpy as np +from numpy import ma +import matplotlib +from matplotlib.testing.decorators import image_comparison, knownfailureif import matplotlib.pyplot as plt +from matplotlib import rcParams, rcParamsDefault from matplotlib.colors import BoundaryNorm from matplotlib.cm import get_cmap from matplotlib.colorbar import ColorbarBase @@ -102,9 +105,9 @@ def test_colorbar_extension_shape(): fig2 = _colorbar_extension_shape('proportional') -@image_comparison( - baseline_images=['colorbar_extensions_uniform', 'colorbar_extensions_proportional'], - extensions=['png']) +@image_comparison(baseline_images=['colorbar_extensions_uniform', + 'colorbar_extensions_proportional'], + extensions=['png']) def test_colorbar_extension_length(): '''Test variable length colorbar extensions.''' # Use default params so matplotlibrc doesn't cause the test to fail. @@ -114,7 +117,77 @@ def test_colorbar_extension_length(): fig2 = _colorbar_extension_length('proportional') -if __name__ == '__main__': - import nose - nose.runmodule(argv=['-s', '--with-doctest'], exit=False) +@image_comparison(baseline_images=['cbar_with_orientation', + 'cbar_locationing', + 'double_cbar', + 'cbar_sharing', + ], + extensions=['png'], remove_text=True, + savefig_kwarg={'dpi': 40}) +def test_colorbar_positioning(): + data = np.arange(1200).reshape(30, 40) + levels = [0, 200, 400, 600, 800, 1000, 1200] + + plt.figure() + plt.contourf(data, levels=levels) + plt.colorbar(orientation='horizontal', use_gridspec=False) + + + locations = ['left', 'right', 'top', 'bottom'] + plt.figure() + for i, location in enumerate(locations): + plt.subplot(2, 2, i+1) + plt.contourf(data, levels=levels) + plt.colorbar(location=location, use_gridspec=False) + + + plt.figure() + # make some other data (random integers) + data_2nd = np.array([[2, 3, 2, 3], [1.5, 2, 2, 3], [2, 3, 3, 4]]) + # make the random data expand to the shape of the main data + data_2nd = np.repeat(np.repeat(data_2nd, 10, axis=1), 10, axis=0) + + color_mappable = plt.contourf(data, levels=levels, extend='both') + # test extend frac here + hatch_mappable = plt.contourf(data_2nd, levels=[1, 2, 3], colors='none', hatches=['/', 'o', '+'], extend='max') + plt.contour(hatch_mappable, colors='black') + plt.colorbar(color_mappable, location='left', label='variable 1', use_gridspec=False) + plt.colorbar(hatch_mappable, location='right', label='variable 2', use_gridspec=False) + + + plt.figure() + ax1 = plt.subplot(211, anchor='NE', aspect='equal') + plt.contourf(data, levels=levels) + ax2 = plt.subplot(223) + plt.contourf(data, levels=levels) + ax3 = plt.subplot(224) + plt.contourf(data, levels=levels) + + plt.colorbar(ax=[ax2, ax3, ax1], location='right', pad=0.0, shrink=0.5, panchor=False, use_gridspec=False) + plt.colorbar(ax=[ax2, ax3, ax1], location='left', shrink=0.5, panchor=False, use_gridspec=False) + plt.colorbar(ax=[ax1], location='bottom', panchor=False, anchor=(0.8, 0.5), shrink=0.6, use_gridspec=False) + + +@image_comparison(baseline_images=['cbar_with_subplots_adjust'], + extensions=['png'], remove_text=True, + savefig_kwarg={'dpi': 40}) +def test_gridspec_make_colorbar(): + plt.figure() + data = np.arange(1200).reshape(30, 40) + levels = [0, 200, 400, 600, 800, 1000, 1200] + + plt.subplot(121) + plt.contourf(data, levels=levels) + plt.colorbar(use_gridspec=True, orientation='vertical') + + plt.subplot(122) + plt.contourf(data, levels=levels) + plt.colorbar(use_gridspec=True, orientation='horizontal') + + plt.subplots_adjust(top=0.95, right=0.95, bottom=0.2, hspace=0.25) + + +if __name__=='__main__': + import nose + nose.runmodule(argv=['-s','--with-doctest'], exit=False)