diff --git a/src/rtichoke/_report_browser.py b/src/rtichoke/_report_browser.py
index f12c357d..ec8b05fd 100644
--- a/src/rtichoke/_report_browser.py
+++ b/src/rtichoke/_report_browser.py
@@ -37,6 +37,81 @@ def _resolve_render_report_symbol(
)
+def _performance_metrics_cheat_sheet_html() -> str:
+ """Generate Performance Metrics Cheat Sheet HTML for browser summary reports."""
+ return (
+ '\n'
+ " Performance Metrics Cheat Sheet
\n"
+ ' \n'
+ '
\n'
+ " Confusion Matrix
\n"
+ ' \n'
+ " \n"
+ " \n"
+ " | \n"
+ " Predicted + | \n"
+ " Predicted - | \n"
+ "
\n"
+ " \n"
+ " \n"
+ " \n"
+ " | Real Positive | \n"
+ " TP | \n"
+ " FN | \n"
+ "
\n"
+ " \n"
+ " | Real Negative | \n"
+ " FP | \n"
+ " TN | \n"
+ "
\n"
+ " \n"
+ "
\n"
+ " \n"
+ '
\n'
+ " Metrics & Formulas
\n"
+ ' \n'
+ " - Prevalence
\n"
+ " (TP + FN) / (TP + FP + TN + FN) \n"
+ " - PPCR
\n"
+ " (TP + FP) / (TP + FP + TN + FN) \n"
+ " - Sensitivity / Recall / TPR
\n"
+ " - \n"
+ "
TP / (TP + FN)
\n"
+ " TP / Real Positives
\n"
+ " P(Predicted Positive | Real Positive)\n"
+ " \n"
+ " - Specificity / TNR
\n"
+ " - \n"
+ "
TN / (TN + FP)
\n"
+ " TN / Real Negatives
\n"
+ " P(Predicted Negative | Real Negative)\n"
+ " \n"
+ " - PPV / Precision
\n"
+ " - \n"
+ "
TP / (TP + FP)
\n"
+ " TP / Predicted Positives
\n"
+ " P(Real Positive | Predicted Positive)\n"
+ " \n"
+ " - NPV
\n"
+ " - \n"
+ "
TN / (TN + FN)
\n"
+ " TN / Predicted Negatives
\n"
+ " P(Real Negative | Predicted Negative)\n"
+ " \n"
+ " - Lift
\n"
+ " PPV / Prevalence \n"
+ " - Net Benefit
\n"
+ " - \n"
+ "
TP / N - FP / N * p_t / (1 - p_t)
\n"
+ " where N = TP + FP + TN + FN\n"
+ " \n"
+ "
\n"
+ " \n"
+ "
\n"
+ " "
+ )
+
+
def _sanitize_nan_values(obj: Any) -> Any:
"""Recursively replace NaN and Inf float values with None for valid JSON serialization."""
if isinstance(obj, dict):
@@ -104,8 +179,14 @@ def _summary_report_density_css() -> str:
class RtichokeBrowserReport:
"""A complete canonical ReportSpec rendered by shared ``rtichoke_viz``."""
- def __init__(self, spec: dict[str, Any]) -> None:
+ def __init__(
+ self,
+ spec: dict[str, Any],
+ *,
+ include_cheat_sheet: bool = False,
+ ) -> None:
self.spec = spec
+ self.include_cheat_sheet = include_cheat_sheet
def write_html(self, path: str | Path) -> Path:
"""Write an offline HTML page that delegates composition to renderReport()."""
@@ -128,6 +209,32 @@ def write_html(self, path: str | Path) -> Path:
else:
doc_title = "rtichoke report"
+ if self.include_cheat_sheet:
+ cheat_sheet_json = json.dumps(
+ _performance_metrics_cheat_sheet_html()
+ ).replace("", "<\\/")
+ mount_js = f""" const reportNode = {render_fn}(spec, {{
+ sectionGroupPresentation: "tabs",
+ groupPresentation: "tabs",
+ sectionComponentPresentation: "tabs"
+ }});
+ const headerNode = reportNode.querySelector(".rtichoke-report__header");
+ const cheatSheetWrapper = document.createElement("div");
+ cheatSheetWrapper.innerHTML = {cheat_sheet_json};
+ const cheatSheetNode = cheatSheetWrapper.firstElementChild;
+ if (headerNode && headerNode.nextSibling) {{
+ reportNode.insertBefore(cheatSheetNode, headerNode.nextSibling);
+ }} else {{
+ reportNode.appendChild(cheatSheetNode);
+ }}
+ document.querySelector("#rtichoke-report").append(reportNode);"""
+ else:
+ mount_js = f""" document.querySelector("#rtichoke-report").append({render_fn}(spec, {{
+ sectionGroupPresentation: "tabs",
+ groupPresentation: "tabs",
+ sectionComponentPresentation: "tabs"
+ }}));"""
+
html_content = f"""
@@ -147,11 +254,7 @@ def write_html(self, path: str | Path) -> Path:
const spec = JSON.parse(
document.querySelector("#rtichoke-report-spec").textContent
);
- document.querySelector("#rtichoke-report").append({render_fn}(spec, {{
- sectionGroupPresentation: "tabs",
- groupPresentation: "tabs",
- sectionComponentPresentation: "tabs"
- }}));
+{mount_js}
diff --git a/src/rtichoke/summary_report/summary_report.py b/src/rtichoke/summary_report/summary_report.py
index d898f88c..42cedc8f 100644
--- a/src/rtichoke/summary_report/summary_report.py
+++ b/src/rtichoke/summary_report/summary_report.py
@@ -636,7 +636,9 @@ def _create_browser_summary_report(
]
report = _build_report_spec_v11(sections, title="Summary Report")
- return RtichokeBrowserReport(cast(dict[str, Any], report)).write_html(output_file)
+ return RtichokeBrowserReport(
+ cast(dict[str, Any], report), include_cheat_sheet=True
+ ).write_html(output_file)
def render_summary_report():
diff --git a/tests/test_quarto_summary_report_browser.py b/tests/test_quarto_summary_report_browser.py
index dfd1ee08..351e4291 100644
--- a/tests/test_quarto_summary_report_browser.py
+++ b/tests/test_quarto_summary_report_browser.py
@@ -130,7 +130,7 @@ def test_quarto_single_browser_summary_report(tmp_path):
assert frame is not None
frame.wait_for_selector("#rtichoke-report")
- frame.wait_for_selector("table")
+ frame.wait_for_selector(".rtichoke-performance-table__table")
frame.wait_for_selector("svg")
assert "Performance" in frame.content()
@@ -230,7 +230,7 @@ def test_quarto_two_browser_summary_reports(tmp_path):
assert frame is not None
frame.wait_for_selector("#rtichoke-report")
- frame.wait_for_selector("table")
+ frame.wait_for_selector(".rtichoke-performance-table__table")
frame.wait_for_selector("svg")
assert "Performance" in frame.content()
diff --git a/tests/test_report_browser.py b/tests/test_report_browser.py
index 374bf756..a23eb183 100644
--- a/tests/test_report_browser.py
+++ b/tests/test_report_browser.py
@@ -12,6 +12,7 @@
)
from rtichoke._report_browser import (
RtichokeBrowserReport,
+ _performance_metrics_cheat_sheet_html,
_resolve_render_report_symbol,
)
from rtichoke._report_spec import _build_report_spec_v11
@@ -253,3 +254,68 @@ def test_resolve_render_report_symbol_export_clauses():
missing_js = "export { foo, bar };"
with pytest.raises(ValueError, match="Could not resolve 'renderReport' export"):
_resolve_render_report_symbol(missing_js)
+
+
+def test_cheat_sheet_html_exact_markup():
+ html = _performance_metrics_cheat_sheet_html()
+ assert html.startswith('\n')
+ assert "Performance Metrics Cheat Sheet
" in html
+ assert "Confusion Matrix
" in html
+ assert "Predicted + | " in html
+ assert "Predicted - | " in html
+ assert "Real Positive | " in html
+ assert "TP | " in html
+ assert "FN | " in html
+ assert "Real Negative | " in html
+ assert "FP | " in html
+ assert "TN | " in html
+ assert "Metrics & Formulas
" in html
+ assert "Prevalence" in html
+ assert "(TP + FN) / (TP + FP + TN + FN)" in html
+ assert "PPCR" in html
+ assert "(TP + FP) / (TP + FP + TN + FN)" in html
+ assert "Sensitivity / Recall / TPR" in html
+ assert "TP / (TP + FN)" in html
+ assert "TP / Real Positives" in html
+ assert "P(Predicted Positive | Real Positive)" in html
+ assert "Specificity / TNR" in html
+ assert "TN / (TN + FP)" in html
+ assert "TN / Real Negatives" in html
+ assert "P(Predicted Negative | Real Negative)" in html
+ assert "PPV / Precision" in html
+ assert "TP / (TP + FP)" in html
+ assert "TP / Predicted Positives" in html
+ assert "P(Real Positive | Predicted Positive)" in html
+ assert "NPV" in html
+ assert "TN / (TN + FN)" in html
+ assert "TN / Predicted Negatives" in html
+ assert "P(Real Negative | Predicted Negative)" in html
+ assert "Lift" in html
+ assert "PPV / Prevalence" in html
+ assert "Net Benefit" in html
+ assert "TP / N - FP / N * p_t / (1 - p_t)" in html
+ assert "where N = TP + FP + TN + FN" in html
+ assert html.endswith(" ")
+
+
+def test_browser_report_include_cheat_sheet_flag_default(tmp_path):
+ report_spec = {"title": "Summary Report", "sections": []}
+ report = RtichokeBrowserReport(report_spec)
+ assert report.include_cheat_sheet is False
+
+ output = report.write_html(tmp_path / "default_report.html")
+ html_content = output.read_text(encoding="utf-8")
+ assert "rtichoke-cheat-sheet" not in html_content
+
+
+def test_browser_report_include_cheat_sheet_opt_in(tmp_path):
+ report_spec = {"title": "Summary Report", "sections": []}
+ report = RtichokeBrowserReport(report_spec, include_cheat_sheet=True)
+ assert report.include_cheat_sheet is True
+
+ output = report.write_html(tmp_path / "cheat_sheet_report.html")
+ html_content = output.read_text(encoding="utf-8")
+ assert "rtichoke-cheat-sheet" in html_content
+ assert "cheatSheetWrapper.innerHTML =" in html_content
+ assert "insertBefore(cheatSheetNode, headerNode.nextSibling)" in html_content
+ assert _embedded_report(html_content) == report_spec
diff --git a/tests/test_summary_report_browser.py b/tests/test_summary_report_browser.py
index 3b6127cb..ca075a7c 100644
--- a/tests/test_summary_report_browser.py
+++ b/tests/test_summary_report_browser.py
@@ -333,6 +333,41 @@ def test_static_performance_table_confusion_matrix_disclosure(tmp_path):
page.on("console", lambda msg: print("CONSOLE:", msg.type, msg.text))
page.on("pageerror", lambda err: print("PAGE ERROR:", err))
+ # 0. Cheat sheet presence, placement, and content in static summary report
+ page.goto(f"{base_url}/{output_thresh.name}")
+ page.wait_for_selector(".rtichoke-report")
+
+ cheat_sheets = page.locator(".rtichoke-cheat-sheet")
+ assert cheat_sheets.count() == 1
+
+ cs = cheat_sheets.first
+ summary_el = cs.locator("summary")
+ assert summary_el.inner_text() == "Performance Metrics Cheat Sheet"
+
+ # Verify element order: .rtichoke-report__header -> .rtichoke-cheat-sheet -> .rtichoke-report__nav
+ is_correct_order = page.evaluate("""() => {
+ const header = document.querySelector('.rtichoke-report__header');
+ const cheatSheet = document.querySelector('.rtichoke-cheat-sheet');
+ const nav = document.querySelector('.rtichoke-report__nav');
+ if (!header || !cheatSheet || !nav) return false;
+ const afterHeader = (header.compareDocumentPosition(cheatSheet) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
+ const beforeNav = (cheatSheet.compareDocumentPosition(nav) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;
+ return afterHeader && beforeNav;
+ }""")
+ assert is_correct_order is True
+
+ # Check cheat sheet text/formulas
+ cs_text = cs.text_content()
+ assert "Confusion Matrix" in cs_text
+ assert "Prevalence" in cs_text
+ assert "PPCR" in cs_text
+ assert "Sensitivity / Recall / TPR" in cs_text
+ assert "Specificity / TNR" in cs_text
+ assert "PPV / Precision" in cs_text
+ assert "NPV" in cs_text
+ assert "Lift" in cs_text
+ assert "Net Benefit" in cs_text
+
# 1. Static probability threshold performance table disclosure
page.goto(f"{base_url}/{output_thresh.name}")
page.wait_for_selector(".rtichoke-performance-table__table")
diff --git a/tests/test_summary_report_times_browser.py b/tests/test_summary_report_times_browser.py
index b4218478..78021feb 100644
--- a/tests/test_summary_report_times_browser.py
+++ b/tests/test_summary_report_times_browser.py
@@ -118,6 +118,7 @@ def _assert_report_rendered(browser: subprocess.CompletedProcess[str]) -> None:
assert "By Predicted Positives Condition Rate (PPCR)" in rendered
assert "= 2
+ assert "rtichoke-cheat-sheet" not in rendered
def test_public_export_create_summary_report_times():