@@ -1048,7 +1048,8 @@ class _Buttons(AxesWidget):
10481048 public on the subclasses.
10491049 """
10501050
1051- def __init__ (self , ax , labels , * , useblit = True , label_props = None , ** kwargs ):
1051+ def __init__ (self , ax , labels , * , useblit = True , label_props = None , layout = None ,
1052+ ** kwargs ):
10521053 super ().__init__ (ax )
10531054
10541055 ax .set_xticks ([])
@@ -1057,7 +1058,7 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
10571058
10581059 self ._useblit = useblit
10591060
1060- self ._init_layout (labels , label_props )
1061+ self ._init_layout (layout , labels , label_props )
10611062 text_size = np .array ([text .get_fontsize () for text in self .labels ]) / 2
10621063
10631064 self ._init_props (text_size , ** kwargs )
@@ -1068,17 +1069,101 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs):
10681069
10691070 self ._observers = cbook .CallbackRegistry (signals = ["clicked" ])
10701071
1071- def _init_layout (self , labels , label_props ):
1072- self ._buttons_xs = [0.15 ] * len (labels )
1073- self ._buttons_ys = np .linspace (1 , 0 , len (labels )+ 2 )[1 :- 1 ]
1072+ def _init_layout (self , layout , labels , label_props ):
10741073
10751074 label_props = _expand_text_props (label_props )
10761075
1076+ if layout is None :
1077+ self ._buttons_xs = [0.15 ] * len (labels )
1078+ self ._buttons_ys = np .linspace (1 , 0 , len (labels )+ 2 )[1 :- 1 ]
1079+ self .labels = [
1080+ self .ax .text (0.25 , y , label , transform = self .ax .transAxes ,
1081+ horizontalalignment = "left" , verticalalignment = "center" ,
1082+ ** props )
1083+ for y , label , props in zip (self ._buttons_ys , labels , label_props )]
1084+ return
1085+
1086+ # New layout algorithm with text measurement
1087+ # Parse layout parameter
1088+ n_labels = len (labels )
1089+ match layout :
1090+ case "vertical" :
1091+ n_rows , n_cols = n_labels , 1
1092+ case "horizontal" :
1093+ n_rows , n_cols = 1 , n_labels
1094+ case (int () as n_rows , int () as n_cols ):
1095+ if n_rows * n_cols < n_labels :
1096+ raise ValueError (
1097+ f"layout { layout } has { n_rows * n_cols } positions but "
1098+ f"{ n_labels } labels were provided"
1099+ )
1100+ case _:
1101+ raise ValueError (
1102+ "layout must be None, 'vertical', 'horizontal', or a (rows, cols) "
1103+ f"tuple; got { layout !r} " )
1104+
1105+ # Define spacing in points for DPI-independent sizing
1106+ fig = self .ax .get_figure (root = False )
1107+ axes_width_display = 72 * self .ax .bbox .transformed (
1108+ fig .dpi_scale_trans .inverted ()
1109+ ).width
1110+ left_margin_display = 11 # points
1111+ button_text_offset_display = 5.5 # points
1112+ col_spacing_display = 11 # points
1113+
1114+ # Convert to axes coordinates
1115+ left_margin = left_margin_display / axes_width_display
1116+ button_text_offset = button_text_offset_display / axes_width_display
1117+ col_spacing = col_spacing_display / axes_width_display
1118+
1119+ # Create text objects to measure widths.
1120+ # We create Text objects directly rather than using ax.text() since we're
1121+ # only measuring them and only later add them to the axes.
10771122 self .labels = [
1078- self .ax .text (0.25 , y , label , transform = self .ax .transAxes ,
1079- horizontalalignment = "left" , verticalalignment = "center" ,
1080- ** props )
1081- for y , label , props in zip (self ._buttons_ys , labels , label_props )]
1123+ mtext .Text (0 , 0 , text = label , transform = self .ax .transAxes ,
1124+ horizontalalignment = "left" , verticalalignment = "center" ,
1125+ ** props )
1126+ for label , props in zip (labels , label_props )
1127+ ]
1128+ # Set figure reference so Text objects can access figure properties
1129+ for text in self .labels :
1130+ text .set_figure (fig )
1131+ # Calculate max text width per column (in axes coordinates)
1132+ col_widths = [
1133+ max (
1134+ (
1135+ text .get_window_extent (
1136+ self .ax .figure .canvas .get_renderer (),
1137+ ).transformed (
1138+ fig .dpi_scale_trans .inverted ()
1139+ ).width * 72
1140+ for text in self .labels [col_idx ::n_cols ]
1141+ ),
1142+ default = 0 ,
1143+ )
1144+ / axes_width_display
1145+ for col_idx in range (n_cols )
1146+ ]
1147+
1148+ # Center rows vertically in the axes
1149+ ys_per_row = np .linspace (1 , 0 , n_rows + 2 )[1 :- 1 ]
1150+ # Calculate x positions based on text widths
1151+ col_x_positions = [left_margin ] # First column starts at left margin
1152+ for col_idx in range (n_cols - 1 ):
1153+ col_x_positions .append (
1154+ col_x_positions [- 1 ] +
1155+ button_text_offset +
1156+ col_widths [col_idx ] +
1157+ col_spacing
1158+ )
1159+ label_idx = np .arange (n_labels )
1160+ self ._buttons_xs = np .take (col_x_positions , label_idx % n_cols )
1161+ self ._buttons_ys = ys_per_row [label_idx // n_cols ]
1162+ for text ,x ,y in zip (self .labels , self ._buttons_xs + button_text_offset ,
1163+ self ._buttons_ys ):
1164+ text .set_x (x )
1165+ text .set_y (y )
1166+ self .ax .add_artist (text )
10821167
10831168 def _init_props (self , text_size , ** kwargs ):
10841169 raise NotImplementedError ("This method should be defined in subclasses" )
@@ -1165,7 +1250,7 @@ class CheckButtons(_Buttons):
11651250 The text label objects of the check buttons.
11661251 """
11671252
1168- def __init__ (self , ax , labels , actives = None , * , useblit = True ,
1253+ def __init__ (self , ax , labels , actives = None , * , layout = None , useblit = True ,
11691254 label_props = None , frame_props = None , check_props = None ):
11701255 """
11711256 Add check buttons to `~.axes.Axes` instance *ax*.
@@ -1179,6 +1264,30 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
11791264 actives : list of bool, optional
11801265 The initial check states of the buttons. The list must have the
11811266 same length as *labels*. If not given, all buttons are unchecked.
1267+ layout : None or "vertical" or "horizontal" or (int, int), default: None
1268+ The layout of the check buttons. Options are:
1269+
1270+ - ``None``: Use legacy vertical layout (default).
1271+ - ``"vertical"``: Arrange buttons in a single column with
1272+ dynamic positioning based on text widths.
1273+ - ``"horizontal"``: Arrange buttons in a single row with
1274+ dynamic positioning based on text widths.
1275+ - ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1276+ specified number of rows and columns. Buttons are placed
1277+ left-to-right, top-to-bottom with dynamic positioning.
1278+
1279+ The layout options "vertical", "horizontal" and ``(rows, cols)``
1280+ create ``mtext.Text`` objects to determine exact text sizes, and
1281+ then they are added axes. This is usually ok, but may cause
1282+ side-effects and has a slight performance impact. Therefore the
1283+ default ``None`` value avoids this.
1284+
1285+ .. admonition:: Provisional
1286+ The new layout options are provisional. Their algorithmic
1287+ behavior, including the exact positions of buttons and labels
1288+ may still change without prior warning.
1289+
1290+ .. versionadded:: 3.11
11821291 useblit : bool, default: True
11831292 Use blitting for faster drawing if supported by the backend.
11841293 See the tutorial :ref:`blitting` for details.
@@ -1208,9 +1317,9 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
12081317 _api .check_isinstance ((dict , None ), label_props = label_props ,
12091318 frame_props = frame_props , check_props = check_props )
12101319
1211- super ().__init__ (ax , labels , useblit = useblit , label_props = label_props ,
1212- actives = actives , frame_props = frame_props ,
1213- check_props = check_props )
1320+ super ().__init__ (ax , labels , layout = layout , useblit = useblit ,
1321+ label_props = label_props , actives = actives ,
1322+ frame_props = frame_props , check_props = check_props )
12141323
12151324 def _init_props (self , text_size , actives , frame_props , check_props ):
12161325 frame_props = {
@@ -1671,7 +1780,7 @@ class RadioButtons(_Buttons):
16711780 The index of the selected button.
16721781 """
16731782
1674- def __init__ (self , ax , labels , active = 0 , activecolor = None , * ,
1783+ def __init__ (self , ax , labels , active = 0 , activecolor = None , * , layout = None ,
16751784 useblit = True , label_props = None , radio_props = None ):
16761785 """
16771786 Add radio buttons to an `~.axes.Axes`.
@@ -1687,6 +1796,30 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16871796 activecolor : :mpltype:`color`
16881797 The color of the selected button. The default is ``'blue'`` if not
16891798 specified here or in *radio_props*.
1799+ layout : None or "vertical" or "horizontal" or (int, int), default: None
1800+ The layout of the radio buttons. Options are:
1801+
1802+ - ``None``: Use legacy vertical layout (default).
1803+ - ``"vertical"``: Arrange buttons in a single column with
1804+ dynamic positioning based on text widths.
1805+ - ``"horizontal"``: Arrange buttons in a single row with
1806+ dynamic positioning based on text widths.
1807+ - ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1808+ specified number of rows and columns. Buttons are placed
1809+ left-to-right, top-to-bottom with dynamic positioning.
1810+
1811+ The layout options "vertical", "horizontal" and ``(rows, cols)``
1812+ create ``mtext.Text`` objects to determine exact text sizes, and
1813+ then they are added axes. This is usually ok, but may cause
1814+ side-effects and has a slight performance impact. Therefore the
1815+ default ``None`` value avoids this.
1816+
1817+ .. admonition:: Provisional
1818+ The new layout options are provisional. Their algorithmic
1819+ behavior, including the exact positions of buttons and labels
1820+ may still change without prior warning.
1821+
1822+ .. versionadded:: 3.11
16901823 useblit : bool, default: True
16911824 Use blitting for faster drawing if supported by the backend.
16921825 See the tutorial :ref:`blitting` for details.
@@ -1726,7 +1859,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
17261859 else :
17271860 activecolor = 'blue' # Default.
17281861 super ().__init__ (ax , labels , useblit = useblit , label_props = label_props ,
1729- active = active , activecolor = activecolor ,
1862+ active = active , layout = layout , activecolor = activecolor ,
17301863 radio_props = radio_props )
17311864
17321865 self ._activecolor = activecolor
0 commit comments