diff --git a/doc/api/next_api_changes/behavior/26788-AL.rst b/doc/api/next_api_changes/behavior/26788-AL.rst new file mode 100644 index 000000000000..14573e870843 --- /dev/null +++ b/doc/api/next_api_changes/behavior/26788-AL.rst @@ -0,0 +1,6 @@ +``axvspan`` and ``axhspan`` now return ``Rectangle``\s, not ``Polygons`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This change allows using `~.Axes.axhspan` to draw an annulus on polar axes. + +This change also affects other elements built via `~.Axes.axvspan` and +`~.Axes.axhspan`, such as ``Slider.poly``. diff --git a/doc/users/next_whats_new/polar-line-spans.rst b/doc/users/next_whats_new/polar-line-spans.rst new file mode 100644 index 000000000000..47bb382dbdbf --- /dev/null +++ b/doc/users/next_whats_new/polar-line-spans.rst @@ -0,0 +1,5 @@ +``axhline`` and ``axhspan`` on polar axes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... now draw circles and circular arcs (`~.Axes.axhline`) or annuli and wedges +(`~.Axes.axhspan`). diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 9997e660f40c..0fcabac8c7c0 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -783,6 +783,7 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): trans = self.get_yaxis_transform(which='grid') l = mlines.Line2D([xmin, xmax], [y, y], transform=trans, **kwargs) self.add_line(l) + l.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS if scaley: self._request_autoscale_view("y") return l @@ -851,6 +852,7 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): trans = self.get_xaxis_transform(which='grid') l = mlines.Line2D([x, x], [ymin, ymax], transform=trans, **kwargs) self.add_line(l) + l.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS if scalex: self._request_autoscale_view("x") return l @@ -978,10 +980,17 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): self._check_no_units([xmin, xmax], ['xmin', 'xmax']) (ymin, ymax), = self._process_unit_info([("y", [ymin, ymax])], kwargs) - verts = (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin) - p = mpatches.Polygon(verts, **kwargs) + p = mpatches.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, **kwargs) p.set_transform(self.get_yaxis_transform(which="grid")) + # For Rectangles and non-separable transforms, add_patch can be buggy + # and update the x limits even though it shouldn't do so for an + # yaxis_transformed patch, so undo that update. + ix = self.dataLim.intervalx + mx = self.dataLim.minposx self.add_patch(p) + self.dataLim.intervalx = ix + self.dataLim.minposx = mx + p.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS self._request_autoscale_view("y") return p @@ -1034,11 +1043,17 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): self._check_no_units([ymin, ymax], ['ymin', 'ymax']) (xmin, xmax), = self._process_unit_info([("x", [xmin, xmax])], kwargs) - verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] - p = mpatches.Polygon(verts, **kwargs) + p = mpatches.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, **kwargs) p.set_transform(self.get_xaxis_transform(which="grid")) - p.get_path()._interpolation_steps = 100 + # For Rectangles and non-separable transforms, add_patch can be buggy + # and update the y limits even though it shouldn't do so for an + # xaxis_transformed patch, so undo that update. + iy = self.dataLim.intervaly.copy() + my = self.dataLim.minposy self.add_patch(p) + self.dataLim.intervaly = iy + self.dataLim.minposy = my + p.get_path()._interpolation_steps = mpl.axis.GRIDLINE_INTERPOLATION_STEPS self._request_autoscale_view("x") return p diff --git a/lib/matplotlib/tests/baseline_images/test_axes/axhvlinespan_interpolation.png b/lib/matplotlib/tests/baseline_images/test_axes/axhvlinespan_interpolation.png new file mode 100644 index 000000000000..3937cdf5b34c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/axhvlinespan_interpolation.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index ab9dab03e543..564bf6a86b52 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8849,3 +8849,15 @@ def test_xylim_changed_shared(): axs[1].callbacks.connect("ylim_changed", events.append) axs[0].set(xlim=[1, 3], ylim=[2, 4]) assert events == [axs[1], axs[1]] + + +@image_comparison(["axhvlinespan_interpolation.png"], style="default") +def test_axhvlinespan_interpolation(): + ax = plt.figure().add_subplot(projection="polar") + ax.set_axis_off() + ax.axvline(.1, c="C0") + ax.axvspan(.2, .3, fc="C1") + ax.axvspan(.4, .5, .1, .2, fc="C2") + ax.axhline(1, c="C0", alpha=.5) + ax.axhspan(.8, .9, fc="C1", alpha=.5) + ax.axhspan(.6, .7, .8, .9, fc="C2", alpha=.5) diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index 0a1d6c6b5e52..8c0c32dc133b 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -142,11 +142,11 @@ def test_nonlinear_containment(): ax.set(xscale="log", ylim=(0, 1)) polygon = ax.axvspan(1, 10) assert polygon.get_path().contains_point( - ax.transData.transform((5, .5)), ax.transData) + ax.transData.transform((5, .5)), polygon.get_transform()) assert not polygon.get_path().contains_point( - ax.transData.transform((.5, .5)), ax.transData) + ax.transData.transform((.5, .5)), polygon.get_transform()) assert not polygon.get_path().contains_point( - ax.transData.transform((50, .5)), ax.transData) + ax.transData.transform((50, .5)), polygon.get_transform()) @image_comparison(['arrow_contains_point.png'], diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index d04b59afa9d7..5a7fd125a29b 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -671,6 +671,7 @@ def intersection(bbox1, bbox2): y1 = np.minimum(bbox1.ymax, bbox2.ymax) return Bbox([[x0, y0], [x1, y1]]) if x0 <= x1 and y0 <= y1 else None + _default_minpos = np.array([np.inf, np.inf]) @@ -1011,6 +1012,10 @@ def minpos(self): """ return self._minpos + @minpos.setter + def minpos(self, val): + self._minpos[:] = val + @property def minposx(self): """ @@ -1022,6 +1027,10 @@ def minposx(self): """ return self._minpos[0] + @minposx.setter + def minposx(self, val): + self._minpos[0] = val + @property def minposy(self): """ @@ -1033,6 +1042,10 @@ def minposy(self): """ return self._minpos[1] + @minposy.setter + def minposy(self, val): + self._minpos[1] = val + def get_points(self): """ Get the points of the bounding box as an array of the form diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 0a31a9dd2529..771cfd714b91 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -433,8 +433,8 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, Notes ----- Additional kwargs are passed on to ``self.poly`` which is the - `~matplotlib.patches.Polygon` that draws the slider knob. See the - `.Polygon` documentation for valid property names (``facecolor``, + `~matplotlib.patches.Rectangle` that draws the slider knob. See the + `.Rectangle` documentation for valid property names (``facecolor``, ``edgecolor``, ``alpha``, etc.). """ super().__init__(ax, orientation, closedmin, closedmax, @@ -577,16 +577,12 @@ def set_val(self, val): ---------- val : float """ - xy = self.poly.xy if self.orientation == 'vertical': - xy[1] = .25, val - xy[2] = .75, val + self.poly.set_height(val - self.poly.get_y()) self._handle.set_ydata([val]) else: - xy[2] = val, .75 - xy[3] = val, .25 + self.poly.set_width(val - self.poly.get_x()) self._handle.set_xdata([val]) - self.poly.xy = xy self.valtext.set_text(self._format(val)) if self.drawon: self.ax.figure.canvas.draw_idle()