This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
nf-metro generates metro-map-style SVG diagrams from Mermaid graph definitions augmented with %%metro directives. It is designed for visualizing bioinformatics pipeline workflows (e.g., nf-core pipelines) as transit-style maps where each analysis route is a colored "metro line."
# Install in development mode (uses hatchling build system)
pip install -e ".[dev]"
# Run CLI
nf-metro render examples/rnaseq_sections.mmd -o output.svg
nf-metro validate examples/rnaseq_sections.mmd
nf-metro info examples/rnaseq_sections.mmd
# Run via module
python -m nf_metro
# Run all tests
pytest
# Run a single test
pytest tests/test_parser.py::test_parse_title
# Lint
ruff check src/ tests/Dependencies: click, drawsvg, networkx, pillow. Dev: pytest, ruff.
Content and tooling are separated:
docs/— hand-written Markdown only (plusdocs/assets/media), as it was under MkDocs. This is the source of truth for page content.website/— the Astro / Starlight site (it replaced MkDocs-Material): theme/layout overrides inwebsite/src/components/, styles inwebsite/src/styles/custom.css, nav + config inwebsite/astro.config.mjs. It loads the Markdown via a symlink:website/src/content/docs -> ../../docs.
The fastest way to preview the site locally is the convenience script, which generates the git-ignored dynamic content and then starts the dev server in one step (serves at http://localhost:4321/nf-metro/):
scripts/serve_docs.sh # generate content if missing, then dev server
scripts/serve_docs.sh --branch X # serve branch X: worktree + its nf_metro + renders
scripts/serve_docs.sh --rebuild # force-refresh gallery + playground content
scripts/serve_docs.sh --skip-content # fastest; assumes content already generated
scripts/serve_docs.sh --preview # production build + preview instead of dev
scripts/serve_docs.sh --help # all flags; `-- <args>` pass through to astroIt needs the nf_metro package importable (e.g. source ~/.local/bin/mm-activate nf-metro)
to build the gallery; without it the server still starts but those pages are empty.
The underlying npm commands, if you prefer to drive Astro directly:
cd website
npm install
npm run dev # serves at http://localhost:4321/nf-metro/
npm run build # static output to website/dist/Maps are embedded on a page with the <Metro> component
(website/src/components/Metro.astro): <Metro src="examples/guide/01_minimal.mmd" />
names a committed .mmd by repo-relative path and renders it live at build
time — source block, CLI command, and the map as three toggle sections (see
contributing.mdx for the purpose/flag options). The
SVG is produced by a Vite plugin (website/src/lib/render-metro.mjs) that
resolves .mmd?metro imports through the nf-metro CLI and caches them under
website/.metro-cache/ (git-ignored); importing the SVG (rather than rendering
to a runtime set:html string) is what keeps its light-dark() chrome and
embedded logo through the production build. Plain ```metro fences are left
alone — nothing intercepts them, so they stay highlight-only snippets.
The Gallery and nf-core pipelines pages are generated by
python scripts/build_gallery.py (writes Markdown emitting <Metro> tags into
docs/{gallery,pipelines}/, git-ignored). The same script also renders the
example SVG corpus into docs/assets/renders/ (git-ignored), but that corpus
now only feeds the CI render-diff (pr-renders.yml + build_render_diff.py)
and the metrics scorecard — no page references it; pages render live via
<Metro>. The interactive playground is static files under
website/public/playground/. Deployment is .github/workflows/docs.yml
(Astro build -> gh-pages branch, preserving PR render previews under _pr/).
The pipeline is: Parse -> Layout -> Render. This section is a map; each
subsystem has a reader-friendly deep dive under docs/dev/ - read the relevant one
before changing that subsystem rather than reverse-engineering it from the code.
| Subsystem | Dir | Role | Deep dive |
|---|---|---|---|
| Parser | src/nf_metro/parser/ |
.mmd text -> MetroGraph via a Lark grammar, %%metro directives, post-parse rewrites (resolve.py) that insert ports/junctions. model.py holds the central MetroGraph dataclass. |
docs/dev/parser.mdx |
| Layout | src/nf_metro/layout/ |
Section-first pipeline of 40+ numbered stages (engine.py orchestrates; phase impls in phases/, re-exported from engine). Also auto_layout.py (infers grid/direction/ports), section_placement.py, ordering.py, layers.py, rail_mode.py (opt-in interchange idiom), geometry.py, constants.py. |
docs/dev/layout_pipeline.mdx; per-stage pre/post/invariants in src/nf_metro/layout/CONTRACT.md |
| Routing | src/nf_metro/layout/routing/ |
Edge routing (horizontal runs + 45-degree diagonals; L-shaped inter-section). core.py is a thin first-match dispatcher over handler families in sibling modules; invariants.py checks output every render. |
docs/dev/routing.mdx; inter-section dispatch table in docs/dev/inter_section_dispatch.mdx |
| Render | src/nf_metro/render/ |
Laid-out MetroGraph -> SVG via drawsvg (svg.py), plus animate.py, bridges.py, html.py, manifest.py, legend.py, icons.py, constants.py. |
docs/dev/render.md |
| Themes | src/nf_metro/themes/ |
Brand identity × display mode as orthogonal axes. Per-brand Theme pairs (nfcore.py, seqera.py, each a light + dark variant); light.py is the transparent embed theme. THEME_MODES maps brand→{light,dark}; resolve_theme(brand, mode) combines them. Chrome colours emit as light-dark() so one render adapts to the viewer's color-scheme. New brand: add light+dark Themes, register in THEME_MODES/THEMES. |
- |
.mmd files use a subset of Mermaid graph LR syntax with %%metro directive extensions:
%%metro title: Pipeline Name
%%metro style: nfcore
%%metro mode: light
%%metro line: line_id | Display Name | #hexcolor | style
%%metro line_order: span
%%metro compact_offsets: true
%%metro legend_min_height: 72
%%metro grid: section_id | col,row
graph LR
subgraph section_id [Section Name]
%%metro entry: left | line1, line2
%%metro exit: right | line1, line2
node_id[Label]
node_id -->|line_id| other_node
end
%% Inter-section edges outside subgraphs
node_a -->|line_id| node_b
Edges support comma-separated line IDs: a -->|line1,line2,line3| b creates one edge per line.
Lines support an optional style (4th field): solid (default), dashed, or dotted. Dashed/dotted lines render with SVG stroke-dasharray on both edge paths and legend swatches.
%%metro style: picks the brand (nfcore, seqera; dark aliases nfcore) and %%metro mode: picks light/dark - independent axes (CLI: --theme, --mode). SVG output carries both palettes via light-dark() and adapts to the viewer's color-scheme, so mode only needs setting to bake a concrete PNG; an unset mode falls to a single global default.
- Stations are mutable dataclasses; layout phases write coordinates directly onto
Station.x/.yfields. - Port stations (
is_port=True) participate in layout but are invisible during rendering. - Layout uses networkx only for DAG operations (topological sort); all coordinate computation is custom.
- Auto-layout (
auto_layout.py) infers everything from the section DAG, so most.mmdfiles need no%%metro grid:directives. Explicit directives override inferred values.
NEVER position a perpendicular port at the same coordinate as an internal station. This is validated by check_station_as_elbow in tests/layout_validator.py (10px tolerance).
- TOP/BOTTOM ports on horizontal-flow (LR/RL) sections must NOT share X with any internal station.
- LEFT/RIGHT ports on vertical-flow (TB/BT) sections must NOT share Y with any internal station.
When fixing routing or alignment issues, do NOT "solve" a kink by moving a port to match a station's coordinate. That creates a station-as-elbow violation where the line visually passes through the station marker. Instead, accept small offsets between ports and stations and handle them via routing (near-vertical drops, gentle curves, etc.).
Primary method: Push to a PR. The CI workflow (.github/workflows/pr-renders.yml) renders all gallery examples on both the PR branch and base, generates a before/after visual diff, and posts a preview link:
https://seqeralabs.github.io/nf-metro/_pr/<PR_NUMBER>/
This is the authoritative visual review and should be used for all layout or rendering changes.
Quick local render (for fast iteration before pushing):
source ~/.local/bin/mm-activate nf-metro
# Render SVG. --no-chrome-css bakes concrete colors so cairosvg can rasterize it;
# without it cairosvg aborts on the var() chrome custom properties.
python -m nf_metro render examples/rnaseq_sections.mmd -o /tmp/rnaseq_sections.svg --x-spacing 60 --y-spacing 40 --no-chrome-css
# Convert SVG to PNG via cairosvg (scale=2 for retina)
python -c "import cairosvg; cairosvg.svg2png(url='/tmp/rnaseq_sections.svg', write_to='/tmp/rnaseq_sections.png', scale=2)"
# Open it
open /tmp/rnaseq_sections.pngThe nf-metro micromamba environment has the project installed in editable mode along with cairosvg for PNG conversion.
- Test fixtures:
tests/fixtures/ - Example pipelines:
examples/(includingrnaseq_sections.mmdwith manual grid andrnaseq_auto.mmdwith fully inferred layout) - Topology stress tests:
examples/topologies/*.mmd- 38 fixtures covering fan-out, fan-in, diamonds, folds, mixed port sides, etc. Seeexamples/topologies/README.mdfor the documented inventory and known issues (note: the README covers ~33 fixtures; a handful of newer regression fixtures are present but not yet catalogued there). tests/layout_validator.py- Programmatic layout checks (section overlap, station containment, port positioning, edge waypoints).tests/test_topology_validation.py- Parametrized tests running all validator checks against every topology fixture.scripts/render_topologies.py- Batch render all fixtures to/tmp/nf_metro_topology_renders/.