Skip to content

Latest commit

 

History

History
189 lines (140 loc) · 12.6 KB

File metadata and controls

189 lines (140 loc) · 12.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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."

Build & Development

# 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.

Documentation site

Content and tooling are separated:

  • docs/ — hand-written Markdown only (plus docs/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 in website/src/components/, styles in website/src/styles/custom.css, nav + config in website/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 astro

It 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/).

Architecture

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. -

Input Format

.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.

Key Design Decisions

  • Stations are mutable dataclasses; layout phases write coordinates directly onto Station.x/.y fields.
  • 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 .mmd files need no %%metro grid: directives. Explicit directives override inferred values.

Station-as-Elbow Constraint (CRITICAL)

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.).

Visual Review

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.png

The nf-metro micromamba environment has the project installed in editable mode along with cairosvg for PNG conversion.

Test Fixtures & Topology Stress Tests

  • Test fixtures: tests/fixtures/
  • Example pipelines: examples/ (including rnaseq_sections.mmd with manual grid and rnaseq_auto.mmd with fully inferred layout)
  • Topology stress tests: examples/topologies/*.mmd - 38 fixtures covering fan-out, fan-in, diamonds, folds, mixed port sides, etc. See examples/topologies/README.md for 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/.