[REFACTOR] Report uniformity: emit_css authority, slot-threaded figures, and a figure-sizing/labeling sweep - #76
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR refactors the report rendering pipeline to enforce uniform figure sizing/layout and CSS styling across report templates by centralizing stylesheet generation and making figure rendering “slot-aware” (render at the same width the template will display).
Changes:
- Centralizes report CSS via
emit_css()injected throughbase.html, and replaces inline raw hex colors with CSS variables (plus tests to prevent regression). - Introduces a slot pipeline (
reports/_slots.py) so figures render at the correct display width and templates choose matching.slot-*classes using recorded slot metadata. - Sweeps plotting helpers and primitives to unify grid/spine styling and standardize figure geometry (square panels, matched colorbars, consistent widths).
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_report_tokens.py | Adds enforcement test to forbid raw hex colors in inline style="..." attributes. |
| tests/unit/test_report_golden.py | Adds rendered-page PNG geometry invariant test for slot-classed figures. |
| tests/fixtures/golden/dune2/instrument/dune2_1_2026_9920_report.html | Re-baselines golden HTML for new CSS injection, slot classes, and inline-style token usage. |
| tests/fixtures/golden/dune2/instrument/dune2_1_2026_2941_report.html | Re-baselines golden HTML for new CSS injection, slot classes, and inline-style token usage. |
| tests/fixtures/golden/dune2/dune2_1_2026_stack_report.html | Re-baselines golden HTML for slot-driven figure sizing and tokenized inline styles. |
| tests/fixtures/golden/dune2/dune2_1_2026_report.html | Re-baselines golden HTML for tokenized inline styles and timestamp display rounding. |
| tests/fixtures/golden/dune2/dune2_1_2026_grid_report.html | Re-baselines golden HTML for slot-driven sizing and tokenized inline styles. |
| oceanarray/reports/templates/stack.html | Removes width overrides and switches selected figures to slot-based sizing. |
| oceanarray/reports/templates/mooring.html | Replaces inline raw hex colors with CSS variables; updates figure borders to token vars. |
| oceanarray/reports/templates/instrument.html | Removes width:100% override so slot widths can take effect; tokenizes inline colors. |
| oceanarray/reports/templates/grid.html | Removes width:100% overrides and switches certain figures to slot-based sizing. |
| oceanarray/reports/templates/base.html | Injects shared generated CSS (`{{ css |
| oceanarray/reports/_stack.py | Uses shared helper styling (grid_despine) and adjusts figure sizing/annotations. |
| oceanarray/reports/_slots.py | Adds new slot adapter to render figures at slot width and expose slot metadata to templates. |
| oceanarray/reports/_plots.py | Routes multiple figure render paths through the new slot-aware renderer. |
| oceanarray/reports/_html_helpers.py | Rounds nanosecond-precision ISO timestamps for report display. |
| oceanarray/reports/_env.py | Registers new Jinja globals (slot_for, css) for slot pipeline and CSS injection. |
| oceanarray/reports/_array.py | Applies unified grid/spine styling via grid_despine. |
| oceanarray/processors/coordinate.py | Corrects magnetic declination units metadata and adds sign convention attribute. |
| oceanarray/plotters/ts.py | Threads width_in through T–S plotting functions to support slot-sized rendering. |
| oceanarray/plotters/timeseries.py | Threads width_in through grid/time-series plotters; standardizes grid styling. |
| oceanarray/plotters/spectrum.py | Threads width_in through spectrum/wavelet plotters and applies unified styling. |
| oceanarray/plotters/primitives.py | Extends square_axes_grid controls and threads width_in into trajectory plotting. |
| oceanarray/plotters/hydrography.py | Threads width_in into hydrography plots and standardizes grid styling. |
| oceanarray/plotters/helpers.py | Enhances grid_despine (axis selection) and switches direction colormap to twilight. |
| oceanarray/plotters/diagnostic.py | Rebuilds knockdown displacement layout using square_axes_grid and threads width_in. |
| oceanarray/plotters/current.py | Threads width_in through current-related plotters and removes unused hab_var parameter. |
| oceanarray/config/parameters.py | Updates vlabel() to emit (1) for known dimensionless variables and preserves unknowns. |
Suppressed comments (1)
oceanarray/reports/_env.py:51
- The slot/debug registries (_slots._SLOT_BY_B64 and _figdebug._COLLECTED) are process-global and never cleared, so building many report pages in one run can accumulate a large amount of base64-keyed state and potentially leak across pages. Consider clearing both registries after each template render (or in a per-page build boundary) once the HTML string has been produced.
_ENV.globals["css"] = emit_css(params.PACKAGE_NAME)
def render_template(name: str, /, **context: Any) -> str:
"""Render report template *name* with *context* and return the HTML string.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…nding, var_color, docstring)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch makes more fixes to ensure uniform appearance of figures. (In some ways, it is prescribing figure sizes the way GMT would do it, specifying the axes sizes rather than figsizes, but does this by computing the sizes.) We make one slot decision travel from the template to the figure and back, put the stylesheet under a single generator, and route the recurring figure-layout patterns (square panels + matched colorbar, grid + spines, per-variable colours, axis labels) through shared helpers so they can't diverge again.
The slot pipeline (the systemic core)
emit_css() is the single stylesheet.
base.htmlnow injects{{ css }}fromemit_css(PACKAGE_NAME)plus a small oceanarray-local block of classes/vars that reference the shared tokens. The 73 raw hex colours inside inlinestyle="…"attributes were swept tovar(--…)tokens across all five templates (chrome → shared tokens; QC-flag legend swatches + the stage-1 line colour → named local vars, since they mirror plot colormaps). A new test forbids raw hex in inline styles so it can't regress.The slot travels with the figure. New package-local
reports/_slots.py:render(draw, *, slot)resolvesSLOTS[slot]→width_in, forwards it to the draw function, and records the slot under the returned PNG; theslot_for(b64)Jinja global reads it back so the template picks its.slot-*width class from the same slot the figure was rendered at. All ~34 reportdraw_*functions gained a keyword-onlywidth_in(they used to hardcodefigsize=(W_FULL/W_HALF/…)), so the width is now an argument, not a constant baked in the plotter. The vendoredrender_b64is untouched — width is resolved in the package-local adapter before the call. Non-full figures (T-S grid, grid trajectory, speed profile) now render at their true display width (e.g. 675 px for a half slot) instead of rendering full and letting the browser downscale.A CSS bug this exposed: page-level
.fig { width:100% }blocks in the grid/stack/instrumentpage_styleswere overriding the.slot-*width and forcing every figure full-width; removed (the border/radius/margin kept). This is why "T-S / speed profile / trajectory went full width" — one fix resolved all three.Geometry regression guard.
test_png_geometry_on_rendered_pageasserts everyclass="fig slot-*"figure's PNG width equalsround(SLOTS[slot]·FIG_DPI). Had this existed, it would have caught thewidth:100%bug above. The debug view (OCEANARRAY_REPORT_DEBUG=1) now shows each figure's recorded slot next to its rendered figsize, so a slot-vs-render mismatch is visible.Figure uniformity
Grid + spines, uniformly. Every grid-on figure now goes through
plotters.helpers.grid_despine(ax)(grid on + top/right spines off, appearance from the mplstyle) — ~20 sites acrossplotters/andreports/that previously hardcoded a dashed grid and left the frame closed.grid_despinegained anaxis=argument for the one-directional (bar/profile) cases.Square panels with matched colorbars. The mooring knockdown-displacement plot is rebuilt on the
square_axes_gridprimitive (deterministic inch-based layout, colorbar in its own reserved column) so both panels are equal squares and the colorbar height matches by construction — replacing aset_box_aspect(1)+ inline-colorbar combination that shrank the colorbar-bearing panel.square_axes_gridgained optionalwgap_in/cbar_txt_inoverrides for the shared-y / long-label case.Other figure fixes: the current-rose grid is now a fixed 4-wide, vary-rows, full-width layout (uniform rose size, no dynamic width); the wavelet scalogram/timeseries heights were retuned; the buoyancy-frequency (N²) colorbar is clipped to 2.5–97.5 %; current-direction uses the cyclic twilight colormap (was hsv); the stack Aquadopp-tilt suptitle no longer overprints the top panel's title; the data-value-distribution histograms colour their "kept" bars with the variable's own
VAR_COLORSline colour (falling back to blue).Registries, labels, and metadata
vlabel()now renders(1)(the CF/UDUNITS dimensionless unit, matching the NetCDFunits="1") for known dimensionless variables such as practical salinity; unknown variables stay bare. The T-S salinity axis routes through it.unitsattribute was"degrees_east"(the CF unit for longitude) — corrected to"degree", with a separatemagnetic_declination_sign_convention="positive_east"attribute for the sign.deployment_time) to 0.1 s for display; the saved file is unchanged.github.com/ocean-uhh/oceanarray.Testing
767 pass, 8 skipped, ruff + format clean. Golden HTML fixtures re-baselined for the intended structural/label changes (slot classes, wordmark, rounded timestamps). New enforcement tests:
test_png_geometry_on_rendered_page(slot width == PNG width) andtest_no_raw_hex_in_template_inline_styles.Breaking changes
magnetic_declination_unitsglobal attribute changes from"degrees_east"to"degree", and a newmagnetic_declination_sign_convention="positive_east"attribute is written. Downstream code reading the old value/unit must update. (Existing NetCDF files are not rewritten; the change applies to newly processed data.)vlabel()behaviour: returns"Label (1)"for a known dimensionless variable (previously"Label"with no unit). Callers that string-matched the old output must update.plot_multi_aquadopp_trajectories: the unusedhab_varparameter was removed (the trajectory labels were simplified to the bare serial). Callers passinghab_var=must drop it.draw_*signatures (additive, non-breaking): every report draw function gained a keyword-onlywidth_inwith a default — existing positional calls are unaffected.Reviewer notes
config/report_tokens.py,config/report.mplstyle,reports/_encode.py,reports/_css.py) are unchanged — the width resolution lives in the package-local_slots.pyadapter so the encoder signature stays frozen.