From eea51e9f4a1ae1f12f4e6ad58d62d528d1220400 Mon Sep 17 00:00:00 2001 From: DavidAG Date: Wed, 17 Dec 2025 19:38:49 +0100 Subject: [PATCH 1/2] Add __repr__ to Formatter and Locator classes (fixes #21898) Added informative __repr__ methods to all Formatter and Locator subclasses in matplotlib.ticker, so that eval(repr(obj)) returns an equivalent object. Formatters: - NullFormatter - FixedFormatter - FuncFormatter - FormatStrFormatter - StrMethodFormatter - LogFormatter - LogitFormatter - EngFormatter - PercentFormatter Locators: - NullLocator - FixedLocator - IndexLocator - LinearLocator - MultipleLocator - MaxNLocator - AutoLocator - AutoMinorLocator - LogLocator - SymmetricalLogLocator - AsinhLocator - LogitLocator Also fixed two pytest.mark.parametrize deprecation warnings that were using zip iterators instead of lists. --- lib/matplotlib/tests/test_ticker.py | 1641 +++++++++++++++++---------- lib/matplotlib/ticker.py | 724 +++++++----- 2 files changed, 1479 insertions(+), 886 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index a9104cc1b839..9619ba6ca7fb 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -16,9 +16,9 @@ class TestMaxNLocator: basic_data = [ - (20, 100, np.array([20., 40., 60., 80., 100.])), - (0.001, 0.0001, np.array([0., 0.0002, 0.0004, 0.0006, 0.0008, 0.001])), - (-1e15, 1e15, np.array([-1.0e+15, -5.0e+14, 0e+00, 5e+14, 1.0e+15])), + (20, 100, np.array([20.0, 40.0, 60.0, 80.0, 100.0])), + (0.001, 0.0001, np.array([0.0, 0.0002, 0.0004, 0.0006, 0.0008, 0.001])), + (-1e15, 1e15, np.array([-1.0e15, -5.0e14, 0e00, 5e14, 1.0e15])), (0, 0.85e-50, np.arange(6) * 2e-51), (-0.85e-50, 0, np.arange(-5, 1) * 2e-51), ] @@ -29,33 +29,42 @@ class TestMaxNLocator: (1, 55, [1, 1.5, 5, 6, 10], np.array([0, 15, 30, 45, 60])), ] - @pytest.mark.parametrize('vmin, vmax, expected', basic_data) + @pytest.mark.parametrize("vmin, vmax, expected", basic_data) def test_basic(self, vmin, vmax, expected): loc = mticker.MaxNLocator(nbins=5) assert_almost_equal(loc.tick_values(vmin, vmax), expected) - @pytest.mark.parametrize('vmin, vmax, steps, expected', integer_data) + @pytest.mark.parametrize("vmin, vmax, steps, expected", integer_data) def test_integer(self, vmin, vmax, steps, expected): loc = mticker.MaxNLocator(nbins=5, integer=True, steps=steps) assert_almost_equal(loc.tick_values(vmin, vmax), expected) - @pytest.mark.parametrize('kwargs, errortype, match', [ - ({'foo': 0}, TypeError, - re.escape("set_params() got an unexpected keyword argument 'foo'")), - ({'steps': [2, 1]}, ValueError, "steps argument must be an increasing"), - ({'steps': 2}, ValueError, "steps argument must be an increasing"), - ({'steps': [2, 11]}, ValueError, "steps argument must be an increasing"), - ]) + @pytest.mark.parametrize( + "kwargs, errortype, match", + [ + ( + {"foo": 0}, + TypeError, + re.escape("set_params() got an unexpected keyword argument 'foo'"), + ), + ({"steps": [2, 1]}, ValueError, "steps argument must be an increasing"), + ({"steps": 2}, ValueError, "steps argument must be an increasing"), + ({"steps": [2, 11]}, ValueError, "steps argument must be an increasing"), + ], + ) def test_errors(self, kwargs, errortype, match): with pytest.raises(errortype, match=match): mticker.MaxNLocator(**kwargs) - @pytest.mark.parametrize('steps, result', [ - ([1, 2, 10], [1, 2, 10]), - ([2, 10], [1, 2, 10]), - ([1, 2], [1, 2, 10]), - ([2], [1, 2, 10]), - ]) + @pytest.mark.parametrize( + "steps, result", + [ + ([1, 2, 10], [1, 2, 10]), + ([2, 10], [1, 2, 10]), + ([1, 2], [1, 2, 10]), + ([2], [1, 2, 10]), + ], + ) def test_padding(self, steps, result): loc = mticker.MaxNLocator(steps=steps) assert (loc._steps == result).all() @@ -82,8 +91,9 @@ def test_set_params(self): assert loc.presets == {(0, 1): []} def test_presets(self): - loc = mticker.LinearLocator(presets={(1, 2): [1, 1.25, 1.75], - (0, 2): [0.5, 1.5]}) + loc = mticker.LinearLocator( + presets={(1, 2): [1, 1.25, 1.75], (0, 2): [0.5, 1.5]} + ) assert loc.tick_values(1, 2) == [1, 1.25, 1.75] assert loc.tick_values(2, 1) == [1, 1.25, 1.75] assert loc.tick_values(0, 2) == [0.5, 1.5] @@ -94,21 +104,21 @@ def test_presets(self): class TestMultipleLocator: def test_basic(self): loc = mticker.MultipleLocator(base=3.147) - test_value = np.array([-9.441, -6.294, -3.147, 0., 3.147, 6.294, - 9.441, 12.588]) + test_value = np.array( + [-9.441, -6.294, -3.147, 0.0, 3.147, 6.294, 9.441, 12.588] + ) assert_almost_equal(loc.tick_values(-7, 10), test_value) def test_basic_with_offset(self): loc = mticker.MultipleLocator(base=3.147, offset=1.2) - test_value = np.array([-8.241, -5.094, -1.947, 1.2, 4.347, 7.494, - 10.641]) + test_value = np.array([-8.241, -5.094, -1.947, 1.2, 4.347, 7.494, 10.641]) assert_almost_equal(loc.tick_values(-7, 10), test_value) def test_view_limits(self): """ Test basic behavior of view limits. """ - with mpl.rc_context({'axes.autolimit_mode': 'data'}): + with mpl.rc_context({"axes.autolimit_mode": "data"}): loc = mticker.MultipleLocator(base=3.147) assert_almost_equal(loc.view_limits(-5, 5), (-5, 5)) @@ -117,7 +127,7 @@ def test_view_limits_round_numbers(self): Test that everything works properly with 'round_numbers' for auto limit. """ - with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}): + with mpl.rc_context({"axes.autolimit_mode": "round_numbers"}): loc = mticker.MultipleLocator(base=3.147) assert_almost_equal(loc.view_limits(-4, 4), (-6.294, 6.294)) @@ -126,7 +136,7 @@ def test_view_limits_round_numbers_with_offset(self): Test that everything works properly with 'round_numbers' for auto limit. """ - with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}): + with mpl.rc_context({"axes.autolimit_mode": "round_numbers"}): loc = mticker.MultipleLocator(base=3.147, offset=1.3) assert_almost_equal(loc.view_limits(-4, 4), (-4.994, 4.447)) @@ -134,7 +144,7 @@ def test_view_limits_single_bin(self): """ Test that 'round_numbers' works properly with a single bin. """ - with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}): + with mpl.rc_context({"axes.autolimit_mode": "round_numbers"}): loc = mticker.MaxNLocator(nbins=1) assert_almost_equal(loc.view_limits(-2.3, 2.3), (-4, 4)) @@ -155,15 +165,37 @@ def test_basic(self): fig, ax = plt.subplots() ax.set_xlim(0, 1.39) ax.minorticks_on() - test_value = np.array([0.05, 0.1, 0.15, 0.25, 0.3, 0.35, 0.45, - 0.5, 0.55, 0.65, 0.7, 0.75, 0.85, 0.9, - 0.95, 1.05, 1.1, 1.15, 1.25, 1.3, 1.35]) + test_value = np.array( + [ + 0.05, + 0.1, + 0.15, + 0.25, + 0.3, + 0.35, + 0.45, + 0.5, + 0.55, + 0.65, + 0.7, + 0.75, + 0.85, + 0.9, + 0.95, + 1.05, + 1.1, + 1.15, + 1.25, + 1.3, + 1.35, + ] + ) assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), test_value) # NB: the following values are assuming that *xlim* is [0, 5] params = [ (0, 0), # no major tick => no minor tick either - (1, 0) # a single major tick => no minor tick + (1, 0), # a single major tick => no minor tick ] def test_first_and_last_minorticks(self): @@ -174,20 +206,69 @@ def test_first_and_last_minorticks(self): fig, ax = plt.subplots() ax.set_xlim(-1.9, 1.9) ax.xaxis.set_minor_locator(mticker.AutoMinorLocator()) - test_value = np.array([-1.9, -1.8, -1.7, -1.6, -1.4, -1.3, -1.2, -1.1, - -0.9, -0.8, -0.7, -0.6, -0.4, -0.3, -0.2, -0.1, - 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.1, - 1.2, 1.3, 1.4, 1.6, 1.7, 1.8, 1.9]) + test_value = np.array( + [ + -1.9, + -1.8, + -1.7, + -1.6, + -1.4, + -1.3, + -1.2, + -1.1, + -0.9, + -0.8, + -0.7, + -0.6, + -0.4, + -0.3, + -0.2, + -0.1, + 0.1, + 0.2, + 0.3, + 0.4, + 0.6, + 0.7, + 0.8, + 0.9, + 1.1, + 1.2, + 1.3, + 1.4, + 1.6, + 1.7, + 1.8, + 1.9, + ] + ) assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), test_value) ax.set_xlim(-5, 5) - test_value = np.array([-5.0, -4.5, -3.5, -3.0, -2.5, -1.5, -1.0, -0.5, - 0.5, 1.0, 1.5, 2.5, 3.0, 3.5, 4.5, 5.0]) + test_value = np.array( + [ + -5.0, + -4.5, + -3.5, + -3.0, + -2.5, + -1.5, + -1.0, + -0.5, + 0.5, + 1.0, + 1.5, + 2.5, + 3.0, + 3.5, + 4.5, + 5.0, + ] + ) assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), test_value) - @pytest.mark.parametrize('nb_majorticks, expected_nb_minorticks', params) - def test_low_number_of_majorticks( - self, nb_majorticks, expected_nb_minorticks): + @pytest.mark.parametrize("nb_majorticks, expected_nb_minorticks", params) + def test_low_number_of_majorticks(self, nb_majorticks, expected_nb_minorticks): # This test is related to issue #8804 fig, ax = plt.subplots() xlims = (0, 5) # easier to test the different code paths @@ -197,24 +278,19 @@ def test_low_number_of_majorticks( ax.xaxis.set_minor_locator(mticker.AutoMinorLocator()) assert len(ax.xaxis.get_minorticklocs()) == expected_nb_minorticks - majorstep_minordivisions = [(1, 5), - (2, 4), - (2.5, 5), - (5, 5), - (10, 5)] + majorstep_minordivisions = [(1, 5), (2, 4), (2.5, 5), (5, 5), (10, 5)] # This test is meant to verify the parameterization for # test_number_of_minor_ticks def test_using_all_default_major_steps(self): - with mpl.rc_context({'_internal.classic_mode': False}): + with mpl.rc_context({"_internal.classic_mode": False}): majorsteps = [x[0] for x in self.majorstep_minordivisions] - np.testing.assert_allclose(majorsteps, - mticker.AutoLocator()._steps) + np.testing.assert_allclose(majorsteps, mticker.AutoLocator()._steps) - @pytest.mark.parametrize('major_step, expected_nb_minordivisions', - majorstep_minordivisions) - def test_number_of_minor_ticks( - self, major_step, expected_nb_minordivisions): + @pytest.mark.parametrize( + "major_step, expected_nb_minordivisions", majorstep_minordivisions + ) + def test_number_of_minor_ticks(self, major_step, expected_nb_minordivisions): fig, ax = plt.subplots() xlims = (0, major_step) ax.set_xlim(*xlims) @@ -224,71 +300,273 @@ def test_number_of_minor_ticks( nb_minor_divisions = len(ax.xaxis.get_minorticklocs()) + 1 assert nb_minor_divisions == expected_nb_minordivisions - limits = [(0, 1.39), (0, 0.139), - (0, 0.11e-19), (0, 0.112e-12), - (-2.0e-07, -3.3e-08), (1.20e-06, 1.42e-06), - (-1.34e-06, -1.44e-06), (-8.76e-07, -1.51e-06)] + limits = [ + (0, 1.39), + (0, 0.139), + (0, 0.11e-19), + (0, 0.112e-12), + (-2.0e-07, -3.3e-08), + (1.20e-06, 1.42e-06), + (-1.34e-06, -1.44e-06), + (-8.76e-07, -1.51e-06), + ] reference = [ - [0.05, 0.1, 0.15, 0.25, 0.3, 0.35, 0.45, 0.5, 0.55, 0.65, 0.7, - 0.75, 0.85, 0.9, 0.95, 1.05, 1.1, 1.15, 1.25, 1.3, 1.35], - [0.005, 0.01, 0.015, 0.025, 0.03, 0.035, 0.045, 0.05, 0.055, 0.065, - 0.07, 0.075, 0.085, 0.09, 0.095, 0.105, 0.11, 0.115, 0.125, 0.13, - 0.135], - [5.00e-22, 1.00e-21, 1.50e-21, 2.50e-21, 3.00e-21, 3.50e-21, 4.50e-21, - 5.00e-21, 5.50e-21, 6.50e-21, 7.00e-21, 7.50e-21, 8.50e-21, 9.00e-21, - 9.50e-21, 1.05e-20, 1.10e-20], - [5.00e-15, 1.00e-14, 1.50e-14, 2.50e-14, 3.00e-14, 3.50e-14, 4.50e-14, - 5.00e-14, 5.50e-14, 6.50e-14, 7.00e-14, 7.50e-14, 8.50e-14, 9.00e-14, - 9.50e-14, 1.05e-13, 1.10e-13], - [-1.95e-07, -1.90e-07, -1.85e-07, -1.75e-07, -1.70e-07, -1.65e-07, - -1.55e-07, -1.50e-07, -1.45e-07, -1.35e-07, -1.30e-07, -1.25e-07, - -1.15e-07, -1.10e-07, -1.05e-07, -9.50e-08, -9.00e-08, -8.50e-08, - -7.50e-08, -7.00e-08, -6.50e-08, -5.50e-08, -5.00e-08, -4.50e-08, - -3.50e-08], - [1.21e-06, 1.22e-06, 1.23e-06, 1.24e-06, 1.26e-06, 1.27e-06, 1.28e-06, - 1.29e-06, 1.31e-06, 1.32e-06, 1.33e-06, 1.34e-06, 1.36e-06, 1.37e-06, - 1.38e-06, 1.39e-06, 1.41e-06, 1.42e-06], - [-1.435e-06, -1.430e-06, -1.425e-06, -1.415e-06, -1.410e-06, - -1.405e-06, -1.395e-06, -1.390e-06, -1.385e-06, -1.375e-06, - -1.370e-06, -1.365e-06, -1.355e-06, -1.350e-06, -1.345e-06], - [-1.48e-06, -1.46e-06, -1.44e-06, -1.42e-06, -1.38e-06, -1.36e-06, - -1.34e-06, -1.32e-06, -1.28e-06, -1.26e-06, -1.24e-06, -1.22e-06, - -1.18e-06, -1.16e-06, -1.14e-06, -1.12e-06, -1.08e-06, -1.06e-06, - -1.04e-06, -1.02e-06, -9.80e-07, -9.60e-07, -9.40e-07, -9.20e-07, - -8.80e-07]] + [ + 0.05, + 0.1, + 0.15, + 0.25, + 0.3, + 0.35, + 0.45, + 0.5, + 0.55, + 0.65, + 0.7, + 0.75, + 0.85, + 0.9, + 0.95, + 1.05, + 1.1, + 1.15, + 1.25, + 1.3, + 1.35, + ], + [ + 0.005, + 0.01, + 0.015, + 0.025, + 0.03, + 0.035, + 0.045, + 0.05, + 0.055, + 0.065, + 0.07, + 0.075, + 0.085, + 0.09, + 0.095, + 0.105, + 0.11, + 0.115, + 0.125, + 0.13, + 0.135, + ], + [ + 5.00e-22, + 1.00e-21, + 1.50e-21, + 2.50e-21, + 3.00e-21, + 3.50e-21, + 4.50e-21, + 5.00e-21, + 5.50e-21, + 6.50e-21, + 7.00e-21, + 7.50e-21, + 8.50e-21, + 9.00e-21, + 9.50e-21, + 1.05e-20, + 1.10e-20, + ], + [ + 5.00e-15, + 1.00e-14, + 1.50e-14, + 2.50e-14, + 3.00e-14, + 3.50e-14, + 4.50e-14, + 5.00e-14, + 5.50e-14, + 6.50e-14, + 7.00e-14, + 7.50e-14, + 8.50e-14, + 9.00e-14, + 9.50e-14, + 1.05e-13, + 1.10e-13, + ], + [ + -1.95e-07, + -1.90e-07, + -1.85e-07, + -1.75e-07, + -1.70e-07, + -1.65e-07, + -1.55e-07, + -1.50e-07, + -1.45e-07, + -1.35e-07, + -1.30e-07, + -1.25e-07, + -1.15e-07, + -1.10e-07, + -1.05e-07, + -9.50e-08, + -9.00e-08, + -8.50e-08, + -7.50e-08, + -7.00e-08, + -6.50e-08, + -5.50e-08, + -5.00e-08, + -4.50e-08, + -3.50e-08, + ], + [ + 1.21e-06, + 1.22e-06, + 1.23e-06, + 1.24e-06, + 1.26e-06, + 1.27e-06, + 1.28e-06, + 1.29e-06, + 1.31e-06, + 1.32e-06, + 1.33e-06, + 1.34e-06, + 1.36e-06, + 1.37e-06, + 1.38e-06, + 1.39e-06, + 1.41e-06, + 1.42e-06, + ], + [ + -1.435e-06, + -1.430e-06, + -1.425e-06, + -1.415e-06, + -1.410e-06, + -1.405e-06, + -1.395e-06, + -1.390e-06, + -1.385e-06, + -1.375e-06, + -1.370e-06, + -1.365e-06, + -1.355e-06, + -1.350e-06, + -1.345e-06, + ], + [ + -1.48e-06, + -1.46e-06, + -1.44e-06, + -1.42e-06, + -1.38e-06, + -1.36e-06, + -1.34e-06, + -1.32e-06, + -1.28e-06, + -1.26e-06, + -1.24e-06, + -1.22e-06, + -1.18e-06, + -1.16e-06, + -1.14e-06, + -1.12e-06, + -1.08e-06, + -1.06e-06, + -1.04e-06, + -1.02e-06, + -9.80e-07, + -9.60e-07, + -9.40e-07, + -9.20e-07, + -8.80e-07, + ], + ] additional_data = list(zip(limits, reference)) - @pytest.mark.parametrize('lim, ref', additional_data) + @pytest.mark.parametrize("lim, ref", additional_data) def test_additional(self, lim, ref): fig, ax = plt.subplots() ax.minorticks_on() - ax.grid(True, 'minor', 'y', linewidth=1) - ax.grid(True, 'major', color='k', linewidth=1) + ax.grid(True, "minor", "y", linewidth=1) + ax.grid(True, "major", color="k", linewidth=1) ax.set_ylim(lim) assert_almost_equal(ax.yaxis.get_ticklocs(minor=True), ref) - @pytest.mark.parametrize('use_rcparam', [False, True]) + @pytest.mark.parametrize("use_rcparam", [False, True]) @pytest.mark.parametrize( - 'lim, ref', [ - ((0, 1.39), - [0.05, 0.1, 0.15, 0.25, 0.3, 0.35, 0.45, 0.5, 0.55, 0.65, 0.7, - 0.75, 0.85, 0.9, 0.95, 1.05, 1.1, 1.15, 1.25, 1.3, 1.35]), - ((0, 0.139), - [0.005, 0.01, 0.015, 0.025, 0.03, 0.035, 0.045, 0.05, 0.055, - 0.065, 0.07, 0.075, 0.085, 0.09, 0.095, 0.105, 0.11, 0.115, - 0.125, 0.13, 0.135]), - ]) + "lim, ref", + [ + ( + (0, 1.39), + [ + 0.05, + 0.1, + 0.15, + 0.25, + 0.3, + 0.35, + 0.45, + 0.5, + 0.55, + 0.65, + 0.7, + 0.75, + 0.85, + 0.9, + 0.95, + 1.05, + 1.1, + 1.15, + 1.25, + 1.3, + 1.35, + ], + ), + ( + (0, 0.139), + [ + 0.005, + 0.01, + 0.015, + 0.025, + 0.03, + 0.035, + 0.045, + 0.05, + 0.055, + 0.065, + 0.07, + 0.075, + 0.085, + 0.09, + 0.095, + 0.105, + 0.11, + 0.115, + 0.125, + 0.13, + 0.135, + ], + ), + ], + ) def test_number_of_minor_ticks_auto(self, lim, ref, use_rcparam): if use_rcparam: - context = {'xtick.minor.ndivs': 'auto', 'ytick.minor.ndivs': 'auto'} + context = {"xtick.minor.ndivs": "auto", "ytick.minor.ndivs": "auto"} kwargs = {} else: context = {} - kwargs = {'n': 'auto'} + kwargs = {"n": "auto"} with mpl.rc_context(context): fig, ax = plt.subplots() @@ -299,20 +577,22 @@ def test_number_of_minor_ticks_auto(self, lim, ref, use_rcparam): assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), ref) assert_almost_equal(ax.yaxis.get_ticklocs(minor=True), ref) - @pytest.mark.parametrize('use_rcparam', [False, True]) + @pytest.mark.parametrize("use_rcparam", [False, True]) @pytest.mark.parametrize( - 'n, lim, ref', [ + "n, lim, ref", + [ (2, (0, 4), [0.5, 1.5, 2.5, 3.5]), (4, (0, 2), [0.25, 0.5, 0.75, 1.25, 1.5, 1.75]), (10, (0, 1), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]), - ]) + ], + ) def test_number_of_minor_ticks_int(self, n, lim, ref, use_rcparam): if use_rcparam: - context = {'xtick.minor.ndivs': n, 'ytick.minor.ndivs': n} + context = {"xtick.minor.ndivs": n, "ytick.minor.ndivs": n} kwargs = {} else: context = {} - kwargs = {'n': n} + kwargs = {"n": n} with mpl.rc_context(context): fig, ax = plt.subplots() @@ -332,30 +612,31 @@ def test_basic(self): with pytest.raises(ValueError): loc.tick_values(0, 1000) - test_value = np.array([1e-5, 1e-3, 1e-1, 1e+1, 1e+3, 1e+5, 1e+7]) + test_value = np.array([1e-5, 1e-3, 1e-1, 1e1, 1e3, 1e5, 1e7]) assert_almost_equal(loc.tick_values(0.001, 1.1e5), test_value) loc = mticker.LogLocator(base=2) - test_value = np.array([.5, 1., 2., 4., 8., 16., 32., 64., 128.]) + test_value = np.array([0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0]) assert_almost_equal(loc.tick_values(1, 100), test_value) def test_polar_axes(self): """ Polar Axes have a different ticking logic. """ - fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) - ax.set_yscale('log') + 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]) 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]) + assert_array_equal( + loc.tick_values(0.45, 0.55), [0.44, 0.46, 0.48, 0.5, 0.52, 0.54, 0.56] + ) # check that we *skip* 1.0, and 10, because this is a minor locator loc = mticker.LogLocator(subs=np.arange(2, 10)) - assert 1.0 not in loc.tick_values(0.9, 20.) - assert 10.0 not in loc.tick_values(0.9, 20.) + assert 1.0 not in loc.tick_values(0.9, 20.0) + assert 10.0 not in loc.tick_values(0.9, 20.0) # don't switch if there's already one major and one minor tick (10 & 20) loc = mticker.LogLocator(subs="auto") tv = loc.tick_values(10, 20) @@ -375,21 +656,76 @@ def test_set_params(self): def test_tick_values_correct(self): ll = mticker.LogLocator(subs=(1, 2, 5)) - test_value = np.array([1.e-01, 2.e-01, 5.e-01, 1.e+00, 2.e+00, 5.e+00, - 1.e+01, 2.e+01, 5.e+01, 1.e+02, 2.e+02, 5.e+02, - 1.e+03, 2.e+03, 5.e+03, 1.e+04, 2.e+04, 5.e+04, - 1.e+05, 2.e+05, 5.e+05, 1.e+06, 2.e+06, 5.e+06, - 1.e+07, 2.e+07, 5.e+07]) + test_value = np.array( + [ + 1.0e-01, + 2.0e-01, + 5.0e-01, + 1.0e00, + 2.0e00, + 5.0e00, + 1.0e01, + 2.0e01, + 5.0e01, + 1.0e02, + 2.0e02, + 5.0e02, + 1.0e03, + 2.0e03, + 5.0e03, + 1.0e04, + 2.0e04, + 5.0e04, + 1.0e05, + 2.0e05, + 5.0e05, + 1.0e06, + 2.0e06, + 5.0e06, + 1.0e07, + 2.0e07, + 5.0e07, + ] + ) assert_almost_equal(ll.tick_values(1, 1e7), test_value) def test_tick_values_not_empty(self): - mpl.rcParams['_internal.classic_mode'] = False + mpl.rcParams["_internal.classic_mode"] = False ll = mticker.LogLocator(subs=(1, 2, 5)) - test_value = np.array([1.e-01, 2.e-01, 5.e-01, 1.e+00, 2.e+00, 5.e+00, - 1.e+01, 2.e+01, 5.e+01, 1.e+02, 2.e+02, 5.e+02, - 1.e+03, 2.e+03, 5.e+03, 1.e+04, 2.e+04, 5.e+04, - 1.e+05, 2.e+05, 5.e+05, 1.e+06, 2.e+06, 5.e+06, - 1.e+07, 2.e+07, 5.e+07, 1.e+08, 2.e+08, 5.e+08]) + test_value = np.array( + [ + 1.0e-01, + 2.0e-01, + 5.0e-01, + 1.0e00, + 2.0e00, + 5.0e00, + 1.0e01, + 2.0e01, + 5.0e01, + 1.0e02, + 2.0e02, + 5.0e02, + 1.0e03, + 2.0e03, + 5.0e03, + 1.0e04, + 2.0e04, + 5.0e04, + 1.0e05, + 2.0e05, + 5.0e05, + 1.0e06, + 2.0e06, + 5.0e06, + 1.0e07, + 2.0e07, + 5.0e07, + 1.0e08, + 2.0e08, + 5.0e08, + ] + ) assert_almost_equal(ll.tick_values(1, 1e8), test_value) def test_multiple_shared_axes(self): @@ -399,7 +735,7 @@ def test_multiple_shared_axes(self): for ax, data in zip(axes.flatten(), dummy_data): ax.hist(data, bins=10) - ax.set_yscale('log', nonpositive='clip') + ax.set_yscale("log", nonpositive="clip") for ax in axes.flatten(): assert all(ax.get_yticks() == axes[0].get_yticks()) @@ -420,8 +756,11 @@ def test_set_params(self): class _LogitHelper: @staticmethod def isclose(x, y): - return (np.isclose(-np.log(1/x-1), -np.log(1/y-1)) - if 0 < x < 1 and 0 < y < 1 else False) + return ( + np.isclose(-np.log(1 / x - 1), -np.log(1 / y - 1)) + if 0 < x < 1 and 0 < y < 1 + else False + ) @staticmethod def assert_almost_equal(x, y): @@ -429,8 +768,8 @@ def assert_almost_equal(x, y): ay = np.array(y) assert np.all(ax > 0) and np.all(ax < 1) assert np.all(ay > 0) and np.all(ay < 1) - lx = -np.log(1/ax-1) - ly = -np.log(1/ay-1) + lx = -np.log(1 / ax - 1) + ly = -np.log(1 / ay - 1) assert_almost_equal(lx, ly) @@ -461,20 +800,15 @@ class TestLogitLocator: @pytest.mark.parametrize( "lims, expected_low_ticks", - zip(ref_basic_limits, ref_basic_major_ticks), + list(zip(ref_basic_limits, ref_basic_major_ticks)), ) def test_basic_major(self, lims, expected_low_ticks): """ Create logit locator with huge number of major, and tests ticks. """ - expected_ticks = sorted( - [*expected_low_ticks, 0.5, *(1 - expected_low_ticks)] - ) + expected_ticks = sorted([*expected_low_ticks, 0.5, *(1 - expected_low_ticks)]) loc = mticker.LogitLocator(nbins=100) - _LogitHelper.assert_almost_equal( - loc.tick_values(*lims), - expected_ticks - ) + _LogitHelper.assert_almost_equal(loc.tick_values(*lims), expected_ticks) @pytest.mark.parametrize("lims", ref_maxn_limits) def test_maxn_major(self, lims): @@ -506,7 +840,7 @@ def test_nbins_major(self, lims): @pytest.mark.parametrize( "lims, expected_low_ticks", - zip(ref_basic_limits, ref_basic_major_ticks), + list(zip(ref_basic_limits, ref_basic_major_ticks)), ) def test_minor(self, lims, expected_low_ticks): """ @@ -514,9 +848,7 @@ def test_minor(self, lims, expected_low_ticks): and assert no minor when major are subsampled. """ - expected_ticks = sorted( - [*expected_low_ticks, 0.5, *(1 - expected_low_ticks)] - ) + expected_ticks = sorted([*expected_low_ticks, 0.5, *(1 - expected_low_ticks)]) basic_needed = len(expected_ticks) loc = mticker.LogitLocator(nbins=100) minor_loc = mticker.LogitLocator(nbins=100, minor=True) @@ -531,7 +863,8 @@ def test_minor(self, lims, expected_low_ticks): else: # subsample _LogitHelper.assert_almost_equal( - sorted([*major_ticks, *minor_ticks]), expected_ticks) + sorted([*major_ticks, *minor_ticks]), expected_ticks + ) def test_minor_attr(self): loc = mticker.LogitLocator(nbins=100) @@ -619,11 +952,11 @@ def test_set_params(self): assert sym.numticks == 8 @pytest.mark.parametrize( - 'vmin, vmax, expected', - [ - (0, 1, [0, 1]), - (-1, 1, [-1, 0, 1]), - ], + "vmin, vmax, expected", + [ + (0, 1, [0, 1]), + (-1, 1, [-1, 0, 1]), + ], ) def test_values(self, vmin, vmax, expected): # https://github.com/matplotlib/matplotlib/issues/25945 @@ -657,9 +990,9 @@ def test_init(self): assert lctr.base == 10 def test_set_params(self): - lctr = mticker.AsinhLocator(linear_width=5, - numticks=17, symthresh=0.125, - base=4, subs=(2.5, 3.25)) + lctr = mticker.AsinhLocator( + linear_width=5, numticks=17, symthresh=0.125, base=4, subs=(2.5, 3.25) + ) assert lctr.numticks == 17 assert lctr.symthresh == 0.125 assert lctr.base == 4 @@ -690,22 +1023,23 @@ def test_set_params(self): def test_linear_values(self): lctr = mticker.AsinhLocator(linear_width=100, numticks=11, base=0) - assert_almost_equal(lctr.tick_values(-1, 1), - np.arange(-1, 1.01, 0.2)) - assert_almost_equal(lctr.tick_values(-0.1, 0.1), - np.arange(-0.1, 0.101, 0.02)) - assert_almost_equal(lctr.tick_values(-0.01, 0.01), - np.arange(-0.01, 0.0101, 0.002)) + assert_almost_equal(lctr.tick_values(-1, 1), np.arange(-1, 1.01, 0.2)) + assert_almost_equal(lctr.tick_values(-0.1, 0.1), np.arange(-0.1, 0.101, 0.02)) + assert_almost_equal( + lctr.tick_values(-0.01, 0.01), np.arange(-0.01, 0.0101, 0.002) + ) def test_wide_values(self): lctr = mticker.AsinhLocator(linear_width=0.1, numticks=11, base=0) - assert_almost_equal(lctr.tick_values(-100, 100), - [-100, -20, -5, -1, -0.2, - 0, 0.2, 1, 5, 20, 100]) - assert_almost_equal(lctr.tick_values(-1000, 1000), - [-1000, -100, -20, -3, -0.4, - 0, 0.4, 3, 20, 100, 1000]) + assert_almost_equal( + lctr.tick_values(-100, 100), + [-100, -20, -5, -1, -0.2, 0, 0.2, 1, 5, 20, 100], + ) + assert_almost_equal( + lctr.tick_values(-1000, 1000), + [-1000, -100, -20, -3, -0.4, 0, 0.4, 3, 20, 100, 1000], + ) def test_near_zero(self): """Check that manually injected zero will supersede nearby tick""" @@ -716,12 +1050,10 @@ def test_near_zero(self): def test_fallback(self): lctr = mticker.AsinhLocator(1.0, numticks=11) - assert_almost_equal(lctr.tick_values(101, 102), - np.arange(101, 102.01, 0.1)) + assert_almost_equal(lctr.tick_values(101, 102), np.arange(101, 102.01, 0.1)) def test_symmetrizing(self): - lctr = mticker.AsinhLocator(linear_width=1, numticks=3, - symthresh=0.25, base=0) + lctr = mticker.AsinhLocator(linear_width=1, numticks=3, symthresh=0.25, base=0) lctr.create_dummy_axis() lctr.axis.set_view_interval(-1, 2) @@ -737,17 +1069,45 @@ def test_symmetrizing(self): assert_almost_equal(lctr(), [1, 1.05, 1.1]) def test_base_rounding(self): - lctr10 = mticker.AsinhLocator(linear_width=1, numticks=8, - base=10, subs=(1, 3, 5)) - assert_almost_equal(lctr10.tick_values(-110, 110), - [-500, -300, -100, -50, -30, -10, -5, -3, -1, - -0.5, -0.3, -0.1, 0, 0.1, 0.3, 0.5, - 1, 3, 5, 10, 30, 50, 100, 300, 500]) + lctr10 = mticker.AsinhLocator( + linear_width=1, numticks=8, base=10, subs=(1, 3, 5) + ) + assert_almost_equal( + lctr10.tick_values(-110, 110), + [ + -500, + -300, + -100, + -50, + -30, + -10, + -5, + -3, + -1, + -0.5, + -0.3, + -0.1, + 0, + 0.1, + 0.3, + 0.5, + 1, + 3, + 5, + 10, + 30, + 50, + 100, + 300, + 500, + ], + ) lctr5 = mticker.AsinhLocator(linear_width=1, numticks=20, base=5) - assert_almost_equal(lctr5.tick_values(-1050, 1050), - [-625, -125, -25, -5, -1, -0.2, 0, - 0.2, 1, 5, 25, 125, 625]) + assert_almost_equal( + lctr5.tick_values(-1050, 1050), + [-625, -125, -25, -5, -1, -0.2, 0, 0.2, 1, 5, 25, 125, 625], + ) class TestScalarFormatter: @@ -765,14 +1125,14 @@ class TestScalarFormatter: (1, 1, 1), (123, 123, 0), # Test cases courtesy of @WeatherGod - (.4538, .4578, .45), + (0.4538, 0.4578, 0.45), (3789.12, 3783.1, 3780), (45124.3, 45831.75, 45000), (0.000721, 0.0007243, 0.00072), (12592.82, 12591.43, 12590), - (9., 12., 0), - (900., 1200., 0), - (1900., 1200., 0), + (9.0, 12.0, 0), + (900.0, 1200.0, 0), + (1900.0, 1200.0, 0), (0.99, 1.01, 1), (9.99, 10.01, 10), (99.99, 100.01, 100), @@ -795,62 +1155,70 @@ class TestScalarFormatter: (True, (-2, 2), (-20, 10), 0, False), (True, (-2, 2), (-110, 120), 2, False), (True, (-2, 2), (-120, 110), 2, False), - (True, (-2, 2), (-.001, 0.002), -3, False), + (True, (-2, 2), (-0.001, 0.002), -3, False), (True, (-7, 7), (0.18e10, 0.83e10), 9, True), (True, (0, 0), (-1e5, 1e5), 5, False), (True, (6, 6), (-1e5, 1e5), 6, False), ] cursor_data = [ - [0., "0.000"], + [0.0, "0.000"], [0.0123, "0.012"], [0.123, "0.123"], - [1.23, "1.230"], + [1.23, "1.230"], [12.3, "12.300"], ] format_data = [ - (.1, "1e-1"), - (.11, "1.1e-1"), + (0.1, "1e-1"), + (0.11, "1.1e-1"), (1e8, "1e8"), (1.1e8, "1.1e8"), ] - @pytest.mark.parametrize('unicode_minus, result', - [(True, "\N{MINUS SIGN}1"), (False, "-1")]) + @pytest.mark.parametrize( + "unicode_minus, result", [(True, "\N{MINUS SIGN}1"), (False, "-1")] + ) def test_unicode_minus(self, unicode_minus, result): - mpl.rcParams['axes.unicode_minus'] = unicode_minus + mpl.rcParams["axes.unicode_minus"] = unicode_minus assert ( plt.gca().xaxis.get_major_formatter().format_data_short(-1).strip() - == result) + == result + ) - @pytest.mark.parametrize('left, right, offset', offset_data) + @pytest.mark.parametrize("left, right, offset", offset_data) def test_offset_value(self, left, right, offset): fig, ax = plt.subplots() formatter = ax.xaxis.get_major_formatter() - with (pytest.warns(UserWarning, match='Attempting to set identical') - if left == right else nullcontext()): + with ( + pytest.warns(UserWarning, match="Attempting to set identical") + if left == right + else nullcontext() + ): ax.set_xlim(left, right) ax.xaxis._update_ticks() assert formatter.offset == offset - with (pytest.warns(UserWarning, match='Attempting to set identical') - if left == right else nullcontext()): + with ( + pytest.warns(UserWarning, match="Attempting to set identical") + if left == right + else nullcontext() + ): ax.set_xlim(right, left) ax.xaxis._update_ticks() assert formatter.offset == offset - @pytest.mark.parametrize('use_offset', use_offset_data) + @pytest.mark.parametrize("use_offset", use_offset_data) def test_use_offset(self, use_offset): - with mpl.rc_context({'axes.formatter.useoffset': use_offset}): + with mpl.rc_context({"axes.formatter.useoffset": use_offset}): tmp_form = mticker.ScalarFormatter() assert use_offset == tmp_form.get_useOffset() assert tmp_form.offset == 0 - @pytest.mark.parametrize('use_math_text', useMathText_data) + @pytest.mark.parametrize("use_math_text", useMathText_data) def test_useMathText(self, use_math_text): - with mpl.rc_context({'axes.formatter.use_mathtext': use_math_text}): + with mpl.rc_context({"axes.formatter.use_mathtext": use_math_text}): tmp_form = mticker.ScalarFormatter() assert use_math_text == tmp_form.get_useMathText() @@ -878,11 +1246,11 @@ def test_set_use_offset_int(self): def test_use_locale(self): conv = locale.localeconv() - sep = conv['thousands_sep'] - if not sep or conv['grouping'][-1:] in ([], [locale.CHAR_MAX]): - pytest.skip('Locale does not apply grouping') # pragma: no cover + sep = conv["thousands_sep"] + if not sep or conv["grouping"][-1:] in ([], [locale.CHAR_MAX]): + pytest.skip("Locale does not apply grouping") # pragma: no cover - with mpl.rc_context({'axes.formatter.use_locale': True}): + with mpl.rc_context({"axes.formatter.use_locale": True}): tmp_form = mticker.ScalarFormatter() assert tmp_form.get_useLocale() @@ -892,7 +1260,8 @@ def test_use_locale(self): assert sep in tmp_form(1e9) @pytest.mark.parametrize( - 'sci_type, scilimits, lim, orderOfMag, fewticks', scilimits_data) + "sci_type, scilimits, lim, orderOfMag, fewticks", scilimits_data + ) def test_scilimits(self, sci_type, scilimits, lim, orderOfMag, fewticks): tmp_form = mticker.ScalarFormatter() tmp_form.set_scientific(sci_type) @@ -906,20 +1275,20 @@ def test_scilimits(self, sci_type, scilimits, lim, orderOfMag, fewticks): tmp_form.set_locs(ax.yaxis.get_majorticklocs()) assert orderOfMag == tmp_form.orderOfMagnitude - @pytest.mark.parametrize('value, expected', format_data) + @pytest.mark.parametrize("value, expected", format_data) def test_format_data(self, value, expected): - mpl.rcParams['axes.unicode_minus'] = False + mpl.rcParams["axes.unicode_minus"] = False sf = mticker.ScalarFormatter() assert sf.format_data(value) == expected - @pytest.mark.parametrize('data, expected', cursor_data) + @pytest.mark.parametrize("data, expected", cursor_data) def test_cursor_precision(self, data, expected): fig, ax = plt.subplots() ax.set_xlim(-1, 1) # Pointing precision of 0.001. fmt = ax.xaxis.get_major_formatter().format_data_short assert fmt(data) == expected - @pytest.mark.parametrize('data, expected', cursor_data) + @pytest.mark.parametrize("data, expected", cursor_data) def test_cursor_dummy_axis(self, data, expected): # Issue #17624 sf = mticker.ScalarFormatter() @@ -931,36 +1300,42 @@ def test_cursor_dummy_axis(self, data, expected): assert sf.axis.get_minpos() == 0 def test_mathtext_ticks(self): - mpl.rcParams.update({ - 'font.family': 'serif', - 'font.serif': 'cmr10', - 'axes.formatter.use_mathtext': False - }) + mpl.rcParams.update( + { + "font.family": "serif", + "font.serif": "cmr10", + "axes.formatter.use_mathtext": False, + } + ) if parse_version(pytest.__version__).major < 8: - with pytest.warns(UserWarning, match='cmr10 font should ideally'): + with pytest.warns(UserWarning, match="cmr10 font should ideally"): fig, ax = plt.subplots() ax.set_xticks([-1, 0, 1]) fig.canvas.draw() else: - with (pytest.warns(UserWarning, match="Glyph 8722"), - pytest.warns(UserWarning, match='cmr10 font should ideally')): + with ( + pytest.warns(UserWarning, match="Glyph 8722"), + pytest.warns(UserWarning, match="cmr10 font should ideally"), + ): fig, ax = plt.subplots() ax.set_xticks([-1, 0, 1]) fig.canvas.draw() def test_cmr10_substitutions(self, caplog): - mpl.rcParams.update({ - 'font.family': 'cmr10', - 'mathtext.fontset': 'cm', - 'axes.formatter.use_mathtext': True, - }) + mpl.rcParams.update( + { + "font.family": "cmr10", + "mathtext.fontset": "cm", + "axes.formatter.use_mathtext": True, + } + ) # Test that it does not log a warning about missing glyphs. - with caplog.at_level(logging.WARNING, logger='matplotlib.mathtext'): + with caplog.at_level(logging.WARNING, logger="matplotlib.mathtext"): fig, ax = plt.subplots() ax.plot([-0.03, 0.05], [40, 0.05]) - ax.set_yscale('log') + ax.set_yscale("log") yticks = [0.02, 0.3, 4, 50] formatter = mticker.LogFormatterSciNotation() ax.set_yticks(yticks, map(formatter, yticks)) @@ -970,35 +1345,49 @@ def test_cmr10_substitutions(self, caplog): def test_empty_locs(self): sf = mticker.ScalarFormatter() sf.set_locs([]) - assert sf(0.5) == '' + assert sf(0.5) == "" class TestLogFormatterExponent: param_data = [ - (True, 4, np.arange(-3, 4.0), np.arange(-3, 4.0), - ['-3', '-2', '-1', '0', '1', '2', '3']), + ( + True, + 4, + np.arange(-3, 4.0), + np.arange(-3, 4.0), + ["-3", "-2", "-1", "0", "1", "2", "3"], + ), # With labelOnlyBase=False, non-integer powers should be nicely # formatted. - (False, 10, np.array([0.1, 0.00001, np.pi, 0.2, -0.2, -0.00001]), - range(6), ['0.1', '1e-05', '3.14', '0.2', '-0.2', '-1e-05']), - (False, 50, np.array([3, 5, 12, 42], dtype=float), range(6), - ['3', '5', '12', '42']), + ( + False, + 10, + np.array([0.1, 0.00001, np.pi, 0.2, -0.2, -0.00001]), + range(6), + ["0.1", "1e-05", "3.14", "0.2", "-0.2", "-1e-05"], + ), + ( + False, + 50, + np.array([3, 5, 12, 42], dtype=float), + range(6), + ["3", "5", "12", "42"], + ), ] base_data = [2.0, 5.0, 10.0, np.pi, np.e] @pytest.mark.parametrize( - 'labelOnlyBase, exponent, locs, positions, expected', param_data) - @pytest.mark.parametrize('base', base_data) - def test_basic(self, labelOnlyBase, base, exponent, locs, positions, - expected): - formatter = mticker.LogFormatterExponent(base=base, - labelOnlyBase=labelOnlyBase) + "labelOnlyBase, exponent, locs, positions, expected", param_data + ) + @pytest.mark.parametrize("base", base_data) + def test_basic(self, labelOnlyBase, base, exponent, locs, positions, expected): + formatter = mticker.LogFormatterExponent(base=base, labelOnlyBase=labelOnlyBase) formatter.create_dummy_axis() formatter.axis.set_view_interval(1, base**exponent) vals = base**locs labels = [formatter(x, pos) for (x, pos) in zip(vals, positions)] - expected = [label.replace('-', '\N{Minus Sign}') for label in expected] + expected = [label.replace("-", "\N{MINUS SIGN}") for label in expected] assert labels == expected def test_blank(self): @@ -1006,208 +1395,211 @@ def test_blank(self): formatter = mticker.LogFormatterExponent(base=10, labelOnlyBase=True) formatter.create_dummy_axis() formatter.axis.set_view_interval(1, 10) - assert formatter(10**0.1) == '' + assert formatter(10**0.1) == "" class TestLogFormatterMathtext: fmt = mticker.LogFormatterMathtext() test_data = [ - (0, 1, '$\\mathdefault{10^{0}}$'), - (0, 1e-2, '$\\mathdefault{10^{-2}}$'), - (0, 1e2, '$\\mathdefault{10^{2}}$'), - (3, 1, '$\\mathdefault{1}$'), - (3, 1e-2, '$\\mathdefault{0.01}$'), - (3, 1e2, '$\\mathdefault{100}$'), - (3, 1e-3, '$\\mathdefault{10^{-3}}$'), - (3, 1e3, '$\\mathdefault{10^{3}}$'), + (0, 1, "$\\mathdefault{10^{0}}$"), + (0, 1e-2, "$\\mathdefault{10^{-2}}$"), + (0, 1e2, "$\\mathdefault{10^{2}}$"), + (3, 1, "$\\mathdefault{1}$"), + (3, 1e-2, "$\\mathdefault{0.01}$"), + (3, 1e2, "$\\mathdefault{100}$"), + (3, 1e-3, "$\\mathdefault{10^{-3}}$"), + (3, 1e3, "$\\mathdefault{10^{3}}$"), ] - @pytest.mark.parametrize('min_exponent, value, expected', test_data) + @pytest.mark.parametrize("min_exponent, value, expected", test_data) def test_min_exponent(self, min_exponent, value, expected): - with mpl.rc_context({'axes.formatter.min_exponent': min_exponent}): + with mpl.rc_context({"axes.formatter.min_exponent": min_exponent}): assert self.fmt(value) == expected class TestLogFormatterSciNotation: test_data = [ - (2, 0.03125, '$\\mathdefault{2^{-5}}$'), - (2, 1, '$\\mathdefault{2^{0}}$'), - (2, 32, '$\\mathdefault{2^{5}}$'), - (2, 0.0375, '$\\mathdefault{1.2\\times2^{-5}}$'), - (2, 1.2, '$\\mathdefault{1.2\\times2^{0}}$'), - (2, 38.4, '$\\mathdefault{1.2\\times2^{5}}$'), - (10, -1, '$\\mathdefault{-10^{0}}$'), - (10, 1e-05, '$\\mathdefault{10^{-5}}$'), - (10, 1, '$\\mathdefault{10^{0}}$'), - (10, 100000, '$\\mathdefault{10^{5}}$'), - (10, 2e-05, '$\\mathdefault{2\\times10^{-5}}$'), - (10, 2, '$\\mathdefault{2\\times10^{0}}$'), - (10, 200000, '$\\mathdefault{2\\times10^{5}}$'), - (10, 5e-05, '$\\mathdefault{5\\times10^{-5}}$'), - (10, 5, '$\\mathdefault{5\\times10^{0}}$'), - (10, 500000, '$\\mathdefault{5\\times10^{5}}$'), + (2, 0.03125, "$\\mathdefault{2^{-5}}$"), + (2, 1, "$\\mathdefault{2^{0}}$"), + (2, 32, "$\\mathdefault{2^{5}}$"), + (2, 0.0375, "$\\mathdefault{1.2\\times2^{-5}}$"), + (2, 1.2, "$\\mathdefault{1.2\\times2^{0}}$"), + (2, 38.4, "$\\mathdefault{1.2\\times2^{5}}$"), + (10, -1, "$\\mathdefault{-10^{0}}$"), + (10, 1e-05, "$\\mathdefault{10^{-5}}$"), + (10, 1, "$\\mathdefault{10^{0}}$"), + (10, 100000, "$\\mathdefault{10^{5}}$"), + (10, 2e-05, "$\\mathdefault{2\\times10^{-5}}$"), + (10, 2, "$\\mathdefault{2\\times10^{0}}$"), + (10, 200000, "$\\mathdefault{2\\times10^{5}}$"), + (10, 5e-05, "$\\mathdefault{5\\times10^{-5}}$"), + (10, 5, "$\\mathdefault{5\\times10^{0}}$"), + (10, 500000, "$\\mathdefault{5\\times10^{5}}$"), ] - @mpl.style.context('default') - @pytest.mark.parametrize('base, value, expected', test_data) + @mpl.style.context("default") + @pytest.mark.parametrize("base, value, expected", test_data) def test_basic(self, base, value, expected): formatter = mticker.LogFormatterSciNotation(base=base) - with mpl.rc_context({'text.usetex': False}): + with mpl.rc_context({"text.usetex": False}): assert formatter(value) == expected class TestLogFormatter: pprint_data = [ - (3.141592654e-05, 0.001, '3.142e-5'), - (0.0003141592654, 0.001, '3.142e-4'), - (0.003141592654, 0.001, '3.142e-3'), - (0.03141592654, 0.001, '3.142e-2'), - (0.3141592654, 0.001, '3.142e-1'), - (3.141592654, 0.001, '3.142'), - (31.41592654, 0.001, '3.142e1'), - (314.1592654, 0.001, '3.142e2'), - (3141.592654, 0.001, '3.142e3'), - (31415.92654, 0.001, '3.142e4'), - (314159.2654, 0.001, '3.142e5'), - (1e-05, 0.001, '1e-5'), - (0.0001, 0.001, '1e-4'), - (0.001, 0.001, '1e-3'), - (0.01, 0.001, '1e-2'), - (0.1, 0.001, '1e-1'), - (1, 0.001, '1'), - (10, 0.001, '10'), - (100, 0.001, '100'), - (1000, 0.001, '1000'), - (10000, 0.001, '1e4'), - (100000, 0.001, '1e5'), - (3.141592654e-05, 0.015, '0'), - (0.0003141592654, 0.015, '0'), - (0.003141592654, 0.015, '0.003'), - (0.03141592654, 0.015, '0.031'), - (0.3141592654, 0.015, '0.314'), - (3.141592654, 0.015, '3.142'), - (31.41592654, 0.015, '31.416'), - (314.1592654, 0.015, '314.159'), - (3141.592654, 0.015, '3141.593'), - (31415.92654, 0.015, '31415.927'), - (314159.2654, 0.015, '314159.265'), - (1e-05, 0.015, '0'), - (0.0001, 0.015, '0'), - (0.001, 0.015, '0.001'), - (0.01, 0.015, '0.01'), - (0.1, 0.015, '0.1'), - (1, 0.015, '1'), - (10, 0.015, '10'), - (100, 0.015, '100'), - (1000, 0.015, '1000'), - (10000, 0.015, '10000'), - (100000, 0.015, '100000'), - (3.141592654e-05, 0.5, '0'), - (0.0003141592654, 0.5, '0'), - (0.003141592654, 0.5, '0.003'), - (0.03141592654, 0.5, '0.031'), - (0.3141592654, 0.5, '0.314'), - (3.141592654, 0.5, '3.142'), - (31.41592654, 0.5, '31.416'), - (314.1592654, 0.5, '314.159'), - (3141.592654, 0.5, '3141.593'), - (31415.92654, 0.5, '31415.927'), - (314159.2654, 0.5, '314159.265'), - (1e-05, 0.5, '0'), - (0.0001, 0.5, '0'), - (0.001, 0.5, '0.001'), - (0.01, 0.5, '0.01'), - (0.1, 0.5, '0.1'), - (1, 0.5, '1'), - (10, 0.5, '10'), - (100, 0.5, '100'), - (1000, 0.5, '1000'), - (10000, 0.5, '10000'), - (100000, 0.5, '100000'), - (3.141592654e-05, 5, '0'), - (0.0003141592654, 5, '0'), - (0.003141592654, 5, '0'), - (0.03141592654, 5, '0.03'), - (0.3141592654, 5, '0.31'), - (3.141592654, 5, '3.14'), - (31.41592654, 5, '31.42'), - (314.1592654, 5, '314.16'), - (3141.592654, 5, '3141.59'), - (31415.92654, 5, '31415.93'), - (314159.2654, 5, '314159.27'), - (1e-05, 5, '0'), - (0.0001, 5, '0'), - (0.001, 5, '0'), - (0.01, 5, '0.01'), - (0.1, 5, '0.1'), - (1, 5, '1'), - (10, 5, '10'), - (100, 5, '100'), - (1000, 5, '1000'), - (10000, 5, '10000'), - (100000, 5, '100000'), - (3.141592654e-05, 100, '0'), - (0.0003141592654, 100, '0'), - (0.003141592654, 100, '0'), - (0.03141592654, 100, '0'), - (0.3141592654, 100, '0.3'), - (3.141592654, 100, '3.1'), - (31.41592654, 100, '31.4'), - (314.1592654, 100, '314.2'), - (3141.592654, 100, '3141.6'), - (31415.92654, 100, '31415.9'), - (314159.2654, 100, '314159.3'), - (1e-05, 100, '0'), - (0.0001, 100, '0'), - (0.001, 100, '0'), - (0.01, 100, '0'), - (0.1, 100, '0.1'), - (1, 100, '1'), - (10, 100, '10'), - (100, 100, '100'), - (1000, 100, '1000'), - (10000, 100, '10000'), - (100000, 100, '100000'), - (3.141592654e-05, 1000000.0, '3.1e-5'), - (0.0003141592654, 1000000.0, '3.1e-4'), - (0.003141592654, 1000000.0, '3.1e-3'), - (0.03141592654, 1000000.0, '3.1e-2'), - (0.3141592654, 1000000.0, '3.1e-1'), - (3.141592654, 1000000.0, '3.1'), - (31.41592654, 1000000.0, '3.1e1'), - (314.1592654, 1000000.0, '3.1e2'), - (3141.592654, 1000000.0, '3.1e3'), - (31415.92654, 1000000.0, '3.1e4'), - (314159.2654, 1000000.0, '3.1e5'), - (1e-05, 1000000.0, '1e-5'), - (0.0001, 1000000.0, '1e-4'), - (0.001, 1000000.0, '1e-3'), - (0.01, 1000000.0, '1e-2'), - (0.1, 1000000.0, '1e-1'), - (1, 1000000.0, '1'), - (10, 1000000.0, '10'), - (100, 1000000.0, '100'), - (1000, 1000000.0, '1000'), - (10000, 1000000.0, '1e4'), - (100000, 1000000.0, '1e5'), + (3.141592654e-05, 0.001, "3.142e-5"), + (0.0003141592654, 0.001, "3.142e-4"), + (0.003141592654, 0.001, "3.142e-3"), + (0.03141592654, 0.001, "3.142e-2"), + (0.3141592654, 0.001, "3.142e-1"), + (3.141592654, 0.001, "3.142"), + (31.41592654, 0.001, "3.142e1"), + (314.1592654, 0.001, "3.142e2"), + (3141.592654, 0.001, "3.142e3"), + (31415.92654, 0.001, "3.142e4"), + (314159.2654, 0.001, "3.142e5"), + (1e-05, 0.001, "1e-5"), + (0.0001, 0.001, "1e-4"), + (0.001, 0.001, "1e-3"), + (0.01, 0.001, "1e-2"), + (0.1, 0.001, "1e-1"), + (1, 0.001, "1"), + (10, 0.001, "10"), + (100, 0.001, "100"), + (1000, 0.001, "1000"), + (10000, 0.001, "1e4"), + (100000, 0.001, "1e5"), + (3.141592654e-05, 0.015, "0"), + (0.0003141592654, 0.015, "0"), + (0.003141592654, 0.015, "0.003"), + (0.03141592654, 0.015, "0.031"), + (0.3141592654, 0.015, "0.314"), + (3.141592654, 0.015, "3.142"), + (31.41592654, 0.015, "31.416"), + (314.1592654, 0.015, "314.159"), + (3141.592654, 0.015, "3141.593"), + (31415.92654, 0.015, "31415.927"), + (314159.2654, 0.015, "314159.265"), + (1e-05, 0.015, "0"), + (0.0001, 0.015, "0"), + (0.001, 0.015, "0.001"), + (0.01, 0.015, "0.01"), + (0.1, 0.015, "0.1"), + (1, 0.015, "1"), + (10, 0.015, "10"), + (100, 0.015, "100"), + (1000, 0.015, "1000"), + (10000, 0.015, "10000"), + (100000, 0.015, "100000"), + (3.141592654e-05, 0.5, "0"), + (0.0003141592654, 0.5, "0"), + (0.003141592654, 0.5, "0.003"), + (0.03141592654, 0.5, "0.031"), + (0.3141592654, 0.5, "0.314"), + (3.141592654, 0.5, "3.142"), + (31.41592654, 0.5, "31.416"), + (314.1592654, 0.5, "314.159"), + (3141.592654, 0.5, "3141.593"), + (31415.92654, 0.5, "31415.927"), + (314159.2654, 0.5, "314159.265"), + (1e-05, 0.5, "0"), + (0.0001, 0.5, "0"), + (0.001, 0.5, "0.001"), + (0.01, 0.5, "0.01"), + (0.1, 0.5, "0.1"), + (1, 0.5, "1"), + (10, 0.5, "10"), + (100, 0.5, "100"), + (1000, 0.5, "1000"), + (10000, 0.5, "10000"), + (100000, 0.5, "100000"), + (3.141592654e-05, 5, "0"), + (0.0003141592654, 5, "0"), + (0.003141592654, 5, "0"), + (0.03141592654, 5, "0.03"), + (0.3141592654, 5, "0.31"), + (3.141592654, 5, "3.14"), + (31.41592654, 5, "31.42"), + (314.1592654, 5, "314.16"), + (3141.592654, 5, "3141.59"), + (31415.92654, 5, "31415.93"), + (314159.2654, 5, "314159.27"), + (1e-05, 5, "0"), + (0.0001, 5, "0"), + (0.001, 5, "0"), + (0.01, 5, "0.01"), + (0.1, 5, "0.1"), + (1, 5, "1"), + (10, 5, "10"), + (100, 5, "100"), + (1000, 5, "1000"), + (10000, 5, "10000"), + (100000, 5, "100000"), + (3.141592654e-05, 100, "0"), + (0.0003141592654, 100, "0"), + (0.003141592654, 100, "0"), + (0.03141592654, 100, "0"), + (0.3141592654, 100, "0.3"), + (3.141592654, 100, "3.1"), + (31.41592654, 100, "31.4"), + (314.1592654, 100, "314.2"), + (3141.592654, 100, "3141.6"), + (31415.92654, 100, "31415.9"), + (314159.2654, 100, "314159.3"), + (1e-05, 100, "0"), + (0.0001, 100, "0"), + (0.001, 100, "0"), + (0.01, 100, "0"), + (0.1, 100, "0.1"), + (1, 100, "1"), + (10, 100, "10"), + (100, 100, "100"), + (1000, 100, "1000"), + (10000, 100, "10000"), + (100000, 100, "100000"), + (3.141592654e-05, 1000000.0, "3.1e-5"), + (0.0003141592654, 1000000.0, "3.1e-4"), + (0.003141592654, 1000000.0, "3.1e-3"), + (0.03141592654, 1000000.0, "3.1e-2"), + (0.3141592654, 1000000.0, "3.1e-1"), + (3.141592654, 1000000.0, "3.1"), + (31.41592654, 1000000.0, "3.1e1"), + (314.1592654, 1000000.0, "3.1e2"), + (3141.592654, 1000000.0, "3.1e3"), + (31415.92654, 1000000.0, "3.1e4"), + (314159.2654, 1000000.0, "3.1e5"), + (1e-05, 1000000.0, "1e-5"), + (0.0001, 1000000.0, "1e-4"), + (0.001, 1000000.0, "1e-3"), + (0.01, 1000000.0, "1e-2"), + (0.1, 1000000.0, "1e-1"), + (1, 1000000.0, "1"), + (10, 1000000.0, "10"), + (100, 1000000.0, "100"), + (1000, 1000000.0, "1000"), + (10000, 1000000.0, "1e4"), + (100000, 1000000.0, "1e5"), ] - @pytest.mark.parametrize('value, domain, expected', pprint_data) + @pytest.mark.parametrize("value, domain, expected", pprint_data) def test_pprint(self, value, domain, expected): fmt = mticker.LogFormatter() label = fmt._pprint_val(value, domain) assert label == expected - @pytest.mark.parametrize('value, long, short', [ - (0.0, "0", "0"), - (0, "0", "0"), - (-1.0, "-10^0", "-1"), - (2e-10, "2x10^-10", "2e-10"), - (1e10, "10^10", "1e+10"), - ]) + @pytest.mark.parametrize( + "value, long, short", + [ + (0.0, "0", "0"), + (0, "0", "0"), + (-1.0, "-10^0", "-1"), + (2e-10, "2x10^-10", "2e-10"), + (1e10, "10^10", "1e+10"), + ], + ) def test_format_data(self, value, long, short): fig, ax = plt.subplots() - ax.set_xscale('log') + ax.set_xscale("log") fmt = ax.xaxis.get_major_formatter() assert fmt.format_data(value) == long assert fmt.format_data_short(value) == short @@ -1217,27 +1609,25 @@ def _sub_labels(self, axis, subs=()): fmt = axis.get_minor_formatter() minor_tlocs = axis.get_minorticklocs() fmt.set_locs(minor_tlocs) - coefs = minor_tlocs / 10**(np.floor(np.log10(minor_tlocs))) + coefs = minor_tlocs / 10 ** (np.floor(np.log10(minor_tlocs))) label_expected = [round(c) in subs for c in coefs] - label_test = [fmt(x) != '' for x in minor_tlocs] + label_test = [fmt(x) != "" for x in minor_tlocs] assert label_test == label_expected - @mpl.style.context('default') + @mpl.style.context("default") def test_sublabel(self): # test label locator fig, ax = plt.subplots() - ax.set_xscale('log') + ax.set_xscale("log") ax.xaxis.set_major_locator(mticker.LogLocator(base=10, subs=[])) - ax.xaxis.set_minor_locator(mticker.LogLocator(base=10, - subs=np.arange(2, 10))) + ax.xaxis.set_minor_locator(mticker.LogLocator(base=10, subs=np.arange(2, 10))) ax.xaxis.set_major_formatter(mticker.LogFormatter(labelOnlyBase=True)) ax.xaxis.set_minor_formatter(mticker.LogFormatter(labelOnlyBase=False)) # axis range above 3 decades, only bases are labeled ax.set_xlim(1, 1e4) fmt = ax.xaxis.get_major_formatter() fmt.set_locs(ax.xaxis.get_majorticklocs()) - show_major_labels = [fmt(x) != '' - for x in ax.xaxis.get_majorticklocs()] + show_major_labels = [fmt(x) != "" for x in ax.xaxis.get_majorticklocs()] assert np.all(show_major_labels) self._sub_labels(ax.xaxis, subs=[]) @@ -1254,7 +1644,7 @@ def test_sublabel(self): # axis range slightly more than 1 decade, but spanning a single major # tick, label subs 2, 3, 4, 6 - ax.set_xlim(.8, 9) + ax.set_xlim(0.8, 9) self._sub_labels(ax.xaxis, subs=[2, 3, 4, 6]) # axis range at 0.4 to 1 decade, label subs 2, 3, 4, 6 @@ -1265,7 +1655,7 @@ def test_sublabel(self): ax.set_xlim(0.5, 0.9) self._sub_labels(ax.xaxis, subs=np.arange(2, 10, dtype=int)) - @pytest.mark.parametrize('val', [1, 10, 100, 1000]) + @pytest.mark.parametrize("val", [1, 10, 100, 1000]) def test_LogFormatter_call(self, val): # test _num_to_string method used in __call__ temp_lf = mticker.LogFormatter() @@ -1273,7 +1663,7 @@ def test_LogFormatter_call(self, val): temp_lf.axis.set_view_interval(1, 10) assert temp_lf(val) == str(val) - @pytest.mark.parametrize('val', [1e-323, 2e-323, 10e-323, 11e-323]) + @pytest.mark.parametrize("val", [1e-323, 2e-323, 10e-323, 11e-323]) def test_LogFormatter_call_tiny(self, val): # test coeff computation in __call__ temp_lf = mticker.LogFormatter() @@ -1303,7 +1693,7 @@ def logit_deformatter(string): comp = match["comp"] is not None mantissa = float(match["mant"]) if match["mant"] else 1 expo = int(match["expo"]) if match["expo"] is not None else 0 - value = mantissa * 10 ** expo + value = mantissa * 10**expo if match["mant"] or match["expo"] is not None: if comp: return 1 - value @@ -1375,10 +1765,9 @@ def test_variablelength(self, x): formatter.set_locs([x - 1 / N, x, x + 1 / N]) sx = formatter(x) sx1 = formatter(x + 1 / N) - d = ( - TestLogitFormatter.logit_deformatter(sx1) - - TestLogitFormatter.logit_deformatter(sx) - ) + d = TestLogitFormatter.logit_deformatter( + sx1 + ) - TestLogitFormatter.logit_deformatter(sx) assert 0 < d < 2 / N lims_minor_major = [ @@ -1441,11 +1830,11 @@ def test_one_half(self): Test the parameter one_half """ form = mticker.LogitFormatter() - assert r"\frac{1}{2}" in form(1/2) + assert r"\frac{1}{2}" in form(1 / 2) form.set_one_half("1/2") - assert "1/2" in form(1/2) + assert "1/2" in form(1 / 2) form.set_one_half("one half") - assert "one half" in form(1/2) + assert "one half" in form(1 / 2) @pytest.mark.parametrize("N", (100, 253, 754)) def test_format_data_short(self, N): @@ -1463,23 +1852,23 @@ def test_format_data_short(self, N): class TestFormatStrFormatter: def test_basic(self): # test % style formatter - tmp_form = mticker.FormatStrFormatter('%05d') - assert '00002' == tmp_form(2) + tmp_form = mticker.FormatStrFormatter("%05d") + assert "00002" == tmp_form(2) class TestStrMethodFormatter: test_data = [ - ('{x:05d}', (2,), False, '00002'), - ('{x:05d}', (2,), True, '00002'), - ('{x:05d}', (-2,), False, '-0002'), - ('{x:05d}', (-2,), True, '\N{MINUS SIGN}0002'), - ('{x:03d}-{pos:02d}', (2, 1), False, '002-01'), - ('{x:03d}-{pos:02d}', (2, 1), True, '002-01'), - ('{x:03d}-{pos:02d}', (-2, 1), False, '-02-01'), - ('{x:03d}-{pos:02d}', (-2, 1), True, '\N{MINUS SIGN}02-01'), + ("{x:05d}", (2,), False, "00002"), + ("{x:05d}", (2,), True, "00002"), + ("{x:05d}", (-2,), False, "-0002"), + ("{x:05d}", (-2,), True, "\N{MINUS SIGN}0002"), + ("{x:03d}-{pos:02d}", (2, 1), False, "002-01"), + ("{x:03d}-{pos:02d}", (2, 1), True, "002-01"), + ("{x:03d}-{pos:02d}", (-2, 1), False, "-02-01"), + ("{x:03d}-{pos:02d}", (-2, 1), True, "\N{MINUS SIGN}02-01"), ] - @pytest.mark.parametrize('format, input, unicode_minus, expected', test_data) + @pytest.mark.parametrize("format, input, unicode_minus, expected", test_data) def test_basic(self, format, input, unicode_minus, expected): with mpl.rc_context({"axes.unicode_minus": unicode_minus}): fmt = mticker.StrMethodFormatter(format) @@ -1491,43 +1880,62 @@ class TestEngFormatter: # outputs respectively returned when (places=None, places=0, places=2) # unicode_minus is a boolean value for the rcParam['axes.unicode_minus'] raw_format_data = [ - (False, -1234.56789, ('-1.23457 k', '-1 k', '-1.23 k')), - (True, -1234.56789, ('\N{MINUS SIGN}1.23457 k', '\N{MINUS SIGN}1 k', - '\N{MINUS SIGN}1.23 k')), - (False, -1.23456789, ('-1.23457', '-1', '-1.23')), - (True, -1.23456789, ('\N{MINUS SIGN}1.23457', '\N{MINUS SIGN}1', - '\N{MINUS SIGN}1.23')), - (False, -0.123456789, ('-123.457 m', '-123 m', '-123.46 m')), - (True, -0.123456789, ('\N{MINUS SIGN}123.457 m', '\N{MINUS SIGN}123 m', - '\N{MINUS SIGN}123.46 m')), - (False, -0.00123456789, ('-1.23457 m', '-1 m', '-1.23 m')), - (True, -0.00123456789, ('\N{MINUS SIGN}1.23457 m', '\N{MINUS SIGN}1 m', - '\N{MINUS SIGN}1.23 m')), - (True, -0.0, ('0', '0', '0.00')), - (True, -0, ('0', '0', '0.00')), - (True, 0, ('0', '0', '0.00')), - (True, 1.23456789e-6, ('1.23457 µ', '1 µ', '1.23 µ')), - (True, 0.123456789, ('123.457 m', '123 m', '123.46 m')), - (True, 0.1, ('100 m', '100 m', '100.00 m')), - (True, 1, ('1', '1', '1.00')), - (True, 1.23456789, ('1.23457', '1', '1.23')), + (False, -1234.56789, ("-1.23457 k", "-1 k", "-1.23 k")), + ( + True, + -1234.56789, + ("\N{MINUS SIGN}1.23457 k", "\N{MINUS SIGN}1 k", "\N{MINUS SIGN}1.23 k"), + ), + (False, -1.23456789, ("-1.23457", "-1", "-1.23")), + ( + True, + -1.23456789, + ("\N{MINUS SIGN}1.23457", "\N{MINUS SIGN}1", "\N{MINUS SIGN}1.23"), + ), + (False, -0.123456789, ("-123.457 m", "-123 m", "-123.46 m")), + ( + True, + -0.123456789, + ( + "\N{MINUS SIGN}123.457 m", + "\N{MINUS SIGN}123 m", + "\N{MINUS SIGN}123.46 m", + ), + ), + (False, -0.00123456789, ("-1.23457 m", "-1 m", "-1.23 m")), + ( + True, + -0.00123456789, + ("\N{MINUS SIGN}1.23457 m", "\N{MINUS SIGN}1 m", "\N{MINUS SIGN}1.23 m"), + ), + (True, -0.0, ("0", "0", "0.00")), + (True, -0, ("0", "0", "0.00")), + (True, 0, ("0", "0", "0.00")), + (True, 1.23456789e-6, ("1.23457 µ", "1 µ", "1.23 µ")), + (True, 0.123456789, ("123.457 m", "123 m", "123.46 m")), + (True, 0.1, ("100 m", "100 m", "100.00 m")), + (True, 1, ("1", "1", "1.00")), + (True, 1.23456789, ("1.23457", "1", "1.23")), # places=0: corner-case rounding - (True, 999.9, ('999.9', '1 k', '999.90')), + (True, 999.9, ("999.9", "1 k", "999.90")), # corner-case rounding for all - (True, 999.9999, ('1 k', '1 k', '1.00 k')), + (True, 999.9999, ("1 k", "1 k", "1.00 k")), # negative corner-case - (False, -999.9999, ('-1 k', '-1 k', '-1.00 k')), - (True, -999.9999, ('\N{MINUS SIGN}1 k', '\N{MINUS SIGN}1 k', - '\N{MINUS SIGN}1.00 k')), - (True, 1000, ('1 k', '1 k', '1.00 k')), - (True, 1001, ('1.001 k', '1 k', '1.00 k')), - (True, 100001, ('100.001 k', '100 k', '100.00 k')), - (True, 987654.321, ('987.654 k', '988 k', '987.65 k')), + (False, -999.9999, ("-1 k", "-1 k", "-1.00 k")), + ( + True, + -999.9999, + ("\N{MINUS SIGN}1 k", "\N{MINUS SIGN}1 k", "\N{MINUS SIGN}1.00 k"), + ), + (True, 1000, ("1 k", "1 k", "1.00 k")), + (True, 1001, ("1.001 k", "1 k", "1.00 k")), + (True, 100001, ("100.001 k", "100 k", "100.00 k")), + (True, 987654.321, ("987.654 k", "988 k", "987.65 k")), # OoR value (> 1000 Q) - (True, 1.23e33, ('1230 Q', '1230 Q', '1230.00 Q')) + (True, 1.23e33, ("1230 Q", "1230 Q", "1230.00 Q")), ] - @pytest.mark.parametrize('unicode_minus, input, expected', raw_format_data) + @pytest.mark.parametrize("unicode_minus, input, expected", raw_format_data) def test_params(self, unicode_minus, input, expected): """ Test the formatting of EngFormatter for various values of the 'places' @@ -1541,9 +1949,9 @@ def test_params(self, unicode_minus, input, expected): Note that cases 2. and 3. are looped over several separator strings. """ - plt.rcParams['axes.unicode_minus'] = unicode_minus - UNIT = 's' # seconds - DIGITS = '0123456789' # %timeit showed 10-20% faster search than set + plt.rcParams["axes.unicode_minus"] = unicode_minus + UNIT = "s" # seconds + DIGITS = "0123456789" # %timeit showed 10-20% faster search than set # Case 0: unit='' (default) and sep=' ' (default). # 'expected' already corresponds to this reference case. @@ -1551,7 +1959,7 @@ def test_params(self, unicode_minus, input, expected): formatters = ( mticker.EngFormatter(), # places=None (default) mticker.EngFormatter(places=0), - mticker.EngFormatter(places=2) + mticker.EngFormatter(places=2), ) for _formatter, _exp_output in zip(formatters, exp_outputs): assert _formatter(input) == _exp_output @@ -1559,12 +1967,14 @@ def test_params(self, unicode_minus, input, expected): # Case 1: unit=UNIT and sep=' ' (default). # Append a unit symbol to the reference case. # Beware of the values in [1, 1000), where there is no prefix! - exp_outputs = (_s + " " + UNIT if _s[-1] in DIGITS # case w/o prefix - else _s + UNIT for _s in expected) + exp_outputs = ( + _s + " " + UNIT if _s[-1] in DIGITS else _s + UNIT # case w/o prefix + for _s in expected + ) formatters = ( mticker.EngFormatter(unit=UNIT), # places=None (default) mticker.EngFormatter(unit=UNIT, places=0), - mticker.EngFormatter(unit=UNIT, places=2) + mticker.EngFormatter(unit=UNIT, places=2), ) for _formatter, _exp_output in zip(formatters, exp_outputs): assert _formatter(input) == _exp_output @@ -1575,13 +1985,18 @@ def test_params(self, unicode_minus, input, expected): # Case 2: unit=UNIT and sep=_sep. # Replace the default space separator from the reference case # with the tested one `_sep` and append a unit symbol to it. - exp_outputs = (_s + _sep + UNIT if _s[-1] in DIGITS # no prefix - else _s.replace(" ", _sep) + UNIT - for _s in expected) + exp_outputs = ( + ( + _s + _sep + UNIT + if _s[-1] in DIGITS # no prefix + else _s.replace(" ", _sep) + UNIT + ) + for _s in expected + ) formatters = ( mticker.EngFormatter(unit=UNIT, sep=_sep), # places=None mticker.EngFormatter(unit=UNIT, places=0, sep=_sep), - mticker.EngFormatter(unit=UNIT, places=2, sep=_sep) + mticker.EngFormatter(unit=UNIT, places=2, sep=_sep), ) for _formatter, _exp_output in zip(formatters, exp_outputs): assert _formatter(input) == _exp_output @@ -1593,7 +2008,7 @@ def test_params(self, unicode_minus, input, expected): formatters = ( mticker.EngFormatter(sep=_sep), # places=None (default) mticker.EngFormatter(places=0, sep=_sep), - mticker.EngFormatter(places=2, sep=_sep) + mticker.EngFormatter(places=2, sep=_sep), ) for _formatter, _exp_output in zip(formatters, exp_outputs): assert _formatter(input) == _exp_output @@ -1603,49 +2018,49 @@ def test_engformatter_usetex_useMathText(): fig, ax = plt.subplots() ax.plot([0, 500, 1000], [0, 500, 1000]) ax.set_xticks([0, 500, 1000]) - for formatter in (mticker.EngFormatter(usetex=True), - mticker.EngFormatter(useMathText=True)): + for formatter in ( + mticker.EngFormatter(usetex=True), + mticker.EngFormatter(useMathText=True), + ): ax.xaxis.set_major_formatter(formatter) fig.canvas.draw() x_tick_label_text = [labl.get_text() for labl in ax.get_xticklabels()] # Checking if the dollar `$` signs have been inserted around numbers # in tick labels. - assert x_tick_label_text == ['$0$', '$500$', '$1$ k'] + assert x_tick_label_text == ["$0$", "$500$", "$1$ k"] @pytest.mark.parametrize( - 'data_offset, noise, oom_center_desired, oom_noise_desired', [ - (271_490_000_000.0, 10, 9, 0), + "data_offset, noise, oom_center_desired, oom_noise_desired", + [ + (271_490_000_000.0, 10, 9, 0), (27_149_000_000_000.0, 10_000_000, 12, 6), - (27.149, 0.01, 0, -3), - (2_714.9, 0.01, 3, -3), - (271_490.0, 0.001, 3, -3), - (271.49, 0.001, 0, -3), + (27.149, 0.01, 0, -3), + (2_714.9, 0.01, 3, -3), + (271_490.0, 0.001, 3, -3), + (271.49, 0.001, 0, -3), # The following sets of parameters demonstrates that when # oom(data_offset)-1 and oom(noise)-2 equal a standard 3*N oom, we get # that oom_noise_desired < oom(noise) - (27_149_000_000.0, 100, 9, +3), - (27.149, 1e-07, 0, -6), - (271.49, 0.0001, 0, -3), - (27.149, 0.0001, 0, -3), + (27_149_000_000.0, 100, 9, +3), + (27.149, 1e-07, 0, -6), + (271.49, 0.0001, 0, -3), + (27.149, 0.0001, 0, -3), # Tests where oom(data_offset) <= oom(noise), those are probably # covered by the part where formatter.offset != 0 - (27_149.0, 10_000, 0, 3), - (27.149, 10_000, 0, 3), - (27.149, 1_000, 0, 3), - (27.149, 100, 0, 0), - (27.149, 10, 0, 0), - ] + (27_149.0, 10_000, 0, 3), + (27.149, 10_000, 0, 3), + (27.149, 1_000, 0, 3), + (27.149, 100, 0, 0), + (27.149, 10, 0, 0), + ], ) def test_engformatter_offset_oom( - data_offset, - noise, - oom_center_desired, - oom_noise_desired + data_offset, noise, oom_center_desired, oom_noise_desired ): UNIT = "eV" fig, ax = plt.subplots() - ydata = data_offset + np.arange(-5, 7, dtype=float)*noise + ydata = data_offset + np.arange(-5, 7, dtype=float) * noise ax.plot(ydata) formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) # So that offset strings will always have the same size @@ -1662,7 +2077,7 @@ def test_engformatter_offset_oom( if formatter.offset: prefix_noise_got = offset_got[2] prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] - prefix_center_got = offset_got[-1-len(UNIT)] + prefix_center_got = offset_got[-1 - len(UNIT)] prefix_center_desired = formatter.ENG_PREFIXES[oom_center_desired] assert prefix_noise_desired == prefix_noise_got assert prefix_center_desired == prefix_center_got @@ -1683,91 +2098,92 @@ def test_engformatter_offset_oom( class TestPercentFormatter: percent_data = [ # Check explicitly set decimals over different intervals and values - (100, 0, '%', 120, 100, '120%'), - (100, 0, '%', 100, 90, '100%'), - (100, 0, '%', 90, 50, '90%'), - (100, 0, '%', -1.7, 40, '-2%'), - (100, 1, '%', 90.0, 100, '90.0%'), - (100, 1, '%', 80.1, 90, '80.1%'), - (100, 1, '%', 70.23, 50, '70.2%'), + (100, 0, "%", 120, 100, "120%"), + (100, 0, "%", 100, 90, "100%"), + (100, 0, "%", 90, 50, "90%"), + (100, 0, "%", -1.7, 40, "-2%"), + (100, 1, "%", 90.0, 100, "90.0%"), + (100, 1, "%", 80.1, 90, "80.1%"), + (100, 1, "%", 70.23, 50, "70.2%"), # 60.554 instead of 60.55: see https://bugs.python.org/issue5118 - (100, 1, '%', -60.554, 40, '-60.6%'), + (100, 1, "%", -60.554, 40, "-60.6%"), # Check auto decimals over different intervals and values - (100, None, '%', 95, 1, '95.00%'), - (1.0, None, '%', 3, 6, '300%'), - (17.0, None, '%', 1, 8.5, '6%'), - (17.0, None, '%', 1, 8.4, '5.9%'), - (5, None, '%', -100, 0.000001, '-2000.00000%'), + (100, None, "%", 95, 1, "95.00%"), + (1.0, None, "%", 3, 6, "300%"), + (17.0, None, "%", 1, 8.5, "6%"), + (17.0, None, "%", 1, 8.4, "5.9%"), + (5, None, "%", -100, 0.000001, "-2000.00000%"), # Check percent symbol - (1.0, 2, None, 1.2, 100, '120.00'), - (75, 3, '', 50, 100, '66.667'), - (42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'), + (1.0, 2, None, 1.2, 100, "120.00"), + (75, 3, "", 50, 100, "66.667"), + (42, None, "^^Foobar$$", 21, 12, "50.0^^Foobar$$"), ] percent_ids = [ # Check explicitly set decimals over different intervals and values - 'decimals=0, x>100%', - 'decimals=0, x=100%', - 'decimals=0, x<100%', - 'decimals=0, x<0%', - 'decimals=1, x>100%', - 'decimals=1, x=100%', - 'decimals=1, x<100%', - 'decimals=1, x<0%', + "decimals=0, x>100%", + "decimals=0, x=100%", + "decimals=0, x<100%", + "decimals=0, x<0%", + "decimals=1, x>100%", + "decimals=1, x=100%", + "decimals=1, x<100%", + "decimals=1, x<0%", # Check auto decimals over different intervals and values - 'autodecimal, x<100%, display_range=1', - 'autodecimal, x>100%, display_range=6 (custom xmax test)', - 'autodecimal, x<100%, display_range=8.5 (autodecimal test 1)', - 'autodecimal, x<100%, display_range=8.4 (autodecimal test 2)', - 'autodecimal, x<-100%, display_range=1e-6 (tiny display range)', + "autodecimal, x<100%, display_range=1", + "autodecimal, x>100%, display_range=6 (custom xmax test)", + "autodecimal, x<100%, display_range=8.5 (autodecimal test 1)", + "autodecimal, x<100%, display_range=8.4 (autodecimal test 2)", + "autodecimal, x<-100%, display_range=1e-6 (tiny display range)", # Check percent symbol - 'None as percent symbol', - 'Empty percent symbol', - 'Custom percent symbol', + "None as percent symbol", + "Empty percent symbol", + "Custom percent symbol", ] latex_data = [ - (False, False, r'50\{t}%'), - (False, True, r'50\\\{t\}\%'), - (True, False, r'50\{t}%'), - (True, True, r'50\{t}%'), + (False, False, r"50\{t}%"), + (False, True, r"50\\\{t\}\%"), + (True, False, r"50\{t}%"), + (True, True, r"50\{t}%"), ] @pytest.mark.parametrize( - 'xmax, decimals, symbol, x, display_range, expected', - percent_data, ids=percent_ids) - def test_basic(self, xmax, decimals, symbol, - x, display_range, expected): + "xmax, decimals, symbol, x, display_range, expected", + percent_data, + ids=percent_ids, + ) + def test_basic(self, xmax, decimals, symbol, x, display_range, expected): formatter = mticker.PercentFormatter(xmax, decimals, symbol) - with mpl.rc_context(rc={'text.usetex': False}): + with mpl.rc_context(rc={"text.usetex": False}): assert formatter.format_pct(x, display_range) == expected - @pytest.mark.parametrize('is_latex, usetex, expected', latex_data) + @pytest.mark.parametrize("is_latex, usetex, expected", latex_data) def test_latex(self, is_latex, usetex, expected): - fmt = mticker.PercentFormatter(symbol='\\{t}%', is_latex=is_latex) - with mpl.rc_context(rc={'text.usetex': usetex}): + fmt = mticker.PercentFormatter(symbol="\\{t}%", is_latex=is_latex) + with mpl.rc_context(rc={"text.usetex": usetex}): assert fmt.format_pct(50, 100) == expected def _impl_locale_comma(): try: - locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') + locale.setlocale(locale.LC_ALL, "de_DE.UTF-8") except locale.Error: - print('SKIP: Locale de_DE.UTF-8 is not supported on this machine') + print("SKIP: Locale de_DE.UTF-8 is not supported on this machine") return ticks = mticker.ScalarFormatter(useMathText=True, useLocale=True) - fmt = '$\\mathdefault{%1.1f}$' + fmt = "$\\mathdefault{%1.1f}$" x = ticks._format_maybe_minus_and_locale(fmt, 0.5) - assert x == '$\\mathdefault{0{,}5}$' + assert x == "$\\mathdefault{0{,}5}$" # Do not change , in the format string - fmt = ',$\\mathdefault{,%1.1f},$' + fmt = ",$\\mathdefault{,%1.1f},$" x = ticks._format_maybe_minus_and_locale(fmt, 0.5) - assert x == ',$\\mathdefault{,0{,}5},$' + assert x == ",$\\mathdefault{,0{,}5},$" # Make sure no brackets are added if not using math text ticks = mticker.ScalarFormatter(useMathText=False, useLocale=True) - fmt = '%1.1f' + fmt = "%1.1f" x = ticks._format_maybe_minus_and_locale(fmt, 0.5) - assert x == '0,5' + assert x == "0,5" def test_locale_comma(): @@ -1776,12 +2192,17 @@ def test_locale_comma(): # test can incorrectly fail instead of skip. # Instead, run this test in a subprocess, which avoids the problem, and the # need to fix the locale after. - proc = mpl.testing.subprocess_run_helper(_impl_locale_comma, timeout=60, - extra_env={'MPLBACKEND': 'Agg'}) - skip_msg = next((line[len('SKIP:'):].strip() - for line in proc.stdout.splitlines() - if line.startswith('SKIP:')), - '') + proc = mpl.testing.subprocess_run_helper( + _impl_locale_comma, timeout=60, extra_env={"MPLBACKEND": "Agg"} + ) + skip_msg = next( + ( + line[len("SKIP:") :].strip() + for line in proc.stdout.splitlines() + if line.startswith("SKIP:") + ), + "", + ) if skip_msg: pytest.skip(skip_msg) @@ -1814,8 +2235,7 @@ def test_minorticks_rc(): fig = plt.figure() def minorticksubplot(xminor, yminor, i): - rc = {'xtick.minor.visible': xminor, - 'ytick.minor.visible': yminor} + rc = {"xtick.minor.visible": xminor, "ytick.minor.visible": yminor} with plt.rc_context(rc=rc): ax = fig.add_subplot(2, 2, i) @@ -1839,6 +2259,7 @@ def test_minorticks_toggle(): `matplotlib.scale.get_scale_names()`. """ fig = plt.figure() + def minortickstoggle(xminor, yminor, scale, i): ax = fig.add_subplot(2, 2, i) ax.set_xscale(scale) @@ -1857,7 +2278,7 @@ def minortickstoggle(xminor, yminor, scale, i): assert (len(ax.xaxis.get_minor_ticks()) > 0) == xminor assert (len(ax.yaxis.get_minor_ticks()) > 0) == yminor - scales = ['linear', 'log', 'asinh', 'logit'] + scales = ["linear", "log", "asinh", "logit"] for scale in scales: minortickstoggle(False, False, scale, 1) minortickstoggle(True, False, scale, 2) @@ -1868,10 +2289,10 @@ def minortickstoggle(xminor, yminor, scale, i): plt.close(fig) -@pytest.mark.parametrize('remove_overlapping_locs, expected_num', - ((True, 6), - (None, 6), # this tests the default - (False, 9))) +@pytest.mark.parametrize( + "remove_overlapping_locs, expected_num", + ((True, 6), (None, 6), (False, 9)), # this tests the default +) def test_remove_overlap(remove_overlapping_locs, expected_num): t = np.arange("2018-11-03", "2018-11-06", dtype="datetime64") x = np.ones(len(t)) @@ -1880,10 +2301,10 @@ def test_remove_overlap(remove_overlapping_locs, expected_num): ax.plot(t, x) ax.xaxis.set_major_locator(mpl.dates.DayLocator()) - ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('\n%a')) + ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("\n%a")) ax.xaxis.set_minor_locator(mpl.dates.HourLocator((0, 6, 12, 18))) - ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter('%H:%M')) + ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter("%H:%M")) # force there to be extra ticks ax.xaxis.get_minor_ticks(15) if remove_overlapping_locs is not None: @@ -1891,10 +2312,10 @@ def test_remove_overlap(remove_overlapping_locs, expected_num): # check that getter/setter exists current = ax.xaxis.remove_overlapping_locs - assert (current == ax.xaxis.get_remove_overlapping_locs()) + assert current == ax.xaxis.get_remove_overlapping_locs() plt.setp(ax.xaxis, remove_overlapping_locs=current) new = ax.xaxis.remove_overlapping_locs - assert (new == ax.xaxis.remove_overlapping_locs) + assert new == ax.xaxis.remove_overlapping_locs # check that the accessors filter correctly # this is the method that does the actual filtering @@ -1902,50 +2323,52 @@ def test_remove_overlap(remove_overlapping_locs, expected_num): # these three are derivative assert len(ax.xaxis.get_minor_ticks()) == expected_num assert len(ax.xaxis.get_minorticklabels()) == expected_num - assert len(ax.xaxis.get_minorticklines()) == expected_num*2 + assert len(ax.xaxis.get_minorticklines()) == expected_num * 2 -@pytest.mark.parametrize('sub', [ - ['hi', 'aardvark'], - np.zeros((2, 2))]) +@pytest.mark.parametrize("sub", [["hi", "aardvark"], np.zeros((2, 2))]) def test_bad_locator_subs(sub): ll = mticker.LogLocator() with pytest.raises(ValueError): ll.set_params(subs=sub) -@pytest.mark.parametrize("numticks, lims, ticks", [ - (1, (.5, 5), [.1, 1, 10]), - (2, (.5, 5), [.1, 1, 10]), - (3, (.5, 5), [.1, 1, 10]), - (9, (.5, 5), [.1, 1, 10]), - (1, (.5, 50), [.1, 10, 1_000]), - (2, (.5, 50), [.1, 1, 10, 100]), - (3, (.5, 50), [.1, 1, 10, 100]), - (9, (.5, 50), [.1, 1, 10, 100]), - (1, (.5, 500), [.1, 10, 1_000]), - (2, (.5, 500), [.01, 1, 100, 10_000]), - (3, (.5, 500), [.1, 1, 10, 100, 1_000]), - (9, (.5, 500), [.1, 1, 10, 100, 1_000]), - (1, (.5, 5000), [.1, 100, 100_000]), - (2, (.5, 5000), [.001, 1, 1_000, 1_000_000]), - (3, (.5, 5000), [.001, 1, 1_000, 1_000_000]), - (9, (.5, 5000), [.1, 1, 10, 100, 1_000, 10_000]), -]) -@mpl.style.context('default') +@pytest.mark.parametrize( + "numticks, lims, ticks", + [ + (1, (0.5, 5), [0.1, 1, 10]), + (2, (0.5, 5), [0.1, 1, 10]), + (3, (0.5, 5), [0.1, 1, 10]), + (9, (0.5, 5), [0.1, 1, 10]), + (1, (0.5, 50), [0.1, 10, 1_000]), + (2, (0.5, 50), [0.1, 1, 10, 100]), + (3, (0.5, 50), [0.1, 1, 10, 100]), + (9, (0.5, 50), [0.1, 1, 10, 100]), + (1, (0.5, 500), [0.1, 10, 1_000]), + (2, (0.5, 500), [0.01, 1, 100, 10_000]), + (3, (0.5, 500), [0.1, 1, 10, 100, 1_000]), + (9, (0.5, 500), [0.1, 1, 10, 100, 1_000]), + (1, (0.5, 5000), [0.1, 100, 100_000]), + (2, (0.5, 5000), [0.001, 1, 1_000, 1_000_000]), + (3, (0.5, 5000), [0.001, 1, 1_000, 1_000_000]), + (9, (0.5, 5000), [0.1, 1, 10, 100, 1_000, 10_000]), + ], +) +@mpl.style.context("default") def test_small_range_loglocator(numticks, lims, ticks): ll = mticker.LogLocator(numticks=numticks) assert_array_equal(ll.tick_values(*lims), ticks) -@mpl.style.context('default') +@mpl.style.context("default") def test_loglocator_properties(): # Test that LogLocator returns ticks satisfying basic desirable properties # for a wide range of inputs. max_numticks = 8 pow_end = 20 for numticks, (lo, hi) in itertools.product( - range(1, max_numticks + 1), itertools.combinations(range(pow_end), 2)): + range(1, max_numticks + 1), itertools.combinations(range(pow_end), 2) + ): ll = mticker.LogLocator(numticks=numticks) decades = np.log10(ll.tick_values(10**lo, 10**hi)).round().astype(int) # There are no more ticks than the requested number, plus exactly one @@ -1953,7 +2376,7 @@ def test_loglocator_properties(): assert len(decades) <= numticks + 2 assert decades[0] < lo <= decades[1] assert decades[-2] <= hi < decades[-1] - stride, = {*np.diff(decades)} # Extract the (constant) stride. + (stride,) = {*np.diff(decades)} # Extract the (constant) stride. # Either the ticks are on integer multiples of the stride... if not (decades % stride == 0).all(): # ... or (for this given stride) no offset would be acceptable, @@ -1966,18 +2389,22 @@ def test_loglocator_properties(): def test_NullFormatter(): formatter = mticker.NullFormatter() - assert formatter(1.0) == '' - assert formatter.format_data(1.0) == '' - assert formatter.format_data_short(1.0) == '' + assert formatter(1.0) == "" + assert formatter.format_data(1.0) == "" + assert formatter.format_data_short(1.0) == "" -@pytest.mark.parametrize('formatter', ( - mticker.FuncFormatter(lambda a: f'val: {a}'), - mticker.FixedFormatter(('foo', 'bar')))) +@pytest.mark.parametrize( + "formatter", + ( + mticker.FuncFormatter(lambda a: f"val: {a}"), + mticker.FixedFormatter(("foo", "bar")), + ), +) def test_set_offset_string(formatter): - assert formatter.get_offset() == '' - formatter.set_offset_string('mpl') - assert formatter.get_offset() == 'mpl' + assert formatter.get_offset() == "" + formatter.set_offset_string("mpl") + assert formatter.get_offset() == "mpl" def test_minorticks_on_multi_fig(): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f82eeedc8918..4e3e9d965552 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -145,16 +145,36 @@ _log = logging.getLogger(__name__) -__all__ = ('TickHelper', 'Formatter', 'FixedFormatter', - 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter', - 'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter', - 'LogFormatterExponent', 'LogFormatterMathtext', - 'LogFormatterSciNotation', - 'LogitFormatter', 'EngFormatter', 'PercentFormatter', - 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', - 'LinearLocator', 'LogLocator', 'AutoLocator', - 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', - 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator') +__all__ = ( + "TickHelper", + "Formatter", + "FixedFormatter", + "NullFormatter", + "FuncFormatter", + "FormatStrFormatter", + "StrMethodFormatter", + "ScalarFormatter", + "LogFormatter", + "LogFormatterExponent", + "LogFormatterMathtext", + "LogFormatterSciNotation", + "LogitFormatter", + "EngFormatter", + "PercentFormatter", + "Locator", + "IndexLocator", + "FixedLocator", + "NullLocator", + "LinearLocator", + "LogLocator", + "AutoLocator", + "MultipleLocator", + "MaxNLocator", + "AutoMinorLocator", + "SymmetricalLogLocator", + "AsinhLocator", + "LogitLocator", +) class _DummyAxis: @@ -200,6 +220,7 @@ class Formatter(TickHelper): """ Create a string based on a tick value and location. """ + # some classes want to see all the locs to help format # individual ones locs = [] @@ -209,7 +230,7 @@ def __call__(self, x, pos=None): Return the format for tick value *x* at position pos. ``pos=None`` indicates an unspecified location. """ - raise NotImplementedError('Derived must override') + raise NotImplementedError("Derived must override") def format_ticks(self, values): """Return the tick labels for all the ticks at once.""" @@ -232,7 +253,7 @@ def format_data_short(self, value): return self.format_data(value) def get_offset(self): - return '' + return "" def set_locs(self, locs): """ @@ -251,9 +272,11 @@ def fix_minus(s): helper method to perform such a replacement when it is enabled via :rc:`axes.unicode_minus`. """ - return (s.replace('-', '\N{MINUS SIGN}') - if mpl.rcParams['axes.unicode_minus'] - else s) + return ( + s.replace("-", "\N{MINUS SIGN}") + if mpl.rcParams["axes.unicode_minus"] + else s + ) def _set_locator(self, locator): """Subclasses may want to override this to set a locator.""" @@ -265,7 +288,10 @@ class NullFormatter(Formatter): def __call__(self, x, pos=None): # docstring inherited - return '' + return "" + + def __repr__(self): + return "NullFormatter()" class FixedFormatter(Formatter): @@ -280,7 +306,7 @@ class FixedFormatter(Formatter): def __init__(self, seq): """Set the sequence *seq* of strings that will be used for labels.""" self.seq = seq - self.offset_string = '' + self.offset_string = "" def __call__(self, x, pos=None): """ @@ -291,7 +317,7 @@ def __call__(self, x, pos=None): strings that this object was initialized with. """ if pos is None or pos >= len(self.seq): - return '' + return "" else: return self.seq[pos] @@ -301,6 +327,9 @@ def get_offset(self): def set_offset_string(self, ofs): self.offset_string = ofs + def __repr__(self): + return f"FixedFormatter({self.seq!r})" + class FuncFormatter(Formatter): """ @@ -329,6 +358,9 @@ def get_offset(self): def set_offset_string(self, ofs): self.offset_string = ofs + def __repr__(self): + return f"FuncFormatter({self.func!r})" + class FormatStrFormatter(Formatter): """ @@ -353,6 +385,9 @@ def __call__(self, x, pos=None): """ return self.fmt % x + def __repr__(self): + return f"FormatStrFormatter({self.fmt!r})" + class _UnicodeMinusFormat(string.Formatter): """ @@ -393,6 +428,9 @@ def __call__(self, x, pos=None): """ return _UnicodeMinusFormat().format(self.fmt, x=x, pos=pos) + def __repr__(self): + return f"StrMethodFormatter({self.fmt!r})" + class ScalarFormatter(Formatter): """ @@ -449,17 +487,18 @@ class ScalarFormatter(Formatter): """ - def __init__(self, useOffset=None, useMathText=None, useLocale=None, *, - usetex=None): - useOffset = mpl._val_or_rc(useOffset, 'axes.formatter.useoffset') - self._offset_threshold = mpl.rcParams['axes.formatter.offset_threshold'] + def __init__( + self, useOffset=None, useMathText=None, useLocale=None, *, usetex=None + ): + useOffset = mpl._val_or_rc(useOffset, "axes.formatter.useoffset") + self._offset_threshold = mpl.rcParams["axes.formatter.offset_threshold"] self.set_useOffset(useOffset) self.set_usetex(usetex) self.set_useMathText(useMathText) self.orderOfMagnitude = 0 - self.format = '' + self.format = "" self._scientific = True - self._powerlimits = mpl.rcParams['axes.formatter.limits'] + self._powerlimits = mpl.rcParams["axes.formatter.limits"] self.set_useLocale(useLocale) def get_usetex(self): @@ -468,7 +507,7 @@ def get_usetex(self): def set_usetex(self, val): """Set whether to use TeX's math mode for rendering numbers in the formatter.""" - self._usetex = mpl._val_or_rc(val, 'text.usetex') + self._usetex = mpl._val_or_rc(val, "text.usetex") usetex = property(fget=get_usetex, fset=set_usetex) @@ -540,7 +579,7 @@ def set_useLocale(self, val): val : bool or None *None* resets to :rc:`axes.formatter.use_locale`. """ - self._useLocale = mpl._val_or_rc(val, 'axes.formatter.use_locale') + self._useLocale = mpl._val_or_rc(val, "axes.formatter.use_locale") useLocale = property(fget=get_useLocale, fset=set_useLocale) @@ -549,13 +588,19 @@ def _format_maybe_minus_and_locale(self, fmt, arg): Format *arg* with *fmt*, applying Unicode minus and locale if desired. """ return self.fix_minus( - # Escape commas introduced by locale.format_string if using math text, - # but not those present from the beginning in fmt. - (",".join(locale.format_string(part, (arg,), True).replace(",", "{,}") - for part in fmt.split(",")) if self._useMathText - else locale.format_string(fmt, (arg,), True)) - if self._useLocale - else fmt % arg) + # Escape commas introduced by locale.format_string if using math text, + # but not those present from the beginning in fmt. + ( + ",".join( + locale.format_string(part, (arg,), True).replace(",", "{,}") + for part in fmt.split(",") + ) + if self._useMathText + else locale.format_string(fmt, (arg,), True) + ) + if self._useLocale + else fmt % arg + ) def get_useMathText(self): """ @@ -579,14 +624,13 @@ def set_useMathText(self, val): *None* resets to :rc:`axes.formatter.use_mathtext`. """ if val is None: - self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] + self._useMathText = mpl.rcParams["axes.formatter.use_mathtext"] if self._useMathText is False: try: from matplotlib import font_manager + ufont = font_manager.findfont( - font_manager.FontProperties( - family=mpl.rcParams["font.family"] - ), + font_manager.FontProperties(family=mpl.rcParams["font.family"]), fallback_to_default=False, ) except ValueError: @@ -607,9 +651,9 @@ def __call__(self, x, pos=None): Return the format for tick value *x* at position *pos*. """ if len(self.locs) == 0: - return '' + return "" else: - xp = (x - self.offset) / (10. ** self.orderOfMagnitude) + xp = (x - self.offset) / (10.0**self.orderOfMagnitude) if abs(xp) < 1e-8: xp = 0 return self._format_maybe_minus_and_locale(self.format, xp) @@ -671,13 +715,15 @@ def format_data_short(self, value): axis_inv_trf = axis_trf.inverted() screen_xy = axis_trf.transform((value, 0)) neighbor_values = axis_inv_trf.transform( - screen_xy + [[-1, 0], [+1, 0]])[:, 0] + screen_xy + [[-1, 0], [+1, 0]] + )[:, 0] else: # yaxis: axis_trf = self.axis.axes.get_yaxis_transform() axis_inv_trf = axis_trf.inverted() screen_xy = axis_trf.transform((0, value)) neighbor_values = axis_inv_trf.transform( - screen_xy + [[0, -1], [0, +1]])[:, 1] + screen_xy + [[0, -1], [0, +1]] + )[:, 1] delta = abs(neighbor_values - value).max() else: # Rough approximation: no more than 1e4 divisions. @@ -691,14 +737,18 @@ def format_data(self, value): e = math.floor(math.log10(abs(value))) s = round(value / 10**e, 10) significand = self._format_maybe_minus_and_locale( - "%d" if s % 1 == 0 else "%1.10g", s) + "%d" if s % 1 == 0 else "%1.10g", s + ) if e == 0: return significand exponent = self._format_maybe_minus_and_locale("%d", e) if self._useMathText or self._usetex: exponent = "10^{%s}" % exponent - return (exponent if s == 1 # reformat 1x10^y as 10^y - else rf"{significand} \times {exponent}") + return ( + exponent + if s == 1 # reformat 1x10^y as 10^y + else rf"{significand} \times {exponent}" + ) else: return f"{significand}e{exponent}" @@ -707,27 +757,27 @@ def get_offset(self): Return scientific notation, plus offset. """ if len(self.locs) == 0: - return '' + return "" if self.orderOfMagnitude or self.offset: - offsetStr = '' - sciNotStr = '' + offsetStr = "" + sciNotStr = "" if self.offset: offsetStr = self.format_data(self.offset) if self.offset > 0: - offsetStr = '+' + offsetStr + offsetStr = "+" + offsetStr if self.orderOfMagnitude: if self._usetex or self._useMathText: - sciNotStr = self.format_data(10 ** self.orderOfMagnitude) + sciNotStr = self.format_data(10**self.orderOfMagnitude) else: - sciNotStr = '1e%d' % self.orderOfMagnitude + sciNotStr = "1e%d" % self.orderOfMagnitude if self._useMathText or self._usetex: - if sciNotStr != '': - sciNotStr = r'\times\mathdefault{%s}' % sciNotStr - s = fr'${sciNotStr}\mathdefault{{{offsetStr}}}$' + if sciNotStr != "": + sciNotStr = r"\times\mathdefault{%s}" % sciNotStr + s = rf"${sciNotStr}\mathdefault{{{offsetStr}}}$" else: - s = ''.join((sciNotStr, offsetStr)) + s = "".join((sciNotStr, offsetStr)) return self.fix_minus(s) - return '' + return "" def set_locs(self, locs): # docstring inherited @@ -762,20 +812,26 @@ def _compute_offset(self): # Note: Internally using oom instead of 10 ** oom avoids some numerical # accuracy issues. oom_max = np.ceil(math.log10(abs_max)) - oom = 1 + next(oom for oom in itertools.count(oom_max, -1) - if abs_min // 10 ** oom != abs_max // 10 ** oom) - if (abs_max - abs_min) / 10 ** oom <= 1e-2: + oom = 1 + next( + oom + for oom in itertools.count(oom_max, -1) + if abs_min // 10**oom != abs_max // 10**oom + ) + if (abs_max - abs_min) / 10**oom <= 1e-2: # Handle the case of straddling a multiple of a large power of ten # (relative to the span). # What is the smallest power of ten such that abs_min and abs_max # are no more than 1 apart at that precision? - oom = 1 + next(oom for oom in itertools.count(oom_max, -1) - if abs_max // 10 ** oom - abs_min // 10 ** oom > 1) + oom = 1 + next( + oom + for oom in itertools.count(oom_max, -1) + if abs_max // 10**oom - abs_min // 10**oom > 1 + ) # Only use offset if it saves at least _offset_threshold digits. n = self._offset_threshold - 1 - self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom - if abs_max // 10 ** oom >= 10**n - else 0) + self.offset = ( + sign * (abs_max // 10**oom) * 10**oom if abs_max // 10**oom >= 10**n else 0 + ) def _set_order_of_magnitude(self): # if scientific notation is to be used, find the appropriate exponent @@ -818,7 +874,7 @@ def _set_format(self): _locs = [*self.locs, *self.axis.get_view_interval()] else: _locs = self.locs - locs = (np.asarray(_locs) - self.offset) / 10. ** self.orderOfMagnitude + locs = (np.asarray(_locs) - self.offset) / 10.0**self.orderOfMagnitude loc_range = np.ptp(locs) # Curvilinear coordinates can yield two identical points. if loc_range == 0: @@ -833,16 +889,16 @@ def _set_format(self): # first estimate: sigfigs = max(0, 3 - loc_range_oom) # refined estimate: - thresh = 1e-3 * 10 ** loc_range_oom + thresh = 1e-3 * 10**loc_range_oom while sigfigs >= 0: if np.abs(locs - np.round(locs, decimals=sigfigs)).max() < thresh: sigfigs -= 1 else: break sigfigs += 1 - self.format = f'%1.{sigfigs}f' + self.format = f"%1.{sigfigs}f" if self._usetex or self._useMathText: - self.format = r'$\mathdefault{%s}$' % self.format + self.format = r"$\mathdefault{%s}$" % self.format class LogFormatter(Formatter): @@ -903,14 +959,14 @@ class LogFormatter(Formatter): ``minor_thresholds=(2, 0.5)``. """ - def __init__(self, base=10.0, labelOnlyBase=False, - minor_thresholds=None, - linthresh=None): + def __init__( + self, base=10.0, labelOnlyBase=False, minor_thresholds=None, linthresh=None + ): self.set_base(base) self.set_label_minor(labelOnlyBase) if minor_thresholds is None: - if mpl.rcParams['_internal.classic_mode']: + if mpl.rcParams["_internal.classic_mode"]: minor_thresholds = (0, 0) else: minor_thresholds = (1, 0.4) @@ -918,6 +974,11 @@ def __init__(self, base=10.0, labelOnlyBase=False, self._sublabels = None self._linthresh = linthresh + def __repr__(self): + return ( + f"LogFormatter(base={self._base}, " f"labelOnlyBase={self.labelOnlyBase})" + ) + def set_base(self, base): """ Change the *base* for labeling. @@ -974,23 +1035,22 @@ def set_locs(self, locs=None): numdec = numticks = 0 if vmin < -linthresh: rhs = min(vmax, -linthresh) - numticks += ( - math.floor(math.log(abs(rhs), b)) - - math.floor(math.nextafter(math.log(abs(vmin), b), -math.inf))) + numticks += math.floor(math.log(abs(rhs), b)) - math.floor( + math.nextafter(math.log(abs(vmin), b), -math.inf) + ) numdec += math.log(vmin / rhs, b) if vmax > linthresh: lhs = max(vmin, linthresh) - numticks += ( - math.floor(math.log(vmax, b)) - - math.floor(math.nextafter(math.log(lhs, b), -math.inf))) + numticks += math.floor(math.log(vmax, b)) - math.floor( + math.nextafter(math.log(lhs, b), -math.inf) + ) numdec += math.log(vmax / lhs, b) else: lmin = math.log(vmin, b) lmax = math.log(vmax, b) # The nextafter call handles the case where vmin is exactly at a # decade (e.g. there's one major tick between 1 and 5). - numticks = (math.floor(lmax) - - math.floor(math.nextafter(lmin, -math.inf))) + numticks = math.floor(lmax) - math.floor(math.nextafter(lmin, -math.inf)) numdec = abs(lmax - lmin) if numticks > self.minor_thresholds[0]: @@ -1000,7 +1060,7 @@ def set_locs(self, locs=None): # Add labels between bases at log-spaced coefficients; # include base powers in case the locations include # "major" and "minor" points, as in colorbar. - c = np.geomspace(1, b, int(b)//2 + 1) + c = np.geomspace(1, b, int(b) // 2 + 1) self._sublabels = set(np.round(c)) # For base 10, this yields (1, 2, 3, 4, 6, 10). else: @@ -1013,7 +1073,7 @@ def _num_to_string(self, x, vmin, vmax): def __call__(self, x, pos=None): # docstring inherited if x == 0.0: # Symlog - return '0' + return "0" x = abs(x) b = self._base @@ -1024,9 +1084,9 @@ def __call__(self, x, pos=None): coeff = round(b ** (fx - exponent)) if self.labelOnlyBase and not is_x_decade: - return '' + return "" if self._sublabels is not None and coeff not in self._sublabels: - return '' + return "" vmin, vmax = self.axis.get_view_interval() vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) @@ -1039,28 +1099,32 @@ def format_data(self, value): def format_data_short(self, value): # docstring inherited - return ('%-12g' % value).rstrip() + return ("%-12g" % value).rstrip() def _pprint_val(self, x, d): # If the number is not too big and it's an int, format it as an int. if abs(x) < 1e4 and x == int(x): - return '%d' % x - fmt = ('%1.3e' if d < 1e-2 else - '%1.3f' if d <= 1 else - '%1.2f' if d <= 10 else - '%1.1f' if d <= 1e5 else - '%1.1e') + return "%d" % x + fmt = ( + "%1.3e" + if d < 1e-2 + else ( + "%1.3f" + if d <= 1 + else "%1.2f" if d <= 10 else "%1.1f" if d <= 1e5 else "%1.1e" + ) + ) s = fmt % x - tup = s.split('e') + tup = s.split("e") if len(tup) == 2: - mantissa = tup[0].rstrip('0').rstrip('.') + mantissa = tup[0].rstrip("0").rstrip(".") exponent = int(tup[1]) if exponent: - s = '%se%d' % (mantissa, exponent) + s = "%se%d" % (mantissa, exponent) else: s = mantissa else: - s = s.rstrip('0').rstrip('.') + s = s.rstrip("0").rstrip(".") return s @@ -1086,14 +1150,14 @@ class LogFormatterMathtext(LogFormatter): def _non_decade_format(self, sign_string, base, fx, usetex): """Return string for non-decade locations.""" - return r'$\mathdefault{%s%s^{%.2f}}$' % (sign_string, base, fx) + return r"$\mathdefault{%s%s^{%.2f}}$" % (sign_string, base, fx) def __call__(self, x, pos=None): # docstring inherited if x == 0: # Symlog - return r'$\mathdefault{0}$' + return r"$\mathdefault{0}$" - sign_string = '-' if x < 0 else '' + sign_string = "-" if x < 0 else "" x = abs(x) b = self._base @@ -1104,26 +1168,26 @@ def __call__(self, x, pos=None): coeff = round(b ** (fx - exponent)) if self.labelOnlyBase and not is_x_decade: - return '' + return "" if self._sublabels is not None and coeff not in self._sublabels: - return '' + return "" if is_x_decade: fx = round(fx) # use string formatting of the base if it is not an integer if b % 1 == 0.0: - base = '%d' % b + base = "%d" % b else: - base = '%s' % b + base = "%s" % b - if abs(fx) < mpl.rcParams['axes.formatter.min_exponent']: - return r'$\mathdefault{%s%g}$' % (sign_string, x) + if abs(fx) < mpl.rcParams["axes.formatter.min_exponent"]: + return r"$\mathdefault{%s%g}$" % (sign_string, x) elif not is_x_decade: - usetex = mpl.rcParams['text.usetex'] + usetex = mpl.rcParams["text.usetex"] return self._non_decade_format(sign_string, base, fx, usetex) else: - return r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx) + return r"$\mathdefault{%s%s^{%d}}$" % (sign_string, base, fx) class LogFormatterSciNotation(LogFormatterMathtext): @@ -1138,8 +1202,12 @@ def _non_decade_format(self, sign_string, base, fx, usetex): coeff = b ** (fx - exponent) if _is_close_to_int(coeff): coeff = round(coeff) - return r'$\mathdefault{%s%g\times%s^{%d}}$' \ - % (sign_string, coeff, base, exponent) + return r"$\mathdefault{%s%g\times%s^{%d}}$" % ( + sign_string, + coeff, + base, + exponent, + ) class LogitFormatter(Formatter): @@ -1187,6 +1255,12 @@ def __init__( self._minor_threshold = minor_threshold self._minor_number = minor_number + def __repr__(self): + return ( + f"LogitFormatter(use_overline={self._use_overline}, " + f"one_half={self._one_half!r}, minor={self._minor})" + ) + def use_overline(self, use_overline): r""" Switch display mode with overline for labelling p>1/2. @@ -1242,8 +1316,7 @@ def set_locs(self, locs): if all( _is_decade(x, rtol=1e-7) or _is_decade(1 - x, rtol=1e-7) - or (_is_close_to_int(2 * x) and - int(np.round(2 * x)) == 1) + or (_is_close_to_int(2 * x) and int(np.round(2 * x)) == 1) for x in locs ): # minor ticks are subsample from ideal, so no label @@ -1265,14 +1338,11 @@ def set_locs(self, locs): np.concatenate(((np.inf,), diff)), np.concatenate((diff, (np.inf,))), ) - space_sum = ( - np.concatenate(((0,), diff)) - + np.concatenate((diff, (0,))) - ) + space_sum = np.concatenate(((0,), diff)) + np.concatenate((diff, (0,))) good_minor = sorted( range(len(self.locs)), key=lambda i: (space_pessimistic[i], space_sum[i]), - )[-self._minor_number:] + )[-self._minor_number :] self._labelled.update(locs[i] for i in good_minor) def _format_value(self, x, locs, sci_notation=True): @@ -1323,7 +1393,7 @@ def __call__(self, x, pos=None): elif x < 0.1: s = self._format_value(x, self.locs) elif x > 0.9: - s = self._one_minus(self._format_value(1-x, 1-self.locs)) + s = self._one_minus(self._format_value(1 - x, 1 - self.locs)) else: s = self._format_value(x, self.locs, sci_notation=False) return r"$\mathdefault{%s}$" % s @@ -1353,24 +1423,32 @@ class EngFormatter(ScalarFormatter): -18: "a", -15: "f", -12: "p", - -9: "n", - -6: "\N{MICRO SIGN}", - -3: "m", - 0: "", - 3: "k", - 6: "M", - 9: "G", - 12: "T", - 15: "P", - 18: "E", - 21: "Z", - 24: "Y", - 27: "R", - 30: "Q" + -9: "n", + -6: "\N{MICRO SIGN}", + -3: "m", + 0: "", + 3: "k", + 6: "M", + 9: "G", + 12: "T", + 15: "P", + 18: "E", + 21: "Z", + 24: "Y", + 27: "R", + 30: "Q", } - def __init__(self, unit="", places=None, sep=" ", *, usetex=None, - useMathText=None, useOffset=False): + def __init__( + self, + unit="", + places=None, + sep=" ", + *, + usetex=None, + useMathText=None, + useOffset=False, + ): r""" Parameters ---------- @@ -1434,7 +1512,7 @@ def __call__(self, x, pos=None): if len(self.locs) == 0 or self.offset == 0: return self.fix_minus(self.format_data(x)) else: - xp = (x - self.offset) / (10. ** self.orderOfMagnitude) + xp = (x - self.offset) / (10.0**self.orderOfMagnitude) if abs(xp) < 1e-8: xp = 0 return self._format_maybe_minus_and_locale(self.format, xp) @@ -1454,9 +1532,9 @@ def set_locs(self, locs): # slightly less readable, so if offset is justified # (decided by self._compute_offset) we set it to better # value: - self.offset = round((vmin + vmax)/2, 3) + self.offset = round((vmin + vmax) / 2, 3) # Use log1000 to use engineers' oom standards - self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000))*3 + self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000)) * 3 self._set_format() # Simplify a bit ScalarFormatter.get_offset: We always want to use @@ -1467,22 +1545,22 @@ def set_locs(self, locs): def get_offset(self): # docstring inherited if len(self.locs) == 0: - return '' + return "" if self.offset: - offsetStr = '' + offsetStr = "" if self.offset: offsetStr = self.format_data(self.offset) if self.offset > 0: - offsetStr = '+' + offsetStr - sciNotStr = self.format_data(10 ** self.orderOfMagnitude) + offsetStr = "+" + offsetStr + sciNotStr = self.format_data(10**self.orderOfMagnitude) if self._useMathText or self._usetex: - if sciNotStr != '': - sciNotStr = r'\times%s' % sciNotStr - s = f'${sciNotStr}{offsetStr}$' + if sciNotStr != "": + sciNotStr = r"\times%s" % sciNotStr + s = f"${sciNotStr}{offsetStr}$" else: s = sciNotStr + offsetStr return self.fix_minus(s) - return '' + return "" def format_eng(self, num): """Alias to EngFormatter.format_data""" @@ -1521,12 +1599,11 @@ def format_data(self, value): pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) - mant = sign * value / (10.0 ** pow10) + mant = sign * value / (10.0**pow10) # Taking care of the cases like 999.9..., which may be rounded to 1000 # instead of 1 k. Beware of the corner case of values that are beyond # the range of SI prefixes (i.e. > 'Y'). - if (abs(float(format(mant, fmt))) >= 1000 - and pow10 < max(self.ENG_PREFIXES)): + if abs(float(format(mant, fmt))) >= 1000 and pow10 < max(self.ENG_PREFIXES): mant /= 1000 pow10 += 3 @@ -1540,6 +1617,9 @@ def format_data(self, value): else: return f"{mant:{fmt}}{suffix}" + def __repr__(self): + return f"EngFormatter(unit={self.unit!r}, places={self.places})" + class PercentFormatter(Formatter): """ @@ -1567,12 +1647,19 @@ class PercentFormatter(Formatter): is_latex : bool If *False*, reserved LaTeX characters in *symbol* will be escaped. """ - def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False): + + def __init__(self, xmax=100, decimals=None, symbol="%", is_latex=False): self.xmax = xmax + 0.0 self.decimals = decimals self._symbol = symbol self._is_latex = is_latex + def __repr__(self): + return ( + f"PercentFormatter(xmax={self.xmax}, " + f"decimals={self.decimals}, symbol={self._symbol!r})" + ) + def __call__(self, x, pos=None): """Format the tick as a percentage with the appropriate scaling.""" ax_min, ax_max = self.axis.get_view_interval() @@ -1619,7 +1706,7 @@ def format_pct(self, x, display_range): decimals = 0 else: decimals = self.decimals - s = f'{x:0.{int(decimals)}f}' + s = f"{x:0.{int(decimals)}f}" return s + self.symbol @@ -1637,13 +1724,13 @@ def symbol(self): """ symbol = self._symbol if not symbol: - symbol = '' - elif not self._is_latex and mpl.rcParams['text.usetex']: + symbol = "" + elif not self._is_latex and mpl.rcParams["text.usetex"]: # Source: http://www.personal.ceu.hu/tex/specchar.htm # Backslash must be first for this to work correctly since # it keeps getting added in - for spec in r'\#$%&~_^{}': - symbol = symbol.replace(spec, '\\' + spec) + for spec in r"\#$%&~_^{}": + symbol = symbol.replace(spec, "\\" + spec) return symbol @symbol.setter @@ -1681,7 +1768,7 @@ def tick_values(self, vmin, vmax): [1, 2, 3, 4] """ - raise NotImplementedError('Derived must override') + raise NotImplementedError("Derived must override") def set_params(self, **kwargs): """ @@ -1689,14 +1776,14 @@ def set_params(self, **kwargs): set_params() function will call this. """ _api.warn_external( - "'set_params()' not defined for locator of type " + - str(type(self))) + "'set_params()' not defined for locator of type " + str(type(self)) + ) def __call__(self): """Return the locations of the ticks.""" # note: some locators return data limits, other return view limits, # hence there is no *one* interface to call self.tick_values. - raise NotImplementedError('Derived must override') + raise NotImplementedError("Derived must override") def raise_if_exceeds(self, locs): """ @@ -1713,7 +1800,11 @@ def raise_if_exceeds(self, locs): _log.warning( "Locator attempting to generate %s ticks ([%s, ..., %s]), " "which exceeds Locator.MAXTICKS (%s).", - len(locs), locs[0], locs[-1], self.MAXTICKS) + len(locs), + locs[0], + locs[-1], + self.MAXTICKS, + ) return locs def nonsingular(self, v0, v1): @@ -1730,7 +1821,7 @@ def nonsingular(self, v0, v1): default view limits. - Otherwise, ``(v0, v1)`` is returned without modification. """ - return mtransforms.nonsingular(v0, v1, expander=.05) + return mtransforms.nonsingular(v0, v1, expander=0.05) def view_limits(self, vmin, vmax): """ @@ -1768,7 +1859,11 @@ def __call__(self): def tick_values(self, vmin, vmax): return self.raise_if_exceeds( - np.arange(vmin + self.offset, vmax + 1, self._base)) + np.arange(vmin + self.offset, vmax + 1, self._base) + ) + + def __repr__(self): + return f"IndexLocator(base={self._base}, offset={self.offset})" class FixedLocator(Locator): @@ -1813,6 +1908,9 @@ def tick_values(self, vmin, vmax): ticks = ticks1 return self.raise_if_exceeds(ticks) + def __repr__(self): + return f"FixedLocator(locs={self.locs!r}, nbins={self.nbins})" + class NullLocator(Locator): """ @@ -1832,6 +1930,9 @@ def tick_values(self, vmin, vmax): """ return [] + def __repr__(self): + return "NullLocator()" + class LinearLocator(Locator): """ @@ -1902,16 +2003,20 @@ def view_limits(self, vmin, vmax): vmin -= 1 vmax += 1 - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": exponent, remainder = divmod( - math.log10(vmax - vmin), math.log10(max(self.numticks - 1, 1))) - exponent -= (remainder < .5) + math.log10(vmax - vmin), math.log10(max(self.numticks - 1, 1)) + ) + exponent -= remainder < 0.5 scale = max(self.numticks - 1, 1) ** (-exponent) vmin = math.floor(scale * vmin) / scale vmax = math.ceil(scale * vmax) / scale return mtransforms.nonsingular(vmin, vmax) + def __repr__(self): + return f"LinearLocator(numticks={self._numticks})" + class MultipleLocator(Locator): """ @@ -1970,7 +2075,7 @@ def view_limits(self, dmin, dmax): """ Set the view limits to the nearest tick values that contain the data. """ - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": vmin = self._edge.le(dmin - self._offset) * self._edge.step + self._offset vmax = self._edge.ge(dmax - self._offset) * self._edge.step + self._offset if vmin == vmax: @@ -1982,6 +2087,9 @@ def view_limits(self, dmin, dmax): return mtransforms.nonsingular(vmin, vmax) + def __repr__(self): + return f"MultipleLocator(base={self._edge.step}, " f"offset={self._offset})" + def scale_range(vmin, vmax, n=1, threshold=100): dv = abs(vmax - vmin) # > 0 as nonsingular is called before. @@ -2049,12 +2157,10 @@ class MaxNLocator(Locator): Finds nice tick locations with no more than :math:`nbins + 1` ticks being within the view limits. Locations beyond the limits are added to support autoscaling. """ - default_params = dict(nbins=10, - steps=None, - integer=False, - symmetric=False, - prune=None, - min_n_ticks=2) + + default_params = dict( + nbins=10, steps=None, integer=False, symmetric=False, prune=None, min_n_ticks=2 + ) def __init__(self, nbins=None, **kwargs): """ @@ -2092,18 +2198,22 @@ def __init__(self, nbins=None, **kwargs): this minimum number of ticks. """ if nbins is not None: - kwargs['nbins'] = nbins + kwargs["nbins"] = nbins self.set_params(**{**self.default_params, **kwargs}) @staticmethod def _validate_steps(steps): if not np.iterable(steps): - raise ValueError('steps argument must be an increasing sequence ' - 'of numbers between 1 and 10 inclusive') + raise ValueError( + "steps argument must be an increasing sequence " + "of numbers between 1 and 10 inclusive" + ) steps = np.asarray(steps) if np.any(np.diff(steps) <= 0) or steps[-1] > 10 or steps[0] < 1: - raise ValueError('steps argument must be an increasing sequence ' - 'of numbers between 1 and 10 inclusive') + raise ValueError( + "steps argument must be an increasing sequence " + "of numbers between 1 and 10 inclusive" + ) if steps[0] != 1: steps = np.concatenate([[1], steps]) if steps[-1] != 10: @@ -2135,27 +2245,27 @@ def set_params(self, **kwargs): min_n_ticks : int, optional see `.MaxNLocator` """ - if 'nbins' in kwargs: - self._nbins = kwargs.pop('nbins') - if self._nbins != 'auto': + if "nbins" in kwargs: + self._nbins = kwargs.pop("nbins") + if self._nbins != "auto": self._nbins = int(self._nbins) - if 'symmetric' in kwargs: - self._symmetric = kwargs.pop('symmetric') - if 'prune' in kwargs: - prune = kwargs.pop('prune') - _api.check_in_list(['upper', 'lower', 'both', None], prune=prune) + if "symmetric" in kwargs: + self._symmetric = kwargs.pop("symmetric") + if "prune" in kwargs: + prune = kwargs.pop("prune") + _api.check_in_list(["upper", "lower", "both", None], prune=prune) self._prune = prune - if 'min_n_ticks' in kwargs: - self._min_n_ticks = max(1, kwargs.pop('min_n_ticks')) - if 'steps' in kwargs: - steps = kwargs.pop('steps') + if "min_n_ticks" in kwargs: + self._min_n_ticks = max(1, kwargs.pop("min_n_ticks")) + if "steps" in kwargs: + steps = kwargs.pop("steps") if steps is None: self._steps = np.array([1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]) else: self._steps = self._validate_steps(steps) self._extended_steps = self._staircase(self._steps) - if 'integer' in kwargs: - self._integer = kwargs.pop('integer') + if "integer" in kwargs: + self._integer = kwargs.pop("integer") if kwargs: raise _api.kwarg_error("set_params", kwargs) @@ -2166,10 +2276,11 @@ def _raw_ticks(self, vmin, vmax): will not be needed, in which case they are trimmed off elsewhere. """ - if self._nbins == 'auto': + if self._nbins == "auto": if self.axis is not None: - nbins = np.clip(self.axis.get_tick_space(), - max(1, self._min_n_ticks - 1), 9) + nbins = np.clip( + self.axis.get_tick_space(), max(1, self._min_n_ticks - 1), 9 + ) else: nbins = 9 else: @@ -2184,14 +2295,14 @@ def _raw_ticks(self, vmin, vmax): igood = (steps < 1) | (np.abs(steps - np.round(steps)) < 0.001) steps = steps[igood] - raw_step = ((_vmax - _vmin) / nbins) - if hasattr(self.axis, "axes") and self.axis.axes.name == '3d': + raw_step = (_vmax - _vmin) / nbins + if hasattr(self.axis, "axes") and self.axis.axes.name == "3d": # Due to the change in automargin behavior in mpl3.9, we need to # adjust the raw step to match the mpl3.8 appearance. The zoom # factor of 2/48, gives us the 23/24 modifier. - raw_step = raw_step * 23/24 + raw_step = raw_step * 23 / 24 large_steps = steps >= raw_step - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": # Classic round_numbers mode may require a larger step. # Get first multiple of steps that are <= _vmin floored_vmins = (_vmin // steps) * steps @@ -2207,10 +2318,12 @@ def _raw_ticks(self, vmin, vmax): # Start at smallest of the steps greater than the raw step, and check # if it provides enough ticks. If not, work backwards through # smaller steps until one is found that provides enough ticks. - for step in steps[:istep+1][::-1]: + for step in steps[: istep + 1][::-1]: - if (self._integer and - np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1): + if ( + self._integer + and np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1 + ): step = max(1, step) best_vmin = (_vmin // step) * step @@ -2236,16 +2349,15 @@ def tick_values(self, vmin, vmax): if self._symmetric: vmax = max(abs(vmin), abs(vmax)) vmin = -vmax - vmin, vmax = mtransforms.nonsingular( - vmin, vmax, expander=1e-13, tiny=1e-14) + vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=1e-13, tiny=1e-14) locs = self._raw_ticks(vmin, vmax) prune = self._prune - if prune == 'lower': + if prune == "lower": locs = locs[1:] - elif prune == 'upper': + elif prune == "upper": locs = locs[:-1] - elif prune == 'both': + elif prune == "both": locs = locs[1:-1] return self.raise_if_exceeds(locs) @@ -2254,14 +2366,16 @@ def view_limits(self, dmin, dmax): dmax = max(abs(dmin), abs(dmax)) dmin = -dmax - dmin, dmax = mtransforms.nonsingular( - dmin, dmax, expander=1e-12, tiny=1e-13) + dmin, dmax = mtransforms.nonsingular(dmin, dmax, expander=1e-12, tiny=1e-13) - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": return self._raw_ticks(dmin, dmax)[[0, -1]] else: return dmin, dmax + def __repr__(self): + return f"MaxNLocator(nbins={self._nbins}, steps={list(self._steps)})" + def _is_decade(x, *, base=10, rtol=None): """Return True if *x* is an integer power of *base*.""" @@ -2282,9 +2396,15 @@ def _decade_less_equal(x, base): If *x* is negative, the exponent will be *greater*. """ - return (x if x == 0 else - -_decade_greater_equal(-x, base) if x < 0 else - base ** np.floor(np.log(x) / np.log(base))) + return ( + x + if x == 0 + else ( + -_decade_greater_equal(-x, base) + if x < 0 + else base ** np.floor(np.log(x) / np.log(base)) + ) + ) def _decade_greater_equal(x, base): @@ -2293,9 +2413,15 @@ def _decade_greater_equal(x, base): If *x* is negative, the exponent will be *smaller*. """ - return (x if x == 0 else - -_decade_less_equal(-x, base) if x < 0 else - base ** np.ceil(np.log(x) / np.log(base))) + return ( + x + if x == 0 + else ( + -_decade_less_equal(-x, base) + if x < 0 + else base ** np.ceil(np.log(x) / np.log(base)) + ) + ) def _decade_less(x, base): @@ -2360,10 +2486,10 @@ def __init__(self, base=10.0, subs=(1.0,), *, numticks=None): otherwise falls back to 9. """ if numticks is None: - if mpl.rcParams['_internal.classic_mode']: + if mpl.rcParams["_internal.classic_mode"]: numticks = 15 else: - numticks = 'auto' + numticks = "auto" self._base = float(base) self._set_subs(subs) self.numticks = numticks @@ -2382,21 +2508,25 @@ def _set_subs(self, subs): Set the minor ticks for the log scaling every ``base**i*subs[j]``. """ if subs is None: # consistency with previous bad API - self._subs = 'auto' + self._subs = "auto" elif isinstance(subs, str): - _api.check_in_list(('all', 'auto'), subs=subs) + _api.check_in_list(("all", "auto"), subs=subs) self._subs = subs else: try: self._subs = np.asarray(subs, dtype=float) except ValueError as e: - raise ValueError("subs must be None, 'all', 'auto' or " - "a sequence of floats, not " - f"{subs}.") from e + raise ValueError( + "subs must be None, 'all', 'auto' or " + "a sequence of floats, not " + f"{subs}." + ) from e if self._subs.ndim != 1: - raise ValueError("A sequence passed to subs must be " - "1-dimensional, not " - f"{self._subs.ndim}-dimensional.") + raise ValueError( + "A sequence passed to subs must be " + "1-dimensional, not " + f"{self._subs.ndim}-dimensional." + ) def __call__(self): """Return the locations of the ticks.""" @@ -2407,15 +2537,22 @@ def _log_b(self, x): # Use specialized logs if possible, as they can be more accurate; e.g. # log(.001) / log(10) = -2.999... (whether math.log or np.log) due to # floating point error. - return (np.log10(x) if self._base == 10 else - np.log2(x) if self._base == 2 else - np.log(x) / np.log(self._base)) + return ( + np.log10(x) + if self._base == 10 + else np.log2(x) if self._base == 2 else np.log(x) / np.log(self._base) + ) def tick_values(self, vmin, vmax): n_request = ( - self.numticks if self.numticks != "auto" else - np.clip(self.axis.get_tick_space(), 2, 9) if self.axis is not None else - 9) + self.numticks + if self.numticks != "auto" + else ( + np.clip(self.axis.get_tick_space(), 2, 9) + if self.axis is not None + else 9 + ) + ) b = self._base if vmin <= 0.0: @@ -2424,7 +2561,8 @@ def tick_values(self, vmin, vmax): if vmin <= 0.0 or not np.isfinite(vmin): raise ValueError( - "Data has no positive values, and therefore cannot be log-scaled.") + "Data has no positive values, and therefore cannot be log-scaled." + ) if vmax < vmin: vmin, vmax = vmax, vmin @@ -2437,12 +2575,12 @@ def tick_values(self, vmin, vmax): if isinstance(self._subs, str): if n_avail >= 10 or b < 3: - if self._subs == 'auto': + if self._subs == "auto": return np.array([]) # no minor or major ticks else: subs = np.array([1.0]) # major ticks else: - _first = 2.0 if self._subs == 'auto' else 1.0 + _first = 2.0 if self._subs == "auto" else 1.0 subs = np.arange(_first, b) else: subs = self._subs @@ -2513,18 +2651,24 @@ def tick_values(self, vmin, vmax): if is_minor: if stride == 1 or n_avail <= 1: # Minor ticks start in the decade preceding the first major tick. - ticklocs = np.concatenate([ - subs * b**decade for decade in range(emin - 1, emax + 1)]) + ticklocs = np.concatenate( + [subs * b**decade for decade in range(emin - 1, emax + 1)] + ) else: ticklocs = np.array([]) else: ticklocs = b ** np.array(decades) - if (len(subs) > 1 - and stride == 1 - and (len(decades) - 2 # major - + ((vmin <= ticklocs) & (ticklocs <= vmax)).sum()) # minor - <= 1): + if ( + len(subs) > 1 + and stride == 1 + and ( + len(decades) + - 2 # major + + ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() + ) # minor + <= 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 major or minor tick, switch to AutoLocator. @@ -2538,7 +2682,7 @@ def view_limits(self, vmin, vmax): vmin, vmax = self.nonsingular(vmin, vmax) - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": vmin = _decade_less_equal(vmin, b) vmax = _decade_greater_equal(vmax, b) @@ -2551,8 +2695,8 @@ 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 " - "log-scaled.") + "Data has no positive values, and therefore cannot be " "log-scaled." + ) vmin, vmax = 1, 10 else: # Consider shared axises @@ -2566,6 +2710,9 @@ def nonsingular(self, vmin, vmax): vmax = _decade_greater(vmax, self._base) return vmin, vmax + def __repr__(self): + return f"LogLocator(base={self._base}, numticks={self.numticks})" + class SymmetricalLogLocator(Locator): """ @@ -2598,8 +2745,9 @@ def __init__(self, transform=None, subs=None, linthresh=None, base=None): self._base = base self._linthresh = linthresh else: - raise ValueError("Either transform, or both linthresh " - "and base, must be provided.") + raise ValueError( + "Either transform, or both linthresh " "and base, must be provided." + ) if subs is None: self._subs = [1.0] else: @@ -2649,9 +2797,9 @@ def tick_values(self, vmin, vmax): return sorted({vmin, 0, vmax}) # Lower log range is present - has_a = (vmin < -linthresh) + has_a = vmin < -linthresh # Upper log range is present - has_c = (vmax > linthresh) + has_c = vmax > linthresh # Check if linear range is present has_b = (has_a and vmax > -linthresh) or (has_c and vmin < linthresh) @@ -2682,8 +2830,7 @@ def get_log_range(lo, hi): decades = [] if has_a: - decades.extend(-1 * (base ** (np.arange(a_lo, a_hi, - stride)[::-1]))) + decades.extend(-1 * (base ** (np.arange(a_lo, a_hi, stride)[::-1]))) if has_b: decades.append(0.0) @@ -2711,7 +2858,7 @@ def view_limits(self, vmin, vmax): if vmax < vmin: vmin, vmax = vmax, vmin - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': + if mpl.rcParams["axes.autolimit_mode"] == "round_numbers": vmin = _decade_less_equal(vmin, b) vmax = _decade_greater_equal(vmax, b) if vmin == vmax: @@ -2720,6 +2867,11 @@ def view_limits(self, vmin, vmax): return mtransforms.nonsingular(vmin, vmax) + def __repr__(self): + return ( + f"SymmetricalLogLocator(base={self._base}, " f"linthresh={self._linthresh})" + ) + class AsinhLocator(Locator): """ @@ -2732,8 +2884,8 @@ class AsinhLocator(Locator): This API is provisional and may be revised in the future based on early user feedback. """ - def __init__(self, linear_width, numticks=11, symthresh=0.2, - base=10, subs=None): + + def __init__(self, linear_width, numticks=11, symthresh=0.2, base=10, subs=None): """ Parameters ---------- @@ -2763,8 +2915,7 @@ def __init__(self, linear_width, numticks=11, symthresh=0.2, self.base = base self.subs = subs - def set_params(self, numticks=None, symthresh=None, - base=None, subs=None): + def set_params(self, numticks=None, symthresh=None, base=None, subs=None): """Set parameters within this locator.""" if numticks is not None: self.numticks = numticks @@ -2786,8 +2937,9 @@ def __call__(self): def tick_values(self, vmin, vmax): # Construct a set of uniformly-spaced "on-screen" locations. - ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) - / self.linear_width) + ymin, ymax = self.linear_width * np.arcsinh( + np.array([vmin, vmax]) / self.linear_width + ) ys = np.linspace(ymin, ymax, self.numticks) zero_dev = abs(ys / (ymax - ymin)) if ymin * ymax < 0: @@ -2796,22 +2948,29 @@ def tick_values(self, vmin, vmax): # Transform the "on-screen" grid to the data space: xs = self.linear_width * np.sinh(ys / self.linear_width) - zero_xs = (ys == 0) + zero_xs = ys == 0 # Round the data-space values to be intuitive base-n numbers, keeping track of # positive and negative values separately and carefully treating the zero value. with np.errstate(divide="ignore"): # base ** log(0) = base ** -inf = 0. if self.base > 1: - pows = (np.sign(xs) - * self.base ** np.floor(np.log(abs(xs)) / math.log(self.base))) + pows = np.sign(xs) * self.base ** np.floor( + np.log(abs(xs)) / math.log(self.base) + ) qs = np.outer(pows, self.subs).flatten() if self.subs else pows else: # No need to adjust sign(pows), as it cancels out when computing qs. - pows = np.where(zero_xs, 1, 10**np.floor(np.log10(abs(xs)))) + pows = np.where(zero_xs, 1, 10 ** np.floor(np.log10(abs(xs)))) qs = pows * np.round(xs / pows) ticks = np.array(sorted(set(qs))) return ticks if len(ticks) >= 2 else np.linspace(vmin, vmax, self.numticks) + def __repr__(self): + return ( + f"AsinhLocator(linear_width={self.linear_width}, " + f"numticks={self.numticks})" + ) + class LogitLocator(MaxNLocator): """ @@ -2831,6 +2990,9 @@ def __init__(self, minor=False, *, nbins="auto"): self._minor = minor super().__init__(nbins=nbins, steps=[1, 2, 5, 10]) + def __repr__(self): + return f"LogitLocator(minor={self._minor}, nbins={self._nbins})" + def set_params(self, minor=None, **kwargs): """Set parameters within this locator.""" if minor is not None: @@ -2864,22 +3026,18 @@ def tick_values(self, vmin, vmax): # linscale: ... 1e-3 1e-2 1e-1 1/2 1-1e-1 1-1e-2 1-1e-3 ... # b-scale : ... -3 -2 -1 0 1 2 3 ... def ideal_ticks(x): - return 10 ** x if x < 0 else 1 - (10 ** (-x)) if x > 0 else 0.5 + return 10**x if x < 0 else 1 - (10 ** (-x)) if x > 0 else 0.5 vmin, vmax = self.nonsingular(vmin, vmax) binf = int( np.floor(np.log10(vmin)) if vmin < 0.5 - else 0 - if vmin < 0.9 - else -np.ceil(np.log10(1 - vmin)) + else 0 if vmin < 0.9 else -np.ceil(np.log10(1 - vmin)) ) bsup = int( np.ceil(np.log10(vmax)) if vmax <= 0.5 - else 1 - if vmax <= 0.9 - else -np.floor(np.log10(1 - vmax)) + else 1 if vmax <= 0.9 else -np.floor(np.log10(1 - vmax)) ) numideal = bsup - binf - 1 if numideal >= 2: @@ -2905,15 +3063,13 @@ def ideal_ticks(x): ticklocs = [] for b in range(binf, bsup): if b < -1: - ticklocs.extend(np.arange(2, 10) * 10 ** b) + ticklocs.extend(np.arange(2, 10) * 10**b) elif b == -1: ticklocs.extend(np.arange(2, 5) / 10) elif b == 0: ticklocs.extend(np.arange(6, 9) / 10) else: - ticklocs.extend( - 1 - np.arange(2, 10)[::-1] * 10 ** (-b - 1) - ) + ticklocs.extend(1 - np.arange(2, 10)[::-1] * 10 ** (-b - 1)) return self.raise_if_exceeds(np.array(ticklocs)) ticklocs = [ideal_ticks(b) for b in range(binf, bsup + 1)] return self.raise_if_exceeds(np.array(ticklocs)) @@ -2939,9 +3095,7 @@ def nonsingular(self, vmin, vmax): vmin, vmax = initial_range else: minpos = ( - self.axis.get_minpos() - if self.axis is not None - else standard_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. @@ -2967,19 +3121,23 @@ class AutoLocator(MaxNLocator): This is a subclass of `~matplotlib.ticker.MaxNLocator`, with parameters *nbins = 'auto'* and *steps = [1, 2, 2.5, 5, 10]*. """ + def __init__(self): """ To know the values of the non-public parameters, please have a look to the defaults of `~matplotlib.ticker.MaxNLocator`. """ - if mpl.rcParams['_internal.classic_mode']: + if mpl.rcParams["_internal.classic_mode"]: nbins = 9 steps = [1, 2, 5, 10] else: - nbins = 'auto' + nbins = "auto" steps = [1, 2, 2.5, 5, 10] super().__init__(nbins=nbins, steps=steps) + def __repr__(self): + return "AutoLocator()" + class AutoMinorLocator(Locator): """ @@ -3006,8 +3164,8 @@ def __init__(self, n=None): def __call__(self): # docstring inherited - if self.axis.get_scale() == 'log': - _api.warn_external('AutoMinorLocator does not work on logarithmic scales') + if self.axis.get_scale() == "log": + _api.warn_external("AutoMinorLocator does not work on logarithmic scales") return [] majorlocs = np.unique(self.axis.get_majorticklocs()) @@ -3020,10 +3178,14 @@ def __call__(self): if self.ndivs is None: self.ndivs = mpl.rcParams[ - 'ytick.minor.ndivs' if self.axis.axis_name == 'y' - else 'xtick.minor.ndivs'] # for x and z axis + ( + "ytick.minor.ndivs" + if self.axis.axis_name == "y" + else "xtick.minor.ndivs" + ) + ] # for x and z axis - if self.ndivs == 'auto': + if self.ndivs == "auto": majorstep_mantissa = 10 ** (np.log10(majorstep) % 1) ndivs = 5 if np.isclose(majorstep_mantissa, [1, 2.5, 5, 10]).any() else 4 else: @@ -3041,4 +3203,8 @@ def __call__(self): def tick_values(self, vmin, vmax): raise NotImplementedError( - f"Cannot get tick locations for a {type(self).__name__}") + f"Cannot get tick locations for a {type(self).__name__}" + ) + + def __repr__(self): + return f"AutoMinorLocator(n={self.ndivs})" From 48e24f93664a4c36f91a970cf8e9ad363f426127 Mon Sep 17 00:00:00 2001 From: DavidAG Date: Wed, 17 Dec 2025 20:38:13 +0100 Subject: [PATCH 2/2] Increase test_fontcache_thread_safe timeout to 30s The 10 second timeout was too short for Windows CI, causing intermittent failures. Increased to 30 seconds to provide sufficient headroom on slow CI machines. --- lib/matplotlib/tests/test_font_manager.py | 224 ++++++++++++---------- 1 file changed, 127 insertions(+), 97 deletions(-) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 24421b8e30b3..a30fb13b8fd8 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -13,23 +13,31 @@ import matplotlib as mpl from matplotlib.font_manager import ( - findfont, findSystemFonts, FontEntry, FontProperties, fontManager, - json_dump, json_load, get_font, is_opentype_cff_font, - MSUserFontDirectories, ttfFontProperty, - _get_fontconfig_fonts, _normalize_weight) + findfont, + findSystemFonts, + FontEntry, + FontProperties, + fontManager, + json_dump, + json_load, + get_font, + is_opentype_cff_font, + MSUserFontDirectories, + ttfFontProperty, + _get_fontconfig_fonts, + _normalize_weight, +) from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing -has_fclist = shutil.which('fc-list') is not None +has_fclist = shutil.which("fc-list") is not None def test_font_priority(): - with rc_context(rc={ - 'font.sans-serif': - ['cmmi10', 'Bitstream Vera Sans']}): + with rc_context(rc={"font.sans-serif": ["cmmi10", "Bitstream Vera Sans"]}): fontfile = findfont(FontProperties(family=["sans-serif"])) - assert Path(fontfile).name == 'cmmi10.ttf' + assert Path(fontfile).name == "cmmi10.ttf" # Smoketest get_charmap, which isn't used internally anymore font = get_font(fontfile) @@ -41,12 +49,19 @@ def test_font_priority(): def test_score_weight(): assert 0 == fontManager.score_weight("regular", "regular") assert 0 == fontManager.score_weight("bold", "bold") - assert (0 < fontManager.score_weight(400, 400) < - fontManager.score_weight("normal", "bold")) - assert (0 < fontManager.score_weight("normal", "regular") < - fontManager.score_weight("normal", "bold")) - assert (fontManager.score_weight("normal", "regular") == - fontManager.score_weight(400, 400)) + assert ( + 0 + < fontManager.score_weight(400, 400) + < fontManager.score_weight("normal", "bold") + ) + assert ( + 0 + < fontManager.score_weight("normal", "regular") + < fontManager.score_weight("normal", "bold") + ) + assert fontManager.score_weight("normal", "regular") == fontManager.score_weight( + 400, 400 + ) def test_json_serialization(tmp_path): @@ -55,56 +70,62 @@ def test_json_serialization(tmp_path): json_dump(fontManager, tmp_path / "fontlist.json") copy = json_load(tmp_path / "fontlist.json") with warnings.catch_warnings(): - warnings.filterwarnings('ignore', 'findfont: Font family.*not found') - for prop in ({'family': 'STIXGeneral'}, - {'family': 'Bitstream Vera Sans', 'weight': 700}, - {'family': 'no such font family'}): + warnings.filterwarnings("ignore", "findfont: Font family.*not found") + for prop in ( + {"family": "STIXGeneral"}, + {"family": "Bitstream Vera Sans", "weight": 700}, + {"family": "no such font family"}, + ): fp = FontProperties(**prop) - assert (fontManager.findfont(fp, rebuild_if_missing=False) == - copy.findfont(fp, rebuild_if_missing=False)) + assert fontManager.findfont(fp, rebuild_if_missing=False) == copy.findfont( + fp, rebuild_if_missing=False + ) def test_otf(): - fname = '/usr/share/fonts/opentype/freefont/FreeMono.otf' + fname = "/usr/share/fonts/opentype/freefont/FreeMono.otf" if Path(fname).exists(): assert is_opentype_cff_font(fname) for f in fontManager.ttflist: - if 'otf' in f.fname: - with open(f.fname, 'rb') as fd: - res = fd.read(4) == b'OTTO' + if "otf" in f.fname: + with open(f.fname, "rb") as fd: + res = fd.read(4) == b"OTTO" assert res == is_opentype_cff_font(f.fname) -@pytest.mark.skipif(sys.platform == "win32" or not has_fclist, - reason='no fontconfig installed') +@pytest.mark.skipif( + sys.platform == "win32" or not has_fclist, reason="no fontconfig installed" +) def test_get_fontconfig_fonts(): assert len(_get_fontconfig_fonts()) > 1 -@pytest.mark.parametrize('factor', [2, 4, 6, 8]) +@pytest.mark.parametrize("factor", [2, 4, 6, 8]) def test_hinting_factor(factor): font = findfont(FontProperties(family=["sans-serif"])) font1 = get_font(font, hinting_factor=1) font1.clear() font1.set_size(12, 100) - font1.set_text('abc') + font1.set_text("abc") expected = font1.get_width_height() hinted_font = get_font(font, hinting_factor=factor) hinted_font.clear() hinted_font.set_size(12, 100) - hinted_font.set_text('abc') + hinted_font.set_text("abc") # Check that hinting only changes text layout by a small (10%) amount. - np.testing.assert_allclose(hinted_font.get_width_height(), expected, - rtol=0.1) + np.testing.assert_allclose(hinted_font.get_width_height(), expected, rtol=0.1) def test_utf16m_sfnt(): try: # seguisbi = Microsoft Segoe UI Semibold - entry = next(entry for entry in fontManager.ttflist - if Path(entry.fname).name == "seguisbi.ttf") + entry = next( + entry + for entry in fontManager.ttflist + if Path(entry.fname).name == "seguisbi.ttf" + ) except StopIteration: pytest.skip("Couldn't find seguisbi.ttf font to test against.") else: @@ -118,7 +139,7 @@ def test_find_ttc(): if Path(findfont(fp)).name != "wqy-zenhei.ttc": pytest.skip("Font wqy-zenhei.ttc may be missing") fig, ax = plt.subplots() - ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) + ax.text(0.5, 0.5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: fig.savefig(BytesIO(), format=fmt) @@ -130,7 +151,7 @@ def test_find_noto(): pytest.skip(f"Noto Sans CJK SC font may be missing (found {name})") fig, ax = plt.subplots() - ax.text(0.5, 0.5, 'Hello, 你好', fontproperties=fp) + ax.text(0.5, 0.5, "Hello, 你好", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: fig.savefig(BytesIO(), format=fmt) @@ -138,38 +159,43 @@ def test_find_noto(): def test_find_invalid(tmp_path): with pytest.raises(FileNotFoundError): - get_font(tmp_path / 'non-existent-font-name.ttf') + get_font(tmp_path / "non-existent-font-name.ttf") with pytest.raises(FileNotFoundError): - get_font(str(tmp_path / 'non-existent-font-name.ttf')) + get_font(str(tmp_path / "non-existent-font-name.ttf")) with pytest.raises(FileNotFoundError): - get_font(bytes(tmp_path / 'non-existent-font-name.ttf')) + get_font(bytes(tmp_path / "non-existent-font-name.ttf")) # Not really public, but get_font doesn't expose non-filename constructor. from matplotlib.ft2font import FT2Font - with pytest.raises(TypeError, match='font file or a binary-mode file'): + + with pytest.raises(TypeError, match="font file or a binary-mode file"): FT2Font(StringIO()) # type: ignore[arg-type] -@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist, - reason='only Linux with fontconfig installed') +@pytest.mark.skipif( + sys.platform != "linux" or not has_fclist, + reason="only Linux with fontconfig installed", +) def test_user_fonts_linux(tmpdir, monkeypatch): - font_test_file = 'mpltest.ttf' + font_test_file = "mpltest.ttf" # Precondition: the test font should not be available fonts = findSystemFonts() if any(font_test_file in font for font in fonts): - pytest.skip(f'{font_test_file} already exists in system fonts') + pytest.skip(f"{font_test_file} already exists in system fonts") # Prepare a temporary user font directory - user_fonts_dir = tmpdir.join('fonts') + user_fonts_dir = tmpdir.join("fonts") user_fonts_dir.ensure(dir=True) - shutil.copyfile(Path(__file__).parent / 'data' / font_test_file, - user_fonts_dir.join(font_test_file)) + shutil.copyfile( + Path(__file__).parent / "data" / font_test_file, + user_fonts_dir.join(font_test_file), + ) with monkeypatch.context() as m: - m.setenv('XDG_DATA_HOME', str(tmpdir)) + m.setenv("XDG_DATA_HOME", str(tmpdir)) _get_fontconfig_fonts.cache_clear() # Now, the font should be available fonts = findSystemFonts() @@ -181,33 +207,37 @@ def test_user_fonts_linux(tmpdir, monkeypatch): def test_addfont_as_path(): """Smoke test that addfont() accepts pathlib.Path.""" - font_test_file = 'mpltest.ttf' - path = Path(__file__).parent / 'data' / font_test_file + font_test_file = "mpltest.ttf" + path = Path(__file__).parent / "data" / font_test_file try: fontManager.addfont(path) - added, = (font for font in fontManager.ttflist - if font.fname.endswith(font_test_file)) + (added,) = ( + font for font in fontManager.ttflist if font.fname.endswith(font_test_file) + ) fontManager.ttflist.remove(added) finally: - to_remove = [font for font in fontManager.ttflist - if font.fname.endswith(font_test_file)] + to_remove = [ + font for font in fontManager.ttflist if font.fname.endswith(font_test_file) + ] for font in to_remove: fontManager.ttflist.remove(font) -@pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') +@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_user_fonts_win32(): - if not (os.environ.get('APPVEYOR') or os.environ.get('TF_BUILD')): - pytest.xfail("This test should only run on CI (appveyor or azure) " - "as the developer's font directory should remain " - "unchanged.") + if not (os.environ.get("APPVEYOR") or os.environ.get("TF_BUILD")): + pytest.xfail( + "This test should only run on CI (appveyor or azure) " + "as the developer's font directory should remain " + "unchanged." + ) pytest.xfail("We need to update the registry for this test to work") - font_test_file = 'mpltest.ttf' + font_test_file = "mpltest.ttf" # Precondition: the test font should not be available fonts = findSystemFonts() if any(font_test_file in font for font in fonts): - pytest.skip(f'{font_test_file} already exists in system fonts') + pytest.skip(f"{font_test_file} already exists in system fonts") user_fonts_dir = MSUserFontDirectories[0] @@ -216,7 +246,7 @@ def test_user_fonts_win32(): os.makedirs(user_fonts_dir) # Copy the test font to the user font directory - shutil.copy(Path(__file__).parent / 'data' / font_test_file, user_fonts_dir) + shutil.copy(Path(__file__).parent / "data" / font_test_file, user_fonts_dir) # Now, the font should be available fonts = findSystemFonts() @@ -229,8 +259,9 @@ def _model_handler(_): plt.close() -@pytest.mark.skipif(not hasattr(os, "register_at_fork"), - reason="Cannot register at_fork handlers") +@pytest.mark.skipif( + not hasattr(os, "register_at_fork"), reason="Cannot register at_fork handlers" +) def test_fork(): _model_handler(0) # Make sure the font cache is filled. ctx = multiprocessing.get_context("fork") @@ -243,8 +274,7 @@ def test_missing_family(caplog): with caplog.at_level("WARNING"): findfont("sans") assert [rec.getMessage() for rec in caplog.records] == [ - "findfont: Font family ['sans'] not found. " - "Falling back to DejaVu Sans.", + "findfont: Font family ['sans'] not found. " "Falling back to DejaVu Sans.", "findfont: Generic family 'sans' not found because none of the " "following families were found: this-font-does-not-exist", ] @@ -284,9 +314,9 @@ def bad_idea(n): def test_fontcache_thread_safe(): - pytest.importorskip('threading') + pytest.importorskip("threading") - subprocess_run_helper(_test_threading, timeout=10) + subprocess_run_helper(_test_threading, timeout=30) def test_lockfilefailure(tmp_path): @@ -304,15 +334,15 @@ def test_lockfilefailure(tmp_path): "import os;" "p = matplotlib.get_cachedir();" "os.chmod(p, 0o555);" - "import matplotlib.font_manager;" + "import matplotlib.font_manager;", ], - env={**os.environ, 'MPLCONFIGDIR': str(tmp_path)}, - check=True + env={**os.environ, "MPLCONFIGDIR": str(tmp_path)}, + check=True, ) def test_fontentry_dataclass(): - fontent = FontEntry(name='font-name') + fontent = FontEntry(name="font-name") png = fontent._repr_png_() img = Image.open(BytesIO(png)) @@ -320,20 +350,20 @@ def test_fontentry_dataclass(): assert img.height > 0 html = fontent._repr_html_() - assert html.startswith("