diff --git a/doc/api/next_api_changes/deprecations/24455-AL.rst b/doc/api/next_api_changes/deprecations/24455-AL.rst new file mode 100644 index 000000000000..8a8f3e497260 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/24455-AL.rst @@ -0,0 +1,3 @@ +``RadioButtons.circles`` +~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated. (RadioButtons now draws itself using `~.Axes.scatter`.) diff --git a/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png b/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png deleted file mode 100644 index e071860dfde6..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png and /dev/null differ diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 446e272610ab..4e1455660da9 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1003,18 +1003,21 @@ def test_check_radio_buttons_image(): plt.subplots_adjust(left=0.3) rax1 = plt.axes([0.05, 0.7, 0.15, 0.15]) rax2 = plt.axes([0.05, 0.2, 0.15, 0.15]) - widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3')) + rb = widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3')) + with pytest.warns(DeprecationWarning): + rb.circles # Trigger the old-style elliptic radiobuttons. widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'), (False, True, True)) -@image_comparison(['check_bunch_of_radio_buttons.png'], - style='mpl20', remove_text=True) -def test_check_bunch_of_radio_buttons(): - rax = plt.axes([0.05, 0.1, 0.15, 0.7]) - widgets.RadioButtons(rax, ('B1', 'B2', 'B3', 'B4', 'B5', 'B6', - 'B7', 'B8', 'B9', 'B10', 'B11', 'B12', - 'B13', 'B14', 'B15')) +@check_figures_equal(extensions=["png"]) +def test_radio_buttons(fig_test, fig_ref): + widgets.RadioButtons(fig_test.subplots(), ["tea", "coffee"]) + ax = fig_ref.add_subplot(xticks=[], yticks=[]) + ax.scatter([.15, .15], [2/3, 1/3], transform=ax.transAxes, + s=(plt.rcParams["font.size"] / 2) ** 2, c=["C0", "none"]) + ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center") + ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center") def test_slider_slidermin_slidermax_invalid(): diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 807e9d360071..c7a0eb0a112e 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1404,41 +1404,23 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): """ super().__init__(ax) self.activecolor = activecolor - self.value_selected = None + self.value_selected = labels[active] ax.set_xticks([]) ax.set_yticks([]) ax.set_navigate(False) - dy = 1. / (len(labels) + 1) - ys = np.linspace(1 - dy, dy, len(labels)) - cnt = 0 - axcolor = ax.get_facecolor() - - # scale the radius of the circle with the spacing between each one - circle_radius = dy / 2 - 0.01 - # default to hard-coded value if the radius becomes too large - circle_radius = min(circle_radius, 0.05) - - self.labels = [] - self.circles = [] - for y, label in zip(ys, labels): - t = ax.text(0.25, y, label, transform=ax.transAxes, - horizontalalignment='left', - verticalalignment='center') - if cnt == active: - self.value_selected = label - facecolor = activecolor - else: - facecolor = axcolor + ys = np.linspace(1, 0, len(labels) + 2)[1:-1] + text_size = mpl.rcParams["font.size"] / 2 - p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black', - facecolor=facecolor, transform=ax.transAxes) - - self.labels.append(t) - self.circles.append(p) - ax.add_patch(p) - cnt += 1 + self.labels = [ + ax.text(0.25, y, label, transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center") + for y, label in zip(ys, labels)] + self._buttons = ax.scatter( + [.15] * len(ys), ys, transform=ax.transAxes, s=text_size**2, + c=[activecolor if i == active else "none" for i in range(len(ys))], + edgecolor="black") self.connect_event('button_press_event', self._clicked) @@ -1448,11 +1430,20 @@ def _clicked(self, event): if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: return pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) + _, inds = self._buttons.contains(event) + coords = self._buttons.get_offset_transform().transform( + self._buttons.get_offsets()) distances = {} - for i, (p, t) in enumerate(zip(self.circles, self.labels)): - if (t.get_window_extent().contains(event.x, event.y) - or np.linalg.norm(pclicked - p.center) < p.radius): - distances[i] = np.linalg.norm(pclicked - p.center) + if hasattr(self, "_circles"): # Remove once circles is removed. + for i, (p, t) in enumerate(zip(self._circles, self.labels)): + if (t.get_window_extent().contains(event.x, event.y) + or np.linalg.norm(pclicked - p.center) < p.radius): + distances[i] = np.linalg.norm(pclicked - p.center) + else: + for i, t in enumerate(self.labels): + if (i in inds["ind"] + or t.get_window_extent().contains(event.x, event.y)): + distances[i] = np.linalg.norm(pclicked - coords[i]) if len(distances) > 0: closest = min(distances, key=distances.get) self.set_active(closest) @@ -1465,19 +1456,14 @@ def set_active(self, index): """ if index not in range(len(self.labels)): raise ValueError(f'Invalid RadioButton index: {index}') - self.value_selected = self.labels[index].get_text() - - for i, p in enumerate(self.circles): - if i == index: - color = self.activecolor - else: - color = self.ax.get_facecolor() - p.set_facecolor(color) - + self._buttons.get_facecolor()[:] = colors.to_rgba("none") + self._buttons.get_facecolor()[index] = colors.to_rgba(self.activecolor) + if hasattr(self, "_circles"): # Remove once circles is removed. + for i, p in enumerate(self._circles): + p.set_facecolor(self.activecolor if i == index else "none") if self.drawon: self.ax.figure.canvas.draw() - if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) @@ -1493,6 +1479,21 @@ def disconnect(self, cid): """Remove the observer with connection id *cid*.""" self._observers.disconnect(cid) + @_api.deprecated("3.7") + @property + def circles(self): + if not hasattr(self, "_circles"): + radius = min(.5 / (len(self.labels) + 1) - .01, .05) + circles = self._circles = [ + Circle(xy=self._buttons.get_offsets()[i], edgecolor="black", + facecolor=self._buttons.get_facecolor()[i], + radius=radius, transform=self.ax.transAxes) + for i in range(len(self.labels))] + self._buttons.set_visible(False) + for circle in circles: + self.ax.add_patch(circle) + return self._circles + class SubplotTool(Widget): """