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

Skip to content

Commit 26580cc

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 26580cc

File tree

3 files changed

+50
-45
lines changed

3 files changed

+50
-45
lines changed
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: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,41 +1404,25 @@ 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()
14161412

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)
1413+
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
1414+
text_size = mpl.rcParams["font.size"] / 2
14211415

14221416
self.labels = []
1423-
self.circles = []
14241417
for y, label in zip(ys, labels):
14251418
t = ax.text(0.25, y, label, transform=ax.transAxes,
14261419
horizontalalignment='left',
14271420
verticalalignment='center')
1428-
1429-
if cnt == active:
1430-
self.value_selected = label
1431-
facecolor = activecolor
1432-
else:
1433-
facecolor = axcolor
1434-
1435-
p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
1436-
facecolor=facecolor, transform=ax.transAxes)
1437-
14381421
self.labels.append(t)
1439-
self.circles.append(p)
1440-
ax.add_patch(p)
1441-
cnt += 1
1422+
self._buttons = ax.scatter(
1423+
[.15] * len(ys), ys, transform=ax.transAxes, s=text_size**2,
1424+
c=[activecolor if i == active else "none" for i in range(len(ys))],
1425+
edgecolor="black")
14421426

14431427
self.connect_event('button_press_event', self._clicked)
14441428

@@ -1448,11 +1432,20 @@ def _clicked(self, event):
14481432
if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
14491433
return
14501434
pclicked = self.ax.transAxes.inverted().transform((event.x, event.y))
1435+
_, inds = self._buttons.contains(event)
1436+
coords = self._buttons.get_offset_transform().transform(
1437+
self._buttons.get_offsets())
14511438
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)
1439+
if hasattr(self, "_circles"): # Remove once circles is removed.
1440+
for i, (p, t) in enumerate(zip(self._circles, self.labels)):
1441+
if (t.get_window_extent().contains(event.x, event.y)
1442+
or np.linalg.norm(pclicked - p.center) < p.radius):
1443+
distances[i] = np.linalg.norm(pclicked - p.center)
1444+
else:
1445+
for i, t in enumerate(self.labels):
1446+
if (i in inds["ind"]
1447+
or t.get_window_extent().contains(event.x, event.y)):
1448+
distances[i] = np.linalg.norm(pclicked - coords[i])
14561449
if len(distances) > 0:
14571450
closest = min(distances, key=distances.get)
14581451
self.set_active(closest)
@@ -1465,19 +1458,14 @@ def set_active(self, index):
14651458
"""
14661459
if index not in range(len(self.labels)):
14671460
raise ValueError(f'Invalid RadioButton index: {index}')
1468-
14691461
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-
1462+
self._buttons.get_facecolor()[:] = colors.to_rgba("none")
1463+
self._buttons.get_facecolor()[index] = colors.to_rgba(self.activecolor)
1464+
if hasattr(self, "_circles"): # Remove once circles is removed.
1465+
for i, p in enumerate(self._circles):
1466+
p.set_facecolor(self.activecolor if i == index else "none")
14781467
if self.drawon:
14791468
self.ax.figure.canvas.draw()
1480-
14811469
if self.eventson:
14821470
self._observers.process('clicked', self.labels[index].get_text())
14831471

@@ -1493,6 +1481,20 @@ def disconnect(self, cid):
14931481
"""Remove the observer with connection id *cid*."""
14941482
self._observers.disconnect(cid)
14951483

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

14971499
class SubplotTool(Widget):
14981500
"""

0 commit comments

Comments
 (0)