Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3c306a3

Browse filesBrowse 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 3c306a3
Copy full SHA for 3c306a3

File tree

Expand file treeCollapse file tree

4 files changed

+58
-51
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+58
-51
lines changed
+3Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_widgets.py
+11-8Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/widgets.py
+44-43Lines changed: 44 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,21 @@ 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+
if not hasattr(self, "_circles"):
1486+
radius = min(.5 / (len(self.labels) + 1) - .01, .05)
1487+
circles = self._circles = [
1488+
Circle(xy=self._buttons.get_offsets()[i], edgecolor="black",
1489+
facecolor=self._buttons.get_facecolor()[i],
1490+
radius=radius, transform=self.ax.transAxes)
1491+
for i in range(len(self.labels))]
1492+
self._buttons.set_visible(False)
1493+
for circle in circles:
1494+
self.ax.add_patch(circle)
1495+
return self._circles
1496+
14961497

14971498
class SubplotTool(Widget):
14981499
"""

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.