Skip to main content

Subway-style pipeline diagrams for matplotlib

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.0.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.0-py3-none-any.whl (30.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: metroplot-0.3.0.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.0.tar.gz
Algorithm Hash digest
SHA256 943290ee72471eb1115593a43de7a91916845b2278a9afa7733bc9b471bc604e
MD5 bf0f590d37fafa09f4a7f84e3733f602
BLAKE2b-256 8210b7c8d6eb7aa1b16bb1fddb42516faf338175ff9ac671ff7c8d1d9a073bad

See more details on using hashes here.

File details

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

File metadata

  • Download URL: metroplot-0.3.0-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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c6f16954c76ff9a9ba102567035b1ca5c7cdf042c686ee8157b28d9578d61fbc
MD5 d615d11506d32ab3ec63ce24d36f9f02
BLAKE2b-256 4658d993d9ddd802ef47222eded03503049e2d1137077938dab84107c019505c

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