Skip to main content

metroplot

tests python license built on matplotlib PyPI version PyPI Downloads

metroplot turns bioinformatics pipelines into subway-style diagrams. You place stations on a grid, connect them with coloured lines, and metroplot handles the rest: parallel-track offsets when lines share a segment, right-angle routing, station circles, and labels.

metroplot animated example


Install

pip install metroplot

The only hard dependency is matplotlib.


Quick start

from metroplot import Diagram
import matplotlib.pyplot as plt

d = Diagram()
d.station("raw",  0, 0, "FASTQ",       "RAW READS")
d.station("qc",   2, 0, "FastQC",      "QUALITY CTRL")
d.station("trim", 4, 0, "Trim Galore", "TRIMMING")
d.station("star", 6, 0, "STAR",        "ALIGNMENT")
d.station("de",   8, 0, "DESeq2",      "DIFF EXPR")

d.line("RNA-seq", "#e8614a", [["raw", "qc", "trim", "star", "de"]])

ax = d.render()
plt.savefig("pipeline.png", bbox_inches="tight")

How it works

There are three building blocks:

Stations are the tools or steps in your pipeline. Each station sits at an (x, y) coordinate on an integer grid. You choose the layout — metroplot does not auto-arrange stations.

d.station("star", 6, 0, "STAR", "ALIGNMENT")
#          key    x  y   label   sub-label

Lines are the coloured tracks connecting stations. A line owns one or more routes (lists of station keys). When a route passes through two stations that aren't on the same row or column, metroplot draws a right-angle bend automatically.

d.line("RNA-seq", "#e8614a", [["raw", "qc", "trim", "star", "de"]])
#       name       colour      routes (list of lists)

Shared segments are handled automatically. When two lines pass through the same pair of stations, they are drawn as side-by-side parallel tracks with a small perpendicular offset — no extra configuration needed.


Edge labels

Label a segment with the data format flowing through it. The track is interrupted at the midpoint; the text matches the line colour and sits at the same visual weight as the track width.

d.line("Bulk RNA-seq", "#1f2a44", [["fastq", "star", "fcounts", "deseq2"]], edge_labels={
    ("fastq",   "star"):    "fastq",
    ("star",    "fcounts"): "BAM",
    ("fcounts", "deseq2"):  "counts",
})

Rule: if a segment is shared by multiple lines, annotate it on all of them or on none. Mixing leaves some parallel tracks labelled and others silent, which looks inconsistent.


Themes

Three built-in themes ship with metroplot. All use a transparent background so diagrams embed cleanly into any document, slide, or web page without a white box around them.

Theme Best for
"light" (default) Papers, reports, light-mode web
"dark" Dark slides or dark-mode docs — adds coloured station rings and a track glow
"minimal" Publications — thinner strokes, no inner dots
d = Diagram(theme="dark")

Pick colours from the built-in palette, or use any hex code:

from metroplot.themes import PALETTES, LOGO_ORANGE

RED, BLUE, TEAL, NAVY = PALETTES["default"]
ORANGE = LOGO_ORANGE

d.line("Bulk RNA-seq", NAVY, ...)
d.line("scRNA-seq",    ORANGE, ...)

Define a custom theme by subclassing or replacing an existing one:

from metroplot.themes import Theme, THEMES

THEMES["mylab"] = Theme(
    name="mylab",
    label_color="#2a2a2a",
    sub_color="#666666",
    palette=["#e63946", "#457b9d", "#a8dadc", "#1d3557"],
)
d = Diagram(theme="mylab")

Animated SVG

Pass animate=True to save_svg to inject metro carts that travel along every line:

d.save_svg("pipeline.svg", animate=True)

The carts follow the exact rendered track positions, including lateral offsets on shared segments.


Layout patterns

metroplot diagrams are hand-laid on a grid, so the visual structure is entirely up to you. The five patterns below cover most pipeline shapes.


Linear

The simplest layout — one line of stations going left to right.

linear layout

d = Diagram()
d.station("raw",   0, 0, "FASTQ",         "RAW READS")
d.station("qc",    2, 0, "FastQC",        "QUALITY CTRL")
d.station("trim",  4, 0, "Trim Galore",   "TRIMMING")
d.station("align", 6, 0, "STAR",          "ALIGNMENT")
d.station("quant", 8, 0, "featureCounts", "QUANTIFICATION")
d.station("de",   10, 0, "DESeq2",        "DIFF EXPR")
d.line("RNA-seq", "#e8614a", [["raw", "qc", "trim", "align", "quant", "de"]])

Parallel lanes

A shared preprocessing trunk forks into independent assay-specific lanes. Each lane sits at a different y-coordinate; stations on the shared trunk appear in every line's route.

parallel lanes layout

# Shared trunk at y=0
d.station("fastq", 0, 0, ...)
d.station("trim",  2, 0, ...)

# One y-level per assay
d.station("star", 4,  2, ...)   # Bulk RNA-seq lane
d.station("cr",   4,  0, ...)   # scRNA-seq lane (stays at y=0)
d.station("bw",   4, -2, ...)   # ATAC-seq lane

# Shared stations are listed in every route
d.line("Bulk RNA-seq", NAVY,  [["fastq", "trim", "star", ...]])
d.line("scRNA-seq",    CORAL, [["fastq", "trim", "cr",   ...]])
d.line("ATAC-seq",     BLUE,  [["fastq", "trim", "bw",   ...]])

Wide fan-out

One line with multiple routes, all originating from a shared hub. Use bend="vh" so each branch first drops to its target row and then extends horizontally — this creates the characteristic vertical trunk with outward branches.

wide fan-out layout

d = Diagram(auto_bend=False)
d.station("input", 0, 0, "FASTQ", "RAW READS")
d.station("hub",   2, 0, "fastp", "TRIMMING")

branches = [
    ("rna",   "Bulk RNA-seq", "DEG ANALYSIS",  2.5),
    ("scrna", "scRNA-seq",    "CELL ATLAS",    1.5),
    ("atac",  "ATAC-seq",     "CHROMATIN",     0.5),
    ("chip",  "ChIP-seq",     "BINDING SITES",-0.5),
    ("meth",  "RRBS",         "METHYLATION",  -1.5),
    ("hic",   "Hi-C",         "3D GENOME",    -2.5),
]
routes = []
for name, label, sub, y in branches:
    d.station(name, 6, y, label, sub, "right")
    routes.append(["input", "hub", name])

d.line("Multi-omics", "#e8614a", routes, bend="vh")

Loop-back

Lines travel right along a shared trunk, bend at a common step, and return left on separate rows — one row per downstream branch. This fits pipelines where multiple analyses diverge from a single expensive step such as alignment.

loop-back layout

# Shared trunk at y=0 — going right
d.station("raw",   0, 0, "FASTQ",       "RAW READS")
d.station("trim",  3, 0, "Trim Galore", "TRIMMING")
d.station("star",  6, 0, "STAR",        "ALIGNMENT")  # bend point

# Each branch returns left on its own row
d.station("rmats", 6, 2, "rMATS", "ALT SPLICING")     # y=2 branch
d.station("maser", 3, 2, "MASER", "SPLICING VIZ")

d.station("quant", 6, 4, "featureCounts", "QUANTIFY") # y=4 branch
d.station("de",    3, 4, "DESeq2",        "DIFF EXPR")
d.station("enrich",0, 4, "clusterProfiler","ENRICHMENT")

d.line("Alt Splicing", TEAL,  [["raw", "trim", "star", "rmats", "maser"]])
d.line("Bulk RNA-seq", CORAL, [["raw", "trim", "star", "quant", "de", "enrich"]])

Serpentine

A long linear pipeline that snakes back and forth across multiple rows. Place the turnaround stations at the same x-coordinate on adjacent rows so metroplot draws a clean vertical connector — no L-bend configuration needed.

serpentine layout

d = Diagram()
# Row 0 — left → right (y=0)
d.station("s00", 0, 0, "FASTQ",         "RAW READS")
d.station("s01", 3, 0, "Trim Galore",   "TRIMMING")
d.station("s02", 6, 0, "STAR",          "ALIGNMENT")
d.station("s03", 9, 0, "UMI-tools",     "DEDUP")       # right turn-around

# Row 1 — right → left (y=2); s03 and s10 share x=9 → vertical connector
d.station("s10", 9, 2, "featureCounts", "QUANTIFY")
d.station("s11", 6, 2, "DESeq2",        "DIFF EXPR")
d.station("s12", 3, 2, "fgsea",         "ENRICHMENT")
d.station("s13", 0, 2, "clusterProfiler","PATHWAYS")   # left turn-around

# Row 2 — left → right (y=4); s13 and s20 share x=0 → vertical connector
d.station("s20", 0, 4, "MultiQC",       "QC REPORT")
d.station("s21", 3, 4, "Volcano",       "DE PLOTS")
d.station("s22", 6, 4, "Heatmap",       "EXPRESSION")
d.station("s23", 9, 4, "Quarto",        "FINAL REPORT")

d.line("RNA-seq", "#e8614a", [[
    "s00","s01","s02","s03",   # row 0 →
    "s10","s11","s12","s13",   # row 1 ←
    "s20","s21","s22","s23",   # row 2 →
]])

Tuning

Pass any of these to Diagram(...) to control the visual output:

Parameter Default Description
line_width 4.0 Track stroke width in points
track_spacing 0.09 Gap between parallel tracks on a shared segment, in data units
station_radius 0.21 Station circle radius in data units
station_linewidth 2.5 Station circle border width in points
corner_radius 0.20 Rounding radius for L-bend corners, in data units
label_font 9 Font size for station main labels, in points
sub_font 7 Font size for station sub-labels, in points
legend_loc None Legend position, e.g. "lower right" — omit to hide
auto_bend True Automatically choose hv/vh bend to avoid overlapping stations
theme "light" "light", "dark", "minimal", or a custom Theme object

Testing

pip install -e ".[dev]"
pytest

CI runs the suite on Python 3.10, 3.11, and 3.12 (see .github/workflows/test.yml).


License

MIT.

Release files for metroplot 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for metroplot 0.3.1
File Size Uploaded
metroplot-0.3.1.tar.gz 34.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for metroplot 0.3.1
File Interpreter ABI Platform
metroplot-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 65.0 kB

Release files / metroplot-0.3.1.tar.gz

Download URL metroplot-0.3.1.tar.gz
Size 34.7 kB
Tags Source
SHA-256 checksum
How to use checksums
be71abf32e3cbfb7384280287e4d07235e183ae9b9d2a9f632d602155ef6ec1f
BLAKE2b-256 checksum
How to use checksums
c394d615261b1368c730a723d51e7d6954f47ad913409be8ab4cd834acfc54c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release files / metroplot-0.3.1-py3-none-any.whl

Download URL metroplot-0.3.1-py3-none-any.whl
Size 30.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe3b2328fd012744bb5ea1766becd42e22b6ea25d2916c9c5a559ab1fb5b6443
BLAKE2b-256 checksum
How to use checksums
94b68ae51ddb3b23e8a328b98a3083e31f04d1b0dbc40d159842a28985046910
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.15

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page