Subway-style pipeline diagrams for matplotlib
Project description
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.
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.
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.
# 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.
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.
# 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.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
943290ee72471eb1115593a43de7a91916845b2278a9afa7733bc9b471bc604e
|
|
| MD5 |
bf0f590d37fafa09f4a7f84e3733f602
|
|
| BLAKE2b-256 |
8210b7c8d6eb7aa1b16bb1fddb42516faf338175ff9ac671ff7c8d1d9a073bad
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6f16954c76ff9a9ba102567035b1ca5c7cdf042c686ee8157b28d9578d61fbc
|
|
| MD5 |
d615d11506d32ab3ec63ce24d36f9f02
|
|
| BLAKE2b-256 |
4658d993d9ddd802ef47222eded03503049e2d1137077938dab84107c019505c
|