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

Commit 7ef400d

Browse files
committed
widgets: Outsource buttons layout calculation
1 parent bbdf65c commit 7ef400d

File tree

2 files changed

+161
-103
lines changed

2 files changed

+161
-103
lines changed

lib/matplotlib/widgets.py

Lines changed: 160 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,150 @@ def disconnect(self, cid):
15581558
self._observers.disconnect(cid)
15591559

15601560

1561+
def _calculate_widget_button_layout(ax, labels, label_props, layout):
1562+
"""
1563+
Calculate positions for button widgets (RadioButtons, CheckButtons).
1564+
1565+
Parameters
1566+
----------
1567+
ax : `~matplotlib.axes.Axes`
1568+
The Axes to calculate positions for.
1569+
labels : list of str
1570+
The button labels.
1571+
label_props : iterable of dict
1572+
Text properties for each label (from _expand_text_props).
1573+
layout : None, "vertical", "horizontal", or (int, int)
1574+
The layout specification:
1575+
- None: Use legacy vertical layout (fixed positions).
1576+
- "vertical": Arrange buttons in a single column.
1577+
- "horizontal": Arrange buttons in a single row.
1578+
- (rows, cols): Arrange buttons in a grid.
1579+
1580+
Returns
1581+
-------
1582+
button_xs : list of float
1583+
X coordinates for buttons in axes coordinates.
1584+
button_ys : list of float
1585+
Y coordinates for buttons in axes coordinates.
1586+
label_xs : list of float
1587+
X coordinates for labels in axes coordinates.
1588+
label_ys : list of float
1589+
Y coordinates for labels in axes coordinates.
1590+
"""
1591+
n_labels = len(labels)
1592+
1593+
if layout is None:
1594+
# Legacy behavior: simple vertical layout with fixed positions
1595+
ys = np.linspace(1, 0, n_labels + 2)[1:-1]
1596+
button_xs = [0.15] * n_labels
1597+
button_ys = list(ys)
1598+
label_xs = [0.25] * n_labels
1599+
label_ys = list(ys)
1600+
return button_xs, button_ys, label_xs, label_ys
1601+
1602+
# New layout algorithm with text measurement
1603+
# Parse layout parameter
1604+
bad_layout_raise_msg = \
1605+
"layout must be None, 'vertical', 'horizontal', or a (rows, cols) tuple; " \
1606+
f"got {layout!r}"
1607+
if isinstance(layout, str):
1608+
if layout == "vertical":
1609+
n_rows, n_cols = n_labels, 1
1610+
elif layout == "horizontal":
1611+
n_rows, n_cols = 1, n_labels
1612+
else:
1613+
raise ValueError(bad_layout_raise_msg)
1614+
elif isinstance(layout, tuple) and len(layout) == 2:
1615+
n_rows, n_cols = layout
1616+
if not (isinstance(n_rows, int) and isinstance(n_cols, int)):
1617+
raise TypeError(
1618+
f"layout tuple must contain two integers; got {layout!r}"
1619+
)
1620+
if n_rows * n_cols < n_labels:
1621+
raise ValueError(
1622+
f"layout {layout} has {n_rows * n_cols} positions but "
1623+
f"{n_labels} labels were provided"
1624+
)
1625+
else:
1626+
raise ValueError(bad_layout_raise_msg)
1627+
1628+
# Define spacing in display units (pixels) for consistency
1629+
# across different axes sizes
1630+
axes_width_display = ax.bbox.width
1631+
left_margin_display = 15 # pixels
1632+
button_text_offset_display = 6.5 # pixels
1633+
col_spacing_display = 15 # pixels
1634+
1635+
# Convert to axes coordinates
1636+
left_margin = left_margin_display / axes_width_display
1637+
button_text_offset = button_text_offset_display / axes_width_display
1638+
col_spacing = col_spacing_display / axes_width_display
1639+
1640+
# Create temporary text objects to measure widths
1641+
temp_texts = []
1642+
for label, props in zip(labels, label_props):
1643+
temp_texts.append(ax.text(
1644+
0,
1645+
0,
1646+
label,
1647+
transform=ax.transAxes,
1648+
**props,
1649+
))
1650+
# Force a draw to get accurate text measurements
1651+
ax.figure.canvas.draw()
1652+
1653+
# Calculate max text width per column (in axes coordinates)
1654+
col_widths = []
1655+
for col_idx in range(n_cols):
1656+
col_texts = []
1657+
for row_idx in range(n_rows):
1658+
label_idx = row_idx * n_cols + col_idx
1659+
if label_idx < n_labels:
1660+
col_texts.append(temp_texts[label_idx])
1661+
if col_texts:
1662+
col_widths.append(
1663+
max(
1664+
text.get_window_extent(
1665+
ax.figure.canvas.get_renderer()
1666+
).width
1667+
for text in col_texts
1668+
) / axes_width_display
1669+
)
1670+
else:
1671+
col_widths.append(0)
1672+
# Remove temporary text objects
1673+
for text in temp_texts:
1674+
text.remove()
1675+
1676+
# Center rows vertically in the axes
1677+
ys_per_row = np.linspace(1, 0, n_rows + 2)[1:-1]
1678+
# Calculate x positions based on text widths
1679+
col_x_positions = [left_margin] # First column starts at left margin
1680+
for col_idx in range(n_cols - 1):
1681+
col_x_positions.append(
1682+
col_x_positions[-1] +
1683+
button_text_offset +
1684+
col_widths[col_idx] +
1685+
col_spacing
1686+
)
1687+
# Create final positions (left-to-right, top-to-bottom)
1688+
button_xs = []
1689+
button_ys = []
1690+
label_xs = []
1691+
label_ys = []
1692+
for label_idx in range(n_labels):
1693+
row_idx = label_idx // n_cols
1694+
col_idx = label_idx % n_cols
1695+
x = col_x_positions[col_idx]
1696+
y = ys_per_row[row_idx]
1697+
button_xs.append(x)
1698+
button_ys.append(y)
1699+
label_xs.append(x + button_text_offset)
1700+
label_ys.append(y)
1701+
1702+
return button_xs, button_ys, label_xs, label_ys
1703+
1704+
15611705
class RadioButtons(AxesWidget):
15621706
"""
15631707
A GUI neutral radio button.
@@ -1582,7 +1726,7 @@ class RadioButtons(AxesWidget):
15821726
"""
15831727

15841728
def __init__(self, ax, labels, active=0, activecolor=None, *,
1585-
layout="vertical", useblit=True, label_props=None, radio_props=None):
1729+
layout=None, useblit=True, label_props=None, radio_props=None):
15861730
"""
15871731
Add radio buttons to an `~.axes.Axes`.
15881732
@@ -1597,14 +1741,18 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
15971741
activecolor : :mpltype:`color`
15981742
The color of the selected button. The default is ``'blue'`` if not
15991743
specified here or in *radio_props*.
1600-
layout : {"vertical", "horizontal"} or (int, int), default: "vertical"
1744+
layout : None, {"vertical", "horizontal"} or (int, int), default: None
16011745
The layout of the radio buttons. Options are:
16021746
1603-
- ``"vertical"``: Arrange buttons in a single column (default).
1604-
- ``"horizontal"``: Arrange buttons in a single row.
1747+
- ``None``: Use legacy vertical layout (default, keeps backward
1748+
compatibility with fixed button and label positions).
1749+
- ``"vertical"``: Arrange buttons in a single column with
1750+
dynamic positioning based on text widths.
1751+
- ``"horizontal"``: Arrange buttons in a single row with
1752+
dynamic positioning based on text widths.
16051753
- ``(rows, cols)`` tuple: Arrange buttons in a grid with the
16061754
specified number of rows and columns. Buttons are placed
1607-
left-to-right, top-to-bottom.
1755+
left-to-right, top-to-bottom with dynamic positioning.
16081756
16091757
.. versionadded:: 3.11
16101758
useblit : bool, default: True
@@ -1640,31 +1788,6 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16401788
labels = list(labels)
16411789
n_labels = len(labels)
16421790

1643-
bad_layout_raise_msg = \
1644-
"layout must be 'vertical', 'horizontal', or a (rows, cols) tuple; " \
1645-
f"got {layout!r}"
1646-
# Parse layout parameter
1647-
if isinstance(layout, str):
1648-
if layout == "vertical":
1649-
n_rows, n_cols = n_labels, 1
1650-
elif layout == "horizontal":
1651-
n_rows, n_cols = 1, n_labels
1652-
else:
1653-
raise ValueError(bad_layout_raise_msg)
1654-
elif isinstance(layout, tuple) and len(layout) == 2:
1655-
n_rows, n_cols = layout
1656-
if not (isinstance(n_rows, int) and isinstance(n_cols, int)):
1657-
raise TypeError(
1658-
f"layout tuple must contain two integers; got {layout!r}"
1659-
)
1660-
if n_rows * n_cols < n_labels:
1661-
raise ValueError(
1662-
f"layout {layout} has {n_rows * n_cols} positions but "
1663-
f"{n_labels} labels were provided"
1664-
)
1665-
else:
1666-
raise ValueError(bad_layout_raise_msg)
1667-
16681791
radio_props = cbook.normalize_kwargs(radio_props,
16691792
collections.PathCollection)
16701793
if activecolor is not None:
@@ -1690,81 +1813,16 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16901813

16911814
label_props = _expand_text_props(label_props)
16921815

1693-
# Define spacing in display units (pixels) for consistency
1694-
# across different axes sizes
1695-
axes_width_display = ax.bbox.width
1696-
left_margin_display = 15 # pixels
1697-
button_text_offset_display = 6.5 # pixels
1698-
col_spacing_display = 15 # pixels
1699-
1700-
# Convert to axes coordinates
1701-
left_margin = left_margin_display / axes_width_display
1702-
button_text_offset = button_text_offset_display / axes_width_display
1703-
col_spacing = col_spacing_display / axes_width_display
1704-
1705-
# Create temporary text objects to measure widths
1706-
temp_texts = []
1707-
for label, props in zip(labels, label_props):
1708-
temp_texts.append(ax.text(
1709-
0,
1710-
0,
1711-
label,
1712-
transform=ax.transAxes,
1713-
**props,
1714-
))
1715-
# Force a draw to get accurate text measurements
1716-
ax.figure.canvas.draw()
1717-
1718-
# Calculate max text width per column (in axes coordinates)
1719-
col_widths = []
1720-
for col_idx in range(n_cols):
1721-
col_texts = []
1722-
for row_idx in range(n_rows):
1723-
label_idx = row_idx * n_cols + col_idx
1724-
if label_idx < n_labels:
1725-
col_texts.append(temp_texts[label_idx])
1726-
if col_texts:
1727-
col_widths.append(
1728-
max(
1729-
text.get_window_extent(
1730-
ax.figure.canvas.get_renderer()
1731-
).width
1732-
for text in col_texts
1733-
) / axes_width_display
1734-
)
1735-
else:
1736-
col_widths.append(0)
1737-
# Remove temporary text objects
1738-
for text in temp_texts:
1739-
text.remove()
1740-
1741-
# Center rows vertically in the axes
1742-
ys_per_row = np.linspace(1, 0, n_rows + 2)[1:-1]
1743-
# Calculate x positions based on text widths
1744-
col_x_positions = [left_margin] # First column starts at left margin
1745-
for col_idx in range(n_cols - 1):
1746-
col_x_positions.append(
1747-
col_x_positions[-1] +
1748-
button_text_offset +
1749-
col_widths[col_idx] +
1750-
col_spacing
1751-
)
1752-
# Create final positions (left-to-right, top-to-bottom)
1753-
xs = []
1754-
ys = []
1755-
for label_idx in range(n_labels):
1756-
row_idx = label_idx // n_cols
1757-
col_idx = label_idx % n_cols
1758-
x = col_x_positions[col_idx]
1759-
y = ys_per_row[row_idx]
1760-
xs.append(x)
1761-
ys.append(y)
1816+
# Calculate button and label positions
1817+
button_xs, button_ys, label_xs, label_ys = _calculate_widget_button_layout(
1818+
ax, labels, label_props, layout
1819+
)
17621820

17631821
self.labels = [
1764-
ax.text(x + button_text_offset, y, label, transform=ax.transAxes,
1822+
ax.text(x, y, label, transform=ax.transAxes,
17651823
horizontalalignment="left", verticalalignment="center",
17661824
**props)
1767-
for x, y, label, props in zip(xs, ys, labels, label_props)]
1825+
for x, y, label, props in zip(label_xs, label_ys, labels, label_props)]
17681826
text_size = np.array([text.get_fontsize() for text in self.labels]) / 2
17691827

17701828
radio_props = {
@@ -1777,7 +1835,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
17771835
radio_props.setdefault('edgecolor', radio_props.get('color', 'black'))
17781836
radio_props.setdefault('facecolor',
17791837
radio_props.pop('color', activecolor))
1780-
self._buttons = ax.scatter(xs, ys, **radio_props)
1838+
self._buttons = ax.scatter(button_xs, button_ys, **radio_props)
17811839
# The user may have passed custom colours in radio_props, so we need to
17821840
# create the radios, and modify the visibility after getting whatever
17831841
# the user set.

lib/matplotlib/widgets.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ class RadioButtons(AxesWidget):
210210
active: int = ...,
211211
activecolor: ColorType | None = ...,
212212
*,
213-
layout: Literal["vertical", "horizontal"] | tuple[int, int] = ...,
213+
layout: Literal["vertical", "horizontal"] | tuple[int, int] | None = ...,
214214
useblit: bool = ...,
215215
label_props: dict[str, Sequence[Any]] | None = ...,
216216
radio_props: dict[str, Any] | None = ...,

0 commit comments

Comments
 (0)