diff --git a/doc/users/next_whats_new/widget_horizontal.rst b/doc/users/next_whats_new/widget_horizontal.rst new file mode 100644 index 000000000000..896bec7a9679 --- /dev/null +++ b/doc/users/next_whats_new/widget_horizontal.rst @@ -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') diff --git a/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png b/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png index f0d5023008ca..2576dacc6bcd 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png and b/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png differ diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index d559ad99ef0f..0307205e00eb 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -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] @@ -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 @@ -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'], diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5e4c52ed2478..9d89e0e7283c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -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*. @@ -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) @@ -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 = { @@ -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, @@ -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. @@ -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`. @@ -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) @@ -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 = { @@ -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. diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index f5de6cb62414..733a768cd76f 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -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: ... @@ -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: ...