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 d5894fdad705..934e0ecfefb2 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 c1600b369be7..8180ab8b9d42 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 1dc1dd5f77ae..2365220770cc 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"):
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: