diff --git a/doc/api/api_changes/2017-12-01-JMK.rst b/doc/api/api_changes/2017-12-01-JMK.rst new file mode 100644 index 000000000000..a6c8c72a2fae --- /dev/null +++ b/doc/api/api_changes/2017-12-01-JMK.rst @@ -0,0 +1,8 @@ +The ticks for colorbar now adjust for the size of the colorbar +-------------------------------------------------------------- + +Colorbar ticks now adjust for the size of the colorbar if the +colorbar is made from a mappable that is not a contour or +doesn't have a BoundaryNorm, or boundaries are not specified. +If boundaries, etc are specified, the colorbar maintains the +original behaviour. diff --git a/doc/users/next_whats_new/colorbarticks.rst b/doc/users/next_whats_new/colorbarticks.rst new file mode 100644 index 000000000000..7b70e0f6ce6d --- /dev/null +++ b/doc/users/next_whats_new/colorbarticks.rst @@ -0,0 +1,7 @@ +Colorbar ticks can now be automatic +----------------------------------- + +The number of ticks on colorbars was appropriate for a large colorbar, but +looked bad if the colorbar was made smaller (i.e. via the ``shrink`` kwarg). +This has been changed so that the number of ticks is now responsive to how +large the colorbar is. diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 142e525c3c89..d3c7da7c7ba7 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -24,6 +24,7 @@ import six from six.moves import xrange, zip +import logging import warnings import numpy as np @@ -44,6 +45,8 @@ import matplotlib._constrained_layout as constrained_layout from matplotlib import docstring +_log = logging.getLogger(__name__) + make_axes_kw_doc = ''' ============= ==================================================== @@ -217,6 +220,63 @@ def _set_ticks_on_axis_warn(*args, **kw): warnings.warn("Use the colorbar set_ticks() method instead.") +class _ColorbarAutoLocator(ticker.MaxNLocator): + """ + AutoLocator for Colorbar + + This locator is just a `.MaxNLocator` except the min and max are + clipped by the norm's min and max (i.e. vmin/vmax from the + image/pcolor/contour object). This is necessary so ticks don't + extrude into the "extend regions". + """ + + def __init__(self, colorbar): + """ + This ticker needs to know the *colorbar* so that it can access + its *vmin* and *vmax*. Otherwise it is the same as + `~.ticker.AutoLocator`. + """ + + self._colorbar = colorbar + nbins = 'auto' + steps = [1, 2, 2.5, 5, 10] + ticker.MaxNLocator.__init__(self, nbins=nbins, steps=steps) + + def tick_values(self, vmin, vmax): + vmin = max(vmin, self._colorbar.norm.vmin) + vmax = min(vmax, self._colorbar.norm.vmax) + return ticker.MaxNLocator.tick_values(self, vmin, vmax) + + +class _ColorbarLogLocator(ticker.LogLocator): + """ + LogLocator for Colorbarbar + + This locator is just a `.LogLocator` except the min and max are + clipped by the norm's min and max (i.e. vmin/vmax from the + image/pcolor/contour object). This is necessary so ticks don't + extrude into the "extend regions". + + """ + def __init__(self, colorbar, *args, **kwargs): + """ + _ColorbarLogLocator(colorbar, *args, **kwargs) + + This ticker needs to know the *colorbar* so that it can access + its *vmin* and *vmax*. Otherwise it is the same as + `~.ticker.LogLocator`. The ``*args`` and ``**kwargs`` are the + same as `~.ticker.LogLocator`. + """ + self._colorbar = colorbar + ticker.LogLocator.__init__(self, *args, **kwargs) + + def tick_values(self, vmin, vmax): + vmin = self._colorbar.norm.vmin + vmax = self._colorbar.norm.vmax + ticks = ticker.LogLocator.tick_values(self, vmin, vmax) + return ticks[(ticks >= vmin) & (ticks <= vmax)] + + class ColorbarBase(cm.ScalarMappable): ''' Draw a colorbar in an existing axes. @@ -346,8 +406,15 @@ def draw_all(self): and do all the drawing. ''' + # sets self._boundaries and self._values in real data units. + # takes into account extend values: self._process_values() + # sets self.vmin and vmax in data units, but just for + # the part of the colorbar that is not part of the extend + # patch: self._find_range() + # returns the X and Y mesh, *but* this was/is in normalized + # units: X, Y = self._mesh() C = self._values[:, np.newaxis] self._config_axes(X, Y) @@ -356,35 +423,105 @@ def draw_all(self): def config_axis(self): ax = self.ax + if (isinstance(self.norm, colors.LogNorm) + and self._use_auto_colorbar_locator()): + # *both* axes are made log so that determining the + # mid point is easier. + ax.set_xscale('log') + ax.set_yscale('log') + if self.orientation == 'vertical': - ax.xaxis.set_ticks([]) - # location is either one of 'bottom' or 'top' - ax.yaxis.set_label_position(self.ticklocation) - ax.yaxis.set_ticks_position(self.ticklocation) + long_axis, short_axis = ax.yaxis, ax.xaxis else: - ax.yaxis.set_ticks([]) - # location is either one of 'left' or 'right' - ax.xaxis.set_label_position(self.ticklocation) - ax.xaxis.set_ticks_position(self.ticklocation) + long_axis, short_axis = ax.xaxis, ax.yaxis + + long_axis.set_label_position(self.ticklocation) + long_axis.set_ticks_position(self.ticklocation) + short_axis.set_ticks([]) + short_axis.set_ticks([], minor=True) self._set_label() + def _get_ticker_locator_formatter(self): + """ + This code looks at the norm being used by the colorbar + and decides what locator and formatter to use. If ``locator`` has + already been set by hand, it just returns + ``self.locator, self.formatter``. + """ + locator = self.locator + formatter = self.formatter + if locator is None: + if self.boundaries is None: + if isinstance(self.norm, colors.NoNorm): + nv = len(self._values) + base = 1 + int(nv / 10) + locator = ticker.IndexLocator(base=base, offset=0) + elif isinstance(self.norm, colors.BoundaryNorm): + b = self.norm.boundaries + locator = ticker.FixedLocator(b, nbins=10) + elif isinstance(self.norm, colors.LogNorm): + locator = _ColorbarLogLocator(self) + elif isinstance(self.norm, colors.SymLogNorm): + # The subs setting here should be replaced + # by logic in the locator. + locator = ticker.SymmetricalLogLocator( + subs=np.arange(1, 10), + linthresh=self.norm.linthresh, + base=10) + else: + if mpl.rcParams['_internal.classic_mode']: + locator = ticker.MaxNLocator() + else: + locator = _ColorbarAutoLocator(self) + else: + b = self._boundaries[self._inside] + locator = ticker.FixedLocator(b, nbins=10) + _log.debug('locator: %r', locator) + return locator, formatter + + def _use_auto_colorbar_locator(self): + """ + Return if we should use an adjustable tick locator or a fixed + one. (check is used twice so factored out here...) + """ + return (self.boundaries is None + and self.values is None + and ((type(self.norm) == colors.Normalize) + or (type(self.norm) == colors.LogNorm))) + def update_ticks(self): """ Force the update of the ticks and ticklabels. This must be called whenever the tick locator and/or tick formatter changes. """ ax = self.ax - ticks, ticklabels, offset_string = self._ticker() - if self.orientation == 'vertical': - ax.yaxis.set_ticks(ticks) - ax.set_yticklabels(ticklabels) - ax.yaxis.get_major_formatter().set_offset_string(offset_string) + # get the locator and formatter. Defaults to + # self.locator if not None.. + locator, formatter = self._get_ticker_locator_formatter() + if self.orientation == 'vertical': + long_axis, short_axis = ax.yaxis, ax.xaxis else: - ax.xaxis.set_ticks(ticks) - ax.set_xticklabels(ticklabels) - ax.xaxis.get_major_formatter().set_offset_string(offset_string) + long_axis, short_axis = ax.xaxis, ax.yaxis + + if self._use_auto_colorbar_locator(): + _log.debug('Using auto colorbar locator on colorbar') + _log.debug('locator: %r', locator) + long_axis.set_major_locator(locator) + long_axis.set_major_formatter(formatter) + if type(self.norm) == colors.LogNorm: + long_axis.set_minor_locator(_ColorbarLogLocator(self, + base=10., subs='auto')) + long_axis.set_minor_formatter( + ticker.LogFormatter() + ) + else: + _log.debug('Using fixed locator on colorbar') + ticks, ticklabels, offset_string = self._ticker(locator, formatter) + long_axis.set_ticks(ticks) + long_axis.set_ticklabels(ticklabels) + long_axis.get_major_formatter().set_offset_string(offset_string) def set_ticks(self, ticks, update_ticks=True): """ @@ -520,6 +657,7 @@ def _add_solids(self, X, Y, C): # since the axes object should already have hold set. _hold = self.ax._hold self.ax._hold = True + _log.debug('Setting pcolormesh') col = self.ax.pcolormesh(*args, **kw) self.ax._hold = _hold #self.add_observer(col) # We should observe, not be observed... @@ -573,39 +711,11 @@ def add_lines(self, levels, colors, linewidths, erase=True): self.ax.add_collection(col) self.stale = True - def _ticker(self): + def _ticker(self, locator, formatter): ''' Return the sequence of ticks (colorbar data locations), ticklabels (strings), and the corresponding offset string. ''' - locator = self.locator - formatter = self.formatter - if locator is None: - if self.boundaries is None: - if isinstance(self.norm, colors.NoNorm): - nv = len(self._values) - base = 1 + int(nv / 10) - locator = ticker.IndexLocator(base=base, offset=0) - elif isinstance(self.norm, colors.BoundaryNorm): - b = self.norm.boundaries - locator = ticker.FixedLocator(b, nbins=10) - elif isinstance(self.norm, colors.LogNorm): - locator = ticker.LogLocator(subs='all') - elif isinstance(self.norm, colors.SymLogNorm): - # The subs setting here should be replaced - # by logic in the locator. - locator = ticker.SymmetricalLogLocator( - subs=np.arange(1, 10), - linthresh=self.norm.linthresh, - base=10) - else: - if mpl.rcParams['_internal.classic_mode']: - locator = ticker.MaxNLocator() - else: - locator = ticker.AutoLocator() - else: - b = self._boundaries[self._inside] - locator = ticker.FixedLocator(b, nbins=10) if isinstance(self.norm, colors.NoNorm) and self.boundaries is None: intv = self._values[0], self._values[-1] else: @@ -845,17 +955,29 @@ def _mesh(self): transposition for a horizontal colorbar are done outside this function. ''' + # if boundaries and values are None, then we can go ahead and + # scale this up for Auto tick location. Otherwise we + # want to keep normalized between 0 and 1 and use manual tick + # locations. + x = np.array([0.0, 1.0]) if self.spacing == 'uniform': y = self._uniform_y(self._central_N()) else: y = self._proportional_y() + if self._use_auto_colorbar_locator(): + y = self.norm.inverse(y) + x = self.norm.inverse(x) self._y = y X, Y = np.meshgrid(x, y) + if self._use_auto_colorbar_locator(): + xmid = self.norm.inverse(0.5) + else: + xmid = 0.5 if self._extend_lower() and not self.extendrect: - X[0, :] = 0.5 + X[0, :] = xmid if self._extend_upper() and not self.extendrect: - X[-1, :] = 0.5 + X[-1, :] = xmid return X, Y def _locate(self, x): diff --git a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png index a94b635b1c64..904e0c3d44a0 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png and b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf index c45f9f78a8f0..33e660759683 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png index 00d0b177c7ae..ef19f1a7b6bc 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg index 816041b724d4..fdad1814fd99 100644 --- a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg +++ b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg @@ -27,1962 +27,1962 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" id="m368fc901b1" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mc63e59a608" style="stroke:#000000;stroke-width:0.5;"/> - - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - - - - - - - - - + - + - - - - - - - - - - + - + - - - - - - - + - + - - - - - - - + - + - - - - - - - + @@ -2828,137 +2683,92 @@ Q 19.53125 74.21875 31.78125 74.21875 +" id="m556f96d829" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m27e32ca04a" style="stroke:#000000;stroke-width:0.5;"/> - - - - - - - - + - + - - - - - - - - + - + - - - - - - - - + - + - - - - - - - + - + - - - - - - - + - + - - - - - - - + - + - - - - - - - + @@ -2966,7 +2776,7 @@ L -4 0 - - - - - - - - - - - + - - - - - - - - - + - - - - - - - - - - - + - - - - - - - - - - - + - - - - - - - - - - - + - - - - - - - - + - + - - - - + + + + + @@ -3183,10 +2863,10 @@ z - + - + diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 539ee8c83416..299ed1eb5b19 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -273,6 +273,49 @@ def test_colorbar_ticks(): assert len(cbar.ax.xaxis.get_ticklocs()) == len(clevs) +def test_colorbar_autoticks(): + # Test new autotick modes. Needs to be classic because + # non-classic doesn't go this route. + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(2, 1) + x = np.arange(-3.0, 4.001) + y = np.arange(-4.0, 3.001) + X, Y = np.meshgrid(x, y) + Z = X * Y + pcm = ax[0].pcolormesh(X, Y, Z) + cbar = fig.colorbar(pcm, ax=ax[0], extend='both', + orientation='vertical') + + pcm = ax[1].pcolormesh(X, Y, Z) + cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both', + orientation='vertical', shrink=0.4) + np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(), + np.arange(-15, 16., 5.)) + np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), + np.arange(-20, 21., 10.)) + + +def test_colorbar_autotickslog(): + # Test new autotick modes... + with rc_context({'_internal.classic_mode': False}): + fig, ax = plt.subplots(2, 1) + x = np.arange(-3.0, 4.001) + y = np.arange(-4.0, 3.001) + X, Y = np.meshgrid(x, y) + Z = X * Y + pcm = ax[0].pcolormesh(X, Y, 10**Z, norm=LogNorm()) + cbar = fig.colorbar(pcm, ax=ax[0], extend='both', + orientation='vertical') + + pcm = ax[1].pcolormesh(X, Y, 10**Z, norm=LogNorm()) + cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both', + orientation='vertical', shrink=0.4) + np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(), + 10**np.arange(-12, 12.2, 4.)) + np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), + 10**np.arange(-12, 13., 12.)) + + def test_colorbar_get_ticks(): # test feature for #5792 plt.figure() diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index 81a51e711ea0..e526f8b80ae7 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -40,7 +40,7 @@ def test_startpoints(): @image_comparison(baseline_images=['streamplot_colormap'], - tol=.02) + tol=.04, remove_text=True) def test_colormap(): X, Y, U, V = velocity_field() plt.streamplot(X, Y, U, V, color=U, density=0.6, linewidth=2, diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c3978c9ec31d..c151a6aca6e0 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -170,6 +170,7 @@ import six import itertools +import logging import locale import math import numpy as np @@ -180,6 +181,7 @@ import warnings +_log = logging.getLogger(__name__) __all__ = ('TickHelper', 'Formatter', 'FixedFormatter', 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter', @@ -2115,6 +2117,7 @@ def tick_values(self, vmin, vmax): "Data has no positive values, and therefore can not be " "log-scaled.") + _log.debug('vmin %s vmax %s', vmin, vmax) vmin = math.log(vmin) / math.log(b) vmax = math.log(vmax) / math.log(b) @@ -2135,8 +2138,8 @@ def tick_values(self, vmin, vmax): else: subs = self._subs + # get decades between major ticks. stride = 1 - if rcParams['_internal.classic_mode']: # Leave the bug left over from the PY2-PY3 transition. while numdec / stride + 1 > numticks: @@ -2157,6 +2160,8 @@ def tick_values(self, vmin, vmax): if stride == 1: ticklocs = np.ravel(np.outer(subs, ticklocs)) else: + # no ticklocs if we have more than one decade + # between major ticks. ticklocs = [] else: if have_subs: @@ -2167,6 +2172,7 @@ def tick_values(self, vmin, vmax): else: ticklocs = b ** decades + _log.debug('ticklocs %r', ticklocs) return self.raise_if_exceeds(np.asarray(ticklocs)) def view_limits(self, vmin, vmax):