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):