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

Skip to content

Commit 0cc8ba5

Browse files
committed
*Buttons: support more layouts
1 parent f6273a0 commit 0cc8ba5

6 files changed

Lines changed: 297 additions & 15 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
RadioButtons and CheckButtons widgets support flexible layouts
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The `.widgets.RadioButtons` and `.widgets.CheckButtons` widgets now support
5+
arranging buttons in different layouts via the new *layout* parameter. You can
6+
arrange buttons vertically (default), horizontally, or in a 2D grid by passing
7+
a ``(rows, cols)`` tuple.
8+
9+
See :doc:`/gallery/widgets/radio_buttons_grid` for a ``(rows, cols)`` example.
10+
11+
.. plot::
12+
:include-source: true
13+
:alt: Multiple sine waves with checkboxes to toggle their visibility.
14+
15+
import matplotlib.pyplot as plt
16+
import numpy as np
17+
from matplotlib.widgets import CheckButtons
18+
19+
t = np.arange(0.0, 2.0, 0.01)
20+
s0 = np.sin(2*np.pi*t)
21+
s1 = np.sin(4*np.pi*t)
22+
s2 = np.sin(6*np.pi*t)
23+
s3 = np.sin(8*np.pi*t)
24+
25+
fig, axes = plt.subplot_mosaic(
26+
[['main'], ['buttons']],
27+
height_ratios=[8, 1],
28+
layout="constrained",
29+
)
30+
31+
l0, = axes['main'].plot(t, s0, lw=2, color='red', label='2 Hz')
32+
l1, = axes['main'].plot(t, s1, lw=2, color='green', label='4 Hz')
33+
l2, = axes['main'].plot(t, s2, lw=2, color='blue', label='6 Hz')
34+
l3, = axes['main'].plot(t, s3, lw=2, color='purple', label='8 Hz')
35+
axes['main'].set_xlabel('Time (s)')
36+
axes['main'].set_ylabel('Amplitude')
37+
38+
lines_by_label = {l.get_label(): l for l in [l0, l1, l2, l3]}
39+
40+
axes['buttons'].set_facecolor('0.9')
41+
check = CheckButtons(
42+
axes['buttons'],
43+
labels=lines_by_label.keys(),
44+
actives=[l.get_visible() for l in lines_by_label.values()],
45+
layout='horizontal'
46+
)
47+
48+
def callback(label):
49+
ln = lines_by_label[label]
50+
ln.set_visible(not ln.get_visible())
51+
fig.canvas.draw_idle()
52+
53+
check.on_clicked(callback)
54+
plt.show()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 ``(rows, cols)``
9+
tuple to the *layout* 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,
28+
2,
29+
figsize=(8, 4),
30+
width_ratios=[4, 1.4],
31+
)
32+
33+
# Create initial plot
34+
(line,) = ax_plot.plot(t, s, lw=2, color="red")
35+
ax_plot.set_xlabel("Time (s)")
36+
ax_plot.set_ylabel("Amplitude")
37+
ax_plot.set_title("Sine Wave - Click a color!")
38+
ax_plot.grid(True, alpha=0.3)
39+
40+
# Configure the radio buttons axes
41+
ax_buttons.set_facecolor("0.9")
42+
ax_buttons.set_title("Line Color", fontsize=12, pad=10)
43+
# Create a 2D grid of color options (3 rows x 2 columns)
44+
colors = ["red", "yellow", "green", "purple", "brown", "gray"]
45+
radio = RadioButtons(ax_buttons, colors, layout=(3, 2))
46+
47+
48+
def color_func(label):
49+
"""Update the line color based on selected button."""
50+
line.set_color(label)
51+
fig.canvas.draw()
52+
53+
54+
radio.on_clicked(color_func)
55+
56+
plt.show()
57+
58+
# %%
59+
#
60+
# .. admonition:: References
61+
#
62+
# The use of the following functions, methods, classes and modules is shown
63+
# in this example:
64+
#
65+
# - `matplotlib.widgets.RadioButtons`
66+
#
67+
# .. tags::
68+
#
69+
# styling: color
70+
# styling: conditional
71+
# plot-type: line
72+
# level: intermediate
73+
# purpose: showcase
21.5 KB
Loading

lib/matplotlib/tests/test_widgets.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,29 @@ def test_radio_buttons_props(fig_test, fig_ref):
11641164
cb.set_radio_props({**radio_props, 's': (24 / 2)**2})
11651165

11661166

1167+
@image_comparison(['check_radio_grid_buttons.png'], style='mpl20', remove_text=True)
1168+
def test_radio_grid_buttons():
1169+
fig = plt.figure()
1170+
rb_horizontal = widgets.RadioButtons(
1171+
fig.add_axes((0.1, 0.05, 0.65, 0.05)),
1172+
["tea", "coffee", "chocolate milk", "water", "soda", "coke"],
1173+
layout='horizontal',
1174+
active=4,
1175+
)
1176+
cb_grid = widgets.CheckButtons(
1177+
fig.add_axes((0.1, 0.15, 0.25, 0.05*3)),
1178+
["Chicken", "Salad", "Rice", "Sushi", "Pizza", "Fries"],
1179+
layout=(3, 2),
1180+
actives=[True, True, False, False, False, True],
1181+
)
1182+
rb_vertical = widgets.RadioButtons(
1183+
fig.add_axes((0.1, 0.35, 0.2, 0.05*4)),
1184+
["Trinity Cream", "Cake", "Ice Cream", "Muhallebi"],
1185+
layout='vertical',
1186+
active=3,
1187+
)
1188+
1189+
11671190
def test_radio_button_active_conflict(ax):
11681191
with pytest.warns(UserWarning,
11691192
match=r'Both the \*activecolor\* parameter'):

lib/matplotlib/widgets.py

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,8 @@ class _Buttons(AxesWidget):
10481048
public on the subclasses.
10491049
"""
10501050

1051-
def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
1051+
def __init__(self, ax, labels, *, useblit=True, label_props=None, layout=None,
1052+
**kwargs):
10521053
super().__init__(ax)
10531054

10541055
ax.set_xticks([])
@@ -1057,7 +1058,7 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
10571058

10581059
self._useblit = useblit
10591060

1060-
self._init_layout(labels, label_props)
1061+
self._init_layout(layout, labels, label_props)
10611062
text_size = np.array([text.get_fontsize() for text in self.labels]) / 2
10621063

10631064
self._init_props(text_size, **kwargs)
@@ -1068,17 +1069,98 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
10681069

10691070
self._observers = cbook.CallbackRegistry(signals=["clicked"])
10701071

1071-
def _init_layout(self, labels, label_props):
1072-
self._buttons_xs = [0.15] * len(labels)
1073-
self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1]
1072+
def _init_layout(self, layout, labels, label_props):
10741073

10751074
label_props = _expand_text_props(label_props)
10761075

1076+
if layout is None:
1077+
# legacy hard-coded vertical layout
1078+
self._buttons_xs = [0.15] * len(labels)
1079+
self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1]
1080+
self.labels = [
1081+
self.ax.text(0.25, y, label, transform=self.ax.transAxes,
1082+
horizontalalignment="left", verticalalignment="center",
1083+
**props)
1084+
for y, label, props in zip(self._buttons_ys, labels, label_props)]
1085+
return
1086+
1087+
# New layout algorithm with text measurement
1088+
# Parse layout parameter
1089+
n_labels = len(labels)
1090+
match layout:
1091+
case "vertical":
1092+
n_rows, n_cols = n_labels, 1
1093+
case "horizontal":
1094+
n_rows, n_cols = 1, n_labels
1095+
case (int() as n_rows, int() as n_cols):
1096+
if n_rows * n_cols < n_labels:
1097+
raise ValueError(
1098+
f"layout {layout} has {n_rows * n_cols} positions but "
1099+
f"{n_labels} labels were provided"
1100+
)
1101+
case _:
1102+
raise ValueError(
1103+
"layout must be None, 'vertical', 'horizontal', or a (rows, cols) "
1104+
f"tuple; got {layout!r}")
1105+
1106+
# Define spacing in points for DPI-independent sizing
1107+
fig = self.ax.get_figure(root=False)
1108+
axes_width_display = 72 * self.ax.bbox.transformed(
1109+
fig.dpi_scale_trans.inverted()
1110+
).width
1111+
left_margin_display = 11 # points
1112+
button_text_offset_display = 5.5 # points
1113+
col_spacing_display = 11 # points
1114+
1115+
# Convert to axes coordinates
1116+
left_margin = left_margin_display / axes_width_display
1117+
button_text_offset = button_text_offset_display / axes_width_display
1118+
col_spacing = col_spacing_display / axes_width_display
1119+
1120+
# Create text objects to measure widths.
1121+
# We create Text objects directly rather than using ax.text() since we're
1122+
# only measuring them and only later add them to the axes.
10771123
self.labels = [
1078-
self.ax.text(0.25, y, label, transform=self.ax.transAxes,
1079-
horizontalalignment="left", verticalalignment="center",
1080-
**props)
1081-
for y, label, props in zip(self._buttons_ys, labels, label_props)]
1124+
mtext.Text(0, 0, text=label, transform=self.ax.transAxes,
1125+
horizontalalignment="left", verticalalignment="center",
1126+
**props)
1127+
for label, props in zip(labels, label_props)
1128+
]
1129+
# Set figure reference so Text objects can access figure properties
1130+
for text in self.labels:
1131+
text.set_figure(fig)
1132+
# Calculate max text width per column (in axes coordinates)
1133+
renderer = self.ax.figure.canvas.get_renderer()
1134+
inv_trans = fig.dpi_scale_trans.inverted()
1135+
col_widths = [
1136+
max(
1137+
(
1138+
text.get_window_extent(renderer).transformed(inv_trans).width * 72
1139+
for text in self.labels[col_idx::n_cols]
1140+
),
1141+
default=0,
1142+
)
1143+
/ axes_width_display
1144+
for col_idx in range(n_cols)
1145+
]
1146+
1147+
# Center rows vertically in the axes
1148+
ys_per_row = np.linspace(1, 0, n_rows + 2)[1:-1]
1149+
# Calculate x positions based on text widths
1150+
col_x_positions = [left_margin] # First column starts at left margin
1151+
for col_idx in range(n_cols - 1):
1152+
col_x_positions.append(
1153+
col_x_positions[-1] +
1154+
button_text_offset +
1155+
col_widths[col_idx] +
1156+
col_spacing
1157+
)
1158+
label_idx = np.arange(n_labels)
1159+
self._buttons_xs = np.take(col_x_positions, label_idx % n_cols)
1160+
self._buttons_ys = ys_per_row[label_idx // n_cols]
1161+
for text, x, y in zip(self.labels, self._buttons_xs, self._buttons_ys):
1162+
text.set_position((x + button_text_offset, y))
1163+
self.ax.add_artist(text)
10821164

10831165
def _init_props(self, text_size, **kwargs):
10841166
raise NotImplementedError("This method should be defined in subclasses")
@@ -1165,7 +1247,7 @@ class CheckButtons(_Buttons):
11651247
The text label objects of the check buttons.
11661248
"""
11671249

1168-
def __init__(self, ax, labels, actives=None, *, useblit=True,
1250+
def __init__(self, ax, labels, actives=None, *, layout=None, useblit=True,
11691251
label_props=None, frame_props=None, check_props=None):
11701252
"""
11711253
Add check buttons to `~.axes.Axes` instance *ax*.
@@ -1179,6 +1261,30 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
11791261
actives : list of bool, optional
11801262
The initial check states of the buttons. The list must have the
11811263
same length as *labels*. If not given, all buttons are unchecked.
1264+
layout : None or "vertical" or "horizontal" or (int, int), default: None
1265+
The layout of the check buttons. Options are:
1266+
1267+
- ``None``: Use legacy vertical layout (default).
1268+
- ``"vertical"``: Arrange buttons in a single column with
1269+
dynamic positioning based on text widths.
1270+
- ``"horizontal"``: Arrange buttons in a single row with
1271+
dynamic positioning based on text widths.
1272+
- ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1273+
specified number of rows and columns. Buttons are placed
1274+
left-to-right, top-to-bottom with dynamic positioning.
1275+
1276+
The layout options "vertical", "horizontal" and ``(rows, cols)``
1277+
create ``mtext.Text`` objects to determine exact text sizes, and
1278+
then they are added to the Axes. This is usually okay, but may cause
1279+
side-effects and has a slight performance impact. Therefore the
1280+
default ``None`` value avoids this.
1281+
1282+
.. admonition:: Provisional
1283+
The new layout options are provisional. Their algorithmic
1284+
behavior, including the exact positions of buttons and labels,
1285+
may still change without prior warning.
1286+
1287+
.. versionadded:: 3.11
11821288
useblit : bool, default: True
11831289
Use blitting for faster drawing if supported by the backend.
11841290
See the tutorial :ref:`blitting` for details.
@@ -1208,9 +1314,9 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
12081314
_api.check_isinstance((dict, None), label_props=label_props,
12091315
frame_props=frame_props, check_props=check_props)
12101316

1211-
super().__init__(ax, labels, useblit=useblit, label_props=label_props,
1212-
actives=actives, frame_props=frame_props,
1213-
check_props=check_props)
1317+
super().__init__(ax, labels, layout=layout, useblit=useblit,
1318+
label_props=label_props, actives=actives,
1319+
frame_props=frame_props, check_props=check_props)
12141320

12151321
def _init_props(self, text_size, actives, frame_props, check_props):
12161322
frame_props = {
@@ -1671,7 +1777,7 @@ class RadioButtons(_Buttons):
16711777
The index of the selected button.
16721778
"""
16731779

1674-
def __init__(self, ax, labels, active=0, activecolor=None, *,
1780+
def __init__(self, ax, labels, active=0, activecolor=None, *, layout=None,
16751781
useblit=True, label_props=None, radio_props=None):
16761782
"""
16771783
Add radio buttons to an `~.axes.Axes`.
@@ -1687,6 +1793,30 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16871793
activecolor : :mpltype:`color`
16881794
The color of the selected button. The default is ``'blue'`` if not
16891795
specified here or in *radio_props*.
1796+
layout : None or "vertical" or "horizontal" or (int, int), default: None
1797+
The layout of the radio buttons. Options are:
1798+
1799+
- ``None``: Use legacy vertical layout (default).
1800+
- ``"vertical"``: Arrange buttons in a single column with
1801+
dynamic positioning based on text widths.
1802+
- ``"horizontal"``: Arrange buttons in a single row with
1803+
dynamic positioning based on text widths.
1804+
- ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1805+
specified number of rows and columns. Buttons are placed
1806+
left-to-right, top-to-bottom with dynamic positioning.
1807+
1808+
The layout options "vertical", "horizontal" and ``(rows, cols)``
1809+
create ``mtext.Text`` objects to determine exact text sizes, and
1810+
then they are added to the Axes. This is usually okay, but may cause
1811+
side-effects and has a slight performance impact. Therefore the
1812+
default ``None`` value avoids this.
1813+
1814+
.. admonition:: Provisional
1815+
The new layout options are provisional. Their algorithmic
1816+
behavior, including the exact positions of buttons and labels,
1817+
may still change without prior warning.
1818+
1819+
.. versionadded:: 3.11
16901820
useblit : bool, default: True
16911821
Use blitting for faster drawing if supported by the backend.
16921822
See the tutorial :ref:`blitting` for details.
@@ -1726,7 +1856,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
17261856
else:
17271857
activecolor = 'blue' # Default.
17281858
super().__init__(ax, labels, useblit=useblit, label_props=label_props,
1729-
active=active, activecolor=activecolor,
1859+
active=active, layout=layout, activecolor=activecolor,
17301860
radio_props=radio_props)
17311861

17321862
self._activecolor = activecolor

0 commit comments

Comments
 (0)