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'],