@@ -1578,7 +1578,8 @@ class RadioButtons(AxesWidget):
15781578 value_selected : str
15791579 The label text of the currently selected button.
15801580 index_selected : int
1581- The index of the selected button.
1581+ The index of the selected button in the flattened array. For 2D grids,
1582+ this is the index when reading left-to-right, top-to-bottom.
15821583 """
15831584
15841585 def __init__ (self , ax , labels , active = 0 , activecolor = None , * ,
@@ -1590,10 +1591,14 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
15901591 ----------
15911592 ax : `~matplotlib.axes.Axes`
15921593 The Axes to add the buttons to.
1593- labels : list of str
1594- The button labels.
1594+ labels : list of str or list of list of str
1595+ The button labels. If a list of strings, buttons are arranged
1596+ vertically. If a list of lists of strings, buttons are arranged
1597+ in a 2D grid where each inner list represents a row.
15951598 active : int
1596- The index of the initially selected button.
1599+ The index of the initially selected button in the flattened array.
1600+ For 2D grids, this is the index when reading left-to-right,
1601+ top-to-bottom.
15971602 activecolor : :mpltype:`color`
15981603 The color of the selected button. The default is ``'blue'`` if not
15991604 specified here or in *radio_props*.
@@ -1606,8 +1611,8 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16061611 label_props : dict of lists, optional
16071612 Dictionary of `.Text` properties to be used for the labels. Each
16081613 dictionary value should be a list of at least a single element. If
1609- the list is of length M, its values are cycled such that the Nth
1610- label gets the (N mod M) property.
1614+ the flat list of labels is of length M, its values are cycled such
1615+ that the Nth label gets the (N mod M) property.
16111616
16121617 .. versionadded:: 3.7
16131618 radio_props : dict, optional
@@ -1627,6 +1632,14 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16271632 _api .check_isinstance ((dict , None ), label_props = label_props ,
16281633 radio_props = radio_props )
16291634
1635+ # Check if labels is 2D (list of lists)
1636+ _is_2d = isinstance (labels [0 ], (list , tuple ))
1637+
1638+ if _is_2d :
1639+ flat_labels = [item for row in labels for item in row ]
1640+ else :
1641+ flat_labels = list (labels )
1642+
16301643 radio_props = cbook .normalize_kwargs (radio_props ,
16311644 collections .PathCollection )
16321645 if activecolor is not None :
@@ -1640,24 +1653,98 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16401653
16411654 self ._activecolor = activecolor
16421655 self ._initial_active = active
1643- self .value_selected = labels [active ]
1656+ self .value_selected = flat_labels [active ]
16441657 self .index_selected = active
16451658
16461659 ax .set_xticks ([])
16471660 ax .set_yticks ([])
16481661 ax .set_navigate (False )
16491662
1650- ys = np .linspace (1 , 0 , len (labels ) + 2 )[1 :- 1 ]
1651-
16521663 self ._useblit = useblit and self .canvas .supports_blit
16531664 self ._background = None
16541665
16551666 label_props = _expand_text_props (label_props )
1667+ # Calculate positions based on layout
1668+ text_x_offset = 0.10
1669+
1670+ if _is_2d :
1671+ n_rows = len (labels )
1672+ n_cols = max (len (row ) for row in labels )
1673+ # Y positions with margins
1674+ y_margin = 0.05
1675+ y_spacing = (1 - 2 * y_margin ) / max (1 , n_rows - 1 ) if n_rows > 1 else 0
1676+
1677+ # Create temporary text objects to measure widths
1678+ flat_label_list = []
1679+ temp_texts = []
1680+ for i , row in enumerate (labels ):
1681+ for j , label in enumerate (row ):
1682+ flat_label_list .append (label )
1683+ for label , props in zip (flat_label_list , label_props ):
1684+ temp_texts .append (ax .text (
1685+ 0 ,
1686+ 0 ,
1687+ label ,
1688+ transform = ax .transAxes ,
1689+ ** props ,
1690+ ))
1691+ # Force a draw to get accurate text measurements
1692+ ax .figure .canvas .draw ()
1693+ # Calculate max text width per column (in axes coordinates)
1694+ col_widths = []
1695+ for col_idx in range (n_cols ):
1696+ col_texts = []
1697+ for row_idx , row in enumerate (labels ):
1698+ if col_idx < len (row ):
1699+ col_texts .append (temp_texts [
1700+ sum (len (labels [r ]) for r in range (row_idx )) + col_idx
1701+ ])
1702+ if col_texts :
1703+ col_widths .append (
1704+ max (
1705+ text .get_window_extent (
1706+ ax .figure .canvas .get_renderer ()
1707+ ).width
1708+ for text in col_texts
1709+ ) / ax .bbox .width
1710+ )
1711+ else :
1712+ col_widths .append (0 )
1713+ # Remove temporary text objects
1714+ for text in temp_texts :
1715+ text .remove ()
1716+ # Calculate x positions based on text widths
1717+ # TODO: Should these be arguments?
1718+ button_x_margin = 0.07 # Left margin for first button
1719+ col_spacing = 0.07 # Space between columns
1720+
1721+ col_x_positions = [button_x_margin ] # First column starts at left margin
1722+ for col_idx in range (n_cols - 1 ):
1723+ col_x_positions .append (sum ([
1724+ col_x_positions [- 1 ],
1725+ text_x_offset ,
1726+ col_widths [col_idx ],
1727+ col_spacing
1728+ ]))
1729+ # Create final positions
1730+ positions = []
1731+ for i , row in enumerate (labels ):
1732+ y = 1 - y_margin - i * y_spacing
1733+ for j , label in enumerate (row ):
1734+ x = col_x_positions [j ]
1735+ positions .append ((x , y ))
1736+ xs = [pos [0 ] for pos in positions ]
1737+ ys = [pos [1 ] for pos in positions ]
1738+ else :
1739+ ys = np .linspace (1 , 0 , len (flat_labels ) + 2 )[1 :- 1 ]
1740+ xs = [0.15 ] * len (ys )
1741+ flat_label_list = flat_labels
1742+
16561743 self .labels = [
1657- ax .text (0.25 , y , label , transform = ax .transAxes ,
1744+ ax .text (x + text_x_offset , y , label , transform = ax .transAxes ,
16581745 horizontalalignment = "left" , verticalalignment = "center" ,
16591746 ** props )
1660- for y , label , props in zip (ys , labels , label_props )]
1747+ for x , y , label , props in zip (xs , ys , flat_label_list , label_props )]
16611748 text_size = np .array ([text .get_fontsize () for text in self .labels ]) / 2
16621749
16631750 radio_props = {
@@ -1670,13 +1757,13 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16701757 radio_props .setdefault ('edgecolor' , radio_props .get ('color' , 'black' ))
16711758 radio_props .setdefault ('facecolor' ,
16721759 radio_props .pop ('color' , activecolor ))
1673- self ._buttons = ax .scatter ([ .15 ] * len ( ys ) , ys , ** radio_props )
1760+ self ._buttons = ax .scatter (xs , ys , ** radio_props )
16741761 # The user may have passed custom colours in radio_props, so we need to
16751762 # create the radios, and modify the visibility after getting whatever
16761763 # the user set.
16771764 self ._active_colors = self ._buttons .get_facecolor ()
16781765 if len (self ._active_colors ) == 1 :
1679- self ._active_colors = np .repeat (self ._active_colors , len (labels ),
1766+ self ._active_colors = np .repeat (self ._active_colors , len (flat_labels ),
16801767 axis = 0 )
16811768 self ._buttons .set_facecolor (
16821769 [activecolor if i == active else "none"
0 commit comments