@@ -209,6 +209,29 @@ 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+ # Phase 13.46.DF C-2: ROOT-convention plot-type aliases. Applied before the
213+ # type-dispatch ladder in DFDraw.draw so e.g. ROOT's "histo" maps to "hist".
214+ # Kept as a module-level dict so it is greppable and trivially extensible.
215+ _TYPE_ALIASES = {'histo' : 'hist' }
216+
217+
218+ def _get_suptitle (fig ):
219+ """Phase 13.46.DF C-4: public-API suptitle text (matplotlib >= 3.8) with a
220+ private-attribute fallback for older matplotlib.
221+
222+ Returns the suptitle string, or '' when the figure has no suptitle.
223+ Source-level counterpart to the test helper that already used the public
224+ API; replaces 9 inline ``fig._suptitle.get_text()`` expressions so the
225+ source no longer depends on the private ``_suptitle`` attribute.
226+ """
227+ try :
228+ t = fig .get_suptitle () # public API, matplotlib >= 3.8
229+ return t if t else ''
230+ except AttributeError :
231+ st = getattr (fig , '_suptitle' , None )
232+ return st .get_text () if st else ''
233+
234+
212235def _suptitle_top_for_title (title_text ):
213236 """Phase 13.42.DF FIX2 (B6): compute a reasonable subplots_adjust top= value
214237 based on suptitle line count, so multi-line titles or dense facet grids
@@ -2299,7 +2322,7 @@ def _dispatch_normalize_render(
22992322 f"{ _main } \n { _sub } " if _sub else _main ,
23002323 fontsize = get_style_value ("axes.titlesize" , 14 ),
23012324 )
2302- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2325+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
23032326 except Exception :
23042327 # Failsafe — same defensive pattern as BUG-002 fix.
23052328 _y_str = (y_list if isinstance (y_list , str )
@@ -2308,7 +2331,7 @@ def _dispatch_normalize_render(
23082331 else str (x_list [0 ] if x_list else '' ))
23092332 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
23102333 fontsize = get_style_value ("axes.titlesize" , 14 ))
2311- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2334+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
23122335
23132336 # --- 9. Build stats dict (M1 scope per v1.1 §7) ------------------------
23142337 # Drop profile_data from user-facing stats — internal-only.
@@ -2642,15 +2665,15 @@ def _dispatch_normalize_grouped_render(
26422665 f"{ _main } \n { _sub } " if _sub else _main ,
26432666 fontsize = get_style_value ("axes.titlesize" , 14 ),
26442667 )
2645- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2668+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
26462669 except Exception :
26472670 _y_str = (y_list if isinstance (y_list , str )
26482671 else f"[{ ',' .join (map (str , y_list ))} ]" )
26492672 _x_str = (x_list if isinstance (x_list , str )
26502673 else str (x_list [0 ] if x_list else '' ))
26512674 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
26522675 fontsize = get_style_value ("axes.titlesize" , 14 ))
2653- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2676+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
26542677
26552678 # --- 11. Build stats dict (M2 grouped contract) -----------------------
26562679 stats_dict : Dict [str , Any ] = {
@@ -2948,15 +2971,15 @@ def _dispatch_normalize_faceted_render(
29482971 f"{ _main } \n { _sub } " if _sub else _main ,
29492972 fontsize = get_style_value ("axes.titlesize" , 14 ),
29502973 )
2951- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2974+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
29522975 except Exception :
29532976 _y_str = (y_list if isinstance (y_list , str )
29542977 else f"[{ ',' .join (map (str , y_list ))} ]" )
29552978 _x_str = (x_list if isinstance (x_list , str )
29562979 else str (x_list [0 ] if x_list else '' ))
29572980 fig .suptitle (f"{ _y_str } vs { _x_str } " ,
29582981 fontsize = get_style_value ("axes.titlesize" , 14 ))
2959- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
2982+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
29602983
29612984 # --- 8. Stats dict ----------------------------------------------------
29622985 stats_dict : Dict [str , Any ] = {
@@ -3543,7 +3566,7 @@ def _dispatch_faceted_render(
35433566 f"{ _main } \n { _sub } " if _sub else _main ,
35443567 fontsize = get_style_value ("axes.titlesize" , 14 ),
35453568 )
3546- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
3569+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
35473570 except Exception :
35483571 # Failsafe: minimal title — prevents auto_title import/build
35493572 # errors from crashing the plot. Same defensive pattern as
@@ -3552,11 +3575,11 @@ def _dispatch_faceted_render(
35523575 else f"[{ ',' .join (y_expr )} ]" )
35533576 fig .suptitle (f"{ y_str } vs { x_expr } " ,
35543577 fontsize = get_style_value ("axes.titlesize" , 14 ))
3555- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
3578+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
35563579
35573580 plt .tight_layout ()
35583581 if title :
3559- plt .subplots_adjust (top = _suptitle_top_for_title (fig . _suptitle . get_text () if getattr ( fig , '_suptitle' , None ) else None ))
3582+ plt .subplots_adjust (top = _suptitle_top_for_title (_get_suptitle ( fig ) or None ))
35603583
35613584 # ---- Combined stats ------------------------------------------------
35623585 combined_stats = {
@@ -3983,6 +4006,51 @@ def draw(
39834006 tuple
39844007 (fig, ax, stats_dict)
39854008 """
4009+ # Phase 13.46.DF C-7: kwarg-typo guard (difflib "did you mean",
4010+ # industry standard — argparse/click/plotly). A kwarg that is a near
4011+ # miss for a known parameter raises with a suggestion (catches the
4012+ # silent facet_by_bin -> facet_by_bins class); a genuinely-unknown
4013+ # kwarg only warns (matplotlib passthrough still works, but the user
4014+ # now SEES it). The known set K is built from the signatures of draw()
4015+ # and every typed plot method PLUS the _*_FORWARDED_NAMES tuples
4016+ # (N-1, Claude48): inner-method-specific kwargs forwarded via **kwargs
4017+ # are legitimate and must not warn.
4018+ if kwargs :
4019+ import difflib
4020+ import warnings
4021+ _known_kwargs = set ()
4022+ for _m in (self .draw , self .hist , self .scatter , self .profile ,
4023+ self .hist2d , self .hexbin ):
4024+ for _pname , _p in inspect .signature (_m ).parameters .items ():
4025+ if _pname == 'self' :
4026+ continue
4027+ if _p .kind in (_p .VAR_KEYWORD , _p .VAR_POSITIONAL ):
4028+ continue
4029+ _known_kwargs .add (_pname )
4030+ for _tup in (self ._DRAW_FORWARDED_NAMES , self ._HIST_FORWARDED_NAMES ,
4031+ self ._SCATTER_FORWARDED_NAMES ,
4032+ self ._PROFILE_FORWARDED_NAMES ,
4033+ self ._HIST2D_FORWARDED_NAMES ):
4034+ _known_kwargs .update (_tup )
4035+ for _name in list (kwargs ):
4036+ if _name in _known_kwargs :
4037+ continue
4038+ _near = difflib .get_close_matches (
4039+ _name , _known_kwargs , n = 1 , cutoff = 0.8 )
4040+ if _near :
4041+ raise ValueError (
4042+ f"Unknown keyword argument { _name !r} . "
4043+ f"Did you mean { _near [0 ]!r} ? "
4044+ f"(dfdraw Phase 13.46.DF C-7 typo guard.)"
4045+ )
4046+ else :
4047+ warnings .warn (
4048+ f"Unknown keyword argument { _name !r} — forwarded to "
4049+ f"matplotlib or ignored. If this is a dfdraw typo, "
4050+ f"check the API." ,
4051+ UserWarning , stacklevel = 2 ,
4052+ )
4053+
39864054 # Phase 13.42.DF: AD-42 guard removed; 'fit=' is now the inline-fit
39874055 # specification per the unified str/dict/callable/list grammar
39884056 # (see plots/fits.py and PHASE_13_42_DF v1.4 §3).
@@ -4086,6 +4154,9 @@ def draw(
40864154 type = "scatter"
40874155
40884156 # Dispatch to specific plot method
4157+ # Phase 13.46.DF C-2: normalize ROOT-convention type aliases (e.g.
4158+ # "histo" -> "hist") before the dispatch ladder.
4159+ type = _TYPE_ALIASES .get (type , type )
40894160 # Phase 13.43.DF v1.0 R-2 (Sonnet54 panel finding): explicitly
40904161 # forward fit / fit_textbox_kwargs / summary_fit at scalar dispatch.
40914162 # These are NAMED params on DFDraw.draw (Phase 13.42 + 13.43), so
0 commit comments