Skip to main content

Subway-style pipeline diagrams

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

metroplot-0.3.1.tar.gz (34.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

metroplot-0.3.1-py3-none-any.whl (30.3 kB view details)

Uploaded Python 3

File details

Details for the file metroplot-0.3.1.tar.gz.

File metadata

  • Download URL: metroplot-0.3.1.tar.gz
  • Upload date:
  • Size: 34.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for metroplot-0.3.1.tar.gz
Algorithm Hash digest
SHA256 be71abf32e3cbfb7384280287e4d07235e183ae9b9d2a9f632d602155ef6ec1f
MD5 bfc075f9a17e2036ad48a646039bc3f8
BLAKE2b-256 c394d615261b1368c730a723d51e7d6954f47ad913409be8ab4cd834acfc54c6

See more details on using hashes here.

File details

Details for the file metroplot-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: metroplot-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 30.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for metroplot-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fe3b2328fd012744bb5ea1766becd42e22b6ea25d2916c9c5a559ab1fb5b6443
MD5 16527b4a644ea80947058aaf61d8eae1
BLAKE2b-256 94b68ae51ddb3b23e8a328b98a3083e31f04d1b0dbc40d159842a28985046910

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page