diff --git a/doc/users/next_whats_new/widget_button_styling.rst b/doc/users/next_whats_new/widget_button_styling.rst new file mode 100644 index 000000000000..4307e6977fc5 --- /dev/null +++ b/doc/users/next_whats_new/widget_button_styling.rst @@ -0,0 +1,31 @@ +Custom styling of button widgets +-------------------------------- + +Additional custom styling of button widgets may be achieved via the +*label_props* and *radio_props* arguments to `.RadioButtons`; and the +*label_props*, *frame_props*, and *check_props* arguments to `.CheckButtons`. + +.. plot:: + + from matplotlib.widgets import CheckButtons, RadioButtons + + fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(5, 2), width_ratios=[1, 2]) + default_rb = RadioButtons(ax[0, 0], ['Apples', 'Oranges']) + styled_rb = RadioButtons(ax[0, 1], ['Apples', 'Oranges'], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + radio_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}) + + default_cb = CheckButtons(ax[1, 0], ['Apples', 'Oranges'], + actives=[True, True]) + styled_cb = CheckButtons(ax[1, 1], ['Apples', 'Oranges'], + actives=[True, True], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + frame_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}, + check_props={'color': ['darkred', 'darkorange']}) + + ax[0, 0].set_title('Default') + ax[0, 1].set_title('Stylized') diff --git a/examples/widgets/check_buttons.py b/examples/widgets/check_buttons.py index 493ac4c3e79a..b96ffb51c0af 100644 --- a/examples/widgets/check_buttons.py +++ b/examples/widgets/check_buttons.py @@ -20,19 +20,23 @@ s2 = np.sin(6*np.pi*t) fig, ax = plt.subplots() -l0, = ax.plot(t, s0, visible=False, lw=2, color='k', label='2 Hz') -l1, = ax.plot(t, s1, lw=2, color='r', label='4 Hz') -l2, = ax.plot(t, s2, lw=2, color='g', label='6 Hz') +l0, = ax.plot(t, s0, visible=False, lw=2, color='black', label='1 Hz') +l1, = ax.plot(t, s1, lw=2, color='red', label='2 Hz') +l2, = ax.plot(t, s2, lw=2, color='green', label='3 Hz') fig.subplots_adjust(left=0.2) lines_by_label = {l.get_label(): l for l in [l0, l1, l2]} +line_colors = [l.get_color() for l in lines_by_label.values()] # Make checkbuttons with all plotted lines with correct visibility rax = fig.add_axes([0.05, 0.4, 0.1, 0.15]) check = CheckButtons( ax=rax, labels=lines_by_label.keys(), - actives=[l.get_visible() for l in lines_by_label.values()] + actives=[l.get_visible() for l in lines_by_label.values()], + label_props={'color': line_colors}, + frame_props={'edgecolor': line_colors}, + check_props={'facecolor': line_colors}, ) diff --git a/examples/widgets/radio_buttons.py b/examples/widgets/radio_buttons.py index 28e446fc5b80..4d3c93e579b6 100644 --- a/examples/widgets/radio_buttons.py +++ b/examples/widgets/radio_buttons.py @@ -25,23 +25,31 @@ axcolor = 'lightgoldenrodyellow' rax = fig.add_axes([0.05, 0.7, 0.15, 0.15], facecolor=axcolor) -radio = RadioButtons(rax, ('2 Hz', '4 Hz', '8 Hz')) +radio = RadioButtons(rax, ('1 Hz', '2 Hz', '4 Hz'), + label_props={'color': 'cmy', 'fontsize': [12, 14, 16]}, + radio_props={'s': [16, 32, 64]}) def hzfunc(label): - hzdict = {'2 Hz': s0, '4 Hz': s1, '8 Hz': s2} + hzdict = {'1 Hz': s0, '2 Hz': s1, '4 Hz': s2} ydata = hzdict[label] l.set_ydata(ydata) - plt.draw() + fig.canvas.draw() radio.on_clicked(hzfunc) rax = fig.add_axes([0.05, 0.4, 0.15, 0.15], facecolor=axcolor) -radio2 = RadioButtons(rax, ('red', 'blue', 'green')) +radio2 = RadioButtons( + rax, ('red', 'blue', 'green'), + label_props={'color': ['red', 'blue', 'green']}, + radio_props={ + 'facecolor': ['red', 'blue', 'green'], + 'edgecolor': ['darkred', 'darkblue', 'darkgreen'], + }) def colorfunc(label): l.set_color(label) - plt.draw() + fig.canvas.draw() radio2.on_clicked(colorfunc) rax = fig.add_axes([0.05, 0.1, 0.15, 0.15], facecolor=axcolor) @@ -50,7 +58,7 @@ def colorfunc(label): def stylefunc(label): l.set_linestyle(label) - plt.draw() + fig.canvas.draw() radio3.on_clicked(stylefunc) plt.show() 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 e96085d9bffd..d6e6004a1732 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 4c4f2fdc240f..5eb32e69901a 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -992,21 +992,38 @@ def test_TextBox(ax, toolbar): @image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True) def test_check_radio_buttons_image(): ax = get_ax() - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 + fig = ax.figure + fig.subplots_adjust(left=0.3) - plt.subplots_adjust(left=0.3) - rax1 = plt.axes([0.05, 0.7, 0.15, 0.15]) - rax2 = plt.axes([0.05, 0.2, 0.15, 0.15]) - rb = widgets.RadioButtons(rax1, ('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')) with pytest.warns(DeprecationWarning, match='The circles attribute was deprecated'): - rb.circles # Trigger the old-style elliptic radiobuttons. - cb = widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'), - (False, True, True)) + rb1.circles # Trigger the old-style elliptic radiobuttons. + + rax2 = fig.add_axes([0.05, 0.5, 0.2, 0.15]) + cb1 = widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'), + (False, True, True)) with pytest.warns(DeprecationWarning, match='The rectangles attribute was deprecated'): - cb.rectangles # Trigger old-style Rectangle check boxes + cb1.rectangles # Trigger old-style Rectangle check boxes + + rax3 = fig.add_axes([0.05, 0.3, 0.2, 0.15]) + rb3 = widgets.RadioButtons( + rax3, ('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']}) + + rax4 = fig.add_axes([0.05, 0.1, 0.2, 0.15]) + cb4 = widgets.CheckButtons( + rax4, ('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']}) @check_figures_equal(extensions=["png"]) @@ -1019,6 +1036,41 @@ def test_radio_buttons(fig_test, fig_ref): ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center") +@check_figures_equal(extensions=['png']) +def test_radio_buttons_props(fig_test, fig_ref): + label_props = {'color': ['red'], 'fontsize': [24]} + radio_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2} + + widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'], + label_props=label_props, radio_props=radio_props) + + cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee']) + cb.set_label_props(label_props) + # Setting the label size automatically increases default marker size, so we + # need to do that here as well. + cb.set_radio_props({**radio_props, 's': (24 / 2)**2}) + + +def test_radio_button_active_conflict(ax): + with pytest.warns(UserWarning, + match=r'Both the \*activecolor\* parameter'): + rb = widgets.RadioButtons(ax, ['tea', 'coffee'], activecolor='red', + radio_props={'facecolor': 'green'}) + # *radio_props*' facecolor wins over *activecolor* + assert mcolors.same_color(rb._buttons.get_facecolor(), ['green', 'none']) + + +@check_figures_equal(extensions=['png']) +def test_radio_buttons_activecolor_change(fig_test, fig_ref): + widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'], + activecolor='green') + + # Test property setter. + cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee'], + activecolor='red') + cb.activecolor = 'green' + + @check_figures_equal(extensions=["png"]) def test_check_buttons(fig_test, fig_ref): widgets.CheckButtons(fig_test.subplots(), ["tea", "coffee"], [True, True]) @@ -1031,6 +1083,29 @@ def test_check_buttons(fig_test, fig_ref): ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center") +@check_figures_equal(extensions=['png']) +def test_check_button_props(fig_test, fig_ref): + label_props = {'color': ['red'], 'fontsize': [24]} + frame_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2} + check_props = {'facecolor': 'red', 'linewidth': 2} + + widgets.CheckButtons(fig_ref.subplots(), ['tea', 'coffee'], [True, True], + label_props=label_props, frame_props=frame_props, + check_props=check_props) + + cb = widgets.CheckButtons(fig_test.subplots(), ['tea', 'coffee'], + [True, True]) + cb.set_label_props(label_props) + # Setting the label size automatically increases default marker size, so we + # need to do that here as well. + cb.set_frame_props({**frame_props, 's': (24 / 2)**2}) + # FIXME: Axes.scatter promotes facecolor to edgecolor on unfilled markers, + # but Collection.update doesn't do that (it forgot the marker already). + # This means we cannot pass facecolor to both setters directly. + check_props['edgecolor'] = check_props.pop('facecolor') + cb.set_check_props({**check_props, 's': (24 / 2)**2}) + + @check_figures_equal(extensions=["png"]) def test_check_buttons_rectangles(fig_test, fig_ref): # Test should be removed once .rectangles is removed diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f3f99b3753af..d169cb488eb9 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -11,13 +11,15 @@ from contextlib import ExitStack import copy +import itertools from numbers import Integral, Number +from cycler import cycler import numpy as np import matplotlib as mpl -from . import (_api, _docstring, backend_tools, cbook, colors, ticker, - transforms) +from . import (_api, _docstring, backend_tools, cbook, collections, colors, + text as mtext, ticker, transforms) from .lines import Line2D from .patches import Circle, Rectangle, Ellipse, Polygon from .transforms import TransformedPatchPath, Affine2D @@ -966,6 +968,11 @@ def on_changed(self, func): return self._observers.connect('changed', lambda val: func(val)) +def _expand_text_props(props): + props = cbook.normalize_kwargs(props, mtext.Text) + return cycler(**props)() if props else itertools.repeat({}) + + class CheckButtons(AxesWidget): r""" A GUI neutral set of check buttons. @@ -989,7 +996,8 @@ class CheckButtons(AxesWidget): each box, but have ``set_visible(False)`` when its box is not checked. """ - def __init__(self, ax, labels, actives=None, *, useblit=True): + def __init__(self, ax, labels, actives=None, *, useblit=True, + label_props=None, frame_props=None, check_props=None): """ Add check buttons to `matplotlib.axes.Axes` instance *ax*. @@ -1005,9 +1013,28 @@ def __init__(self, ax, labels, actives=None, *, useblit=True): useblit : bool, default: True Use blitting for faster drawing if supported by the backend. See the tutorial :doc:`/tutorials/advanced/blitting` for details. + label_props : dict, optional + Dictionary of `.Text` properties to be used for the labels. + + .. versionadded:: 3.7 + frame_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + check button frame. Defaults (label font size / 2)**2 size, black + edgecolor, no facecolor, and 1.0 linewidth. + + .. versionadded:: 3.7 + check_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + check button check. Defaults to (label font size / 2)**2 size, + black color, and 1.0 linewidth. + + .. versionadded:: 3.7 """ super().__init__(ax) + _api.check_isinstance((dict, None), label_props=label_props, + frame_props=frame_props, check_props=check_props) + ax.set_xticks([]) ax.set_yticks([]) ax.set_navigate(False) @@ -1019,22 +1046,39 @@ def __init__(self, ax, labels, actives=None, *, useblit=True): self._background = None ys = np.linspace(1, 0, len(labels)+2)[1:-1] - text_size = mpl.rcParams["font.size"] / 2 + label_props = _expand_text_props(label_props) self.labels = [ ax.text(0.25, y, label, transform=ax.transAxes, - horizontalalignment="left", verticalalignment="center") - for y, label in zip(ys, labels)] - - self._squares = ax.scatter( - [0.15] * len(ys), ys, marker='s', s=text_size**2, - c="none", linewidth=1, transform=ax.transAxes, edgecolor="k" - ) - self._crosses = ax.scatter( - [0.15] * len(ys), ys, marker='x', linewidth=1, s=text_size**2, - c=["k" if active else "none" for active in actives], - transform=ax.transAxes, animated=self._useblit, - ) + horizontalalignment="left", verticalalignment="center", + **props) + for y, label, props in zip(ys, labels, label_props)] + text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 + + frame_props = { + 's': text_size**2, + 'linewidth': 1, + **cbook.normalize_kwargs(frame_props, collections.PathCollection), + 'marker': 's', + 'transform': ax.transAxes, + } + 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) + check_props = { + 'linewidth': 1, + 's': text_size**2, + **cbook.normalize_kwargs(check_props, collections.PathCollection), + 'marker': 'x', + 'transform': ax.transAxes, + 'animated': self._useblit, + } + check_props.setdefault('facecolor', check_props.pop('color', 'black')) + self._checks = ax.scatter([0.15] * len(ys), 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. + self._init_status(actives) self.connect_event('button_press_event', self._clicked) if self._useblit: @@ -1047,7 +1091,7 @@ def _clear(self, event): if self.ignore(event): return self._background = self.canvas.copy_from_bbox(self.ax.bbox) - self.ax.draw_artist(self._crosses) + self.ax.draw_artist(self._checks) if hasattr(self, '_lines'): for l1, l2 in self._lines: self.ax.draw_artist(l1) @@ -1066,18 +1110,71 @@ def _clicked(self, event): and y0 <= pclicked[1] <= y0 + p.get_height())): distances[i] = np.linalg.norm(pclicked - p.get_center()) else: - _, square_inds = self._squares.contains(event) - coords = self._squares.get_offset_transform().transform( - self._squares.get_offsets() + _, frame_inds = self._frames.contains(event) + coords = self._frames.get_offset_transform().transform( + self._frames.get_offsets() ) for i, t in enumerate(self.labels): - if (i in square_inds["ind"] + if (i in frame_inds["ind"] or t.get_window_extent().contains(event.x, event.y)): distances[i] = np.linalg.norm(pclicked - coords[i]) if len(distances) > 0: closest = min(distances, key=distances.get) self.set_active(closest) + def set_label_props(self, props): + """ + Set properties of the `.Text` labels. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Text` properties to be used for the labels. + """ + _api.check_isinstance(dict, props=props) + props = _expand_text_props(props) + for text, prop in zip(self.labels, props): + text.update(prop) + + def set_frame_props(self, props): + """ + Set properties of the check button frames. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the check + button frames. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + self._frames.update(props) + + def set_check_props(self, props): + """ + Set properties of the check button checks. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the check + button check. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + actives = self.get_status() + self._checks.update(props) + # If new colours are supplied, then we must re-apply the status. + self._init_status(actives) + def set_active(self, index): """ Toggle (activate or deactivate) a check button by index. @@ -1097,15 +1194,15 @@ def set_active(self, index): if index not in range(len(self.labels)): raise ValueError(f'Invalid CheckButton index: {index}') - cross_facecolors = self._crosses.get_facecolor() - cross_facecolors[index] = colors.to_rgba( - "black" - if colors.same_color( - cross_facecolors[index], colors.to_rgba("none") - ) - else "none" + invisible = colors.to_rgba('none') + + facecolors = self._checks.get_facecolor() + facecolors[index] = ( + self._active_check_colors[index] + if colors.same_color(facecolors[index], invisible) + else invisible ) - self._crosses.set_facecolor(cross_facecolors) + self._checks.set_facecolor(facecolors) if hasattr(self, "_lines"): l1, l2 = self._lines[index] @@ -1116,7 +1213,7 @@ def set_active(self, index): if self._useblit: if self._background is not None: self.canvas.restore_region(self._background) - self.ax.draw_artist(self._crosses) + self.ax.draw_artist(self._checks) if hasattr(self, "_lines"): for l1, l2 in self._lines: self.ax.draw_artist(l1) @@ -1128,12 +1225,28 @@ def set_active(self, index): if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) + def _init_status(self, actives): + """ + Initialize properties to match active status. + + The user may have passed custom colours in *check_props* to the + constructor, or to `.set_check_props`, so we need to modify the + visibility after getting whatever the user set. + """ + self._active_check_colors = self._checks.get_facecolor() + if len(self._active_check_colors) == 1: + self._active_check_colors = np.repeat(self._active_check_colors, + len(actives), axis=0) + self._checks.set_facecolor( + [ec if active else "none" + for ec, active in zip(self._active_check_colors, actives)]) + def get_status(self): """ Return a list of the status (True/False) of all of the check buttons. """ return [not colors.same_color(color, colors.to_rgba("none")) - for color in self._crosses.get_facecolors()] + for color in self._checks.get_facecolors()] def on_clicked(self, func): """ @@ -1147,7 +1260,8 @@ def disconnect(self, cid): """Remove the observer with connection id *cid*.""" self._observers.disconnect(cid) - @_api.deprecated("3.7") + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") @property def rectangles(self): if not hasattr(self, "_rectangles"): @@ -1162,7 +1276,7 @@ def rectangles(self): ) for i, y in enumerate(ys) ] - self._squares.set_visible(False) + self._frames.set_visible(False) for rectangle in rectangles: self.ax.add_patch(rectangle) if not hasattr(self, "_lines"): @@ -1170,12 +1284,13 @@ def rectangles(self): _ = self.lines return self._rectangles - @_api.deprecated("3.7") + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") @property def lines(self): if not hasattr(self, "_lines"): ys = np.linspace(1, 0, len(self.labels)+2)[1:-1] - self._crosses.set_visible(False) + self._checks.set_visible(False) dy = 1. / (len(self.labels) + 1) w, h = dy / 2, dy / 2 self._lines = [] @@ -1484,8 +1599,8 @@ class RadioButtons(AxesWidget): The label text of the currently selected button. """ - def __init__(self, ax, labels, active=0, activecolor='blue', *, - useblit=True): + def __init__(self, ax, labels, active=0, activecolor=None, *, + useblit=True, label_props=None, radio_props=None): """ Add radio buttons to an `~.axes.Axes`. @@ -1498,13 +1613,44 @@ def __init__(self, ax, labels, active=0, activecolor='blue', *, active : int The index of the initially selected button. activecolor : color - The color of the selected button. + The color of the selected button. The default is ``'blue'`` if not + specified here or in *radio_props*. useblit : bool, default: True Use blitting for faster drawing if supported by the backend. See the tutorial :doc:`/tutorials/advanced/blitting` for details. + label_props : dict or list of dict, optional + Dictionary of `.Text` properties to be used for the labels. + + .. versionadded:: 3.7 + radio_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + radio buttons. Defaults to (label font size / 2)**2 size, black + edgecolor, and *activecolor* facecolor (when active). + + .. note:: + If a facecolor is supplied in *radio_props*, it will override + *activecolor*. This may be used to provide an active color per + button. + + .. versionadded:: 3.7 """ super().__init__(ax) - self.activecolor = activecolor + + _api.check_isinstance((dict, None), label_props=label_props, + radio_props=radio_props) + + radio_props = cbook.normalize_kwargs(radio_props, + collections.PathCollection) + if activecolor is not None: + if 'facecolor' in radio_props: + _api.warn_external( + 'Both the *activecolor* parameter and the *facecolor* ' + 'key in the *radio_props* parameter has been specified. ' + '*activecolor* will be ignored.') + else: + activecolor = 'blue' # Default. + + self._activecolor = activecolor self.value_selected = labels[active] ax.set_xticks([]) @@ -1512,19 +1658,39 @@ def __init__(self, ax, labels, active=0, activecolor='blue', *, ax.set_navigate(False) ys = np.linspace(1, 0, len(labels) + 2)[1:-1] - text_size = mpl.rcParams["font.size"] / 2 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") - for y, label in zip(ys, labels)] - self._buttons = ax.scatter( - [.15] * len(ys), ys, transform=ax.transAxes, s=text_size**2, - c=[activecolor if i == active else "none" for i in range(len(ys))], - edgecolor="black", animated=self._useblit) + horizontalalignment="left", verticalalignment="center", + **props) + for y, label, props in zip(ys, labels, label_props)] + text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 + + radio_props = { + 's': text_size**2, + **radio_props, + 'marker': 'o', + 'transform': ax.transAxes, + 'animated': self._useblit, + } + 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) + # 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. + self._active_colors = self._buttons.get_facecolor() + if len(self._active_colors) == 1: + self._active_colors = np.repeat(self._active_colors, len(labels), + axis=0) + self._buttons.set_facecolor( + [activecolor if i == active else "none" + for i, activecolor in enumerate(self._active_colors)]) self.connect_event('button_press_event', self._clicked) if self._useblit: @@ -1564,6 +1730,61 @@ def _clicked(self, event): closest = min(distances, key=distances.get) self.set_active(closest) + def set_label_props(self, props): + """ + Set properties of the `.Text` labels. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Text` properties to be used for the labels. + """ + _api.check_isinstance(dict, props=props) + props = _expand_text_props(props) + for text, prop in zip(self.labels, props): + text.update(prop) + + def set_radio_props(self, props): + """ + Set properties of the `.Text` labels. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the radio + buttons. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + self._buttons.update(props) + self._active_colors = self._buttons.get_facecolor() + if len(self._active_colors) == 1: + self._active_colors = np.repeat(self._active_colors, + len(self.labels), axis=0) + self._buttons.set_facecolor( + [activecolor if text.get_text() == self.value_selected else "none" + for text, activecolor in zip(self.labels, self._active_colors)]) + + @property + def activecolor(self): + return self._activecolor + + @activecolor.setter + def activecolor(self, activecolor): + colors._check_color_like(activecolor=activecolor) + self._activecolor = activecolor + self.set_radio_props({'facecolor': activecolor}) + # Make sure the deprecated version is updated. + # Remove once circles is removed. + labels = [label.get_text() for label in self.labels] + with cbook._setattr_cm(self, eventson=False): + self.set_active(labels.index(self.value_selected)) + def set_active(self, index): """ Select button with number *index*. @@ -1575,7 +1796,7 @@ def set_active(self, index): self.value_selected = self.labels[index].get_text() button_facecolors = self._buttons.get_facecolor() button_facecolors[:] = colors.to_rgba("none") - button_facecolors[index] = colors.to_rgba(self.activecolor) + button_facecolors[index] = colors.to_rgba(self._active_colors[index]) self._buttons.set_facecolor(button_facecolors) if hasattr(self, "_circles"): # Remove once circles is removed. for i, p in enumerate(self._circles): @@ -1609,7 +1830,8 @@ def disconnect(self, cid): """Remove the observer with connection id *cid*.""" self._observers.disconnect(cid) - @_api.deprecated("3.7") + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") @property def circles(self): if not hasattr(self, "_circles"):