Building Reports#
Scientific Concepts and Principles#
Reports are the boundary where computation meets the reader. The
coco_pipe.report module is built around a few principles that keep
that boundary trustworthy, reproducible, and offline-friendly.
—
1. Tree of Sections, Tree of Elements#
A Report is a container of
Section objects. Each section is itself
a container of Element instances —
images, Plotly figures, tables, code blocks, callouts, accordions, and
so on (see Elements and Assets).
Report
├── Section ("Overview")
│ ├── TableElement
│ └── CalloutElement
├── Section ("Performance")
│ ├── PlotlyElement
│ ├── TableElement
│ └── ImageElement
└── Section ("Provenance")
└── CodeBlockElement
This tree is rendered in two passes:
Collect — every element pushes payload data into a global registry keyed by UUID.
Render — Jinja templates emit HTML; elements that own payload data emit a placeholder
<div data-id="…" class="lazy-plot"></div>; the browser hydrates them on demand.
—
2. Self-Contained, Lazy-Hydrating HTML#
A rendered report is a single .html file. Heavy data (Plotly
figures, interactive tables) is not duplicated per element — it goes
through one registry:
Report.render():
registry = {} # one dict, all elements
self.collect_payload(registry) # walk tree, populate
payload = json.dumps(registry).encode("utf-8")
payload = gzip.compress(payload)
payload = base64.b64encode(payload).decode("utf-8")
<html>...
<script type="application/json" id="report-payload">{payload}</script>
<script>
const REPORT_DATA = JSON.parse(pako.inflate(atob(...)));
// lazy-plot div hydration on intersection
</script>
</html>
This contract lets us:
compress all data once (typically 60-90 % size reduction),
render only what scrolls into view (no upfront Plotly cost),
keep the file self-contained: no sibling assets to ship.
The Report.render
docstring spells out the contract; the matching browser-side decoder
lives in templates/static/report_scripts.html.
—
3. Three Asset Modes#
Reports load three JavaScript bundles (Plotly, Tailwind Play CDN, pako).
The Report constructor takes asset_urls= controlling how
they’re sourced:
|
Behavior |
|---|---|
|
|
|
Override one or more URLs (e.g. self-hosted copies). |
|
Download the bundles once (cached at |
See JavaScript Asset Modes for the full asset story and the
vendor_assets() helper for pre-warming
the cache on an internet-connected machine.
—
4. Provenance Is Automatic, Not Optional#
Every Report captures a
ProvenanceConfig at construction
time:
git_hash— current commit (or"Unknown"outside a git repo).python_version,os_platform— runtime metadata.coco_pipe_version— installed package version.versions— versions of every imported scientific package.timestamp_utc— when the report was assembled.command— the command-line invocation that produced it.
Provenance lives under the Run Info drawer in the rendered HTML and is also serialized into the on-page payload. To audit a report after the fact, callers don’t need the original code — the report carries enough metadata to reproduce the environment.
You can override or extend any field via the config argument to
Report; see Configuration and Provenance.
—
5. Section Adders Are Functions, Bound at Import Time#
Each add_decoding_* / add_reduction_* method on
Report is a top-level function in
coco_pipe.report.decoding or
coco_pipe.report.dim_reduction, attached to Report at
package import:
# In coco_pipe/report/decoding.py:
def add_decoding_overview(self: Report, result, *, name="Overview"):
...
Report.add_decoding_overview = add_decoding_overview
This pattern keeps core.py free of domain-specific imports (no
circular dependency on viz or decoding) while preserving the
fluent report.add_decoding_overview(...) API. The standalone
functions remain importable for testing.
See Custom Elements and Adders for the same pattern applied to your own custom section adders.
—
6. Data-Quality Findings Are First-Class#
When a DataContainer is added via
Report.add_container,
the section is automatically populated with quality findings:
Missingness (NaN fraction)
Flatline detection (zero-variance signals)
Outliers (z-score thresholding)
Constant columns
Each finding is a CheckResult
with a severity and status ("OK" / "WARN" / "FAIL"). The
section’s overall status reflects the worst finding (FAIL > WARN > OK).
The sidebar TOC then surfaces sections needing attention with colored
dots.
See Data-Quality Checks and Findings.
—
7. Three Audiences, One API#
The module is structured so each audience uses just enough of it:
Audience |
Surface |
|---|---|
Run-and-save user |
|
Composer |
|
Extender |
Subclass |
Most users stay at the first level. The lower levels are available when needed and don’t require the upper levels to know about them.
Section and Report#
Section and
Report are the two container classes
that hold the tree of HTML elements assembled by every other module.
—
1. Quick Reference#
from coco_pipe.report import Report, Section
from coco_pipe.report.elements import PlotlyElement, TableElement
report = Report(title="My Analysis")
sec = Section(title="Performance", description="CV scores per fold")
sec.add_element(TableElement(scores_df))
sec.add_element(PlotlyElement(roc_figure))
report.add_section(sec)
report.save("performance.html")
Every method on Report and Section returns self
so they chain:
Report("Demo").add_container(c).add_decoding_overview(result).save("r.html")
—
2. Section#
A logical group of elements with a title, optional icon, optional
description, tags (for sidebar filtering), and a status ("OK" /
"WARN" / "FAIL").
Section(
title="Quality Findings",
icon="⚠",
tags=["quality", "preprocessing"],
status="WARN",
description="2 channels flatlined during run 02.",
code="reducer.fit(X)", # rendered behind a "Source" modal
metadata={"input": "X.npy", "rows": 1024},
)
Methods
|
Append any |
|
Render children in a CSS grid row. |
|
Attach a |
|
Render the section to HTML (called by Report). |
Status semantics
FAIL is sticky. Once a section’s status is FAIL it cannot be
downgraded by a later WARN. The sidebar marks any
WARN / FAIL section with a colored dot and surfaces an
“Attention Needed” summary at the bottom of the sidebar.
—
3. Report#
The top-level container. Owns the asset URLs, theme, configuration, provenance metadata, and the section list.
Report(
title="CoCo Analysis Report",
config={"experiment_name": "demo"}, # dict or ReportConfig
theme="paper", # "paper" | "notebook" | "poster"
asset_urls=None, # None | dict | "inline"
)
Construction
The constructor delegates to three helpers (kept lazy and side-effect-free in their own right):
|
Sets matplotlib rcParams and the Plotly |
|
Returns the |
|
Coerces |
Section adders
Method |
Use |
|---|---|
|
Append a pre-built |
|
Shortcut: wraps a matplotlib / Plotly figure in its own section. |
|
Inspect a |
|
Interactive Plotly preview for raw arrays or |
|
Top-of-report key/value stat strip (e.g., |
|
Append a markdown block (inherited from |
|
Append any |
Domain-specific adders (add_decoding_*, add_reduction_*,
add_reduction, add_comparison) are bound at import time from
decoding and dim_reduction
— see Decoding Sections and
Dimensionality-Reduction Sections.
Rendering
|
Collect payload + render full HTML. |
|
Render and write to disk. |
|
Open the rendered report in a new browser tab via the stdlib |
|
Allows |
The rendered HTML is fully self-contained: heavy data (Plotly figures, interactive tables) is collected into one gzip+base64 payload embedded in the page (see Scientific Concepts and Principles).
—
4. Common Patterns#
4.1 One-shot save#
from coco_pipe.report import Report
from coco_pipe.report.elements import PlotlyElement
Report("Quick").add_element(PlotlyElement(fig)).save("quick.html")
4.2 Build a multi-section report by hand#
from coco_pipe.report import Report, Section
from coco_pipe.report.elements import (
CalloutElement, CodeBlockElement, PlotlyElement, TableElement,
)
report = Report(title="Cross-validation comparison")
overview = Section(title="Overview", description="3 models, 5-fold CV")
overview.add_element(CalloutElement(
"All models trained on the same splits; see Provenance for details.",
kind="info",
))
overview.add_element(TableElement(scores_summary_df))
report.add_section(overview)
plots = Section(title="ROC + Calibration")
plots.add_columns([
PlotlyElement(roc_fig, height="380px"),
PlotlyElement(cal_fig, height="380px"),
])
report.add_section(plots)
src = Section(title="Source", code=open("script.py").read())
src.add_element(CodeBlockElement(open("script.py").read(), language="python"))
report.add_section(src)
report.save("comparison.html")
4.3 Display inline in a notebook#
report # _repr_html_ auto-displays the rendered HTML in Jupyter
4.4 Serve over HTTP#
report.show() # picks a free port, opens browser
report.show(port=8123)
—
5. State and Identity#
Each section is given an HTML id derived from its title (slugified). Collisions are auto-suffixed (
"repeated"→"repeated-2").The collected payload registry uses element-level UUIDs; nothing in the HTML hard-codes a section count or position.
The TOC sidebar is built from the section tree; it updates the active link via an
IntersectionObserverand setsaria-current="location"on the active link for screen readers.
—
6. Failure Modes#
Action |
Behavior |
|---|---|
Missing reducer |
|
Empty comparison frame |
|
Broken |
|
Section produces an error |
The factories ( |
The factory functions are deliberately permissive: a single bad section never blocks the rest of the report.
Factories and Assembly#
coco_pipe.report.api exposes seven top-level helpers — six
from_* factories that produce a populated Report from a
domain object, plus merge_reports() for combining existing
reports.
These are convenience entry points. Internally they call the same
add_* methods documented in Section and Report,
Decoding Sections, and
Dimensionality-Reduction Sections.
—
1. Factory at a Glance#
Factory |
Input Output sections |
|---|---|
|
|
|
BIDS root directory Data overview from loaded container |
|
Path to CSV / parquet / etc. Data overview from loaded container |
|
|
list of scored |
|
|
|
|
Every factory accepts the standard report kwargs: title,
config, theme, asset_urls, output_path.
—
2. Container Factories#
2.1 from_container#
The simplest path: render a DataContainer as a report.
from coco_pipe.io import load_data
from coco_pipe.report import from_container
container = load_data("scores.csv", mode="tabular", target_col="label")
report = from_container(
container,
title="Input Inspection",
raw_preview=True, # adds an interactive scroller
output_path="input.html",
)
The single “Data Overview” section includes dimensions, coordinates, data-quality findings (missingness, flatline, outliers, constant columns — see Data-Quality Checks and Findings), and a sample histogram.
2.2 from_bids#
Wraps coco_pipe.io.load_data() in BIDS mode + from_container.
from coco_pipe.report import from_bids
report = from_bids(
root="/data/bids_root",
task="resting",
output_path="bids_overview.html",
)
2.3 from_tabular#
Wraps coco_pipe.io.load_data() in tabular mode + from_container.
from coco_pipe.report import from_tabular
report = from_tabular(
path="data/scores.csv",
sep=",",
target_col="label",
title="Score Table Inspection",
)
2.4 from_embeddings#
Standalone embedding plot — useful when you have an embedding array
but no fitted DimReduction to ship alongside.
from coco_pipe.report import from_embeddings
report = from_embeddings(
X_emb=embedding,
labels=class_ids,
metadata={"subject": subject_ids},
title="UMAP embedding",
)
—
3. Result Factories#
3.1 from_reductions#
Builds a multi-reducer report. Pass container= to inject a “Data
Overview” section, embeddings= to enable embedding/trajectory
plots, raw_preview=True to add a raw-data scroller after the data
overview.
from coco_pipe.report import from_reductions
report = from_reductions(
reductions=[pca, umap, phate],
container=container,
embeddings=[pca_emb, umap_emb, phate_emb],
labels=container.y,
title="PCA vs UMAP vs PHATE",
output_path="reduction.html",
)
Equivalent to make_reduction_report() plus an extra
add_container call.
3.2 from_experiment_result#
Builds a full decoding report from an
ExperimentResult.
from coco_pipe.report import from_experiment_result
report = from_experiment_result(
result,
feature_metadata=meta_df, # for sensor topomap sections
info=raw.info, # for topomap sensor coordinates
title="Decoding Report",
output_path="decoding.html",
)
Identical surface to make_decoding_report(); choose whichever
import path reads better in your script.
—
4. merge_reports#
Combine multiple reports for cross-run / cross-cohort comparison. Every source section is deep-copied, its title prefixed with the source report’s title, and appended to the merged output in the order supplied.
from coco_pipe.report import from_experiment_result, merge_reports
r1 = from_experiment_result(result_cohort_a, title="Cohort A")
r2 = from_experiment_result(result_cohort_b, title="Cohort B")
merged = merge_reports(r1, r2, title="Cross-Cohort Comparison")
merged.save("comparison.html")
Raises ValueError if fewer than two reports are supplied.
The merged report inherits the provenance, theme, and asset mode of the first input. Override by setting them on the merged report before saving:
merged = merge_reports(r1, r2)
merged.theme = "poster"
merged.asset_mode = "inline" # rebuild urls accordingly with _resolve_assets
—
5. Argument Conventions#
Argument |
Purpose |
|---|---|
|
Report title; shown in the header and the browser tab. |
|
Dict of free-form parameters. Stored on the |
|
|
|
|
|
Convenience: when set, the factory also calls |
|
|
When in doubt, prefer the factory functions over the lower-level
make_*_report functions — they accept everything make_*_report
does, plus optional container= / raw_preview= for additional
inspection sections.
Configuration and Provenance#
coco_pipe.report.config defines two pydantic models that govern
how a report identifies itself and the environment that produced it:
ReportConfig— title, author, description, run parameters.ProvenanceConfig— git hash, Python / OS, package versions, command line, timestamp.
Both are strict (pydantic extra="allow" for forward-compatibility),
serializable to JSON, and rendered into the report’s “Run Info”
drawer.
—
1. ReportConfig#
from coco_pipe.report.config import ReportConfig
ReportConfig(
title="EEG Decoding — Cohort A",
author="Hamza Abdelhedi",
description="3-class motor imagery, 12 subjects, LOSO CV.",
run_params={
"experiment_id": "exp_007",
"max_iter": 500,
"scaler": "StandardScaler",
},
# provenance defaults to ProvenanceConfig.from_env()
)
Field |
Description |
|---|---|
|
Report title. Defaults to |
|
Optional author name. |
|
Optional one-line summary. |
|
|
|
Free-form dict of analysis parameters, exposed in the Run Info drawer. |
Pydantic extra="allow" is set, so unknown fields are preserved on
the model (accessible via attribute access).
—
2. Passing Config to a Report#
Three styles, ordered by ceremony:
# 1. Title only (most common)
Report(title="Quick", config={"experiment": "demo"})
# 2. Dict — pydantic-coerced to ReportConfig
Report(config={
"title": "Detailed",
"description": "Quarterly QC",
"run_params": {"experiment": "Q1", "subjects": 24},
})
# 3. Typed ReportConfig
cfg = ReportConfig(title="Typed", description="QC")
Report(config=cfg)
The Report._resolve_config() helper handles the coercion. If
the dict is malformed (raises pydantic ValidationError), the
report falls back to a minimal ReportConfig(title=title,
run_params=config) — the report still renders rather than failing
hard.
When the config dict carries its own "title", that value wins
over the title= constructor argument.
—
3. ProvenanceConfig#
Captures runtime metadata automatically:
Field |
Source |
|---|---|
|
User-tagged data source (e.g., |
|
Output of |
|
UTC ISO-ish timestamp at config-instantiation. |
|
Original |
|
|
|
|
|
|
|
|
3.1 Auto-capture#
from coco_pipe.report.config import ProvenanceConfig
prov = ProvenanceConfig.from_env(source="BIDS")
prov.git_hash, prov.python_version, prov.coco_pipe_version
This is what Report.__init__() calls when no provenance is
passed. The captured snapshot is then frozen onto the report — even
if the working tree changes after rendering.
3.2 Manual override#
prov = ProvenanceConfig(
source="cluster-shared",
git_hash="a1b2c3d",
command="python train.py --cohort A",
)
cfg = ReportConfig(title="Override", provenance=prov)
report = Report(config=cfg)
Useful for batch runs where the auto-captured command would be
the orchestrator’s command (e.g., snakemake) rather than the
actual analysis invocation.
—
4. Where Config Shows Up in the Report#
Field |
Location in rendered HTML |
|---|---|
|
|
|
Header subheading and Run Info > Environment. |
|
Header summary “Git” column and Run Info. |
|
Run Info > Environment. |
|
Run Info > Environment. |
|
Run Info > Environment and the footer. |
|
Run Info > Configuration (rendered as syntax- highlighted JSON). |
|
Run Info > Execution Command + footer. |
The Run Info drawer slides in from the right when the user clicks the “Run Info” button in the header.
—
5. Serializing and Inspecting Config#
Both models are standard pydantic — model_dump(),
model_dump_json(), model_validate() all work.
from coco_pipe.report.config import ReportConfig
cfg = ReportConfig(title="serializable", run_params={"k": 1})
payload = cfg.model_dump_json(indent=2)
restored = ReportConfig.model_validate_json(payload)
The rendered report also embeds the full config as syntax-highlighted JSON in the Run Info drawer, so the report itself is its own configuration audit trail.
—
6. Custom Fields#
Because extra="allow" is set on both models, any extra fields on a
config dict are kept:
rep = Report(config={"title": "Demo", "extra_field": 42})
rep.config.extra_field # -> 42
Use this sparingly — extras don’t get a dedicated section in the
template and just live on the pydantic instance. For analysis
parameters, prefer the typed run_params field, which gets a real
home in the Run Info drawer.