Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eae36f5

Browse files
committed
Draw RadioButtons using scatter to ensure circular buttons.
To ensure backcompat without bothering the majority of users who don't actually access the .circles attribute, dynamically (and irreversibly) switch back to the old draw method (list of Circles) whenever that attribute is accessed for the first time (if ever).
1 parent 1fa7467 commit eae36f5

File tree

4 files changed

+57
-51
lines changed

4 files changed

+57
-51
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``RadioButtons.circles``
2+
~~~~~~~~~~~~~~~~~~~~~~~~
3+
... is deprecated. (RadioButtons now draws itself using `~.Axes.scatter`.)
Binary file not shown.

lib/matplotlib/tests/test_widgets.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,18 +1003,21 @@ def test_check_radio_buttons_image():
10031003
plt.subplots_adjust(left=0.3)
10041004
rax1 = plt.axes([0.05, 0.7, 0.15, 0.15])
10051005
rax2 = plt.axes([0.05, 0.2, 0.15, 0.15])
1006-
widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3'))
1006+
rb = widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3'))
1007+
with pytest.warns(DeprecationWarning):
1008+
rb.circles # Trigger the old-style elliptic radiobuttons.
10071009
widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'),
10081010
(False, True, True))
10091011

10101012

1011-
@image_comparison(['check_bunch_of_radio_buttons.png'],
1012-
style='mpl20', remove_text=True)
1013-
def test_check_bunch_of_radio_buttons():
1014-
rax = plt.axes([0.05, 0.1, 0.15, 0.7])
1015-
widgets.RadioButtons(rax, ('B1', 'B2', 'B3', 'B4', 'B5', 'B6',
1016-
'B7', 'B8', 'B9', 'B10', 'B11', 'B12',
1017-
'B13', 'B14', 'B15'))
1013+
@check_figures_equal(extensions=["png"])
1014+
def test_radio_buttons(fig_test, fig_ref):
1015+
widgets.RadioButtons(fig_test.subplots(), ["tea", "coffee"])
1016+
ax = fig_ref.add_subplot(xticks=[], yticks=[])
1017+
ax.scatter([.15, .15], [2/3, 1/3], transform=ax.transAxes,
1018+
s=(plt.rcParams["font.size"] / 2) ** 2, c=["C0", "none"])
1019+
ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center")
1020+
ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center")
10181021

10191022

10201023
def test_slider_slidermin_slidermax_invalid():

lib/matplotlib/widgets.py

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,41 +1404,23 @@ def __init__(self, ax, labels, active=0, activecolor='blue'):
14041404
"""
14051405
super().__init__(ax)
14061406
self.activecolor = activecolor
1407-
self.value_selected = None
1407+
self.value_selected = labels[active]
14081408

14091409
ax.set_xticks([])
14101410
ax.set_yticks([])
14111411
ax.set_navigate(False)
1412-
dy = 1. / (len(labels) + 1)
1413-
ys = np.linspace(1 - dy, dy, len(labels))
1414-
cnt = 0
1415-
axcolor = ax.get_facecolor()
1416-
1417-
# scale the radius of the circle with the spacing between each one
1418-
circle_radius = dy / 2 - 0.01
1419-
# default to hard-coded value if the radius becomes too large
1420-
circle_radius = min(circle_radius, 0.05)
1421-
1422-
self.labels = []
1423-
self.circles = []
1424-
for y, label in zip(ys, labels):
1425-
t = ax.text(0.25, y, label, transform=ax.transAxes,
1426-
horizontalalignment='left',
1427-
verticalalignment='center')
14281412

1429-
if cnt == active:
1430-
self.value_selected = label
1431-
facecolor = activecolor
1432-
else:
1433-
facecolor = axcolor
1413+
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
1414+
text_size = mpl.rcParams["font.size"] / 2
14341415

1435-
p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
1436-
facecolor=facecolor, transform=ax.transAxes)
1437-
1438-
self.labels.append(t)
1439-
self.circles.append(p)
1440-
ax.add_patch(p)
1441-
cnt += 1
1416+
self.labels = [
1417+
ax.text(0.25, y, label, transform=ax.transAxes,
1418+
horizontalalignment="left", verticalalignment="center")
1419+
for y, label in zip(ys, labels)]
1420+
self._buttons = ax.scatter(
1421+
[.15] * len(ys), ys, transform=ax.transAxes, s=text_size**2,
1422+
c=[activecolor if i == active else "none" for i in range(len(ys))],
1423+
edgecolor="black")
14421424

14431425
self.connect_event('button_press_event', self._clicked)
14441426

@@ -1448,11 +1430,20 @@ def _clicked(self, event):
14481430
if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
14491431
return
14501432
pclicked = self.ax.transAxes.inverted().transform((event.x, event.y))
1433+
_, inds = self._buttons.contains(event)
1434+
coords = self._buttons.get_offset_transform().transform(
1435+
self._buttons.get_offsets())
14511436
distances = {}
1452-
for i, (p, t) in enumerate(zip(self.circles, self.labels)):
1453-
if (t.get_window_extent().contains(event.x, event.y)
1454-
or np.linalg.norm(pclicked - p.center) < p.radius):
1455-
distances[i] = np.linalg.norm(pclicked - p.center)
1437+
if hasattr(self, "_circles"): # Remove once circles is removed.
1438+
for i, (p, t) in enumerate(zip(self._circles, self.labels)):
1439+
if (t.get_window_extent().contains(event.x, event.y)
1440+
or np.linalg.norm(pclicked - p.center) < p.radius):
1441+
distances[i] = np.linalg.norm(pclicked - p.center)
1442+
else:
1443+
for i, t in enumerate(self.labels):
1444+
if (i in inds["ind"]
1445+
or t.get_window_extent().contains(event.x, event.y)):
1446+
distances[i] = np.linalg.norm(pclicked - coords[i])
14561447
if len(distances) > 0:
14571448
closest = min(distances, key=distances.get)
14581449
self.set_active(closest)
@@ -1465,19 +1456,14 @@ def set_active(self, index):
14651456
"""
14661457
if index not in range(len(self.labels)):
14671458
raise ValueError(f'Invalid RadioButton index: {index}')
1468-
14691459
self.value_selected = self.labels[index].get_text()
1470-
1471-
for i, p in enumerate(self.circles):
1472-
if i == index:
1473-
color = self.activecolor
1474-
else:
1475-
color = self.ax.get_facecolor()
1476-
p.set_facecolor(color)
1477-
1460+
self._buttons.get_facecolor()[:] = colors.to_rgba("none")
1461+
self._buttons.get_facecolor()[index] = colors.to_rgba(self.activecolor)
1462+
if hasattr(self, "_circles"): # Remove once circles is removed.
1463+
for i, p in enumerate(self._circles):
1464+
p.set_facecolor(self.activecolor if i == index else "none")
14781465
if self.drawon:
14791466
self.ax.figure.canvas.draw()
1480-
14811467
if self.eventson:
14821468
self._observers.process('clicked', self.labels[index].get_text())
14831469

@@ -1493,6 +1479,20 @@ def disconnect(self, cid):
14931479
"""Remove the observer with connection id *cid*."""
14941480
self._observers.disconnect(cid)
14951481

1482+
@_api.deprecated("3.7")
1483+
@property
1484+
def circles(self):
1485+
radius = min(.5 / (len(self.labels) + 1) - .01, .05)
1486+
circles = self._circles = [
1487+
Circle(xy=self._buttons.get_offsets()[i], edgecolor="black",
1488+
facecolor=self._buttons.get_facecolor()[i],
1489+
radius=radius, transform=self.ax.transAxes)
1490+
for i in range(len(self.labels))]
1491+
self._buttons.set_visible(False)
1492+
for circle in self._circles:
1493+
self.ax.add_patch(circle)
1494+
return circles
1495+
14961496

14971497
class SubplotTool(Widget):
14981498
"""

0 commit comments

Comments
 (0)