From e44c8ccd0f398d21cec3b259471fb5e7a5b12230 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 11 Feb 2019 14:45:26 +0100 Subject: [PATCH] Add nonsingular to the locator base class, and use it in set_*lim too. Currently, some Locator subclasses define a nonsingular() method, which is used by autoscale_view() in case the autoscaling would return a degenerate interval, to expand the view limits; autoscale_view() falls back on a default in case the nonsingular() method does not exist. Move that default to the Locator base class. Also use that nonsingular() method when a degenerate interval is passed to set_xlim/set_ylim/etc., instead of always hardcoding the same linear expansion. (Semantically the correct place for this nonsingular() method is probably on the scale class rather than on the locator...) The changes to test_ticker are due to the fact that the default expansion is now by 5%, so there's no offset text anymore in the left=right=123 case (as it gets expanded to 116.85, 129.15). --- doc/api/next_api_changes/2019-02-12-AL.rst | 7 ++++ lib/matplotlib/axes/_base.py | 37 +++++++----------- lib/matplotlib/tests/test_dates.py | 3 +- lib/matplotlib/tests/test_ticker.py | 2 +- lib/matplotlib/ticker.py | 15 +++++--- lib/mpl_toolkits/mplot3d/axes3d.py | 45 ++++++++-------------- 6 files changed, 48 insertions(+), 61 deletions(-) create mode 100644 doc/api/next_api_changes/2019-02-12-AL.rst diff --git a/doc/api/next_api_changes/2019-02-12-AL.rst b/doc/api/next_api_changes/2019-02-12-AL.rst new file mode 100644 index 000000000000..6fd465ec13fa --- /dev/null +++ b/doc/api/next_api_changes/2019-02-12-AL.rst @@ -0,0 +1,7 @@ +Changes in handling of degenerate bounds passed to `set_xlim` +````````````````````````````````````````````````````````````` + +When bounds passed to `set_xlim` (`set_xlim`, etc.) are degenerate (i.e. the +lower and upper value are equal), the method used to "expand" the bounds now +matches the expansion behavior of autoscaling when the plot contains a single +x-value, and should in particular produce nicer limits for non-linear scales. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 2e8359b4288b..03aad5797c99 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2423,13 +2423,7 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, bb = mtransforms.BboxBase.union(dl) x0, x1 = getattr(bb, interval) locator = axis.get_major_locator() - try: - # e.g., DateLocator has its own nonsingular() - x0, x1 = locator.nonsingular(x0, x1) - except AttributeError: - # Default nonsingular for, e.g., MaxNLocator - x0, x1 = mtransforms.nonsingular( - x0, x1, increasing=False, expander=0.05) + x0, x1 = locator.nonsingular(x0, x1) # Add the margin in figure space and then transform back, to handle # non-linear scales. @@ -2443,7 +2437,7 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minpos) x0t, x1t = transform.transform([x0, x1]) - if (np.isfinite(x1t) and np.isfinite(x0t)): + if np.isfinite(x1t) and np.isfinite(x0t): delta = (x1t - x0t) * margin else: # If at least one bound isn't finite, set margin to zero @@ -3205,13 +3199,6 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, if right is None: right = old_right - if left == right: - cbook._warn_external( - ('Attempting to set identical left==right results\n' - 'in singular transformations; automatically expanding.\n' - 'left=%s, right=%s') % (left, right)) - left, right = mtransforms.nonsingular(left, right, increasing=False) - if self.get_xscale() == 'log': if left <= 0: cbook._warn_external( @@ -3225,7 +3212,11 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, 'log-scaled axis.\n' 'Invalid limit will be ignored.') right = old_right - + if left == right: + cbook._warn_external( + f"Attempting to set identical left == right == {left} results " + f"in singular transformations; automatically expanding.") + left, right = self.xaxis.get_major_locator().nonsingular(left, right) left, right = self.xaxis.limit_range_for_scale(left, right) self.viewLim.intervalx = (left, right) @@ -3592,14 +3583,6 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, if top is None: top = old_top - if bottom == top: - cbook._warn_external( - ('Attempting to set identical bottom==top results\n' - 'in singular transformations; automatically expanding.\n' - 'bottom=%s, top=%s') % (bottom, top)) - - bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) - if self.get_yscale() == 'log': if bottom <= 0: cbook._warn_external( @@ -3613,6 +3596,12 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, 'log-scaled axis.\n' 'Invalid limit will be ignored.') top = old_top + if bottom == top: + cbook._warn_external( + f"Attempting to set identical bottom == top == {bottom} " + f"results in singular transformations; automatically " + f"expanding.") + bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) bottom, top = self.yaxis.limit_range_for_scale(bottom, top) self.viewLim.intervaly = (bottom, top) diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 4a232c45e87a..f934fee5d039 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -156,7 +156,8 @@ def test_too_many_date_ticks(): with pytest.warns(UserWarning) as rec: ax.set_xlim((t0, tf), auto=True) assert len(rec) == 1 - assert 'Attempting to set identical left==right' in str(rec[0].message) + assert \ + 'Attempting to set identical left == right' in str(rec[0].message) ax.plot([], []) ax.xaxis.set_major_locator(mdates.DayLocator()) with pytest.raises(RuntimeError): diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 2ca5cbe58466..9dddb69cf764 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -302,7 +302,7 @@ class TestScalarFormatter(object): (1233999, 1234001, 1234000), (-1234001, -1233999, -1234000), (1, 1, 1), - (123, 123, 120), + (123, 123, 0), # Test cases courtesy of @WeatherGod (.4538, .4578, .45), (3789.12, 3783.1, 3780), diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 13ffd9f48b6a..42fd84bf8ea0 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1411,9 +1411,9 @@ class Locator(TickHelper): """ Determine the tick locations; - Note, you should not use the same locator between different - :class:`~matplotlib.axis.Axis` because the locator stores references to - the Axis data and view limits + Note that the same locator should not be used across multiple + `~matplotlib.axis.Axis` because the locator stores references to the Axis + data and view limits. """ # Some automatic tick locators can generate so many ticks they @@ -1463,12 +1463,15 @@ def raise_if_exceeds(self, locs): len(locs), locs[0], locs[-1])) return locs + def nonsingular(self, v0, v1): + """Modify the endpoints of a range as needed to avoid singularities.""" + return mtransforms.nonsingular(v0, v1, increasing=False, expander=.05) + def view_limits(self, vmin, vmax): """ - select a scale for the range from vmin to vmax + Select a scale for the range from vmin to vmax. - Normally this method is overridden by subclasses to - change locator behaviour. + Subclasses should override this method to change locator behaviour. """ return mtransforms.nonsingular(vmin, vmax) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index afef95223dc0..eb9b9ee46433 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -536,11 +536,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self._shared_x_axes.clean() x0, x1 = self.xy_dataLim.intervalx xlocator = self.xaxis.get_major_locator() - try: - x0, x1 = xlocator.nonsingular(x0, x1) - except AttributeError: - x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, - expander=0.05) + x0, x1 = xlocator.nonsingular(x0, x1) if self._xmargin > 0: delta = (x1 - x0) * self._xmargin x0 -= delta @@ -553,11 +549,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self._shared_y_axes.clean() y0, y1 = self.xy_dataLim.intervaly ylocator = self.yaxis.get_major_locator() - try: - y0, y1 = ylocator.nonsingular(y0, y1) - except AttributeError: - y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, - expander=0.05) + y0, y1 = ylocator.nonsingular(y0, y1) if self._ymargin > 0: delta = (y1 - y0) * self._ymargin y0 -= delta @@ -570,11 +562,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self._shared_z_axes.clean() z0, z1 = self.zz_dataLim.intervalx zlocator = self.zaxis.get_major_locator() - try: - z0, z1 = zlocator.nonsingular(z0, z1) - except AttributeError: - z0, z1 = mtransforms.nonsingular(z0, z1, increasing=False, - expander=0.05) + z0, z1 = zlocator.nonsingular(z0, z1) if self._zmargin > 0: delta = (z1 - z0) * self._zmargin z0 -= delta @@ -633,10 +621,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False, if left == right: cbook._warn_external( - ('Attempting to set identical left==right results\n' - 'in singular transformations; automatically expanding.\n' - 'left=%s, right=%s') % (left, right)) - left, right = mtransforms.nonsingular(left, right, increasing=False) + f"Attempting to set identical left == right == {left} results " + f"in singular transformations; automatically expanding.") + left, right = self.xaxis.get_major_locator().nonsingular(left, right) left, right = self.xaxis.limit_range_for_scale(left, right) self.xy_viewLim.intervalx = (left, right) @@ -689,12 +676,12 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, if top is None: top = old_top - if top == bottom: + if bottom == top: cbook._warn_external( - ('Attempting to set identical bottom==top results\n' - 'in singular transformations; automatically expanding.\n' - 'bottom=%s, top=%s') % (bottom, top)) - bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + f"Attempting to set identical bottom == top == {bottom} " + f"results in singular transformations; automatically " + f"expanding.") + bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) bottom, top = self.yaxis.limit_range_for_scale(bottom, top) self.xy_viewLim.intervaly = (bottom, top) @@ -747,12 +734,12 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, if top is None: top = old_top - if top == bottom: + if bottom == top: cbook._warn_external( - ('Attempting to set identical bottom==top results\n' - 'in singular transformations; automatically expanding.\n' - 'bottom=%s, top=%s') % (bottom, top)) - bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + f"Attempting to set identical bottom == top == {bottom} " + f"results in singular transformations; automatically " + f"expanding.") + bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top) bottom, top = self.zaxis.limit_range_for_scale(bottom, top) self.zz_viewLim.intervalx = (bottom, top)