diff --git a/doc/api/next_api_changes/2018-11-23-AL.rst b/doc/api/next_api_changes/2018-11-23-AL.rst new file mode 100644 index 000000000000..1936d3a8d97c --- /dev/null +++ b/doc/api/next_api_changes/2018-11-23-AL.rst @@ -0,0 +1,6 @@ +Log-scaled axes avoid having zero or only one tick +`````````````````````````````````````````````````` + +When the default `LogLocator` would generate no ticks for an axis (e.g., an +axis with limits from 0.31 to 0.39) or only a single tick, it now instead falls +back on the linear `AutoLocator` to pick reasonable tick positions. diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 69fa646f6b85..5a7b8f446349 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1,7 +1,7 @@ import warnings import numpy as np -from numpy.testing import assert_almost_equal +from numpy.testing import assert_almost_equal, assert_array_equal import pytest import matplotlib @@ -179,6 +179,11 @@ def test_basic(self): test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.]) assert_almost_equal(loc.tick_values(1, 100), test_value) + def test_switch_to_autolocator(self): + loc = mticker.LogLocator(subs="all") + assert_array_equal(loc.tick_values(0.45, 0.55), + [0.44, 0.46, 0.48, 0.5, 0.52, 0.54, 0.56]) + def test_set_params(self): """ Create log locator with default value, base=10.0, subs=[1.0], diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index fb39bfba9dc7..3e299b31303b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2165,13 +2165,13 @@ def tick_values(self, vmin, vmax): "log-scaled.") _log.debug('vmin %s vmax %s', vmin, vmax) - vmin = math.log(vmin) / math.log(b) - vmax = math.log(vmax) / math.log(b) if vmax < vmin: vmin, vmax = vmax, vmin + log_vmin = math.log(vmin) / math.log(b) + log_vmax = math.log(vmax) / math.log(b) - numdec = math.floor(vmax) - math.ceil(vmin) + numdec = math.floor(log_vmax) - math.ceil(log_vmin) if isinstance(self._subs, str): _first = 2.0 if self._subs == 'auto' else 1.0 @@ -2195,11 +2195,12 @@ def tick_values(self, vmin, vmax): while numdec // stride + 1 > numticks: stride += 1 - # Does subs include anything other than 1? + # Does subs include anything other than 1? Essentially a hack to know + # whether we're a major or a minor locator. have_subs = len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0) - decades = np.arange(math.floor(vmin) - stride, - math.ceil(vmax) + 2 * stride, stride) + decades = np.arange(math.floor(log_vmin) - stride, + math.ceil(log_vmax) + 2 * stride, stride) if hasattr(self, '_transform'): ticklocs = self._transform.inverted().transform(decades) @@ -2207,20 +2208,27 @@ 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 = [] + # No ticklocs if we have >1 decade between major ticks. + ticklocs = np.array([]) else: if have_subs: - ticklocs = [] if stride == 1: - for decadeStart in b ** decades: - ticklocs.extend(subs * decadeStart) + ticklocs = np.concatenate( + [subs * decade_start for decade_start in b ** decades]) + else: + ticklocs = np.array([]) else: ticklocs = b ** decades _log.debug('ticklocs %r', ticklocs) - return self.raise_if_exceeds(np.asarray(ticklocs)) + if (len(subs) > 1 + and stride == 1 + and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1): + # If we're a minor locator *that expects at least two ticks per + # decade* and the major locator stride is 1 and there's no more + # than one minor tick, switch to AutoLocator. + return AutoLocator().tick_values(vmin, vmax) + return self.raise_if_exceeds(ticklocs) def view_limits(self, vmin, vmax): 'Try to choose the view limits intelligently'