@@ -209,6 +209,26 @@ def _filter_facet_value(df, col, value, bins=None, quantiles=None):
209209 return df [mask .fillna (False ) if mask .dtype == object else mask ]
210210
211211
212+ def _suptitle_top_for_title (title_text ):
213+ """Phase 13.42.DF FIX2 (B6): compute a reasonable subplots_adjust top= value
214+ based on suptitle line count, so multi-line titles or dense facet grids
215+ don't overlap subplot titles.
216+
217+ Production-gate B6 finding (Phase 13.42 close): single-line auto_title +
218+ long-facet-tuple subtitle frequently overlapped the row-0 subplot titles
219+ when facet_by was 2D. The fixed top=0.92 reserved 8% for the suptitle
220+ block, which is insufficient for 2-3 line titles.
221+
222+ Returns top fraction in [0.84, 0.94] depending on the number of newlines
223+ in title_text. None → falls back to 0.92 (existing behavior).
224+ """
225+ if not title_text :
226+ return 0.92
227+ n_lines = str (title_text ).count ('\n ' ) + 1
228+ # 1 line → 0.93; 2 lines → 0.89; 3 lines → 0.86; 4+ → 0.84
229+ return max (0.84 , 0.93 - 0.035 * max (0 , n_lines - 1 ))
230+
231+
212232def _compute_global_ranges (df , x_expr , y_expr , plot_kind ):
213233 """For 3D share_across_figures=True, compute global x/y ranges.
214234
@@ -839,6 +859,11 @@ def _auto_label(self, y_expr, x_expr=None):
839859 'time_format' ,
840860 # Phase 13.42.DF: Inline fits
841861 'fit' ,
862+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
863+ # fit textbox formatting overrides. Pattern B (forwarded inward):
864+ # draw_profile() has fit_textbox_kwargs as an explicit parameter
865+ # since FIX1 and consumes it via render_fit_textbox.
866+ 'fit_textbox_kwargs' ,
842867 )
843868
844869 _HIST_FORWARDED_NAMES = (
@@ -886,6 +911,9 @@ def _auto_label(self, y_expr, x_expr=None):
886911 'cumulative' ,
887912 # Phase 13.42.DF: Inline fits
888913 'fit' ,
914+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
915+ # fit textbox formatting overrides. See _PROFILE_FORWARDED_NAMES note.
916+ 'fit_textbox_kwargs' ,
889917 )
890918
891919 _SCATTER_FORWARDED_NAMES = (
@@ -910,9 +938,10 @@ def _auto_label(self, y_expr, x_expr=None):
910938 'vector_compose' , 'delta_facet' ,
911939 # Phase 13.42.DF: Inline fits
912940 'fit' ,
941+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
942+ # fit textbox formatting overrides. See _PROFILE_FORWARDED_NAMES note.
943+ 'fit_textbox_kwargs' ,
913944 )
914-
915- # Phase 13.32.DF Sub-fix 3: hist2d gets its own FORWARDED_NAMES tuple
916945 # (previously absent — hist2d used inline kwargs handling).
917946 _HIST2D_FORWARDED_NAMES = (
918947 'selection' , 'sample' , 'bins' , 'range' , 'norm' , 'stats' ,
@@ -2108,7 +2137,7 @@ def _dispatch_normalize_render(
21082137 f"{ _main } \n { _sub } " if _sub else _main ,
21092138 fontsize = get_style_value ("axes.titlesize" , 14 ),
21102139 )
2111- plt .subplots_adjust (top = 0.92 )
2140+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
21122141 except Exception :
21132142 # Failsafe — same defensive pattern as BUG-002 fix.
21142143 _y_str = (y_list if isinstance (y_list , str )
@@ -2117,7 +2146,7 @@ def _dispatch_normalize_render(
21172146 else str (x_list [0 ] if x_list else '' ))
21182147 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
21192148 fontsize = get_style_value ("axes.titlesize" , 14 ))
2120- plt .subplots_adjust (top = 0.92 )
2149+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
21212150
21222151 # --- 9. Build stats dict (M1 scope per v1.1 §7) ------------------------
21232152 # Drop profile_data from user-facing stats — internal-only.
@@ -2451,15 +2480,15 @@ def _dispatch_normalize_grouped_render(
24512480 f"{ _main } \n { _sub } " if _sub else _main ,
24522481 fontsize = get_style_value ("axes.titlesize" , 14 ),
24532482 )
2454- plt .subplots_adjust (top = 0.92 )
2483+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
24552484 except Exception :
24562485 _y_str = (y_list if isinstance (y_list , str )
24572486 else f"[{ ',' .join (map (str , y_list ))} ]" )
24582487 _x_str = (x_list if isinstance (x_list , str )
24592488 else str (x_list [0 ] if x_list else '' ))
24602489 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
24612490 fontsize = get_style_value ("axes.titlesize" , 14 ))
2462- plt .subplots_adjust (top = 0.92 )
2491+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
24632492
24642493 # --- 11. Build stats dict (M2 grouped contract) -----------------------
24652494 stats_dict : Dict [str , Any ] = {
@@ -2757,15 +2786,15 @@ def _dispatch_normalize_faceted_render(
27572786 f"{ _main } \n { _sub } " if _sub else _main ,
27582787 fontsize = get_style_value ("axes.titlesize" , 14 ),
27592788 )
2760- plt .subplots_adjust (top = 0.92 )
2789+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
27612790 except Exception :
27622791 _y_str = (y_list if isinstance (y_list , str )
27632792 else f"[{ ',' .join (map (str , y_list ))} ]" )
27642793 _x_str = (x_list if isinstance (x_list , str )
27652794 else str (x_list [0 ] if x_list else '' ))
27662795 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
27672796 fontsize = get_style_value ("axes.titlesize" , 14 ))
2768- plt .subplots_adjust (top = 0.92 )
2797+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
27692798
27702799 # --- 8. Stats dict ----------------------------------------------------
27712800 stats_dict : Dict [str , Any ] = {
@@ -3352,7 +3381,7 @@ def _dispatch_faceted_render(
33523381 f"{ _main } \n { _sub } " if _sub else _main ,
33533382 fontsize = get_style_value ("axes.titlesize" , 14 ),
33543383 )
3355- plt .subplots_adjust (top = 0.92 )
3384+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
33563385 except Exception :
33573386 # Failsafe: minimal title — prevents auto_title import/build
33583387 # errors from crashing the plot. Same defensive pattern as
@@ -3361,11 +3390,11 @@ def _dispatch_faceted_render(
33613390 else f"[{ ',' .join (y_expr )} ]" )
33623391 fig .suptitle (f"{ y_str } vs { x_expr } " ,
33633392 fontsize = get_style_value ("axes.titlesize" , 14 ))
3364- plt .subplots_adjust (top = 0.92 )
3393+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
33653394
33663395 plt .tight_layout ()
33673396 if title :
3368- plt .subplots_adjust (top = 0.92 )
3397+ plt .subplots_adjust (top = _suptitle_top_for_title ( fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ) )
33693398
33703399 # ---- Combined stats ------------------------------------------------
33713400 combined_stats = {
@@ -3682,6 +3711,11 @@ def draw(
36823711 normalize_layout : str = "overlay+diff" ,
36833712 # Phase 13.42.DF: Inline fit specification (architect 2026-05-22).
36843713 fit : Optional [Union [str , Dict , Callable , List ]] = None ,
3714+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
3715+ # per-call fit textbox formatting overrides. Pattern B —
3716+ # forwarded inward; consumed by inner draw_hist/profile/scatter
3717+ # via render_fit_textbox.
3718+ fit_textbox_kwargs : Optional [Dict ] = None ,
36853719 ** kwargs
36863720 ) -> DrawResult :
36873721 """
@@ -3950,6 +3984,11 @@ def hist(
39503984 linestyle_cycle : bool = False ,
39513985 # Phase 13.42.DF: Inline fit specification
39523986 fit : Optional [Union [str , Dict , Callable , List ]] = None ,
3987+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
3988+ # per-call fit textbox formatting overrides. Pattern B —
3989+ # forwarded inward; consumed by inner draw_hist/profile/scatter
3990+ # via render_fit_textbox.
3991+ fit_textbox_kwargs : Optional [Dict ] = None ,
39533992 ** kwargs
39543993 ) -> DrawResult :
39553994 """
@@ -4006,6 +4045,37 @@ def hist(
40064045 # Parse expression (take first part only for 1D)
40074046 y_expr , x_expr = self ._parse_expr (expr )
40084047
4048+ # Phase 13.42.DF FIX2 (ADV-1, Sonnet55 v1.2 panel finding, v1.2 §8
4049+ # D9(d) commitment): stacked=True + selection_vector (>1 selection)
4050+ # + fit= has ambiguous semantics in the current architecture (does
4051+ # each selection get its own stack? overlay without sum? fit per
4052+ # selection or per-stack-component or on the stacked total?).
4053+ # Architect did not ratify any of these in v1.2. Until a concrete
4054+ # use case drives the design, raise NotImplementedError with a clear
4055+ # actionable suggestion so users don't get silently wrong output.
4056+ # Placed BEFORE vector dispatch so the 3-axis-inner length-equality
4057+ # validator inside _compute_vector_iteration_indices does not pre-empt
4058+ # this more-specific architectural error.
4059+ # Note: `stacked` is forwarded via **kwargs on DFDraw.hist (not a
4060+ # named outer param), so we read it from kwargs.
4061+ if (kwargs .get ('stacked' ) is True
4062+ and selection_vector is not None and len (selection_vector ) > 1
4063+ and fit is not None ):
4064+ raise NotImplementedError (
4065+ "Phase 13.42.DF FIX2 (ADV-1): the combination "
4066+ "stacked=True + selection_vector (>1 selection) + fit= is not "
4067+ "supported. Selection-vector semantics for stacked-hist fits "
4068+ "were not ratified in Phase 13.42 (v1.2 §8 D9(d) — coder did "
4069+ "not specify which of {per-selection fit, per-stack-component "
4070+ "fit, fit on the stacked total} applies). "
4071+ "Fix options: "
4072+ "(a) drop stacked=True (per-group fits work via D9/R4); or "
4073+ "(b) loop over selections in user code with separate hist() "
4074+ "calls; or "
4075+ "(c) drop fit= and use Phase 13.43 summary_fit= when "
4076+ "available."
4077+ )
4078+
40094079 # Phase 13.16.DF: Vector dispatch
40104080 # Phase 13.16.DF FIX1: tuple-driven forwarding via _HIST_FORWARDED_NAMES
40114081 # + R4 fail-fast guard on facet=True + vector.
@@ -4154,6 +4224,9 @@ def hist(
41544224 cumulative = cumulative ,
41554225 # Phase 13.42.DF: inline fits — recursive forwarding through facet
41564226 fit = fit ,
4227+ # Phase 13.42.DF FIX2 (ADV-3): fit_textbox_kwargs paired
4228+ # with fit= (R6 validator now requires explicit forwarding).
4229+ fit_textbox_kwargs = fit_textbox_kwargs ,
41574230 ** kwargs
41584231 )
41594232 # Facet mode (legacy path, same=True ignored in facet mode)
@@ -4200,6 +4273,7 @@ def hist(
42004273 cumulative = cumulative ,
42014274 # Phase 13.42.DF: inline fits (QRC v1.32 #6 recursive forwarding)
42024275 fit = fit ,
4276+ fit_textbox_kwargs = fit_textbox_kwargs ,
42034277 ** kwargs
42044278 )
42054279 axes = ax
@@ -4275,6 +4349,11 @@ def scatter(
42754349 delta_facet : Optional [str ] = None ,
42764350 # Phase 13.42.DF: Inline fit specification
42774351 fit : Optional [Union [str , Dict , Callable , List ]] = None ,
4352+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
4353+ # per-call fit textbox formatting overrides. Pattern B —
4354+ # forwarded inward; consumed by inner draw_hist/profile/scatter
4355+ # via render_fit_textbox.
4356+ fit_textbox_kwargs : Optional [Dict ] = None ,
42784357 ** kwargs
42794358 ) -> DrawResult :
42804359 """
@@ -4477,6 +4556,7 @@ def scatter(
44774556 share_across_figures = share_across_figures ,
44784557 # Phase 13.42.DF: inline fits
44794558 fit = fit ,
4559+ fit_textbox_kwargs = fit_textbox_kwargs ,
44804560 ** kwargs
44814561 )
44824562 # Facet mode (legacy path, same=True ignored in facet mode)
@@ -4506,6 +4586,7 @@ def scatter(
45064586 time_format = time_format ,
45074587 # Phase 13.42.DF: inline fits
45084588 fit = fit ,
4589+ fit_textbox_kwargs = fit_textbox_kwargs ,
45094590 ** kwargs
45104591 )
45114592 axes = ax
@@ -4607,6 +4688,11 @@ def profile(
46074688 linestyle_cycle : bool = False ,
46084689 # Phase 13.42.DF: Inline fit specification
46094690 fit : Optional [Union [str , Dict , Callable , List ]] = None ,
4691+ # Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
4692+ # per-call fit textbox formatting overrides. Pattern B —
4693+ # forwarded inward; consumed by inner draw_hist/profile/scatter
4694+ # via render_fit_textbox.
4695+ fit_textbox_kwargs : Optional [Dict ] = None ,
46104696 ** kwargs
46114697 ) -> DrawResult :
46124698 """
@@ -5114,6 +5200,7 @@ def profile(
51145200 share_across_figures = share_across_figures ,
51155201 # Phase 13.42.DF: inline fits
51165202 fit = fit ,
5203+ fit_textbox_kwargs = fit_textbox_kwargs ,
51175204 ** kwargs
51185205 )
51195206 else :
@@ -5146,6 +5233,7 @@ def profile(
51465233 time_format = time_format ,
51475234 # Phase 13.42.DF: inline fits
51485235 fit = fit ,
5236+ fit_textbox_kwargs = fit_textbox_kwargs ,
51495237 ** kwargs
51505238 )
51515239 axes = ax
0 commit comments