From 8b08c82aeb1f356d5dd25224bb7b8872f07ab8f5 Mon Sep 17 00:00:00 2001 From: Eleanor Frajka-Williams Date: Sun, 16 Aug 2026 07:17:47 +0200 Subject: [PATCH 1/4] refactor: re-vendor report CSS/tokens, route figures through slots --- oceanarray/cli.py | 4 +- oceanarray/config/report_tokens.py | 4 +- oceanarray/plotters/helpers.py | 10 +-- oceanarray/plotters/timeseries.py | 2 +- oceanarray/reports/_css.py | 72 +++++++++++++-- oceanarray/reports/_plots.py | 13 ++- oceanarray/reports/templates/array.html | 11 +-- oceanarray/reports/templates/base.html | 6 +- oceanarray/reports/templates/grid.html | 39 ++++---- oceanarray/reports/templates/instrument.html | 20 ++--- oceanarray/reports/templates/mooring.html | 30 +++---- oceanarray/reports/templates/stack.html | 41 ++++----- .../dune2/dune2_1_2026_grid_report.html | 89 ++++++++++++------- .../golden/dune2/dune2_1_2026_report.html | 74 +++++++++------ .../dune2/dune2_1_2026_stack_report.html | 85 ++++++++++++------ .../instrument/dune2_1_2026_2941_report.html | 64 ++++++++----- .../instrument/dune2_1_2026_9920_report.html | 66 +++++++++----- tests/unit/test_report_golden.py | 7 +- tests/unit/test_report_tokens.py | 2 + 19 files changed, 393 insertions(+), 246 deletions(-) diff --git a/oceanarray/cli.py b/oceanarray/cli.py index 832a956..caa1273 100644 --- a/oceanarray/cli.py +++ b/oceanarray/cli.py @@ -829,13 +829,11 @@ def _stage_token(value: str) -> "int | str": If *value* is not recognised by :func:`~oceanarray.processors.resolve_stage`. """ - import argparse as _ap - coerced: int | str = int(value) if value.isdigit() else value try: resolve_stage(coerced) except ValueError as exc: - raise _ap.ArgumentTypeError(str(exc)) from exc + raise argparse.ArgumentTypeError(str(exc)) from exc return coerced diff --git a/oceanarray/config/report_tokens.py b/oceanarray/config/report_tokens.py index 8750523..51c23e9 100644 --- a/oceanarray/config/report_tokens.py +++ b/oceanarray/config/report_tokens.py @@ -59,7 +59,7 @@ # Invariant asserted by the slot-contract test: inches == W_FULL * fraction, so # display_px / fig_in is identical for every figure and one font size renders at # one on-screen size everywhere. Test 2 asserts each saved PNG is exactly -# round(inches * FIG_DPI) px wide (1350 / 900 / 810 / 675 / 540 / 450). +# round(inches * FIG_DPI) px wide (1350 / 900 / 810 / 675 / 540 / 450 / 338). SLOTS: dict[str, tuple[float, float]] = { "full": (1.0, 9.0), "twothirds": (2 / 3, 6.0), @@ -67,6 +67,7 @@ "half": (0.5, 4.5), "two-fifths": (0.4, 3.6), "third": (1 / 3, 3.0), + "quarter": (0.25, 2.25), } # Ergonomic width aliases (inches) for plotter call sites; derived from SLOTS. @@ -75,6 +76,7 @@ W_HALF: float = SLOTS["half"][1] W_TWO_FIFTHS: float = SLOTS["two-fifths"][1] W_THIRD: float = SLOTS["third"][1] +W_QUARTER: float = SLOTS["quarter"][1] # Aspect-locked figure constants (spec §14). SECTION_STRETCH: float = ( diff --git a/oceanarray/plotters/helpers.py b/oceanarray/plotters/helpers.py index 1361e10..3629308 100644 --- a/oceanarray/plotters/helpers.py +++ b/oceanarray/plotters/helpers.py @@ -3,14 +3,8 @@ Provides colormap helpers and rose-diagram rendering used across multiple Tier-2 plotter modules. -Post-OdB: still to migrate from report/_plots.py: - _instrument_panels, _CANONICAL_PANELS, _COMPACT_PANEL_VARS, - _ts_heatmap_panel, _add_sigma0_contours, _xyz_to_enu_2d. - -Also migrate _instrument_label from plotter.py. - -Note: _fig_to_base64 stays in report/_html_helpers.py (called only by -Tier-3 wrappers in report/_plots.py; plotters/ never serialises to base64). +Note: _fig_to_base64 stays in reports/_html_helpers.py (called only by +Tier-3 wrappers in reports/_plots.py; plotters/ never serialises to base64). """ from __future__ import annotations diff --git a/oceanarray/plotters/timeseries.py b/oceanarray/plotters/timeseries.py index 2c4dc4b..313cc17 100644 --- a/oceanarray/plotters/timeseries.py +++ b/oceanarray/plotters/timeseries.py @@ -288,7 +288,7 @@ def draw_grid_velocity_stacked( """ import matplotlib.pyplot as plt - from ..reports._plots import _velocity_panel_style + from .helpers import _velocity_panel_style vel_vars = [ "east_velocity", diff --git a/oceanarray/reports/_css.py b/oceanarray/reports/_css.py index 504d428..236945b 100644 --- a/oceanarray/reports/_css.py +++ b/oceanarray/reports/_css.py @@ -76,7 +76,9 @@ def emit_css(package: str) -> str: are the current values, not a redesign). The only additions over the previous hand-written stylesheet are the package accent's two homes — a masthead ``.wordmark`` and the footer's ``border-top`` — plus the ``78ch`` - reading measure and the structural ``break-inside`` print rules. + reading measure and the ``@media print`` rules for browser printing. (The + PDF renderer additionally injects :func:`print_css`, which layers ``@page`` + geometry on top for the WeasyPrint path.) Parameters ---------- @@ -98,15 +100,15 @@ def emit_css(package: str) -> str: p, li {{ max-width: 78ch; }} .masthead {{ background: var(--ocean); color: #fff; position: relative; - padding: 1.6rem 2rem; border-radius: 8px; margin-bottom: 2rem; + padding: 1.6rem 2rem; border-radius: var(--radius-card); margin-bottom: 2rem; }} .masthead h1 {{ margin: 0 0 0.3rem; font-size: var(--fs-h1); font-weight: 700; }} -.masthead .sub {{ font-size: var(--fs-meta); opacity: 0.85; margin: 0 0 0.15rem; }} +.masthead .sub {{ font-size: var(--fs-meta); opacity: 0.85; margin: 0 0 0.15rem; max-width: none; }} .wordmark {{ position: absolute; right: 2rem; bottom: 1.2rem; font-size: var(--fs-dt); font-weight: 700; letter-spacing: 0.04em; color: var(--package-accent); background: #fff; opacity: 0.85; - padding: 0.1rem 0.5rem; border-radius: 999px; text-decoration: none; + padding: 0.1rem 0.5rem; border-radius: var(--radius-pill); text-decoration: none; }} .wordmark:hover {{ opacity: 1; }} .meta-grid {{ @@ -130,10 +132,23 @@ def emit_css(package: str) -> str: text-decoration: none; margin-left: auto; }} .top-link:hover {{ color: var(--ocean); text-decoration: underline; }} -.note {{ +.caption {{ color: var(--gray-5); font-size: var(--fs-note); - margin-top: -0.5rem; margin-bottom: 0.75rem; + margin-top: 0.75rem; margin-bottom: 0.75rem; }} +h2 + .caption {{ margin-top: -0.5rem; }} +.explainer {{ + font-size: var(--fs-note); color: var(--text); + background: var(--bg-sunken); border-left: 3px solid var(--muted); + padding: var(--sp-3); margin: -0.25rem 0 0.75rem; border-radius: var(--radius-btn); +}} +.warn {{ + font-size: var(--fs-note); color: var(--text); + background: var(--warn-bg); border-left: 3px solid var(--warn); + padding: var(--sp-3); margin-bottom: 0.5rem; border-radius: var(--radius-btn); +}} +.warn::before {{ content: "⚠ "; }} +.warn.error {{ border-left-color: var(--error); }} .jump-nav {{ background: var(--seafoam); padding: 0.55rem 1rem; border-radius: 6px; margin-bottom: 1.5rem; @@ -152,10 +167,26 @@ def emit_css(package: str) -> str: .fig-col {{ display: flex; flex-direction: column; gap: 0.75rem; }} figure {{ margin: 0; }} figure img {{ - border: 1px solid var(--rule); border-radius: 4px; + border: 1px solid var(--rule); border-radius: var(--radius-btn); display: block; width: 100%; height: auto; }} figcaption {{ font-size: var(--fs-cap); color: var(--gray-5); margin-top: 0.25rem; }} +table {{ + width: 100%; border-collapse: collapse; + font-size: var(--fs-note); margin: 0.6rem 0 1.2rem; +}} +th {{ + background: var(--package-accent); color: #fff; text-align: left; + font-weight: 600; padding: 0.4rem 0.65rem; +}} +th.num {{ text-align: right; }} +td {{ + padding: 0.35rem 0.65rem; border-bottom: 1px solid var(--rule); + vertical-align: top; +}} +tr:nth-child(even) td {{ background: var(--bg-sunken); }} +td.num, .num {{ text-align: right; font-variant-numeric: tabular-nums; }} +td.mono {{ font-family: var(--font-mono); font-size: var(--fs-xs); }} {slots} .masthead-header {{ display: flex; justify-content: space-between; align-items: flex-start; @@ -169,7 +200,7 @@ def emit_css(package: str) -> str: .nav-btns {{ display: flex; gap: 0.5rem; }} .btn-nav {{ background: var(--ocean); color: #fff; padding: 0.25rem 0.75rem; - border-radius: 999px; text-decoration: none; font-size: var(--fs-nav); + border-radius: var(--radius-pill); text-decoration: none; font-size: var(--fs-nav); }} .btn-nav:hover {{ opacity: 0.85; }} footer {{ @@ -190,6 +221,31 @@ def emit_css(package: str) -> str: """ +def print_css(*, terse: bool = False) -> str: + """Return the print stylesheet, injected at PDF render time (spec §8.1). + + These WeasyPrint-specific rules are kept out of the screen stylesheet so + screen output is byte-identical; a PDF renderer appends them at render time. + The shared stylesheet keeps only the structural ``@media print`` rules (page + breaks). With *terse* ``True`` the ``.explainer`` prose is hidden (the + ``--pdf-terse`` variant); by default nothing is hidden — captions, explainers + and warnings all print. + """ + css = """\ +@page { size: A4; margin: 18mm 16mm 20mm 16mm; } +body { max-width: 100%; padding: 0; } +p, li, .explainer { max-width: 78ch; } +img { max-width: 100%; height: auto; } +.masthead { -webkit-print-color-adjust: exact; print-color-adjust: exact; padding: 0.9rem 1.25rem; } +.masthead-header h1, .masthead h1 { font-size: var(--fs-h1); } +.meta-grid { grid-template-columns: repeat(4, 1fr); gap: 0.3rem 1rem; font-size: var(--fs-xs); } +.jump-nav, .nav-btns, .btn-nav, .top-link { display: none; } +""" + if terse: + css += ".explainer { display: none; }\n" + return css + + _JS_TOP_LINKS: str = """\