6

I am using the matplotlib.widgets to create radio buttons in my widgets, the buttons coming are stacked vertically, I would like them to be stacked horizontally.

MVCE:

import matplotlib.pyplot as plt
from matplotlib.widgets import RadioButtons
plt.subplots_adjust(left=0.2)
rax = plt.axes([0.5,0.05,0.1,0.1])
radio =  RadioButtons(rax ,['1','2','3'], active=0, activecolor='blue' )
plt.show()

As you can see with this example you can get the radio buttons like this Plot with radio buttons stacked vertically,

I am wondering is there a way to stack these radio buttons horizontally.

anand_v.singh
  • 2,768
  • 1
  • 16
  • 35
  • 1
    I couldn't find a way to do this with ```matplotlib.widgets``` ```RadioButtons```, however, it's easy to do directly with tkinter, eg. see [here](https://stackoverflow.com/questions/45509602/how-to-place-radiobuttons-horizontal-in-python) or [here](https://stackoverflow.com/questions/10178652/tkinter-pack-geometry-1-column-with-4-radio-buttons-in-one-row) – jwalton Mar 11 '19 at 09:39
  • @Jack Thanks, actually there is a lot of work that is easy in matplotlib, that's why I am sticking to it, right now I have switched to a Slider rather than radio buttons, but still Radio buttons would be a better option. – anand_v.singh Mar 11 '19 at 10:24
  • Yes, a ```matplotlib```-only solution to this problem would be nice. Hopefully somebody more knowledgeable than me will come along and solve this for you! – jwalton Mar 11 '19 at 10:29

2 Answers2

10

There is currently an attempt to introduce an orientation argument to RadioButtons in PR #13374; this has not yet been finalized.

As I had commented in this PR, an alternative option would be to use a scatter plot for the buttons. The following shows how I would imagine this implementation. There are two main enhancements compared to the usual buttons:

  • Radio buttons are always round independent of the size of the axes.
  • They can be aligned arbitrarily, in particular horizontally.

This is achieved by creating a legend internally, which has all the required options readily available. Any valid arguments to Legend can be used for the Buttons as well.

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

class MyRadioButtons(RadioButtons):

    def __init__(self, ax, labels, active=0, activecolor='blue', size=49,
                 orientation="vertical", **kwargs):
        """
        Add radio buttons to an `~.axes.Axes`.
        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`
            The axes to add the buttons to.
        labels : list of str
            The button labels.
        active : int
            The index of the initially selected button.
        activecolor : color
            The color of the selected button.
        size : float
            Size of the radio buttons
        orientation : str
            The orientation of the buttons: 'vertical' (default), or 'horizontal'.
        Further parameters are passed on to `Legend`.
        """
        AxesWidget.__init__(self, ax)
        self.activecolor = activecolor
        axcolor = ax.get_facecolor()
        self.value_selected = None

        ax.set_xticks([])
        ax.set_yticks([])
        ax.set_navigate(False)

        circles = []
        for i, label in enumerate(labels):
            if i == active:
                self.value_selected = label
                facecolor = activecolor
            else:
                facecolor = axcolor
            p = ax.scatter([],[], s=size, marker="o", edgecolor='black',
                           facecolor=facecolor)
            circles.append(p)
        if orientation == "horizontal":
            kwargs.update(ncol=len(labels), mode="expand")
        kwargs.setdefault("frameon", False)    
        self.box = ax.legend(circles, labels, loc="center", **kwargs)
        self.labels = self.box.texts
        self.circles = self.box.legendHandles
        for c in self.circles:
            c.set_picker(5)
        self.cnt = 0
        self.observers = {}

        self.connect_event('pick_event', self._clicked)


    def _clicked(self, event):
        if (self.ignore(event) or event.mouseevent.button != 1 or
            event.mouseevent.inaxes != self.ax):
            return
        if event.artist in self.circles:
            self.set_active(self.circles.index(event.artist))

Use it as

plt.subplots_adjust(left=0.2)
rax = plt.axes([0.5,0.05,0.4,0.07])
radio =  MyRadioButtons(rax ,['1','2','3'], active=0, activecolor='crimson',
                        orientation="horizontal")

plt.show()

enter image description here

Or

rax = plt.axes([0.2,0.5,0.25,0.1])
radio =  MyRadioButtons(rax ,["AA", "BB", "CC", "DD"], ncol=2)

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Any idea how to put the legends below the circles? Been playing with bbox_to_anchor at ax.legend with no success so far. How about placing the circles closer to each other? – Trmotta Mar 22 '19 at 19:17
  • @Trmotta How close the circles are is determined foremost by the extent of the `plt.axes` used for this box, if you make it smaller, they are closer together. Any further arguments to `ax.legend()` can be used as well, refer to the `legend` documentation for that. Since legends do not allow for label below the handles, this solution is not very useful for that matter; instead not using a legend would work better in that case. In fact you can refer to the linked PR for a possible alternative option with labels below the circles. – ImportanceOfBeingErnest Mar 23 '19 at 01:27
  • @ImportanceOfBeingErnest I keep getting ```lib/python3.9/site-packages/matplotlib/_api/deprecation.py", line 218, in __set__ return super().__set__(instance, value) AttributeError: can't set attribute``` – shcrela Jun 15 '21 at 23:28
  • awesome answer! I ported it to matplotlib >3.3 (fixes the error above) – raphael Feb 28 '22 at 19:33
1

Since stackexchange does not want this as an edit, here's an updated version of the above answer from @ImportanceOfBeingErnest that works with recent matplotlib-versions (e.g. >= v3.3)

import matplotlib.pyplot as plt
from matplotlib.widgets import AxesWidget, RadioButtons
from matplotlib import cbook

class MyRadioButtons(RadioButtons):

    def __init__(self, ax, labels, active=0, activecolor='blue', size=49,
                 orientation="vertical", **kwargs):
        """
        Add radio buttons to an `~.axes.Axes`.
        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`
            The axes to add the buttons to.
        labels : list of str
            The button labels.
        active : int
            The index of the initially selected button.
        activecolor : color
            The color of the selected button.
        size : float
            Size of the radio buttons
        orientation : str
            The orientation of the buttons: 'vertical' (default), or 'horizontal'.
        Further parameters are passed on to `Legend`.
        """
        AxesWidget.__init__(self, ax)
        self.activecolor = activecolor
        axcolor = ax.get_facecolor()
        self.value_selected = None

        ax.set_xticks([])
        ax.set_yticks([])
        ax.set_navigate(False)

        circles = []
        for i, label in enumerate(labels):
            if i == active:
                self.value_selected = label
                facecolor = activecolor
            else:
                facecolor = axcolor
            p = ax.scatter([],[], s=size, marker="o", edgecolor='black',
                           facecolor=facecolor)
            circles.append(p)
        if orientation == "horizontal":
            kwargs.update(ncol=len(labels), mode="expand")
        kwargs.setdefault("frameon", False)    
        self.box = ax.legend(circles, labels, loc="center", **kwargs)
        self.labels = self.box.texts
        self.circles = self.box.legendHandles
        for c in self.circles:
            c.set_picker(5)
        
        self._observers = cbook.CallbackRegistry()
        
        self.connect_event('pick_event', self._clicked)


    def _clicked(self, event):
        if (self.ignore(event) or event.mouseevent.button != 1 or
            event.mouseevent.inaxes != self.ax):
            return
        if event.artist in self.circles:
            self.set_active(self.circles.index(event.artist))

Use it as

plt.subplots_adjust(left=0.2)
rax = plt.axes([0.5,0.05,0.4,0.07])
radio =  MyRadioButtons(rax ,['1','2','3'], active=0, activecolor='crimson',
                        orientation="horizontal")

plt.show()
raphael
  • 2,159
  • 17
  • 20