diff --git a/doc/api/next_api_changes/behavior/24436-DS.rst b/doc/api/next_api_changes/behavior/24436-DS.rst new file mode 100644 index 000000000000..88b3679bfd48 --- /dev/null +++ b/doc/api/next_api_changes/behavior/24436-DS.rst @@ -0,0 +1,9 @@ +Improved tick location logic in ``LogLocator`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.LogLocator.tick_values` previously returned two ticks that were outside +the range ``{vmin, vmax}``. This has been updated to only return all ticks +for the decades containing ``vmin`` and ``vmax``. If ``vmin`` or ``vmax`` +are exactly on a decade this means no ticks above/below are returned. +Otherwise a single major tick is returned above/below, and for minor ticks +all the ticks in the decade containing ``vmin`` or ``vmax`` are returned. diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index deb6c3fb2ba1..04278d0de9f2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6419,8 +6419,8 @@ def test_auto_numticks_log(): fig, ax = plt.subplots() mpl.rcParams['axes.autolimit_mode'] = 'round_numbers' ax.loglog([1e-20, 1e5], [1e-16, 10]) - assert (np.log10(ax.get_xticks()) == np.arange(-26, 18, 4)).all() - assert (np.log10(ax.get_yticks()) == np.arange(-20, 10, 3)).all() + assert_array_equal(np.log10(ax.get_xticks()), np.arange(-22, 14, 4)) + assert_array_equal(np.log10(ax.get_yticks()), np.arange(-17, 7, 3)) def test_broken_barh_empty(): diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 5ab3ccf13f80..58f34d0416e8 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -493,10 +493,10 @@ def test_colorbar_autotickslog(): 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.2, 4.)) + # note only -12 to +12 are visible np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), - 10**np.arange(-24., 25., 12.)) + 10**np.arange(-12., 13., 12.)) def test_colorbar_get_ticks(): @@ -584,6 +584,7 @@ def test_colorbar_log_minortick_labels(): def test_colorbar_renorm(): x, y = np.ogrid[-4:4:31j, -4:4:31j] z = 120000*np.exp(-x**2 - y**2) + # min/max of z vals is 1.5196998658913011e-09, 120000.0 fig, ax = plt.subplots() im = ax.imshow(z) @@ -597,7 +598,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, 6, 16)) # note that set_norm removes the FixedLocator... assert np.isclose(cbar.vmin, z.min()) cbar.set_ticks([1, 2, 3]) @@ -616,6 +617,7 @@ def test_colorbar_format(fmt): # make sure that format is passed properly x, y = np.ogrid[-4:4:31j, -4:4:31j] z = 120000*np.exp(-x**2 - y**2) + # min/max of z vals is 1.5196998658913011e-09, 120000.0 fig, ax = plt.subplots() im = ax.imshow(z) @@ -633,7 +635,7 @@ def test_colorbar_format(fmt): im.set_norm(LogNorm(vmin=0.1, vmax=10)) fig.canvas.draw() assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() == - '$\\mathdefault{10^{-2}}$') + '$\\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 2bea8c999067..5d6c8f2d81bd 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -194,20 +194,40 @@ def test_additional(self, lim, ref): class TestLogLocator: + @pytest.mark.parametrize( + 'vmin, vmax, expected_ticks', + ( + [1, 10, [1, 10]], + [0.9, 10.1, [0.1, 1, 10, 100]], + [0.9, 9, [0.1, 1, 10]], + [1.1, 11, [1, 10, 100]], + [0.5, 983, [0.1, 1, 10, 100, 1000]] + ) + ) + def test_tick_locs(self, vmin, vmax, expected_ticks): + ll = mticker.LogLocator(base=10.0, subs=(1.0,)) + np.testing.assert_equal(ll.tick_values(vmin, vmax), expected_ticks) + def test_basic(self): loc = mticker.LogLocator(numticks=5) - with pytest.raises(ValueError): - loc.tick_values(0, 1000) - - test_value = np.array([1.00000000e-05, 1.00000000e-03, 1.00000000e-01, + test_value = np.array([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) + 1.00000000e+07]) + assert_almost_equal(loc.tick_values(1e-3, 1.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_invalid_lim_error(self): + loc = mticker.LogLocator() + with pytest.raises( + ValueError, + match='Data has non-positive values, ' + 'and therefore can not be log-scaled.' + ): + loc.tick_values(0, 1000) + def test_polar_axes(self): """ Polar axes have a different ticking logic. @@ -215,7 +235,7 @@ def test_polar_axes(self): fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) ax.set_yscale('log') ax.set_ylim(1, 100) - assert_array_equal(ax.get_yticks(), [10, 100, 1000]) + assert_array_equal(ax.get_yticks(), [10, 100]) def test_switch_to_autolocator(self): loc = mticker.LogLocator(subs="all") diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 45aa4b9b6035..699e3e53f83e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2344,6 +2344,15 @@ def __call__(self): return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): + """ + Return the values of the located ticks given **vmin** and **vmax**. + + Notes + ----- + The tick values returned include all ticks for the decades containing + ``vmin`` and ``vmax``, which can result in some tick values below + ``vmin`` and above ``vmax``. + """ if self.numticks == 'auto': if self.axis is not None: numticks = np.clip(self.axis.get_tick_space(), 2, 9) @@ -2359,7 +2368,7 @@ def tick_values(self, vmin, vmax): if vmin <= 0.0 or not np.isfinite(vmin): raise ValueError( - "Data has no positive values, and therefore can not be " + "Data has non-positive values, and therefore can not be " "log-scaled.") _log.debug('vmin %s vmax %s', vmin, vmax) @@ -2399,8 +2408,10 @@ def tick_values(self, vmin, vmax): # 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) + # Return one tick below (or equal to) vmin, + # and one tick above (or equal to) vmax + decades = np.arange(math.floor(log_vmin), + math.ceil(log_vmax) + stride, stride) if hasattr(self, '_transform'): ticklocs = self._transform.inverted().transform(decades) @@ -2450,7 +2461,7 @@ def nonsingular(self, vmin, vmax): vmin, vmax = 1, 10 # Initial range, no data plotted yet. elif vmax <= 0: _api.warn_external( - "Data has no positive values, and therefore cannot be " + "Data has non-positive values, and therefore cannot be " "log-scaled.") vmin, vmax = 1, 10 else: