🌐 AI搜索 & 代理 主页
Skip to content

Commit b660bc5

Browse files
doronbeharclaude
andcommitted
RadioButtons: Add 2D grid labels layout support
The RadioButtons widget now supports arranging buttons in a 2D grid by passing a list of lists of strings as the labels parameter. Each inner list represents a row in the grid. Key features: - Active index and index_selected remain as single integers for the flattened array (reading left-to-right, top-to-bottom) - Column positions are automatically calculated based on the maximum text width in each column for optimal spacing - Text offset is now consistent between 1D and 2D layouts (0.10) - Includes new example: galleries/examples/widgets/radio_buttons_grid.py Closes #13374 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4fcf990 commit b660bc5

File tree

4 files changed

+220
-14
lines changed

4 files changed

+220
-14
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
RadioButtons widget supports 2D grid layout
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The `.widgets.RadioButtons` widget now supports arranging buttons in a 2D grid
5+
layout. Pass a list of lists of strings as the *labels* parameter to arrange
6+
buttons in a grid where each inner list represents a row.
7+
8+
The *active* parameter and the `.widgets.RadioButtons.index_selected` attribute
9+
continue to use a single integer index into the flattened array, reading
10+
left-to-right, top-to-bottom. The column positions are automatically calculated
11+
based on the maximum text width in each column, ensuring optimal spacing.
12+
13+
See :doc:`/gallery/widgets/radio_buttons_grid` for a complete example.
14+
15+
.. plot::
16+
:include-source: true
17+
:alt: A sine wave plot with a 3x3 grid of radio buttons for selecting line color.
18+
19+
import matplotlib.pyplot as plt
20+
import numpy as np
21+
from matplotlib.widgets import RadioButtons
22+
23+
t = np.arange(0.0, 2.0, 0.01)
24+
s = np.sin(2*np.pi*t)
25+
26+
fig, (ax_plot, ax_buttons) = plt.subplots(1, 2, figsize=(8, 4),
27+
width_ratios=[3, 1])
28+
29+
line, = ax_plot.plot(t, s, lw=2, color='red')
30+
ax_plot.set_xlabel('Time (s)')
31+
ax_plot.set_ylabel('Amplitude')
32+
33+
ax_buttons.set_facecolor('lightgray')
34+
ax_buttons.set_title('Line Color', fontsize=12, pad=10)
35+
36+
colors = [
37+
['red', 'orange', 'yellow'],
38+
['green', 'blue', 'purple'],
39+
['brown', 'pink', 'gray'],
40+
]
41+
42+
radio = RadioButtons(ax_buttons, colors, active=0)
43+
44+
def color_func(label):
45+
line.set_color(label)
46+
fig.canvas.draw()
47+
48+
radio.on_clicked(color_func)
49+
plt.show()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
==================
3+
Radio Buttons Grid
4+
==================
5+
6+
Using radio buttons in a 2D grid layout.
7+
8+
Radio buttons can be arranged in a 2D grid by passing a list of lists of
9+
strings as the *labels* parameter. This is useful when you have multiple
10+
related options that are best displayed in a grid format rather than a
11+
vertical list.
12+
13+
In this example, we create a color picker using a 2D grid of radio buttons
14+
to select the line color of a plot.
15+
"""
16+
17+
import matplotlib.pyplot as plt
18+
import numpy as np
19+
20+
from matplotlib.widgets import RadioButtons
21+
22+
# Generate sample data
23+
t = np.arange(0.0, 2.0, 0.01)
24+
s = np.sin(2*np.pi*t)
25+
26+
fig, (ax_plot, ax_buttons) = plt.subplots(
27+
1, 2,
28+
figsize=(8, 4),
29+
width_ratios=[4, 1],
30+
)
31+
32+
# Create initial plot
33+
line, = ax_plot.plot(t, s, lw=2, color='red')
34+
ax_plot.set_xlabel('Time (s)')
35+
ax_plot.set_ylabel('Amplitude')
36+
ax_plot.set_title('Sine Wave - Click a color!')
37+
ax_plot.grid(True, alpha=0.3)
38+
39+
# Configure the radio buttons axes
40+
ax_buttons.set_facecolor('lightgray')
41+
ax_buttons.set_title('Line Color', fontsize=12, pad=10)
42+
43+
# Create a 2D grid of color options (3 rows x 2 columns)
44+
colors = [
45+
['red', 'yellow'],
46+
['green', 'purple'],
47+
['brown', 'gray'],
48+
]
49+
50+
radio = RadioButtons(ax_buttons, colors, active=0)
51+
52+
53+
def color_func(label):
54+
"""Update the line color based on selected button."""
55+
line.set_color(label)
56+
fig.canvas.draw()
57+
58+
59+
radio.on_clicked(color_func)
60+
61+
plt.show()
62+
63+
# %%
64+
#
65+
# .. admonition:: References
66+
#
67+
# The use of the following functions, methods, classes and modules is shown
68+
# in this example:
69+
#
70+
# - `matplotlib.widgets.RadioButtons`

lib/matplotlib/widgets.py

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,7 +1578,8 @@ class RadioButtons(AxesWidget):
15781578
value_selected : str
15791579
The label text of the currently selected button.
15801580
index_selected : int
1581-
The index of the selected button.
1581+
The index of the selected button in the flattened array. For 2D grids,
1582+
this is the index when reading left-to-right, top-to-bottom.
15821583
"""
15831584

15841585
def __init__(self, ax, labels, active=0, activecolor=None, *,
@@ -1590,10 +1591,14 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
15901591
----------
15911592
ax : `~matplotlib.axes.Axes`
15921593
The Axes to add the buttons to.
1593-
labels : list of str
1594-
The button labels.
1594+
labels : list of str or list of list of str
1595+
The button labels. If a list of strings, buttons are arranged
1596+
vertically. If a list of lists of strings, buttons are arranged
1597+
in a 2D grid where each inner list represents a row.
15951598
active : int
1596-
The index of the initially selected button.
1599+
The index of the initially selected button in the flattened array.
1600+
For 2D grids, this is the index when reading left-to-right,
1601+
top-to-bottom.
15971602
activecolor : :mpltype:`color`
15981603
The color of the selected button. The default is ``'blue'`` if not
15991604
specified here or in *radio_props*.
@@ -1606,8 +1611,8 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16061611
label_props : dict of lists, optional
16071612
Dictionary of `.Text` properties to be used for the labels. Each
16081613
dictionary value should be a list of at least a single element. If
1609-
the list is of length M, its values are cycled such that the Nth
1610-
label gets the (N mod M) property.
1614+
the flat list of labels is of length M, its values are cycled such
1615+
that the Nth label gets the (N mod M) property.
16111616
16121617
.. versionadded:: 3.7
16131618
radio_props : dict, optional
@@ -1627,6 +1632,14 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16271632
_api.check_isinstance((dict, None), label_props=label_props,
16281633
radio_props=radio_props)
16291634

1635+
# Check if labels is 2D (list of lists)
1636+
_is_2d = isinstance(labels[0], (list, tuple))
1637+
1638+
if _is_2d:
1639+
flat_labels = [item for row in labels for item in row]
1640+
else:
1641+
flat_labels = list(labels)
1642+
16301643
radio_props = cbook.normalize_kwargs(radio_props,
16311644
collections.PathCollection)
16321645
if activecolor is not None:
@@ -1640,24 +1653,98 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16401653

16411654
self._activecolor = activecolor
16421655
self._initial_active = active
1643-
self.value_selected = labels[active]
1656+
self.value_selected = flat_labels[active]
16441657
self.index_selected = active
16451658

16461659
ax.set_xticks([])
16471660
ax.set_yticks([])
16481661
ax.set_navigate(False)
16491662

1650-
ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
1651-
16521663
self._useblit = useblit and self.canvas.supports_blit
16531664
self._background = None
16541665

16551666
label_props = _expand_text_props(label_props)
1667+
# Calculate positions based on layout
1668+
text_x_offset = 0.10
1669+
1670+
if _is_2d:
1671+
n_rows = len(labels)
1672+
n_cols = max(len(row) for row in labels)
1673+
# Y positions with margins
1674+
y_margin = 0.05
1675+
y_spacing = (1 - 2 * y_margin) / max(1, n_rows - 1) if n_rows > 1 else 0
1676+
1677+
# Create temporary text objects to measure widths
1678+
flat_label_list = []
1679+
temp_texts = []
1680+
for i, row in enumerate(labels):
1681+
for j, label in enumerate(row):
1682+
flat_label_list.append(label)
1683+
for label, props in zip(flat_label_list, label_props):
1684+
temp_texts.append(ax.text(
1685+
0,
1686+
0,
1687+
label,
1688+
transform=ax.transAxes,
1689+
**props,
1690+
))
1691+
# Force a draw to get accurate text measurements
1692+
ax.figure.canvas.draw()
1693+
# Calculate max text width per column (in axes coordinates)
1694+
col_widths = []
1695+
for col_idx in range(n_cols):
1696+
col_texts = []
1697+
for row_idx, row in enumerate(labels):
1698+
if col_idx < len(row):
1699+
col_texts.append(temp_texts[
1700+
sum(len(labels[r]) for r in range(row_idx)) + col_idx
1701+
])
1702+
if col_texts:
1703+
col_widths.append(
1704+
max(
1705+
text.get_window_extent(
1706+
ax.figure.canvas.get_renderer()
1707+
).width
1708+
for text in col_texts
1709+
) / ax.bbox.width
1710+
)
1711+
else:
1712+
col_widths.append(0)
1713+
# Remove temporary text objects
1714+
for text in temp_texts:
1715+
text.remove()
1716+
# Calculate x positions based on text widths
1717+
# TODO: Should these be arguments?
1718+
button_x_margin = 0.07 # Left margin for first button
1719+
col_spacing = 0.07 # Space between columns
1720+
1721+
col_x_positions = [button_x_margin] # First column starts at left margin
1722+
for col_idx in range(n_cols - 1):
1723+
col_x_positions.append(sum([
1724+
col_x_positions[-1],
1725+
text_x_offset,
1726+
col_widths[col_idx],
1727+
col_spacing
1728+
]))
1729+
# Create final positions
1730+
positions = []
1731+
for i, row in enumerate(labels):
1732+
y = 1 - y_margin - i * y_spacing
1733+
for j, label in enumerate(row):
1734+
x = col_x_positions[j]
1735+
positions.append((x, y))
1736+
xs = [pos[0] for pos in positions]
1737+
ys = [pos[1] for pos in positions]
1738+
else:
1739+
ys = np.linspace(1, 0, len(flat_labels) + 2)[1:-1]
1740+
xs = [0.15] * len(ys)
1741+
flat_label_list = flat_labels
1742+
16561743
self.labels = [
1657-
ax.text(0.25, y, label, transform=ax.transAxes,
1744+
ax.text(x + text_x_offset, y, label, transform=ax.transAxes,
16581745
horizontalalignment="left", verticalalignment="center",
16591746
**props)
1660-
for y, label, props in zip(ys, labels, label_props)]
1747+
for x, y, label, props in zip(xs, ys, flat_label_list, label_props)]
16611748
text_size = np.array([text.get_fontsize() for text in self.labels]) / 2
16621749

16631750
radio_props = {
@@ -1670,13 +1757,13 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16701757
radio_props.setdefault('edgecolor', radio_props.get('color', 'black'))
16711758
radio_props.setdefault('facecolor',
16721759
radio_props.pop('color', activecolor))
1673-
self._buttons = ax.scatter([.15] * len(ys), ys, **radio_props)
1760+
self._buttons = ax.scatter(xs, ys, **radio_props)
16741761
# The user may have passed custom colours in radio_props, so we need to
16751762
# create the radios, and modify the visibility after getting whatever
16761763
# the user set.
16771764
self._active_colors = self._buttons.get_facecolor()
16781765
if len(self._active_colors) == 1:
1679-
self._active_colors = np.repeat(self._active_colors, len(labels),
1766+
self._active_colors = np.repeat(self._active_colors, len(flat_labels),
16801767
axis=0)
16811768
self._buttons.set_facecolor(
16821769
[activecolor if i == active else "none"

lib/matplotlib/widgets.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class RadioButtons(AxesWidget):
206206
def __init__(
207207
self,
208208
ax: Axes,
209-
labels: Iterable[str],
209+
labels: Iterable[str] | Iterable[Iterable[str]],
210210
active: int = ...,
211211
activecolor: ColorType | None = ...,
212212
*,

0 commit comments

Comments
 (0)