From b676ca18cc90c85aae0bddce6ae49864ed934878 Mon Sep 17 00:00:00 2001 From: Jean-Benoist Leger Date: Tue, 30 Jul 2019 16:29:02 +0200 Subject: [PATCH 1/4] LogitLocator: allows nonsingular works with no plotted values solves #14743 --- lib/matplotlib/ticker.py | 61 ++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 16e8e02b2c5d..5aa069134e81 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2792,33 +2792,46 @@ def ideal_ticks(x): return MaxNLocator.tick_values(self, vmin, vmax) def nonsingular(self, vmin, vmax): - initial_range = (1e-7, 1 - 1e-7) - if not np.isfinite(vmin) or not np.isfinite(vmax): - return initial_range # no data plotted yet - + standard_minpos = 1e-7 + initial_range = (standard_minpos, 1 - standard_minpos) + swap_vlims = False if vmin > vmax: + swap_vlims = True vmin, vmax = vmax, vmin - - # what to do if a window beyond ]0, 1[ is chosen - if self.axis is not None: - minpos = self.axis.get_minpos() - if not np.isfinite(minpos): - return initial_range # again, no data plotted + if not np.isfinite(vmin) or not np.isfinite(vmax): + vmin, vmax = initial_range # Initial range, no data plotted yet. + elif vmax <= 0: + cbook._warn_external( + "Data has no positive values, and therefore cannot be " + "logit-scaled." + ) + vmin, vmax = initial_range + elif vmin >= 1: + cbook._warn_external( + "Data has no values smaller than one, and therefore cannot " + "be logit-scaled." + ) + vmin, vmax = initial_range else: - minpos = 1e-7 # should not occur in normal use - - # NOTE: for vmax, we should query a property similar to get_minpos, but - # related to the maximal, less-than-one data point. Unfortunately, - # Bbox._minpos is defined very deep in the BBox and updated with data, - # so for now we use 1 - minpos as a substitute. - - if vmin <= 0: - vmin = minpos - if vmax >= 1: - vmax = 1 - minpos - if vmin == vmax: - return 0.1 * vmin, 1 - 0.1 * vmin - + minpos = ( + self.axis.get_minpos() + if self.axis is not None + else standard_minpos + ) + if not np.isfinite(minpos): + minpos = standard_minpos # This should never take effect. + if vmin <= 0: + vmin = minpos + # NOTE: for vmax, we should query a property similar to get_minpos, + # but related to the maximal, less-than-one data point. + # Unfortunately, Bbox._minpos is defined very deep in the BBox and + # updated with data, so for now we use 1 - minpos as a substitute. + if vmax >= 1: + vmax = 1 - minpos + if vmin == vmax: + vmin, vmax = 0.1 * vmin, 1 - 0.1 * vmin + if swap_vlims: + vmin, vmax = vmax, vmin return vmin, vmax From 787f6b8bb164df7c0b5c4f58213a75634aa39987 Mon Sep 17 00:00:00 2001 From: Jean-Benoist Leger Date: Tue, 30 Jul 2019 16:34:07 +0200 Subject: [PATCH 2/4] TestLogitLocator: append a test on nonsingular to avoid issues as #14743 --- lib/matplotlib/tests/test_ticker.py | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 191afa0b9950..117642186b08 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1,5 +1,6 @@ import warnings import re +import itertools import numpy as np from numpy.testing import assert_almost_equal, assert_array_equal @@ -366,6 +367,44 @@ def test_minor_attr(self): loc.set_params(minor=False) assert not loc.minor + acceptable_vmin_vmax = [ + *(2.5 ** np.arange(-3, 0)), + *(1 - 2.5 ** np.arange(-3, 0)), + ] + + @pytest.mark.parametrize( + "lims", + [ + (a, b) + for (a, b) in itertools.product(acceptable_vmin_vmax, repeat=2) + if a != b + ], + ) + def test_nonsingular_ok(self, lims): + """ + Create logit locator, and test the nonsingular method for acceptable + value + """ + loc = mticker.LogitLocator() + lims2 = loc.nonsingular(*lims) + assert lims == lims2 + + @pytest.mark.parametrize("okval", acceptable_vmin_vmax) + def test_nonsingular_nok(self, okval): + """ + Create logit locator, and test the nonsingular method for non + acceptable value + """ + loc = mticker.LogitLocator() + vmin, vmax = (-1, okval) + vmin2, vmax2 = loc.nonsingular(vmin, vmax) + assert vmax2 == vmax + assert 0 < vmin2 < vmax2 + vmin, vmax = (okval, 2) + vmin2, vmax2 = loc.nonsingular(vmin, vmax) + assert vmin2 == vmin + assert vmin2 < vmax2 < 1 + class TestFixedLocator: def test_set_params(self): From dfa83677f97235a689b6a72351e4aec87579939a Mon Sep 17 00:00:00 2001 From: Jean-Benoist Leger Date: Wed, 31 Jul 2019 14:47:14 +0200 Subject: [PATCH 3/4] LogitLocator: merge two warnings in nonsingular method --- lib/matplotlib/ticker.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 5aa069134e81..d1b58febde56 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2800,18 +2800,14 @@ def nonsingular(self, vmin, vmax): vmin, vmax = vmax, vmin if not np.isfinite(vmin) or not np.isfinite(vmax): vmin, vmax = initial_range # Initial range, no data plotted yet. - elif vmax <= 0: + elif vmax <= 0 or vmin >= 1: + # vmax <= 0 occurs when all values are negative + # vmin >= 1 occurs when all values are greater than one cbook._warn_external( - "Data has no positive values, and therefore cannot be " + "Data has no values between 0 and 1, and therefore cannot be " "logit-scaled." ) vmin, vmax = initial_range - elif vmin >= 1: - cbook._warn_external( - "Data has no values smaller than one, and therefore cannot " - "be logit-scaled." - ) - vmin, vmax = initial_range else: minpos = ( self.axis.get_minpos() From e14ee8452de03b7d08b5bdcc41162563205bb437 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 4 Sep 2019 12:16:55 -0400 Subject: [PATCH 4/4] MNT: remove the swap_vlim dance In https://github.com/matplotlib/matplotlib/pull/14624 we reverted changing the order of the limits to match the input order. Doing the same here for consistency. Sort the limits before comparing. We want to make sure that it works with the values reversed, but do not expect non-singular to preserve the order. --- lib/matplotlib/tests/test_ticker.py | 2 +- lib/matplotlib/ticker.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 117642186b08..165f8b8c81e4 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -387,7 +387,7 @@ def test_nonsingular_ok(self, lims): """ loc = mticker.LogitLocator() lims2 = loc.nonsingular(*lims) - assert lims == lims2 + assert sorted(lims) == sorted(lims2) @pytest.mark.parametrize("okval", acceptable_vmin_vmax) def test_nonsingular_nok(self, okval): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index d1b58febde56..1aacec42941b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2794,9 +2794,7 @@ def ideal_ticks(x): def nonsingular(self, vmin, vmax): standard_minpos = 1e-7 initial_range = (standard_minpos, 1 - standard_minpos) - swap_vlims = False if vmin > vmax: - swap_vlims = True vmin, vmax = vmax, vmin if not np.isfinite(vmin) or not np.isfinite(vmax): vmin, vmax = initial_range # Initial range, no data plotted yet. @@ -2826,8 +2824,7 @@ def nonsingular(self, vmin, vmax): vmax = 1 - minpos if vmin == vmax: vmin, vmax = 0.1 * vmin, 1 - 0.1 * vmin - if swap_vlims: - vmin, vmax = vmax, vmin + return vmin, vmax