From 0911f8abe14fab9670c9b9e19a565b117520dcf7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 18 Apr 2019 12:00:59 +0200 Subject: [PATCH] Don't let margins expand polar plots to negative radii by default. This is implemented by altering the semantics of sticky edges as documented in the changelog. As it turns out, this change also improves consistency for streamplot(): - in test_streamplot.py::test_{linewidth,mask_and_nans}, the change is necessary because the axes limits would now be (-3, 3) (matching the vector field limits), whereas they were previously xlim=(-3.0, 2.9999999999999947), ylim=(-3.0000000000000004, 2.9999999999999947) (they can be inspected with `plt.gcf().canvas.draw(); print(plt.gca().get_xlim(), plt.gca().get_ylim())`). - in test_streamplot.py::test_maxlength, note that the previous version expanded the axes limits *beyond* (-3, 3), whereas the current doesn't do so anymore. It doesn't actually make much sense if the vector field limits are applied if the streamplot goes all the way to the edges, but are ignored otherwise. --- doc/api/next_api_changes/2019-04-17-AL.rst | 24 ++++++++++ lib/matplotlib/axes/_base.py | 51 ++++++++++++---------- lib/matplotlib/tests/test_axes.py | 6 +++ lib/matplotlib/tests/test_streamplot.py | 24 +++++++--- 4 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 doc/api/next_api_changes/2019-04-17-AL.rst diff --git a/doc/api/next_api_changes/2019-04-17-AL.rst b/doc/api/next_api_changes/2019-04-17-AL.rst new file mode 100644 index 000000000000..d6e3862bf9ea --- /dev/null +++ b/doc/api/next_api_changes/2019-04-17-AL.rst @@ -0,0 +1,24 @@ +Change in the application of ``Artist.sticky_edges`` +```````````````````````````````````````````````````` + +Previously, the ``sticky_edges`` attribute of artists was a list of values such +that if an axis limit coincides with a sticky edge, it would not be expanded by +the axes margins (this is the mechanism that e.g. prevents margins from being +added around images). + +``sticky_edges`` now have an additional effect on margins application: even if +an axis limit did not coincide with a sticky edge, it cannot *cross* a sticky +edge through margin application -- instead, the margins will only expand the +axis limit until it bumps against the sticky edge. + +This change improves the margins of axes displaying a `~Axes.streamplot`: + +- if the streamplot goes all the way to the edges of the vector field, then the + axis limits are set to match exactly the vector field limits (whereas they + would be sometimes be off by a small floating point error previously). +- if the streamplot does not reach the edges of the vector field (e.g., due to + the use of ``start_points`` and ``maxlength``), then margins expansion will + not cross the the vector field limits anymore. + +This change is also used internally to ensure that polar plots don't display +negative *r* values unless the user really passes in a negative value. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 42a95d735abf..bfbe715b703d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2402,14 +2402,14 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): (self._xmargin and scalex and self._autoscaleXon) or (self._ymargin and scaley and self._autoscaleYon)): stickies = [artist.sticky_edges for artist in self.get_children()] - x_stickies = np.array([x for sticky in stickies for x in sticky.x]) - y_stickies = np.array([y for sticky in stickies for y in sticky.y]) - if self.get_xscale().lower() == 'log': - x_stickies = x_stickies[x_stickies > 0] - if self.get_yscale().lower() == 'log': - y_stickies = y_stickies[y_stickies > 0] else: # Small optimization. - x_stickies, y_stickies = [], [] + stickies = [] + x_stickies = np.sort([x for sticky in stickies for x in sticky.x]) + y_stickies = np.sort([y for sticky in stickies for y in sticky.y]) + if self.get_xscale().lower() == 'log': + x_stickies = x_stickies[x_stickies > 0] + if self.get_yscale().lower() == 'log': + y_stickies = y_stickies[y_stickies > 0] def handle_single_axis(scale, autoscaleon, shared_axes, interval, minpos, axis, margin, stickies, set_bound): @@ -2450,29 +2450,34 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, locator = axis.get_major_locator() x0, x1 = locator.nonsingular(x0, x1) + # Prevent margin addition from crossing a sticky value. Small + # tolerances (whose values come from isclose()) must be used due to + # floating point issues with streamplot. + def tol(x): return 1e-5 * abs(x) + 1e-8 + # Index of largest element < x0 + tol, if any. + i0 = stickies.searchsorted(x0 + tol(x0)) - 1 + x0bound = stickies[i0] if i0 != -1 else None + # Index of smallest element > x1 - tol, if any. + i1 = stickies.searchsorted(x1 - tol(x1)) + x1bound = stickies[i1] if i1 != len(stickies) else None + # Add the margin in figure space and then transform back, to handle # non-linear scales. minpos = getattr(bb, minpos) transform = axis.get_transform() inverse_trans = transform.inverted() - # We cannot use exact equality due to floating point issues e.g. - # with streamplot. - do_lower_margin = not np.any(np.isclose(x0, stickies)) - do_upper_margin = not np.any(np.isclose(x1, stickies)) 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): - delta = (x1t - x0t) * margin - else: - # If at least one bound isn't finite, set margin to zero - delta = 0 - - if do_lower_margin: - x0t -= delta - if do_upper_margin: - x1t += delta - x0, x1 = inverse_trans.transform([x0t, x1t]) + delta = (x1t - x0t) * margin + if not np.isfinite(delta): + delta = 0 # If a bound isn't finite, set margin to zero. + x0, x1 = inverse_trans.transform([x0t - delta, x1t + delta]) + + # Apply sticky bounds. + if x0bound is not None: + x0 = max(x0, x0bound) + if x1bound is not None: + x1 = min(x1, x1bound) if not self._tight: x0, x1 = locator.view_limits(x0, x1) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index b4ea5768a0b6..fea961a201ee 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -797,6 +797,12 @@ def test_polar_rlim_bottom(fig_test, fig_ref): ax.set_rmin(.5) +def test_polar_rlim_zero(): + ax = plt.figure().add_subplot(projection='polar') + ax.plot(np.arange(10), np.arange(10) + .01) + assert ax.get_ylim()[0] == 0 + + @image_comparison(baseline_images=['axvspan_epoch']) def test_axvspan_epoch(): from datetime import datetime diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index a712680cd2ca..c715ad16e009 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -55,9 +55,13 @@ def test_linewidth(): X, Y, U, V = velocity_field() speed = np.hypot(U, V) lw = 5 * speed / speed.max() - df = 25 / 30 # Compatibility factor for old test image - plt.streamplot(X, Y, U, V, density=[0.5 * df, 1. * df], color='k', - linewidth=lw) + # Compatibility for old test image + df = 25 / 30 + ax = plt.figure().subplots() + ax.set(xlim=(-3.0, 2.9999999999999947), + ylim=(-3.0000000000000004, 2.9999999999999947)) + ax.streamplot(X, Y, U, V, density=[0.5 * df, 1. * df], color='k', + linewidth=lw) @image_comparison(baseline_images=['streamplot_masks_and_nans'], @@ -69,16 +73,24 @@ def test_masks_and_nans(): mask[40:60, 40:60] = 1 U[:20, :20] = np.nan U = np.ma.array(U, mask=mask) + # Compatibility for old test image + ax = plt.figure().subplots() + ax.set(xlim=(-3.0, 2.9999999999999947), + ylim=(-3.0000000000000004, 2.9999999999999947)) with np.errstate(invalid='ignore'): - plt.streamplot(X, Y, U, V, color=U, cmap=plt.cm.Blues) + ax.streamplot(X, Y, U, V, color=U, cmap=plt.cm.Blues) @image_comparison(baseline_images=['streamplot_maxlength'], extensions=['png'], remove_text=True, style='mpl20') def test_maxlength(): x, y, U, V = swirl_velocity_field() - plt.streamplot(x, y, U, V, maxlength=10., start_points=[[0., 1.5]], - linewidth=2, density=2) + ax = plt.figure().subplots() + ax.streamplot(x, y, U, V, maxlength=10., start_points=[[0., 1.5]], + linewidth=2, density=2) + assert ax.get_xlim()[-1] == ax.get_ylim()[-1] == 3 + # Compatibility for old test image + ax.set(xlim=(None, 3.2555988021882305), ylim=(None, 3.078326760195413)) @image_comparison(baseline_images=['streamplot_direction'],