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)