Skip to content

Commit 1d77702

Browse files
author
miranov25
committed
Phase 13.46.DF v1.0: audit bucket 1 fixes (C-1/C-2/C-4/C-7/C-9).
C-1 fit='gaus' ROOT TF1 alias (plots/fits.py) C-2 type='histo' ROOT type alias (drawer.py _TYPE_ALIASES) C-4 source _get_suptitle helper + 9 sites (drawer.py; retires private fig._suptitle access, closes 13.42 FIX2 2.2 disclosure) C-7 kwarg-typo guard, difflib did-you-mean (drawer.py draw() entry; K built from all method sigs + FORWARDED_NAMES per reviewer note N-1) C-9 range= on scatter via the shared resolve_range_2d (plots/scatter.py; non-faceted exact, all strategies, honest stats; original profile/ hist unpack bug fixed) 8 invariance tests F.64-F.70 (F.69 split a/b) in TestPhase1346AuditFixes. Taxonomy staged in-commit (+4 features; invariance 340->348). CRR 2.1 disclosure: faceted scatter range applies at the shared-axis / global level (consistent with faceted hist), NOT per-cell, because facet grids use shared axes. Per-cell ranges remain available to users via facet_by=[list] + share_x='none'. Per-cell tightened-strategy windows and the facet_by='string' share_x='none' gap are FIX1 candidates. Spec: PHASE_13_46_DF_v1_3_AuditFixes_Proposal.md (panel-approved). Predecessor: PHASE_13_43_DF_END @ gate 1014. Gate: 1014 -> 1022 / 0 / 0 / 1.
1 parent 02510a2 commit 1d77702

7 files changed

Lines changed: 387 additions & 15 deletions

File tree

UTILS/dfextensions/dfdraw/docs/CAPABILITY_MATRIX.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Capability Matrix — dfdraw
22

3-
**Generated:** 2026-05-27 18:36 UTC
3+
**Generated:** 2026-05-28 10:51 UTC
44
**Phase:** 13.15.DF
55
**Generator:** `scripts/generate_capability_matrix.py`
66
**Sources:** `tests/feature_taxonomy.py` + `tests/test_layer_classification.py`
@@ -9,13 +9,13 @@
99

1010
| Status | Count | % |
1111
|--------|------:|--:|
12-
| ✅ Verified | 51 | 47% |
13-
| ☑️ Smoke-only | 57 | 52% |
12+
| ✅ Verified | 55 | 49% |
13+
| ☑️ Smoke-only | 57 | 50% |
1414
| 🧨 Broken | 0 | 0% |
1515
| 📋 Planned | 1 | 1% |
16-
| **Total features** | **109** | |
17-
| **Total proof tests** | **569** | |
18-
| **Invariance tests** | **340** | |
16+
| **Total features** | **113** | |
17+
| **Total proof tests** | **577** | |
18+
| **Invariance tests** | **348** | |
1919

2020
**Status key:**
2121
- ✅ Verified — has at least one invariance test (A ≡ B check)
@@ -174,6 +174,13 @@
174174
| | **FIT** | | |
175175
|| **FIT.inline** — Inline fits (fit= parameter on hist/profile/scatter/draw) | 41 | 0 |
176176
|| **FIT.summary** — Summary fit — standalone table + params figure | 27 | 0 |
177+
|| **FIT.root_aliases** — ROOT-convention aliases (fit='gaus', type='histo') | 2 | 0 |
178+
| | **API** | | |
179+
|| **API.kwarg_typo_guard** — Kwarg-typo guard (difflib did-you-mean at draw() entry) | 1 | 0 |
180+
| | **RANGE** | | |
181+
|| **RANGE.scatter** — range= on scatter via shared 2D resolver | 4 | 0 |
182+
| | **TITLE** | | |
183+
|| **TITLE.get_suptitle**_get_suptitle public-API helper (mpl >= 3.8 + fallback) | 1 | 0 |
177184

178185
---
179186

UTILS/dfextensions/dfdraw/drawer.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
212235
def _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

UTILS/dfextensions/dfdraw/plots/fits.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def _init_registry():
247247
"""Populate the predefined registry (called once at module import)."""
248248
register_fit('gauss', _gaussian, _gaussian_guess)
249249
register_fit('gaussian', _gaussian, _gaussian_guess)
250+
register_fit('gaus', _gaussian, _gaussian_guess) # Phase 13.46 C-1: ROOT TF1 convention
250251
register_fit('linear', _linear, _linear_guess)
251252
register_fit('pol1', _linear, _linear_guess)
252253
register_fit(

UTILS/dfextensions/dfdraw/plots/scatter.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def draw_scatter(
7272
fit_textbox_kwargs: Optional[Dict[str, Any]] = None,
7373
# Phase 13.42.DF: Inline fit specification
7474
fit: Optional[Union[str, Dict, Callable, List]] = None,
75+
# Phase 13.46.DF C-9: range= support for scatter, resolved through the
76+
# SAME shared 2D resolver hist/profile/2D use (no scatter special-case).
77+
# Accepts a strategy name ('minmax'/'hybrid'/'percentile_99'/...) or an
78+
# explicit ((xlo,xhi),(ylo,yhi)) tuple. Applied after plotting.
79+
range: Optional[Union[str, Tuple]] = None,
7580
**kwargs
7681
) -> Tuple[plt.Figure, plt.Axes, Dict[str, Any]]:
7782
"""
@@ -403,6 +408,37 @@ def draw_scatter(
403408
ax.xaxis.set_major_formatter(mdates.DateFormatter(time_format))
404409
fig.autofmt_xdate()
405410

411+
# Phase 13.46.DF C-9: range= support via the SHARED 2D resolver (AD-74
412+
# per-axis), identical handling to hist/profile/2D — only the application
413+
# differs (set_xlim/set_ylim vs binning, because scatter has no bins).
414+
# Applied AFTER plotting so ax.scatter()'s autoscale cannot override it.
415+
if range is not None:
416+
from ._autorange import resolve_range_2d
417+
(_xr, _yr), _strategy = resolve_range_2d(
418+
range, x_data, y_data,
419+
style_strategy=get_style_value("autorange.strategy", "hybrid"),
420+
style_k_robust=get_style_value("autorange.k_robust", 4.0),
421+
style_k_outlier=get_style_value("autorange.k_outlier", 1.5),
422+
style_percentile=get_style_value("autorange.percentile", (1.0, 99.0)),
423+
)
424+
# Per-cell limit application is suppressed in facet mode: faceted axes
425+
# are SHARED (sharex/sharey), so a per-cell set_xlim would propagate
426+
# and the last cell would clobber all others (silently wrong). In a
427+
# facet grid the shared axes autoscale to the global data extent
428+
# instead. Per-cell strategy tightening under faceting (non-minmax)
429+
# is a Phase 13.46.DF FIX1 item — see CRR §2.
430+
if not _facet_mode:
431+
# Degenerate/empty guard (single point, all-filtered, min==max,
432+
# non-finite): skip set_*lim and let matplotlib autoscale.
433+
if np.all(np.isfinite(_xr)) and _xr[0] < _xr[1]:
434+
ax.set_xlim(_xr)
435+
if np.all(np.isfinite(_yr)) and _yr[0] < _yr[1]:
436+
ax.set_ylim(_yr)
437+
# Honest stats: record the strategy actually resolved (not the
438+
# unconditional "minmax" the default path records above).
439+
stats_dict["autorange_used"] = (_xr, _yr)
440+
stats_dict["autorange_strategy"] = _strategy
441+
406442
return fig, ax, stats_dict
407443

408444

UTILS/dfextensions/dfdraw/tests/feature_taxonomy.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,4 +1473,40 @@
14731473
"test_phase_13_43.py::TestPhase1343SummaryFit::test_f56c_draw_scalar_forwards_fit_and_summary_fit",
14741474
],
14751475
},
1476+
{
1477+
"id": "FIT.root_aliases",
1478+
"name": "ROOT-convention aliases (fit='gaus', type='histo')",
1479+
"category": "FIT",
1480+
"tests": [
1481+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f64_gaus_root_alias",
1482+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f65_histo_type_alias",
1483+
],
1484+
},
1485+
{
1486+
"id": "API.kwarg_typo_guard",
1487+
"name": "Kwarg-typo guard (difflib did-you-mean at draw() entry)",
1488+
"category": "API",
1489+
"tests": [
1490+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f66_kwarg_typo_guard",
1491+
],
1492+
},
1493+
{
1494+
"id": "RANGE.scatter",
1495+
"name": "range= on scatter via shared 2D resolver",
1496+
"category": "RANGE",
1497+
"tests": [
1498+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f67_scatter_range_minmax_nonfaceted",
1499+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f68_scatter_range_minmax_faceted",
1500+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f69a_scatter_range_strategy_parity",
1501+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f69b_profile_hist_range_minmax_no_unpack_error",
1502+
],
1503+
},
1504+
{
1505+
"id": "TITLE.get_suptitle",
1506+
"name": "_get_suptitle public-API helper (mpl >= 3.8 + fallback)",
1507+
"category": "TITLE",
1508+
"tests": [
1509+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f70_get_suptitle_live_path",
1510+
],
1511+
},
14761512
]

UTILS/dfextensions/dfdraw/tests/test_layer_classification.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,5 +432,15 @@
432432
"test_phase_13_43.py::TestPhase1343SummaryFit::test_f56b_faceted_no_group_by": "invariance",
433433
"test_phase_13_43.py::TestPhase1343SummaryFit::test_f56c_draw_scalar_forwards_fit_and_summary_fit": "invariance",
434434

435+
# Phase 13.46.DF — audit bucket ① fixes
436+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f64_gaus_root_alias": "invariance",
437+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f65_histo_type_alias": "invariance",
438+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f66_kwarg_typo_guard": "invariance",
439+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f67_scatter_range_minmax_nonfaceted": "invariance",
440+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f68_scatter_range_minmax_faceted": "invariance",
441+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f69a_scatter_range_strategy_parity": "invariance",
442+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f69b_profile_hist_range_minmax_no_unpack_error": "invariance",
443+
"test_phase_13_46_df_audit_fixes.py::TestPhase1346AuditFixes::test_f70_get_suptitle_live_path": "invariance",
444+
435445
# Everything else defaults to "smoke"
436446
}

0 commit comments

Comments
 (0)