Tutorial
This tutorial is an end-to-end walkthrough of every user-facing feature in pycmplot, working from the same synthetic dataset throughout so that every snippet is directly runnable. It’s longer and more granular than Quickstart — read the quickstart first if you just want to see the shape of the API in a page.
The tutorial is organised as a progression:
Setup: a synthetic dataset — a synthetic sumstats file you can paste-and-run.
The three-step mental model — the three-step load / prep / plot mental model.
Linear Manhattan plots — single- and multi-track linear Manhattan plots.
Circular (Circos) plots — the Circos-style circular layout.
Highlighting — thresholds, colours, and highlight controls.
Automatic gene annotation — automatic nearest-gene labels and the hits table.
QQ plots and compute_pvals — QQ plots and the
compute_pvalsopt-in.Caching and warm resume — the per-track cache and warm-resume behaviour.
The hits overlay TSV — the user-editable hits-overlay TSV.
Per-locus highlight colours — per-locus highlight colours.
Per-locus categories and the custom legend — per-locus categories and the custom legend.
Multi-panel canvas — multiple sumstats groups on one canvas.
Mixed genome builds — mixed genome builds.
GUI editor for the hits overlay — browser-based GUI for editing the hits overlay.
CLI equivalents — CLI equivalents for every step above.
Troubleshooting — troubleshooting and common gotchas.
Setup: a synthetic dataset
Every snippet below assumes the following synthetic sumstats file on disk. Save it once and reuse throughout the tutorial:
import numpy as np
import pandas as pd
rng = np.random.default_rng(0)
n = 200_000
df = pd.DataFrame({
"CHR": rng.choice([str(i) for i in range(1, 23)], size=n),
"BP": rng.integers(1, 200_000_000, size=n),
"SNP": [f"rs{i}" for i in range(n)],
"P": rng.uniform(1e-12, 1.0, size=n),
})
# Plant ~20 genome-wide-significant hits
hits_idx = rng.choice(n, 20, replace=False)
df.loc[hits_idx, "P"] = 10 ** -rng.uniform(9, 15, 20)
df.to_csv("hb.tsv", sep="\t", index=False)
# A second trait, for multi-track examples
df2 = df.sample(frac=1.0, random_state=1).reset_index(drop=True)
df2["P"] = rng.uniform(1e-12, 1.0, size=n)
df2.loc[rng.choice(n, 15, replace=False), "P"] = \
10 ** -rng.uniform(9, 15, 15)
df2.to_csv("mcv.tsv", sep="\t", index=False)
The three-step mental model
pycmplot separates loading from rendering so that expensive per-file work (I/O, liftover, lead extraction, hits-table construction) happens exactly once even when you produce multiple plot types (linear + circular + QQ) from the same data. Every workflow in this tutorial uses the same three steps:
prep()— resolve column names and delimiters for each input file.load()— load, trim, lift over (if needed), extract leads, build the hits table. Returns abundledict.A plotter (
linear(),circular(), or one of the QQ plotters) — consumes fields frombundle.
Here is the canonical shape:
from pycmplot import (
prep,
load,
)
files, labels = ["hb.tsv"], ["Hb"]
file_info = prep(sum_stats=files, labels=labels)
bundle = load(
sum_stats=files, labels=labels, file_info=file_info,
logp=True, trim_pval=0.01, signif_threshold=5e-8,
)
# bundle has: 'dfs', 'sectors', 'annot', 'lines', 'pvals'
print(list(bundle))
Linear Manhattan plots
Single-track
from pycmplot import linear
linear(
sumstats_loaded=bundle["dfs"],
signif_lines=bundle["lines"],
hits_table=bundle["annot"],
logp=True,
colors=["steelblue", "silver"],
plot_title="Hb",
output_dir="./out", output_format="png", dpi=300,
)
Multi-track
Passing more than one sumstats file stacks the tracks vertically, one axes per file, sharing the chromosomal x-axis:
files, labels = ["hb.tsv", "mcv.tsv"], ["Hb", "MCV"]
file_info = prep(sum_stats=files, labels=labels)
bundle = load(
sum_stats=files, labels=labels, file_info=file_info,
logp=True, trim_pval=0.01, signif_threshold=5e-8,
)
linear(
sumstats_loaded=bundle["dfs"],
signif_lines=bundle["lines"],
hits_table=bundle["annot"],
logp=True,
colors=["steelblue", "silver"],
output_dir="./out",
)
Colour scheme is applied per chromosome (alternating), not per track, matching classic Manhattan convention.
Circular (Circos) plots
Same bundle, different plotter — plus one extra input,
sector_sizes, which controls the sector layout:
from pycmplot import circular
circular(
sumstats_loaded=bundle["dfs"],
sector_sizes=bundle["sectors"],
signif_lines=bundle["lines"],
hits_table=bundle["annot"],
logp=True,
colors=["steelblue", "silver"],
plot_title="RBC Traits",
output_dir="./out",
)
If you plan to render both linear and circular from the same data,
call the loader once and pass the same bundle to both — the
loader is the expensive step.
Highlighting
Set highlight=True at both load and plot time to colour signals
above a threshold:
bundle = load(
sum_stats=files, labels=labels, file_info=file_info,
logp=True, trim_pval=0.01,
highlight=True, # extract hits during load
highlight_thresh=5e-8, # p-value cutoff
signif_threshold=5e-8,
)
linear(
sumstats_loaded=bundle["dfs"],
hits_table=bundle["annot"],
signif_lines=bundle["lines"],
highlight=True, # render coloured
highlight_color="brown", # default colour
logp=True, output_dir="./out",
)
The default highlight_color applies to every hit unless a specific
locus has been given a custom colour in the hits overlay
(see Per-locus highlight colours).
Automatic gene annotation
Add annotate="GENE" to label each lead SNP with its nearest gene:
linear(
sumstats_loaded=bundle["dfs"],
hits_table=bundle["annot"],
signif_lines=bundle["lines"],
highlight=True, logp=True,
annotate="GENE", label_col="top_gene",
output_dir="./out",
)
The hits table produced by the loader (bundle["annot"]) is a
tidy summary you can inspect or export directly:
print(bundle["annot"].head())
# CHR POS SNP P LABEL top_gene source
# 1 14822344 rs123 3.4e-11 Hb FTO auto
# ...
Columns you’ll see include CHR, POS, SNP, P, LABEL
(track name), top_gene, plus the overlay columns
source, highlight_color, and category (introduced by the
overlay system — see The hits overlay TSV).
Distance semantics
Every distance-based field on the hits table
(nearest_gene_distance, upstream_distance,
downstream_distance, and the numeric part of the intergenic
top_gene label) is computed against the near edge of the gene
body, not the TSS. Concretely:
nearest_upstream_gene— the closest gene whoseEND < POS, measured asPOS − END(the SNP’s distance to the gene’s right edge, which for a left-flanker is the edge facing the SNP).nearest_downstream_gene— the closest gene whoseSTART > POS, measured asSTART − POS.nearest_gene— the closest gene bymin(|POS − START|, |POS − END|), regardless of which side of the SNP it sits on;0when the SNP falls inside a gene body (genic = True).
This convention is deliberately strand-blind. It matches what
bedtools closest, VEP’s “nearest gene” annotation, and ANNOVAR’s
gene_dist column report, so pycmplot’s numbers line up directly
with those tools. A gene whose body extends toward the SNP wins its
side even when a more compact gene sits closer to the SNP’s
mid-point — the near-edge rule is what makes the answer independent
of gene length.
The only field where strand still matters is
promoter_upstream_flag, which uses a 2 kb window
5’ of each gene’s TSS ([START − 2 kb, START) for + strand,
(END, END + 2 kb] for − strand). That’s genuinely a
biological concept and would be misleading if computed positionally,
so it stays strand-aware.
See the manuscript’s annotation_schematic.pdf for a visual
walkthrough of the near-edge rule under different gene layouts.
In one paragraph
Left/right flanker selection is a pure coordinate comparison: a gene
enters nearest_upstream_gene iff its body ends at a lower
coordinate than the SNP (END < POS, so the whole body sits on
the numerically-lower side) and nearest_downstream_gene iff it
starts at a higher one (START > POS); on each side the winner is
the gene whose near edge (END for the left flanker, START for
the right) minimises the base-pair gap to the SNP. This is
strictly orientational — a statement about where the gene body
sits on the coordinate axis relative to the SNP — and does not
reference the gene’s strand; two genes with identical coordinates
but opposite strands would be classified identically. The one
place strand is retained is
promoter_upstream_flag, which is set when the SNP falls in
the 2 kb window immediately 5’ of any gene’s TSS —
[START − 2 kb, START) for + strand genes and
(END, END + 2 kb] for − strand genes. “Promoter” is a
genuinely biological concept defined relative to transcription
direction, so it is the only field where strand information matters.
QQ plots and compute_pvals
Important
As of pycmplot 0.4.0, the loader does not materialise the full
sorted p-value array by default. Pass compute_pvals=True if
you plan to make a QQ plot:
from pycmplot import qq_combined, qq_overlay, qq_separate
bundle = load(
sum_stats=files, labels=labels, file_info=file_info,
logp=True, trim_pval=0.01,
compute_pvals=True, # <-- opt in
)
qq_combined(
pval_dict=bundle["pvals"],
thin=True, max_points=50_000,
ncols=2, title="RBC",
output_path="./out/rbc_qq", fig_format="png",
)
qq_overlay(
pval_dict=bundle["pvals"],
thin=True, max_points=50_000,
title="RBC", output_path="./out/rbc_qq_overlay",
)
qq_separate(
pval_dict=bundle["pvals"], base_name="RBC",
thin=True, max_points=50_000,
output_path="./out/rbc_qq",
)
Every QQ plotter draws the 95% CI band around the diagonal and annotates each track with its genomic inflation factor λ. The CI band is not in the legend — it’s visually self-evident, and removing it from the legend keeps the top corner uncluttered.
Caching and warm resume
Loading is the expensive step (I/O + trim + liftover + lead
extraction). Turn on caching and every re-run of the same
(files, parameters) combination completes in milliseconds:
bundle = load(
sum_stats=files, labels=labels, file_info=file_info,
logp=True, trim_pval=0.01, highlight=True,
cache=True, # enable
cache_dir="./.pycmplot", # where to store artefacts
resume=True, # reuse existing cache entries
)
How keying works. Each track gets a cache entry keyed on
SHA-256(raw_file_sha256 + cache_version + Stage-1 params). If any
of those change — you re-genotype and re-run, or you bump
highlight_thresh — the affected tracks silently regenerate and
the rest are reused. There are no stale-cache accidents.
Cache layout (rooted at cache_dir):
.pycmplot/
├── tracks/
│ ├── Hb.<key>.parquet # loaded rows
│ ├── Hb.<key>.leads.parquet # per-track hits
│ ├── Hb.<key>.pvals.npz # sorted float32 pvals (only if compute_pvals=True)
│ └── Hb.<key>.meta.json
└── annotations/
└── hits.<group_key>.tsv # merged hits overlay (see next section)
Disabling resume. resume=False forces regeneration but still
writes the fresh entries to disk (useful for benchmarking or after
you’ve bumped CACHE_VERSION upstream).
Clearing the cache. Delete cache_dir on the filesystem, or
use pycmplot --clear_cache --cache_dir ./.pycmplot from the CLI.
The hits overlay TSV
When caching is enabled, the loader writes the hits table to a group-scoped TSV that you’re expected to hand-edit. The path is:
<cache_dir>/annotations/hits.<group_key>.tsv
where group_key is a short SHA-256 of the sorted list of per-track
cache keys — so distinct (files, parameters) combinations get
distinct overlay files and never clobber each other, even on the same
canvas.
Every row starts with source='auto' (populated from the sumstats)
and three overlay columns you can edit:
# pycmplot hits overlay
# Edit any row's highlight_color / category to customise the plot.
# source=user rows are added by you; source=auto rows come from the loader.
CHR POS SNP P LABEL top_gene source highlight_color category
1 14822344 rs123 3.4e-11 Hb FTO auto auto significant
2 91344001 rs456 2.1e-10 Hb MYADM auto auto significant
...
You can:
Edit ``highlight_color`` to give any locus its own colour (see Per-locus highlight colours).
Edit ``category`` to give any locus a legend label (see Per-locus categories and the custom legend).
Add rows with ``source=user`` to force annotation of loci that didn’t make the automatic cutoff.
Persistence across cache regenerations. When Stage-1 parameters
change and auto rows are re-derived, your custom values are inherited
by (CHR, POS) lookup — you don’t have to also change
source='auto' to user to keep your edits.
Example batch edit hits.<group_key>.tsv files using awk in commandline:
cachedir=/your/cachedir
for i in ${cachedir}/annotations/hits.*.tsv; do
awk '
OFS="\t"
{
if($1 ~ /^#/) {print $0}
else{
if($1 ~ /^source/) {print $0}
else{
if($5 >= 5e-08) {$26="orange"; $27="marginally significant (P < 1e-07)"}
else {$27="significant (P < 5e-08)"} {print $0}
}
}
}' ${i} > ${i}.bak
mv ${i}.bak ${i}
done
This highlights all signals with
P < 5e-08with the defaulbrowncolor and all signals
not reaching the genome-wide significance threshold but have P < 1e-07 with orange.
It updates the categories for both to
significant (P < 5e-08)andmarginally significant (P < 1e-07)
respectively. This would be used to add a custom legend to the Manhattan plots, making it self explanatory.
Per-locus highlight colours
Set the highlight_color column on any row to a matplotlib-parseable
colour (name, #rrggbb hex, or an (r, g, b) tuple):
CHR POS ... source highlight_color category
1 14822344 ... auto red significant
2 91344001 ... auto #00cc44 significant
3 50000000 ... auto auto significant # falls back to default
The sentinel auto (or blank / NaN / invalid) falls back to the
plot-time highlight_color argument. Invalid colours emit a
warning naming the offending value so typos are easy to fix.
Matching is done by nearest-lead within 500 kb on the same chromosome, so re-running with a different thinning or trim never loses your colour choices.
Per-locus categories and the custom legend
The category column lets you group loci in the legend:
CHR POS ... highlight_color category
1 14822344 ... red novel
2 91344001 ... #00cc44 replicated
3 50000000 ... auto significant
Both the linear and circular plotters render a “Highlighted
Categories” legend with one entry per unique category in
first-appearance order — reorder your rows in the TSV to reorder the
legend. When everything is left at defaults (all rows
category=significant and highlight_color=auto), no legend
is added, preserving the pre-feature layout.
Same-category-different-colour is deduplicated: the first colour wins and a warning names the loser so you can fix the conflict.
Missing columns (e.g. loading a legacy cache) are tolerated — the helper simply returns no legend entries in that case.
Multi-panel canvas
To place multiple groups of sumstats on the same figure — each
group is its own stacked linear (or circular) sub-plot — pass an
explicit matplotlib Axes (or SubFigure) via ax=:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(14, 8), constrained_layout=True)
sub_top, sub_bot = fig.subfigures(2, 1)
for sub, group_files, group_labels in [
(sub_top, ["hb.tsv"], ["Hb"]),
(sub_bot, ["mcv.tsv"], ["MCV"]),
]:
fi = prep(
sum_stats=group_files, labels=group_labels,
)
b = load(
sum_stats=group_files, labels=group_labels, file_info=fi,
logp=True, highlight=True, cache=True, cache_dir="./.pycmplot",
)
linear(
sumstats_loaded=b["dfs"],
hits_table=b["annot"], signif_lines=b["lines"],
logp=True, highlight=True,
ax=sub, # <-- render into this subfigure
)
fig.savefig("./out/two_panel.png", dpi=300)
Every cache file, hits overlay, per-locus colour, and category legend is group-scoped, so two panels with different sumstats never clobber each other.
Mixed genome builds
If your files were generated on different reference panels, pycmplot
can lift over hg18 and hg19 coordinates to hg38 before plotting.
Supply the builds either through a BUILD column in the files or
by passing build_list= (Python) / --build (CLI):
bundle = load(
sum_stats=["study_hg18.tsv", "study_hg19.tsv", "study_hg38.tsv"],
labels=["A", "B", "C"],
build_list=["hg18", "hg19", "hg38"],
logp=True, trim_pval=0.01,
file_info=prep(
sum_stats=["study_hg18.tsv", "study_hg19.tsv", "study_hg38.tsv"],
labels=["A", "B", "C"],
),
)
Liftover is cached alongside the loaded rows, so subsequent runs
(with cache=True) skip it entirely.
GUI editor for the hits overlay
If hand-editing the hits.<group>.tsv in a text editor feels
awkward — especially picking colours by typing hex codes — pycmplot
ships an optional browser-based editor. Install the extra and launch
it against your cache directory:
pip install "pycmplot[editor]"
pycmplot edit --cache_dir ./.pycmplot_cache
That opens a local Streamlit app (default http://localhost:8501)
with a spreadsheet-style view of the overlay. Highlights of the UI:
source,highlight_color, andcategoryare rendered as typed columns —sourceis a dropdown ofauto/user;categoryis a selectbox pre-populated with every category already in use (type a new value to add it).Rows can be added or deleted inline for
source='user'loci that didn’t make the automatic cutoff.A “Colour preview” strip below the table shows each row as a labelled swatch — valid colours render at their true colour;
autorenders as a dashed grey chip; invalid values render red so typos are impossible to miss.Save writes through the same
write_hits_overlay()the loader uses, so atomic writes and inheritance-across-regenerations behave identically to the text-editor workflow.Preview plot re-renders a linear Manhattan against the cached tracks and the in-memory overlay, so you can see colour / category changes reflected before saving.
Discard & reload drops unsaved changes and re-reads the TSV.
When the cache directory contains multiple hits.<group>.tsv files
(different (files, parameters) combinations sharing one
cache_dir), the editor prints the available group keys and asks
you to re-launch with --group <key>. Full flag list:
pycmplot edit --cache_dir PATH [--group KEY] [--host HOST] [--port PORT] [--tui]
The extra is opt-in so headless / CI pipelines don’t pay the Streamlit install cost.
Terminal-UI backend for cluster sessions
Many clusters (secured HPC facilities, hospital compute, etc.) don’t allow port forwarding or run headless compute nodes without a graphical display — the browser editor is unusable there. For those cases, pycmplot ships a terminal UI built on Textual that renders in any ANSI terminal and needs nothing more than a plain SSH session:
pip install "pycmplot[editor-tui]"
pycmplot edit --cache_dir ./.pycmplot_cache --tui
The TUI has feature parity with the browser backend, adjusted for keyboard-only ergonomics:
Spreadsheet-style
DataTable— arrow keys / Home / End / PgUp / PgDn to navigate; F2 or Enter to open the cell-edit modal.The modal opens with the current value pre-selected so typing replaces; a live colour swatch renders the resolved colour in truecolor if your terminal supports it, and invalid values render with a red frame so typos are caught before save.
Category cells show existing labels as a hint line — copy-paste to reuse or type a new value to add.
Footer bindings: Ctrl+S save, Ctrl+R reload (prompts if dirty), Ctrl+N add
source=userrow, Ctrl+D delete row, Ctrl+P render a preview PNG to<cache_dir>/preview.png(most terminals can’t render images inline), Ctrl+Q quit (prompts if dirty).Save delegates to the same
write_hits_overlay()the browser backend uses, so atomic-write + inheritance guarantees hold identically.
The two backends are independent extras — install just the one you
need, or both. A vanilla pip install pycmplot still pulls
neither.
Filter, multi-select, and batch edit (TUI)
For overlays with dozens to hundreds of loci, single-cell editing is tedious. The TUI adds a filter + multi-select + batch-edit flow:
Press
/to open a filter modal. Type any pandasquery()expression —P < 5e-8,category == "significant",CHR == "6" and BP.between(28e6, 34e6). The grid re-renders showing only matching rows;Escclears the filter (and any selection) in one keystroke.Press
Spaceon a row to toggle its selection (visible as a green ● in the leftmost ✓ column). Selection is tied to the original dataframe index so it survives filtering and refresh.Press
Ctrl+Ato select every row currently visible under the filter./ P < 5e-8thenCtrl+Ais the standard “select every genome-wide hit” move.Press
Ctrl+Eto open the batch-edit modal. Toggle between thehighlight_colorandcategorycolumns, type one value, and it’s applied to every selected row. The colour swatch preview and validation are identical to the single-cell modal. When no rows are explicitly selected, batch-edit falls back to “the current filtered view” — so/ … Ctrl+Eis a two-step batch flow.
Batch CLI (pycmplot hits)
For pipelines, Makefiles, and reproducible analysis notebooks, a
scripted “colour every genome-wide-significant novel hit red” step
belongs in code rather than a GUI. The pycmplot hits
subcommand exposes the same filter grammar in a batch CLI:
# Preview what would change
pycmplot hits set \
--cache_dir ./.pycmplot_cache \
--where 'P < 5e-8' \
--color '#00cc44' --category "genome-wide" \
--dry-run
# Actually apply
pycmplot hits set \
--cache_dir ./.pycmplot_cache \
--where 'P < 5e-8' \
--color '#00cc44' --category "genome-wide"
# Inspect the overlay
pycmplot hits list \
--cache_dir ./.pycmplot_cache \
--where 'category == "novel"' \
--columns CHR,POS,SNP,P,category
# Pipe TSV output to awk / cut / etc.
pycmplot hits list --cache_dir ./.pycmplot_cache --tsv \
| awk -F'\\t' 'NR > 1 && $4 < 5e-8 {print $3}'
--where uses pandas’ query() grammar —
identical to the TUI’s filter, so users learn one syntax. Invalid
colours are rejected pre-write; a bad --where expression prints
the offending part rather than a pandas traceback. --dry-run
prints the N rows that would change and exits without touching
disk. Writes go through the same write_hits_overlay()
path as every other backend — atomic writes and inheritance
guarantees apply identically.
The CLI needs no extras — no Streamlit, no Textual — just the base package. Ideal for a headless CI step that reproducibly applies a colouring policy without any interactive editing.
CLI equivalents
Everything above has a CLI equivalent. A representative one-liner:
pycmplot \
--sum_stats hb.tsv,mcv.tsv \
--labels Hb,MCV \
--logp --signif_line --highlight \
--annotate GENE \
--trim_pval 0.01 \
--cache --cache_dir ./.pycmplot \
--output_dir ./out
Cache flags (all mirror the Python API):
--cache— enable caching + hits overlay.--cache_dir PATH— where to store artefacts (default./.pycmplot).--no_resume— regenerate everything but still write fresh entries.--clear_cache— delete the cache tree and exit.-qq / --qq_plot— impliescompute_pvals=Trueunder the hood.-V / --version— print version and exit.
See Command-Line Interface for the full reference.
Troubleshooting
- QQ plot fails with
bundle['pvals']beingNone You forgot to pass
compute_pvals=Trueto the loader (see QQ plots and compute_pvals). The default flipped fromTruetoFalsein 0.4.0 to avoid the ~80 MB-per-track memory cost when you’re not making QQ plots.- Cache says HIT for track A but MISS for track B on a warm re-run
Almost always means Stage-1 parameters differ between the two tracks (e.g.
signif_thresholdwas auto-computed for one but explicitly set for the other). Explicitly pass every Stage-1 parameter you care about to lock in reproducibility.- Hex colours in my TSV are being truncated / dropped
Don’t use
pd.read_csv(comment='#')to inspect the overlay — it treats#mid-cell as a comment marker and eats hex-color values. The pycmplot reader strips only leading#header lines; if you inspect the file yourself, strip leading#lines manually and then read as a plain TSV.- Highlight legend didn’t appear
build_highlight_legend_entries()returns an empty list when everything is at defaults (category=significantfor all rows ANDhighlight_color=autofor all rows) — the plotter then skips the legend to preserve the pre-feature layout. Edit at least one row’scategoryorhighlight_colorto trigger it.- “Category X appears with multiple colours” warning
You set two loci to the same
categorybut gave them differenthighlight_colorvalues. The first colour wins in the legend; the warning names the value that lost so you can fix the TSV.- Multi-panel run overwrote another panel’s hits overlay
This shouldn’t happen from 0.4.0 onward — each
(files, parameters)group gets its ownhits.<group_key>.tsv. If you see it, check that you’re really passing distinctsum_statslists to each panel’s loader call.- Hits overlay TSV got corrupted (crash mid-save, truncated line, etc.)
The loader logs
"Hits overlay unreadable (…); ignoring."and regenerates the hits table from the cached per-track leads (<label>.<key>.leads.parquet) — no raw sumstats reload, no per-track cache invalidation. The plot renders fine. User edits in the corrupted file are not recovered, though: anysource=userrows you added,highlight_coloroverrides, andcategorylabels need to be re-applied in the freshly written TSV. If you make heavy manual edits, keep a copy of the overlay TSV under version control alongside your analysis scripts.- Legend text overlaps with Annotations
The legend text font size inherits from the track label size (
track_label_size). If you find that legend text and annotations are cluttering, try redusing the size of track labels and/or the annotation size.
Next steps
Command-Line Interface — full CLI reference.
API Reference — complete Python API reference.
Python API Tutorial — an executable Jupyter walkthrough.
Changelog — what changed in each release.