@@ -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,98 @@ 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+ # legacy hard-coded vertical layout
1078+ self ._buttons_xs = [0.15 ] * len (labels )
1079+ self ._buttons_ys = np .linspace (1 , 0 , len (labels )+ 2 )[1 :- 1 ]
1080+ self .labels = [
1081+ self .ax .text (0.25 , y , label , transform = self .ax .transAxes ,
1082+ horizontalalignment = "left" , verticalalignment = "center" ,
1083+ ** props )
1084+ for y , label , props in zip (self ._buttons_ys , labels , label_props )]
1085+ return
1086+
1087+ # New layout algorithm with text measurement
1088+ # Parse layout parameter
1089+ n_labels = len (labels )
1090+ match layout :
1091+ case "vertical" :
1092+ n_rows , n_cols = n_labels , 1
1093+ case "horizontal" :
1094+ n_rows , n_cols = 1 , n_labels
1095+ case (int () as n_rows , int () as n_cols ):
1096+ if n_rows * n_cols < n_labels :
1097+ raise ValueError (
1098+ f"layout { layout } has { n_rows * n_cols } positions but "
1099+ f"{ n_labels } labels were provided"
1100+ )
1101+ case _:
1102+ raise ValueError (
1103+ "layout must be None, 'vertical', 'horizontal', or a (rows, cols) "
1104+ f"tuple; got { layout !r} " )
1105+
1106+ # Define spacing in points for DPI-independent sizing
1107+ fig = self .ax .get_figure (root = False )
1108+ axes_width_display = 72 * self .ax .bbox .transformed (
1109+ fig .dpi_scale_trans .inverted ()
1110+ ).width
1111+ left_margin_display = 11 # points
1112+ button_text_offset_display = 5.5 # points
1113+ col_spacing_display = 11 # points
1114+
1115+ # Convert to axes coordinates
1116+ left_margin = left_margin_display / axes_width_display
1117+ button_text_offset = button_text_offset_display / axes_width_display
1118+ col_spacing = col_spacing_display / axes_width_display
1119+
1120+ # Create text objects to measure widths.
1121+ # We create Text objects directly rather than using ax.text() since we're
1122+ # only measuring them and only later add them to the axes.
10771123 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 )]
1124+ mtext .Text (0 , 0 , text = label , transform = self .ax .transAxes ,
1125+ horizontalalignment = "left" , verticalalignment = "center" ,
1126+ ** props )
1127+ for label , props in zip (labels , label_props )
1128+ ]
1129+ # Set figure reference so Text objects can access figure properties
1130+ for text in self .labels :
1131+ text .set_figure (fig )
1132+ # Calculate max text width per column (in axes coordinates)
1133+ renderer = self .ax .figure .canvas .get_renderer ()
1134+ inv_trans = fig .dpi_scale_trans .inverted ()
1135+ col_widths = [
1136+ max (
1137+ (
1138+ text .get_window_extent (renderer ).transformed (inv_trans ).width * 72
1139+ for text in self .labels [col_idx ::n_cols ]
1140+ ),
1141+ default = 0 ,
1142+ )
1143+ / axes_width_display
1144+ for col_idx in range (n_cols )
1145+ ]
1146+
1147+ # Center rows vertically in the axes
1148+ ys_per_row = np .linspace (1 , 0 , n_rows + 2 )[1 :- 1 ]
1149+ # Calculate x positions based on text widths
1150+ col_x_positions = [left_margin ] # First column starts at left margin
1151+ for col_idx in range (n_cols - 1 ):
1152+ col_x_positions .append (
1153+ col_x_positions [- 1 ] +
1154+ button_text_offset +
1155+ col_widths [col_idx ] +
1156+ col_spacing
1157+ )
1158+ label_idx = np .arange (n_labels )
1159+ self ._buttons_xs = np .take (col_x_positions , label_idx % n_cols )
1160+ self ._buttons_ys = ys_per_row [label_idx // n_cols ]
1161+ for text , x , y in zip (self .labels , self ._buttons_xs , self ._buttons_ys ):
1162+ text .set_position ((x + button_text_offset , y ))
1163+ self .ax .add_artist (text )
10821164
10831165 def _init_props (self , text_size , ** kwargs ):
10841166 raise NotImplementedError ("This method should be defined in subclasses" )
@@ -1165,7 +1247,7 @@ class CheckButtons(_Buttons):
11651247 The text label objects of the check buttons.
11661248 """
11671249
1168- def __init__ (self , ax , labels , actives = None , * , useblit = True ,
1250+ def __init__ (self , ax , labels , actives = None , * , layout = None , useblit = True ,
11691251 label_props = None , frame_props = None , check_props = None ):
11701252 """
11711253 Add check buttons to `~.axes.Axes` instance *ax*.
@@ -1179,6 +1261,30 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
11791261 actives : list of bool, optional
11801262 The initial check states of the buttons. The list must have the
11811263 same length as *labels*. If not given, all buttons are unchecked.
1264+ layout : None or "vertical" or "horizontal" or (int, int), default: None
1265+ The layout of the check buttons. Options are:
1266+
1267+ - ``None``: Use legacy vertical layout (default).
1268+ - ``"vertical"``: Arrange buttons in a single column with
1269+ dynamic positioning based on text widths.
1270+ - ``"horizontal"``: Arrange buttons in a single row with
1271+ dynamic positioning based on text widths.
1272+ - ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1273+ specified number of rows and columns. Buttons are placed
1274+ left-to-right, top-to-bottom with dynamic positioning.
1275+
1276+ The layout options "vertical", "horizontal" and ``(rows, cols)``
1277+ create ``mtext.Text`` objects to determine exact text sizes, and
1278+ then they are added to the Axes. This is usually okay, but may cause
1279+ side-effects and has a slight performance impact. Therefore the
1280+ default ``None`` value avoids this.
1281+
1282+ .. admonition:: Provisional
1283+ The new layout options are provisional. Their algorithmic
1284+ behavior, including the exact positions of buttons and labels,
1285+ may still change without prior warning.
1286+
1287+ .. versionadded:: 3.11
11821288 useblit : bool, default: True
11831289 Use blitting for faster drawing if supported by the backend.
11841290 See the tutorial :ref:`blitting` for details.
@@ -1208,9 +1314,9 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
12081314 _api .check_isinstance ((dict , None ), label_props = label_props ,
12091315 frame_props = frame_props , check_props = check_props )
12101316
1211- super ().__init__ (ax , labels , useblit = useblit , label_props = label_props ,
1212- actives = actives , frame_props = frame_props ,
1213- check_props = check_props )
1317+ super ().__init__ (ax , labels , layout = layout , useblit = useblit ,
1318+ label_props = label_props , actives = actives ,
1319+ frame_props = frame_props , check_props = check_props )
12141320
12151321 def _init_props (self , text_size , actives , frame_props , check_props ):
12161322 frame_props = {
@@ -1671,7 +1777,7 @@ class RadioButtons(_Buttons):
16711777 The index of the selected button.
16721778 """
16731779
1674- def __init__ (self , ax , labels , active = 0 , activecolor = None , * ,
1780+ def __init__ (self , ax , labels , active = 0 , activecolor = None , * , layout = None ,
16751781 useblit = True , label_props = None , radio_props = None ):
16761782 """
16771783 Add radio buttons to an `~.axes.Axes`.
@@ -1687,6 +1793,30 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
16871793 activecolor : :mpltype:`color`
16881794 The color of the selected button. The default is ``'blue'`` if not
16891795 specified here or in *radio_props*.
1796+ layout : None or "vertical" or "horizontal" or (int, int), default: None
1797+ The layout of the radio buttons. Options are:
1798+
1799+ - ``None``: Use legacy vertical layout (default).
1800+ - ``"vertical"``: Arrange buttons in a single column with
1801+ dynamic positioning based on text widths.
1802+ - ``"horizontal"``: Arrange buttons in a single row with
1803+ dynamic positioning based on text widths.
1804+ - ``(rows, cols)`` tuple: Arrange buttons in a grid with the
1805+ specified number of rows and columns. Buttons are placed
1806+ left-to-right, top-to-bottom with dynamic positioning.
1807+
1808+ The layout options "vertical", "horizontal" and ``(rows, cols)``
1809+ create ``mtext.Text`` objects to determine exact text sizes, and
1810+ then they are added to the Axes. This is usually okay, but may cause
1811+ side-effects and has a slight performance impact. Therefore the
1812+ default ``None`` value avoids this.
1813+
1814+ .. admonition:: Provisional
1815+ The new layout options are provisional. Their algorithmic
1816+ behavior, including the exact positions of buttons and labels,
1817+ may still change without prior warning.
1818+
1819+ .. versionadded:: 3.11
16901820 useblit : bool, default: True
16911821 Use blitting for faster drawing if supported by the backend.
16921822 See the tutorial :ref:`blitting` for details.
@@ -1726,7 +1856,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,
17261856 else :
17271857 activecolor = 'blue' # Default.
17281858 super ().__init__ (ax , labels , useblit = useblit , label_props = label_props ,
1729- active = active , activecolor = activecolor ,
1859+ active = active , layout = layout , activecolor = activecolor ,
17301860 radio_props = radio_props )
17311861
17321862 self ._activecolor = activecolor
0 commit comments