Skip to content

v2.0.0

2026-09-05 · GitHub release · Diff

A major release: roughly 300 pull requests since 1.1.0. The version number is driven by two changes to how invalid .mmd input is handled, described first. A map that already renders cleanly under 1.1.0 keeps rendering, and most look tidier: section boxes now hug their content, inter-section turns are drawn at their full radius, and a long list of fan, merge and bypass routing defects is gone.

Two behaviours that nf-metro validate already rejected are now rejected by nf-metro render as well, so both commands accept the same maps.

  • An edge annotated with a line no %%metro line: declares is an error at render time. A map that declared no lines at all used to slip past the render-side check: every route came out in the placeholder grey the themes reserve for inactive lines, with an empty legend and exit code 0, while validate reported one error per edge. Such a map now needs one %%metro line: directive per id its edges name. The error names the missing ids with the source line of each.
  • A duplicate %%metro line: id keeps the first declaration, not the last, and warns. Redeclaring a line late in the file was a working, if accidental, way to change its colour, style or inactive state. The redeclaration is now reported and dropped, so put the intended values on the first declaration of each id.

Alongside these, unknown values and unresolvable references in %%metro directives are reported instead of silently ignored. style: validates against the theme names; off_track:, group:, marker:, grid:, line_spread:, file:, files:, dir:, entry:, exit: and interchange: warn on a station, section or line id the map never defines; and a line: declaration missing its id, name or colour is rejected whole rather than registering a partial line. Line ids are also constrained to the same identifier character set as station and section ids. See Typos and duplicate declarations in the guide.

Section bounding boxes default to hugging their own content. Previously a short section sharing a row with a taller one was stretched to share the taller box’s top edge, leaving a band of dead space above its stations. The new row_align option (%%metro row_align: content|top, --row-align) defaults to content; set it to top to restore the forced alignment for a map that was tuned around it. Stations, ports and routes do not move either way, only the box edges.

The nf-core/riboseq map, new in the gallery this release, is the clearest example:

A single map defines every line a pipeline can run, but a given run usually exercises a subset. Mark the lines that did not run as inactive and they render in a muted grey, along with any station, label, legend swatch or terminus icon touched only by inactive lines, so the active path stands out.

Add a fifth field, the literal inactive, to a %%metro line: directive to grey a line out by default:

%%metro line: star | STAR alignment (default) | #2db572
%%metro line: salmon | Salmon pseudo-alignment (inactive) | #ff8c00 | solid | inactive
%%metro line: sv | Structural variants (inactive) | #e63946 | dashed | inactive

To decide the set per render instead, pass --inactive-lines salmon,sv to nf-metro render or render-many. It replaces the map’s own inactive marks outright, so an empty value forces every line active. The render_string API takes the same set as inactive_line_ids.

%%metro stroke_scale: / --stroke-scale thickens the track strokes and station pills, widening bundle spacing, marker clearance and rail pitch to match. It is the knob for a large map that gets downscaled to fit a screen: font_scale only scales text, and because label widths drive station spacing, raising it also grows the canvas and gives back a fraction of what it costs. Stroke weight has no such coupling, so coarsening the ink is close to free in canvas terms. The nf-core/sarek map uses stroke_scale: 1.6:

Automatic section numbers now follow connected visual routes rather than the order sections appear in the file. Numbering prefers the nearest connected section on the current row, keeps parallel branch starts together, completes independent inputs before their merge, and lets a clear primary row finish before a secondary route that rejoins it. A section-scoped %%metro number: <n> pins a badge explicitly; automatic sections take the lowest numbers not reserved, and duplicate or invalid overrides warn.

A layout or render guard failure normally aborts the render with no output. --permissive (or %%metro permissive: true) downgrades every guard failure to a labelled warning block on stderr and renders best-effort on whatever geometry the engine computed, so you can see what went wrong instead of guessing from a traceback. It overrides --strict. The browser playground always renders permissively, showing the best-effort map and the error banner together.

  • render takes several input files. Each writes its own sibling .svg in one process, every file is attempted even if an earlier one fails, and the exit code is non-zero if any failed. -o is rejected with more than one input. render and render-many now share their execution machinery, and render-many accepts the full render option set.
  • Honest handling of imperfect input. A file with no stations is refused by name with a typed EmptyGraphError instead of an internal max() error, and validate reports the same condition. Every numeric flag enforces the bounds its registry declares (--font-scale 0, --x-spacing 0 and --width 0 used to render) and refuses non-finite values. Warnings are collected and printed as one labelled block on stderr rather than as raw UserWarning lines. NF_METRO_DEBUG=1 re-raises on every rejection path, batch runs included.
  • --theme takes the same names as %%metro style:: nfcore, seqera, their -light and -dark variants, the transparent light embed theme and the legacy dark alias. serve and serve-multi take the same choice list and no longer fall back silently on an unknown name.
  • convert reports the feedback edges it removes. nf-metro needs a DAG, so convert and render --from-nextflow break cycles, but they used to do it silently. They now warn with every removed connection and append a %% comment block listing them to the converted .mmd.
  • Any unexpected exception in a single-file render surfaces as a clean error message rather than a traceback.

Two integrations keep a pipeline repository’s rendered SVG in sync with its .mmd source. Both are pinned to the nf-metro release they ship with, so @2.0.0 renders with nf-metro==2.0.0.

A composite GitHub Action:

- uses: actions/checkout@v4
- uses: seqeralabs/nf-metro@2.0.0
id: metro
with:
input: assets/metro_map.mmd
theme: nfcore
# steps.metro.outputs.output-path - where the SVG was written
# steps.metro.outputs.changed - 'true' when it differs from the committed SVG

And a pre-commit hook:

repos:
- repo: https://github.com/seqeralabs/nf-metro
rev: 2.0.0
hooks:
- id: nf-metro

See CI & automation for the inputs and defaults.

Most of the code change in this release is the completion of the route-planning programme begun in 1.0.0. Inter-section routing used to have two places that could choose geometry: a planner that described a route, and a first-match dispatcher that production then sent edges through, with compatibility repairs reconciling the two afterwards. That dispatcher and its repair passes are retired. Normal inter-section routing is now one pipeline: planning classifies each edge once, one typed owner plans each route system’s geometry (exit-bundle turns, perpendicular entries and exits, fans, convergence systems, bypasses), shared corridors and lane spacing settle against a frozen reservation ledger before the geometry freezes, and emission draws what was recorded. If no owner can supply complete geometry the render stops with a diagnostic rather than falling back.

Visible consequences:

  • Every inter-section turn is a formed curve at its full radius. Turns that used to pinch into a near-right-angle where runway was short now get the runway reserved for them at plan time.
  • Corridors are reserved, not discovered. Cross-row descents, bypass bands and merge-feeder channels claim their width up front, so lines sharing a band nest at the bundle pitch instead of colliding or drifting out of band, and a discretionary trunk reorder can no longer resize its own corridor.
  • Convergence systems decide one shared channel per route system, so every merge feeder lands on the trunk it converges onto, and an adjacent feeder with a clear path reaches the merge directly.
  • Symmetric fans are symmetric. A trunkless symmetric fan centres its entry port and reconvergence join, a fork hub sits on its join hub’s centreline, a diamond’s fan-in seats off the join hub rather than off a branch label, and fan placement follows diamond_style.
  • Packed cells and bypasses route around a cell-mate standing on the target entry row, keep a steep multi-line bypass bundle on distinct slots, and minimise bypass lane crossings geometrically.
  • Off-track outputs sit on their own row for a dead-end producer, beside the trunk in TB/BT sections, and clear of the next divergence.
  • Direction inference covers all four orientations. A BT section presents flow-aligned ports, an LR section fed from directly below takes a BOTTOM entry, and a new guard rejects any entry landing that would backtrack through the section’s own stations.

Layout and rendering fixes outside routing proper:

  • Station labels wrap on whitespace, never mid-word with a hyphen, and a re-flowed label re-evaluates its side and anchor for its new shape. A pinned --x-spacing too narrow for the labels now warns instead of mangling words.
  • The canvas grows for ink drawn outside the section-box envelope on the left and top, so a wrap-around route is no longer clipped at the canvas edge.
  • Terminus icon geometry scales with font_scale, multi-line %%metro file: labels render, and the stacked-files icon’s back page faces away from the station marker.
  • Section-level cycles are rejected with a named diagnostic.
  • Text metrics are computed once, deterministically, so a render is byte-identical across runs, platforms and hash seeds.
  • Theme handling for muted elements is consistent: inactive labels, captions, marker outlines and icon labels stay muted under the chrome CSS.
  • The package ships a py.typed marker, and render_string, prepare_graph, render_graph and RenderConfig are importable from the nf_metro root.
  • NfMetroError is a common base for every input-authoring error render_string can raise (CyclicGraphError, BackwardFlowError, FoldThresholdError, PhaseInvariantError and the rest), so an embedder catches one type. Each keeps its previous base class, so existing except ValueError clauses still work. The contract is enumerated in the docstrings and in Embedding.
  • The live progress server’s state snapshot has a versioned JSON Schema and a normative description in the manifest docs, so a consumer with its own task state needs only the manifest and state schema, not the weblog server.
  • The theme constants are named by brand and mode. NFCORE_THEME and SEQERA_THEME are removed; use NFCORE_DARK_THEME and SEQERA_DARK_THEME. The Python modules are not covered by the semver promise, but this is the one rename an embedder is likely to notice.

Directive-authored text is escaped at every SVG and HTML injection point. A %%metro line: colour or marker: fill like red" onload="alert(1) used to break out of its attribute; a %%metro logo: data: URI reaches xlink:href escaped, and a malformed base64 payload is rejected with a clear error. The interactive HTML output’s driver escapes line colours and labels before inserting them into the DOM, and the JSON embedded in its inline script can no longer break out of the <script> block.

The browser playground is rebuilt as a full-height editor: source controls beside the editor, style, layout and edit controls beside the map, and Pyodide running in a Web Worker so typing stays responsive while a render is in flight. It adds draft recovery, a recent-maps list, file and Nextflow DAG import, source completion, command search, diagnostics, direct map editing, and improved export and share flows. Bug-report links no longer exceed GitHub’s URL limit on large maps.

  • New Theming page explaining how one SVG adapts to a light or dark viewer with light-dark(), and how to reuse the technique outside nf-metro. The README hero is a live demonstration of it.
  • New CI & automation page for the action and hook above.
  • The CLI reference covers all eleven commands and every render option, and the guide’s directive reference and precedence table list every directive.
  • The nf-core/riboseq map joins the gallery, and the topology fixture corpus is catalogued in full.
  • Every shipped example renders from any working directory: logo paths resolve relative to the map file, as the guide documents.
  • Gallery and pipeline pages get build-time Open Graph images.
  • Development status is Production/Stable, and the project URLs point at the seqeralabs organisation.
  • New validate extra installs jsonschema for nf-metro validate-svg, which previously had no supported install path.
  • The wheel no longer ships the layout contract document or the candidate-execution harness, neither of which is read at runtime, and the sdist excludes tests, examples, docs and CI material.
  • CI builds the wheel, checks it with twine, installs it into a clean venv and renders an example from outside the checkout. The publish workflow refuses a release tag that does not match the built wheel’s version.