Skip to content

Add support for horizontal CheckButtons #27946

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions doc/users/next_whats_new/widget_horizontal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
CheckButtons / RadioButtons widget may now be laid out horizontally
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The `.CheckButtons` and `.RadioButtons` widget's primary layout direction may
now be specified with the *layout_direction* keyword argument:

.. plot::
:include-source:

import matplotlib.pyplot as plt
from matplotlib.widgets import CheckButtons, RadioButtons

fig = plt.figure(figsize=(4, 2))

# Default orientation is vertical:
rbv = RadioButtons(fig.add_axes((0.05, 0.6, 0.2, 0.35)),
('Radio 1', 'Radio 2', 'Radio 3'),
layout_direction='vertical')
cbv = CheckButtons(fig.add_axes((0.05, 0.2, 0.2, 0.35)),
('Check 1', 'Check 2', 'Check 3'),
layout_direction='vertical')

# Alternatively, a horizontal orientation may be used:
rbh = RadioButtons(fig.add_axes((0.3, 0.6, 0.6, 0.35)),
('Radio 1', 'Radio 2', 'Radio 3'),
layout_direction='horizontal')
cbh = CheckButtons(fig.add_axes((0.3, 0.2, 0.6, 0.35)),
('Check 1', 'Check 2', 'Check 3'),
layout_direction='horizontal')
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 45 additions & 15 deletions lib/matplotlib/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,9 +1042,11 @@ def test_lasso_set_props(ax):
assert line.get_alpha() == 0.3


def test_CheckButtons(ax):
@pytest.mark.parametrize('layout_direction', ['vertical', 'horizontal'])
def test_CheckButtons(ax, layout_direction):
labels = ('a', 'b', 'c')
check = widgets.CheckButtons(ax, labels, (True, False, True))
check = widgets.CheckButtons(ax, labels, (True, False, True),
layout_direction=layout_direction)
assert check.get_status() == [True, False, True]
check.set_active(0)
assert check.get_status() == [False, False, True]
Expand Down Expand Up @@ -1097,8 +1099,10 @@ def test_TextBox(ax, toolbar):
assert text_change_event.call_count == 3


def test_RadioButtons(ax):
radio = widgets.RadioButtons(ax, ('Radio 1', 'Radio 2', 'Radio 3'))
@pytest.mark.parametrize('layout_direction', ['vertical', 'horizontal'])
def test_RadioButtons(ax, layout_direction):
radio = widgets.RadioButtons(ax, ('Radio 1', 'Radio 2', 'Radio 3'),
layout_direction=layout_direction)
radio.set_active(1)
assert radio.value_selected == 'Radio 2'
assert radio.index_selected == 1
Expand All @@ -1109,28 +1113,54 @@ def test_RadioButtons(ax):

@image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True)
def test_check_radio_buttons_image():
ax = get_ax()
fig = ax.figure
fig.subplots_adjust(left=0.3)
fig = plt.figure()

rb1 = widgets.RadioButtons(fig.add_axes((0.05, 0.7, 0.2, 0.15)),
('Radio 1', 'Radio 2', 'Radio 3'))

rax1 = fig.add_axes((0.05, 0.7, 0.2, 0.15))
rb1 = widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3'))
rb2 = widgets.RadioButtons(fig.add_axes((0.3, 0.7, 0.6, 0.15)),
('Radio 1', 'Radio 2', 'Radio 3'),
layout_direction='horizontal')

rax2 = fig.add_axes((0.05, 0.5, 0.2, 0.15))
cb1 = widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'),
cb1 = widgets.CheckButtons(fig.add_axes((0.05, 0.5, 0.2, 0.15)),
('Check 1', 'Check 2', 'Check 3'),
(False, True, True))

rax3 = fig.add_axes((0.05, 0.3, 0.2, 0.15))
cb2 = widgets.CheckButtons(fig.add_axes((0.3, 0.5, 0.6, 0.15)),
('Check 1', 'Check 2', 'Check 3'),
(False, True, True),
layout_direction='horizontal')

rb3 = widgets.RadioButtons(
rax3, ('Radio 1', 'Radio 2', 'Radio 3'),
fig.add_axes((0.05, 0.3, 0.2, 0.15)),
('Radio 1', 'Radio 2', 'Radio 3'),
label_props={'fontsize': [8, 12, 16],
'color': ['red', 'green', 'blue']},
radio_props={'edgecolor': ['red', 'green', 'blue'],
'facecolor': ['mistyrose', 'palegreen', 'lightblue']})

rb4 = widgets.RadioButtons(
fig.add_axes((0.3, 0.3, 0.6, 0.15)),
('Radio 1', 'Radio 2', 'Radio 3'),
layout_direction='horizontal',
label_props={'fontsize': [8, 12, 16],
'color': ['red', 'green', 'blue']},
radio_props={'edgecolor': ['red', 'green', 'blue'],
'facecolor': ['mistyrose', 'palegreen', 'lightblue']})

rax4 = fig.add_axes((0.05, 0.1, 0.2, 0.15))
cb3 = widgets.CheckButtons(
fig.add_axes((0.05, 0.1, 0.2, 0.15)),
('Check 1', 'Check 2', 'Check 3'), (False, True, True),
label_props={'fontsize': [8, 12, 16],
'color': ['red', 'green', 'blue']},
frame_props={'edgecolor': ['red', 'green', 'blue'],
'facecolor': ['mistyrose', 'palegreen', 'lightblue']},
check_props={'color': ['red', 'green', 'blue']})

cb4 = widgets.CheckButtons(
rax4, ('Check 1', 'Check 2', 'Check 3'), (False, True, True),
fig.add_axes((0.3, 0.1, 0.6, 0.15)),
('Check 1', 'Check 2', 'Check 3'), (False, True, True),
layout_direction='horizontal',
label_props={'fontsize': [8, 12, 16],
'color': ['red', 'green', 'blue']},
frame_props={'edgecolor': ['red', 'green', 'blue'],
Expand Down
72 changes: 59 additions & 13 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,8 @@ class CheckButtons(AxesWidget):
"""

def __init__(self, ax, labels, actives=None, *, useblit=True,
label_props=None, frame_props=None, check_props=None):
label_props=None, frame_props=None, check_props=None,
layout_direction='vertical'):
"""
Add check buttons to `~.axes.Axes` instance *ax*.

Expand Down Expand Up @@ -1026,9 +1027,16 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
black color, and 1.0 linewidth.

.. versionadded:: 3.7
layout_direction : {'vertical', 'horizontal'}
The orientation of the buttons: 'vertical' places buttons from top
to bottom, 'horizontal' places buttons from left to right.

.. versionadded:: 3.10
"""
super().__init__(ax)

_api.check_in_list(['vertical', 'horizontal'],
layout_direction=layout_direction)
_api.check_isinstance((dict, None), label_props=label_props,
frame_props=frame_props, check_props=check_props)

Expand All @@ -1042,14 +1050,29 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
self._useblit = useblit and self.canvas.supports_blit
self._background = None

ys = np.linspace(1, 0, len(labels)+2)[1:-1]
if layout_direction == 'vertical':
# Place buttons from top to bottom with buttons at (0.15, y) and labels
# at (0.25, y), where y is evenly spaced within the Axes.
button_ys = label_ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
button_xs = np.full_like(button_ys, 0.15)
label_xs = np.full_like(label_ys, 0.25)
label_ha = 'left'
label_va = 'center'
else:
# Place buttons from left to right with buttons at (x, 0.15) and labels
# at (x, 0.25), where x is evenly spaced within the Axes.
button_xs = label_xs = np.linspace(0, 1, len(labels) + 2)[1:-1]
button_ys = np.full_like(button_xs, 0.15)
label_ys = np.full_like(label_xs, 0.25)
label_ha = 'center'
label_va = 'bottom'

label_props = _expand_text_props(label_props)
self.labels = [
ax.text(0.25, y, label, transform=ax.transAxes,
horizontalalignment="left", verticalalignment="center",
ax.text(x, y, label, transform=ax.transAxes,
horizontalalignment=label_ha, verticalalignment=label_va,
**props)
for y, label, props in zip(ys, labels, label_props)]
for x, y, label, props in zip(label_xs, label_ys, labels, label_props)]
text_size = np.array([text.get_fontsize() for text in self.labels]) / 2

frame_props = {
Expand All @@ -1061,7 +1084,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
}
frame_props.setdefault('facecolor', frame_props.get('color', 'none'))
frame_props.setdefault('edgecolor', frame_props.pop('color', 'black'))
self._frames = ax.scatter([0.15] * len(ys), ys, **frame_props)
self._frames = ax.scatter(button_xs, button_ys, **frame_props)
check_props = {
'linewidth': 1,
's': text_size**2,
Expand All @@ -1071,7 +1094,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
'animated': self._useblit,
}
check_props.setdefault('facecolor', check_props.pop('color', 'black'))
self._checks = ax.scatter([0.15] * len(ys), ys, **check_props)
self._checks = ax.scatter(button_xs, button_ys, **check_props)
# The user may have passed custom colours in check_props, so we need to
# create the checks (above), and modify the visibility after getting
# whatever the user set.
Expand Down Expand Up @@ -1557,7 +1580,8 @@ class RadioButtons(AxesWidget):
"""

def __init__(self, ax, labels, active=0, activecolor=None, *,
useblit=True, label_props=None, radio_props=None):
useblit=True, label_props=None, radio_props=None,
layout_direction='vertical'):
"""
Add radio buttons to an `~.axes.Axes`.

Expand Down Expand Up @@ -1593,9 +1617,16 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
button.

.. versionadded:: 3.7
layout_direction : {'vertical', 'horizontal'}
The orientation of the buttons: 'vertical' places buttons from top
to bottom, 'horizontal' places buttons from left to right.

.. versionadded:: 3.10
"""
super().__init__(ax)

_api.check_in_list(['vertical', 'horizontal'],
layout_direction=layout_direction)
_api.check_isinstance((dict, None), label_props=label_props,
radio_props=radio_props)

Expand All @@ -1619,17 +1650,32 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
ax.set_yticks([])
ax.set_navigate(False)

ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
if layout_direction == 'vertical':
# Place buttons from top to bottom with buttons at (0.15, y) and labels
# at (0.25, y), where y is evenly spaced within the Axes.
button_ys = label_ys = np.linspace(1, 0, len(labels) + 2)[1:-1]
button_xs = np.full_like(button_ys, 0.15)
label_xs = np.full_like(label_ys, 0.25)
label_ha = 'left'
label_va = 'center'
else:
# Place buttons from left to right with buttons at (x, 0.15) and labels
# at (x, 0.25), where x is evenly spaced within the Axes.
button_xs = label_xs = np.linspace(0, 1, len(labels) + 2)[1:-1]
button_ys = np.full_like(button_xs, 0.15)
label_ys = np.full_like(label_xs, 0.25)
label_ha = 'center'
label_va = 'bottom'

self._useblit = useblit and self.canvas.supports_blit
self._background = None

label_props = _expand_text_props(label_props)
self.labels = [
ax.text(0.25, y, label, transform=ax.transAxes,
horizontalalignment="left", verticalalignment="center",
ax.text(x, y, label, transform=ax.transAxes,
horizontalalignment=label_ha, verticalalignment=label_va,
**props)
for y, label, props in zip(ys, labels, label_props)]
for x, y, label, props in zip(label_xs, label_ys, labels, label_props)]
text_size = np.array([text.get_fontsize() for text in self.labels]) / 2

radio_props = {
Expand All @@ -1642,7 +1688,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
radio_props.setdefault('edgecolor', radio_props.get('color', 'black'))
radio_props.setdefault('facecolor',
radio_props.pop('color', activecolor))
self._buttons = ax.scatter([.15] * len(ys), ys, **radio_props)
self._buttons = ax.scatter(button_xs, button_ys, **radio_props)
# The user may have passed custom colours in radio_props, so we need to
# create the radios, and modify the visibility after getting whatever
# the user set.
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/widgets.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class CheckButtons(AxesWidget):
label_props: dict[str, Any] | None = ...,
frame_props: dict[str, Any] | None = ...,
check_props: dict[str, Any] | None = ...,
layout_direction: Literal["vertical", "horizontal"] = ...,
) -> None: ...
def set_label_props(self, props: dict[str, Any]) -> None: ...
def set_frame_props(self, props: dict[str, Any]) -> None: ...
Expand Down Expand Up @@ -210,6 +211,7 @@ class RadioButtons(AxesWidget):
useblit: bool = ...,
label_props: dict[str, Any] | Sequence[dict[str, Any]] | None = ...,
radio_props: dict[str, Any] | None = ...,
layout_direction: Literal["vertical", "horizontal"] = ...,
) -> None: ...
def set_label_props(self, props: dict[str, Any]) -> None: ...
def set_radio_props(self, props: dict[str, Any]) -> None: ...
Expand Down
Loading