diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 191afa0b9950..165f8b8c81e4 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1,5 +1,6 @@ import warnings import re +import itertools import numpy as np from numpy.testing import assert_almost_equal, assert_array_equal @@ -366,6 +367,44 @@ def test_minor_attr(self): loc.set_params(minor=False) assert not loc.minor + acceptable_vmin_vmax = [ + *(2.5 ** np.arange(-3, 0)), + *(1 - 2.5 ** np.arange(-3, 0)), + ] + + @pytest.mark.parametrize( + "lims", + [ + (a, b) + for (a, b) in itertools.product(acceptable_vmin_vmax, repeat=2) + if a != b + ], + ) + def test_nonsingular_ok(self, lims): + """ + Create logit locator, and test the nonsingular method for acceptable + value + """ + loc = mticker.LogitLocator() + lims2 = loc.nonsingular(*lims) + assert sorted(lims) == sorted(lims2) + + @pytest.mark.parametrize("okval", acceptable_vmin_vmax) + def test_nonsingular_nok(self, okval): + """ + Create logit locator, and test the nonsingular method for non + acceptable value + """ + loc = mticker.LogitLocator() + vmin, vmax = (-1, okval) + vmin2, vmax2 = loc.nonsingular(vmin, vmax) + assert vmax2 == vmax + assert 0 < vmin2 < vmax2 + vmin, vmax = (okval, 2) + vmin2, vmax2 = loc.nonsingular(vmin, vmax) + assert vmin2 == vmin + assert vmin2 < vmax2 < 1 + class TestFixedLocator: def test_set_params(self): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 16e8e02b2c5d..1aacec42941b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2792,32 +2792,38 @@ def ideal_ticks(x): return MaxNLocator.tick_values(self, vmin, vmax) def nonsingular(self, vmin, vmax): - initial_range = (1e-7, 1 - 1e-7) - if not np.isfinite(vmin) or not np.isfinite(vmax): - return initial_range # no data plotted yet - + standard_minpos = 1e-7 + initial_range = (standard_minpos, 1 - standard_minpos) if vmin > vmax: vmin, vmax = vmax, vmin - - # what to do if a window beyond ]0, 1[ is chosen - if self.axis is not None: - minpos = self.axis.get_minpos() - if not np.isfinite(minpos): - return initial_range # again, no data plotted + if not np.isfinite(vmin) or not np.isfinite(vmax): + vmin, vmax = initial_range # Initial range, no data plotted yet. + elif vmax <= 0 or vmin >= 1: + # vmax <= 0 occurs when all values are negative + # vmin >= 1 occurs when all values are greater than one + cbook._warn_external( + "Data has no values between 0 and 1, and therefore cannot be " + "logit-scaled." + ) + vmin, vmax = initial_range else: - minpos = 1e-7 # should not occur in normal use - - # NOTE: for vmax, we should query a property similar to get_minpos, but - # related to the maximal, less-than-one data point. Unfortunately, - # Bbox._minpos is defined very deep in the BBox and updated with data, - # so for now we use 1 - minpos as a substitute. - - if vmin <= 0: - vmin = minpos - if vmax >= 1: - vmax = 1 - minpos - if vmin == vmax: - return 0.1 * vmin, 1 - 0.1 * vmin + minpos = ( + self.axis.get_minpos() + if self.axis is not None + else standard_minpos + ) + if not np.isfinite(minpos): + minpos = standard_minpos # This should never take effect. + if vmin <= 0: + vmin = minpos + # NOTE: for vmax, we should query a property similar to get_minpos, + # but related to the maximal, less-than-one data point. + # Unfortunately, Bbox._minpos is defined very deep in the BBox and + # updated with data, so for now we use 1 - minpos as a substitute. + if vmax >= 1: + vmax = 1 - minpos + if vmin == vmax: + vmin, vmax = 0.1 * vmin, 1 - 0.1 * vmin return vmin, vmax