diff --git a/doc/api/next_api_changes/behavior/21177-DS.rst b/doc/api/next_api_changes/behavior/21177-DS.rst new file mode 100644 index 000000000000..f42f029ef17b --- /dev/null +++ b/doc/api/next_api_changes/behavior/21177-DS.rst @@ -0,0 +1,10 @@ +``LogLocator`` now respects ``numticks`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The algorithm within `~matplotlib.ticker.LogLocator` has been modified to +respect the ``numticks`` argument. Whereas previously more ticks than +``numticks`` could be returned, ``numticks`` is now an upper limit on the +number of ticks returned. + +In some cases, this change results in one more tick being added on a +log-scaled axis, and correctly adds some ticks to log-scaled errorbar plots +that were previously missing. diff --git a/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks_log.png b/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks_log.png index 7a31434f501f..b9b93d3a116c 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks_log.png and b/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks_log.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/test_loglog_nonpos.png b/lib/matplotlib/tests/baseline_images/test_axes/test_loglog_nonpos.png index 5dd4757445fe..75c07c801c04 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/test_loglog_nonpos.png and b/lib/matplotlib/tests/baseline_images/test_axes/test_loglog_nonpos.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_scale/logscale_nonpos_values.png b/lib/matplotlib/tests/baseline_images/test_scale/logscale_nonpos_values.png index a7e233e8ecd8..f9f03e692b8d 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_scale/logscale_nonpos_values.png and b/lib/matplotlib/tests/baseline_images/test_scale/logscale_nonpos_values.png differ diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 6b691713e7e6..575c2df36b4f 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -414,6 +414,7 @@ def test_colorbar_autotickslog(): x = np.arange(-3.0, 4.001) y = np.arange(-4.0, 3.001) X, Y = np.meshgrid(x, y) + # Z ranges from -12 to 12 Z = X * Y Z = Z[:-1, :-1] pcm = ax[0].pcolormesh(X, Y, 10**Z, norm=LogNorm()) @@ -423,12 +424,10 @@ def test_colorbar_autotickslog(): pcm = ax[1].pcolormesh(X, Y, 10**Z, norm=LogNorm()) cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical', shrink=0.4) - # note only -12 to +12 are visible np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(), - 10**np.arange(-16., 16.2, 4.)) - # note only -24 to +24 are visible + 10**np.arange(-12, 12.01, 4)) np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), - 10**np.arange(-24., 25., 12.)) + 10**np.arange(-12, 12.01, 12)) def test_colorbar_get_ticks(): @@ -515,6 +514,7 @@ def test_colorbar_log_minortick_labels(): def test_colorbar_renorm(): x, y = np.ogrid[-4:4:31j, -4:4:31j] + # z ranges from ~1.52e-9 to 1.2e5 z = 120000*np.exp(-x**2 - y**2) fig, ax = plt.subplots() @@ -529,7 +529,7 @@ def test_colorbar_renorm(): norm = LogNorm(z.min(), z.max()) im.set_norm(norm) np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(), - np.logspace(-10, 7, 18)) + np.logspace(-9, 5, 8)) # note that set_norm removes the FixedLocator... assert np.isclose(cbar.vmin, z.min()) cbar.set_ticks([1, 2, 3]) @@ -564,7 +564,7 @@ def test_colorbar_format(): im.set_norm(LogNorm(vmin=0.1, vmax=10)) fig.canvas.draw() assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() == - r'$\mathdefault{10^{-2}}$') + r'$\mathdefault{10^{-1}}$') def test_colorbar_scale_reset(): diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index d99fd5a306b9..72ecd77174c9 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -198,13 +198,11 @@ def test_basic(self): with pytest.raises(ValueError): loc.tick_values(0, 1000) - test_value = np.array([1.00000000e-05, 1.00000000e-03, 1.00000000e-01, - 1.00000000e+01, 1.00000000e+03, 1.00000000e+05, - 1.00000000e+07, 1.000000000e+09]) - assert_almost_equal(loc.tick_values(0.001, 1.1e5), test_value) + test_value = np.array([1e-3, 1e-1, 1e1, 1e3, 1e5]) + assert_almost_equal(loc.tick_values(1e-3, 1e5), test_value) loc = mticker.LogLocator(base=2) - test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.]) + test_value = np.array([1, 2, 4, 8, 16, 32, 64, 128]) assert_almost_equal(loc.tick_values(1, 100), test_value) def test_switch_to_autolocator(self): @@ -1373,5 +1371,35 @@ def test_small_range_loglocator(numticks): ll = mticker.LogLocator() ll.set_params(numticks=numticks) for top in [5, 7, 9, 11, 15, 50, 100, 1000]: - ticks = ll.tick_values(.5, top) - assert (np.diff(np.log10(ll.tick_values(6, 150))) == 1).all() + ticks = ll.tick_values(0.5, top) + assert len(ticks) > 1 + + +# Test vmin/vmax both on and off an exact decade +@pytest.mark.parametrize('vmin', [0.1, 0.2]) +@pytest.mark.parametrize('vmax', [100, 110]) +@pytest.mark.parametrize('numticks', np.arange(2, 11)) +def test_loglocator_numticks(numticks, vmin, vmax): + ll = mticker.LogLocator(numticks=numticks) + assert len(ll.tick_values(vmin, vmax)) <= numticks + + +@pytest.mark.parametrize('base', [2, 5, 10, 3.8]) +def test_loglocator_bases(base): + ll = mticker.LogLocator(base=base, numticks=2) + vmin, vmax = base**1, base**2 + # Ticks should be exactly on a decade + np.testing.assert_equal(ll.tick_values(vmin, vmax), + [base**1, base**2]) + # Ticks should extend to cover whole range of values + np.testing.assert_equal(ll.tick_values(vmin - 1, vmax + 1), + [base**0, base**3]) + # Even if the range is less than a decade, + # we should always get two ticks back + # + # Range falls within two decades + np.testing.assert_equal(ll.tick_values(vmin + 1, vmax - 1), + [base**1, base**2]) + # Range falls across a decade + np.testing.assert_equal(ll.tick_values(vmin - 1, vmin + 1), + [base**0, base**2]) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 6d8fa5419bbf..67b06e2184f6 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2292,6 +2292,14 @@ def __call__(self): return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): + """ + Return tick locations. + + Notes + ----- + If vmin/vmax are not exactly on a decade, a single tick lower/higher + than vmin/vmax can be returned. + """ if self.numticks == 'auto': if self.axis is not None: numticks = np.clip(self.axis.get_tick_space(), 2, 9) @@ -2322,10 +2330,11 @@ def tick_values(self, vmin, vmax): if vmax < vmin: vmin, vmax = vmax, vmin - log_vmin = math.log(vmin) / math.log(b) - log_vmax = math.log(vmax) / math.log(b) + log_vmin = np.log10(vmin) / np.log10(b) + log_vmax = np.log10(vmax) / np.log10(b) - numdec = math.floor(log_vmax) - math.ceil(log_vmin) + # Number of decades fully containing range [vmin, vmax] + numdec = math.ceil(log_vmax) - math.floor(log_vmin) if isinstance(self._subs, str): _first = 2.0 if self._subs == 'auto' else 1.0 @@ -2342,22 +2351,15 @@ def tick_values(self, vmin, vmax): # Get decades between major ticks. stride = (max(math.ceil(numdec / (numticks - 1)), 1) if mpl.rcParams['_internal.classic_mode'] else - (numdec + 1) // numticks + 1) - - # if we have decided that the stride is as big or bigger than - # the range, clip the stride back to the available range - 1 - # with a floor of 1. This prevents getting axis with only 1 tick - # visible. - if stride >= numdec: - stride = max(1, numdec - 1) + max(numdec // numticks, 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(log_vmin) - stride, - math.ceil(log_vmax) + 2 * stride, stride) - + decades = np.arange(math.floor(log_vmin), + math.ceil(log_vmax) + 1, + stride) if hasattr(self, '_transform'): ticklocs = self._transform.inverted().transform(decades) if have_subs: @@ -2368,7 +2370,7 @@ def tick_values(self, vmin, vmax): ticklocs = np.array([]) else: if have_subs: - if stride == 1: + if stride == 1 and len(decades): ticklocs = np.concatenate( [subs * decade_start for decade_start in b ** decades]) else: