Pangenome (pggb)
2 videos in this tutorial
The tutorials target the JBrowse v5 beta, and the v4.3.0 release
on the download page lacks some of what they show. To install the
beta of JBrowse Web, run npm install -g @jbrowse/cli@next, then
jbrowse create jbrowse2 --branch v5.0.0-beta.9. Desktop beta builds are coming soon.
We build a five-strain E. coli pangenome graph with pggb, then load what it produces in JBrowse: the graph's linear projections as ordinary tracks on the K12 genome, and the graph itself as a track you browse by locus. We:
- run pggb over five RefSeq genomes to build the graph
- draw its synteny, pangenome variants, whole-genome alignment and depth projections on K12's coordinates
- browse the graph itself in the graph genome view, and jump from one of its segments into the strain that carries it
The graph view is a beta plugin, and this tutorial covers experimental ideas. We welcome your feedback.
Prerequisites
dockerorsingularity, for the pggb imagesamtoolsbedGraphToBigWig(UCSC kentUtils)python3- htslib (
bgzip,tabix) node, for the JBrowse CLI- the NCBI
datasetsCLI unzip- the GraphGenomeView plugin, for the graph itself
On Debian/Ubuntu, apt install samtools tabix unzip python3 covers four of
those. Docker installs from
docs.docker.com; the NCBI datasets
CLI and bedGraphToBigWig are each a
single-binary download; and node
comes from nodejs.org.
Where the data comes from
Five E. coli RefSeq assemblies, fetched by accession with the NCBI datasets CLI and concatenated into one PanSN-named FASTA for pggb.
- K12: https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/000/005/845/GCF_000005845.2_ASM584v2/
- Sakai: https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/000/008/865/GCF_000008865.2_ASM886v2/
- CFT073: https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/000/007/445/GCF_000007445.1_ASM744v1/
- NCTC86: https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/002/007/705/GCF_002007705.1_ASM200770v1/
- IAI39: https://ftp.ncbi.nlm.nih.gov/genomes/all/GCF/000/026/345/GCF_000026345.1_ASM2634v1/
- nanopore reads from an unrelated isolate, E. coli E146: https://www.ebi.ac.uk/ena/browser/view/DRR193901
- the pggb and minigraph graphs' segments, links and bubbles, tabix-indexed and rehosted: https://jbrowse.org/demos/ecoli_pangenome/
The linear projections
A pangenome graph collapses many genomes into one structure. Shared sequence is a single path every sample walks, and the path branches where samples differ. pggb, Minigraph-Cactus and progressiveCactus build these graphs, and odgi manipulates them.
The graph's linear projections flatten it onto one reference genome's coordinates. Every builder emits them:
| Projection | What it shows | From the graph | JBrowse track |
|---|---|---|---|
| Synteny | The blocks each pair of genomes shares | odgi untangle, halSynteny | synteny track |
| Pangenome variants | Every difference the graph calls, across all samples | pggb -V, cactus-pangenome --vcf, vg deconstruct | multi-sample variant track |
| Whole-genome alignment | The multiple alignment, column by column | pggb -M, hal2maf | User guide: MAF track |
| Pangenome depth | How many genomes cover each reference base (core/accessory) | odgi depth, odgi pav | quantitative track |
Building the graph with pggb
pggb takes one FASTA of all the genomes,
PanSN-named
sample#haplotype#contig. Concatenate the five strains (haplotype 1, these
being haploid) and index the result, chromosomes only:
for strain in K12 Sakai CFT073 NCTC86 IAI39; do
awk -v s="$strain" '/^>/{print ">" s "#1#chr"; next} {print}' "$strain.fa"
done > all.fa
bgzip all.fa
samtools faidx all.fa.gzThe image pins all five tools at once and carries
odgi, so wrap the docker run once.
-V K12:10000 decomposes the graph into a VCF against the K12 path, and -M
writes the MAF:
in_pggb() {
docker run --rm -u "$(id -u):$(id -g)" -w /data -v "$PWD":/data \
ghcr.io/pangenome/pggb:202603141454453ade6b "$@"
}
in_pggb pggb -i /data/all.fa.gz -o /data/pggb \
-n 5 -c 4 -p 90 -s 5000 -V K12:10000 -M -t "$(nproc)"-n is the number of haplotypes, -p the minimum alignment identity and -s
the segment length; -p 90 -s 5000 suits a bacterial pangenome, and -c
(mappings kept per segment) should rise alongside -n. Under singularity,
singularity exec --bind "$PWD":/data --pwd /data docker://<image> replaces the
wrapper body.
Resolve the graph's two spellings once, since the glob has to expand on the host:
gfa=$(ls pggb/*.smooth.final.gfa)
og=$(ls pggb/*.smooth.final.og)Synteny projection
Two files answer this, a track each.
The alignment the graph was induced from
pggb's first step is a wfmash all-vs-all PAF, the same input the
all-vs-all synteny tutorial loads. Index it
with jbrowse make-pif and load it with an
MultiGenomeIndexedPAFAdapter:
cp pggb/*.alignments.wfmash.paf ecoli_pggb_ava.paf
jbrowse make-pif ecoli_pggb_ava.paf # -> ecoli_pggb_ava.pif.gz (+ .tbi)Goes in the tracks array of config.json. See Tracks.
{
"type": "SyntenyTrack",
"trackId": "ecoli_pggb_ava",
"name": "pggb graph: all-vs-all synteny (wfmash)",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"adapter": {
"type": "MultiGenomeIndexedPAFAdapter",
"uri": "ecoli_pggb_ava.pif.gz",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"]
}
}jbrowse add-track ecoli_pggb_ava.pif.gz \
--trackId ecoli_pggb_ava \
--name "pggb graph: all-vs-all synteny (wfmash)" \
--assemblyNames K12,Sakai,CFT073,NCTC86,IAI39 \
--adapterType MultiGenomeIndexedPAFAdapter \
--load copyIn JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "SyntenyTrack",
"trackId": "ecoli_pggb_ava",
"name": "pggb graph: all-vs-all synteny (wfmash)",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"adapter": {
"type": "MultiGenomeIndexedPAFAdapter",
"uri": "ecoli_pggb_ava.pif.gz",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"]
}
}ecoli_pggb_ava.pif.gz is relative to a config.json. Replace it with its URL or its path on this computer.
That shows it in the linear view. For the synteny view, open Add → Linear synteny view, pick the track under Quick start, and click Launch.
Stack the five strains in a linear synteny view as the
all-vs-all tutorial
describes; the PanSN sample# prefix maps each record to its strain.
The projection from odgi untangle
odgi untangle
walks each query path and reports which stretch of the reference path it
traverses, so it reads homology the way the graph resolved it:
printf 'K12#1#chr\n' > target.txt
printf 'Sakai#1#chr\nCFT073#1#chr\nNCTC86#1#chr\nIAI39#1#chr\n' > query.txt
# -m merges short runs into the previous segment; -j is the minimum jaccard
# -e forces a boundary every N bp; omit it on graphs with many haplotypes
# -p asks for PAF, so make-pif reads it directly
in_pggb odgi untangle -i "/data/$og" \
-R /data/target.txt -Q /data/query.txt -m 1000 -j 0.5 -e 5000 -p -t "$(nproc)" \
> ecoli_pggb_untangle.paf
jbrowse make-pif ecoli_pggb_untangle.pafLoad it as a separate SyntenyTrack:
Goes in the tracks array of config.json. See Tracks.
{
"type": "SyntenyTrack",
"trackId": "ecoli_pggb_untangle",
"name": "pggb graph: synteny from the graph (odgi untangle)",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"adapter": {
"type": "MultiGenomeIndexedPAFAdapter",
"uri": "ecoli_pggb_untangle.pif.gz",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"]
}
}jbrowse add-track ecoli_pggb_untangle.pif.gz \
--trackId ecoli_pggb_untangle \
--name "pggb graph: synteny from the graph (odgi untangle)" \
--assemblyNames K12,Sakai,CFT073,NCTC86,IAI39 \
--adapterType MultiGenomeIndexedPAFAdapter \
--load copyIn JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "SyntenyTrack",
"trackId": "ecoli_pggb_untangle",
"name": "pggb graph: synteny from the graph (odgi untangle)",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"adapter": {
"type": "MultiGenomeIndexedPAFAdapter",
"uri": "ecoli_pggb_untangle.pif.gz",
"assemblyNames": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"]
}
}ecoli_pggb_untangle.pif.gz is relative to a config.json. Replace it with its URL or its path on this computer.
That shows it in the linear view. For the synteny view, open Add → Linear synteny view, pick the track under Quick start, and click Launch.
Put the reference between the strains you want to compare.
The wfmash alignment and the untangle projection differ at a repeat, a reference span more than one segment of the same query lands on:
gzip -dc ecoli_pggb_untangle.pif.gz | awk -F'\t' 'substr($1,1,1)=="q"' \
| cut -f1,3,4,8,9 | sort -k4,4nTwo K12 spans come back where Sakai, NCTC86 and IAI39 land twice and CFT073 once, a repeat a pairwise PAF collapses to one.
One lane per strain, on the K12 axis
The untangle PAF, drawn as a multi-row feature track, puts every strain on the reference at once, orientation read down each row.
untangle_to_bed.py projects the PAF onto the per-strain BED schema
build_minigraph_paths.sh
defines, so rows and the colors carry across:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/untangle_to_bed.py
python3 untangle_to_bed.py ecoli_pggb_untangle.paf chr > ecoli_pggb_untangle_rows.bed
# the header line is `#`-prefixed; sort-bed keeps it on top (sort -k1,1 -k2,2n)
jbrowse sort-bed ecoli_pggb_untangle_rows.bed | bgzip > ecoli_pggb_untangle_rows.bed.gz
tabix -p bed ecoli_pggb_untangle_rows.bed.gzLoad the result as a FeatureTrack with a LinearMultiRowFeatureDisplay:
Goes in the tracks array of config.json. See Tracks.
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_untangle_rows",
"name": "pggb graph: untangle per strain (orientation, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "BedTabixAdapter",
"uri": "ecoli_pggb_untangle_rows.bed.gz"
},
"displays": [
{
"type": "LinearMultiRowFeatureDisplay",
"rows": {
"field": "strain",
"domain": ["Sakai", "CFT073", "NCTC86", "IAI39"]
},
"color": {
"scale": "identity",
"domain": ["rgb(153,153,153)", "rgb(214,39,40)"],
"labels": ["Same orientation as K12", "Inverted"]
}
}
]
}jbrowse add-track-json '{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_untangle_rows",
"name": "pggb graph: untangle per strain (orientation, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "BedTabixAdapter",
"uri": "ecoli_pggb_untangle_rows.bed.gz"
},
"displays": [
{
"type": "LinearMultiRowFeatureDisplay",
"rows": {
"field": "strain",
"domain": ["Sakai", "CFT073", "NCTC86", "IAI39"]
},
"color": {
"scale": "identity",
"domain": ["rgb(153,153,153)", "rgb(214,39,40)"],
"labels": ["Same orientation as K12", "Inverted"]
}
}
]
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_untangle_rows",
"name": "pggb graph: untangle per strain (orientation, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "BedTabixAdapter",
"uri": "ecoli_pggb_untangle_rows.bed.gz"
},
"displays": [
{
"type": "LinearMultiRowFeatureDisplay",
"rows": {
"field": "strain",
"domain": ["Sakai", "CFT073", "NCTC86", "IAI39"]
},
"color": {
"scale": "identity",
"domain": ["rgb(153,153,153)", "rgb(214,39,40)"],
"labels": ["Same orientation as K12", "Inverted"]
}
}
]
}ecoli_pggb_untangle_rows.bed.gz is relative to a config.json. Replace it with its URL or its path on this computer.
selfCov in the popup goes above 1 where a segment repeats within the same
strain; jexl:feature.selfCov>1 in Edit filters cuts the lane to those
repeats.
Pangenome variants projection
pggb -V writes a VCF of every variant the graph decomposes against the K12
path, genotyped across the other four strains. Its CHROM is the PanSN path
(K12#1#chr), so rename it to the assembly's refName (chr) with bcftools:
printf 'K12#1#chr\tchr\n' > rename_chrs.tsv
in_pggb bash -c "bcftools annotate --rename-chrs /data/rename_chrs.tsv \
/data/pggb/*.smooth.final.K12.decomposed.vcf \
| bcftools sort -Oz -o /data/ecoli_pggb.vcf.gz && tabix -p vcf /data/ecoli_pggb.vcf.gz"Load it as a VariantTrack on K12 with the
multi-sample display, one row per sample:
Goes in the tracks array of config.json. See Tracks.
{
"type": "VariantTrack",
"trackId": "ecoli_pggb_variants",
"name": "pggb graph: pangenome variants (vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "VcfTabixAdapter",
"uri": "ecoli_pggb.vcf.gz"
},
"displays": [{ "type": "LinearMultiSampleVariantDisplay" }]
}jbrowse add-track-json '{
"type": "VariantTrack",
"trackId": "ecoli_pggb_variants",
"name": "pggb graph: pangenome variants (vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "VcfTabixAdapter",
"uri": "ecoli_pggb.vcf.gz"
},
"displays": [{ "type": "LinearMultiSampleVariantDisplay" }]
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "VariantTrack",
"trackId": "ecoli_pggb_variants",
"name": "pggb graph: pangenome variants (vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "VcfTabixAdapter",
"uri": "ecoli_pggb.vcf.gz"
},
"displays": [{ "type": "LinearMultiSampleVariantDisplay" }]
}ecoli_pggb.vcf.gz is relative to a config.json. Replace it with its URL or its path on this computer.
The variant calls, stacked over the MAF alignment, sit over the alignment they were decomposed from.
Why the reference path takes a length
With a length, pggb runs vcfbub
-l 0 -a LEN piped into vcfwave,
decomposing any site whose alleles run past LEN into the primitive variants
*.decomposed.vcf carries.
Keep the raw file, similarly renamed, as a second track:
in_pggb bash -c "bcftools annotate --rename-chrs /data/rename_chrs.tsv \
/data/pggb/*.smooth.final.K12.vcf \
| bcftools sort -Oz -o /data/ecoli_pggb_snarls.vcf.gz && tabix -p vcf /data/ecoli_pggb_snarls.vcf.gz"LV/PS give the snarl tree. Filter on LV in Edit filters to pick one
level. The coarse tier below is built from the LV=0 records.
Whole-genome alignment (MAF) projection
pggb -M writes the multiple alignment as a MAF, read as a
Config guide: MAF track. Its blocks are smoothxg's POA blocks,
with two consequences.
Block order. Re-root every block on K12, drop blocks that lack it, and
rename the PanSN names to sample.chr:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/reroot_maf.py
# keeps K12-containing blocks, puts K12 first, sorts by K12 position, and
# splits a repeat-collapsed block's second K12 row into its own block
python3 reroot_maf.py pggb/*.smooth.maf ecoli_pggb.maf K12#1#chrBlock padding. A smoothxg bug leaves block padding on some rows;
reroot_maf.py crops around it.
Then convert the MAF to the tabix-indexed BED the
MafTabixAdapter reads, with maf_to_bed.py,
which takes row 0 as the reference:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/maf_to_bed.py
python3 maf_to_bed.py ecoli_pggb.maf ecoli_pggb.maf.bed
bgzip ecoli_pggb.maf.bed
tabix -p bed ecoli_pggb.maf.bed.gzFor row order,
odgi similarity
reports how much of the graph each pair shares; UPGMA over
1 - estimated.identity gives the Newick read as nhLocation:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/odgi_similarity_to_newick.py
in_pggb odgi similarity -i "/data/$og" -D '#' -p 1 > ecoli_pggb_similarity.tsv
python3 odgi_similarity_to_newick.py ecoli_pggb_similarity.tsv ecoli_pggb.nhGoes in the tracks array of config.json. See Tracks.
{
"type": "MafTrack",
"trackId": "ecoli_pggb_maf",
"name": "pggb graph: whole-genome alignment (MAF, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MafTabixAdapter",
"samples": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"nhLocation": { "uri": "ecoli_pggb.nh" },
"uri": "ecoli_pggb.maf.bed.gz"
}
}jbrowse add-track-json '{
"type": "MafTrack",
"trackId": "ecoli_pggb_maf",
"name": "pggb graph: whole-genome alignment (MAF, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MafTabixAdapter",
"samples": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"nhLocation": { "uri": "ecoli_pggb.nh" },
"uri": "ecoli_pggb.maf.bed.gz"
}
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "MafTrack",
"trackId": "ecoli_pggb_maf",
"name": "pggb graph: whole-genome alignment (MAF, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MafTabixAdapter",
"samples": ["K12", "Sakai", "CFT073", "NCTC86", "IAI39"],
"nhLocation": { "uri": "ecoli_pggb.nh" },
"uri": "ecoli_pggb.maf.bed.gz"
}
}ecoli_pggb.nh, ecoli_pggb.maf.bed.gz are relative to a config.json. Replace each with its URL or its path on this computer.
The track colors each variant-lane cell by that strain's genotype:
- grey where the strain matches K12
- blue where it carries the alternate allele
- olive where the site is uncalled
- a numbered purple box for an insertion, the number giving its length in bases beyond K12
An insertion consumes no reference, so the record spans one base and the marker carries its length.
Drag across the rows and the menu that opens on release lists each strain the selection covers under two submenus:
- Open aligned genome at the matching region opens that strain's own genome at the aligned stretch
- Linear synteny view, K12 vs... opens the two as a linear synteny view
Pangenome depth projection (core vs accessory)
odgi depth
counts how many paths traverse each reference base, near the strain count over
core sequence and toward 1 over K12-private sequence:
reflen=$(awk -v p="K12#1#chr" '$1 == p {print $2}' all.fa.gz.fai)
awk -v p="K12#1#chr" -v len="$reflen" -v w=500 \
'BEGIN { for (s = 0; s < len; s += w) { e = s + w; if (e > len) e = len
print p "\t" s "\t" e } }' > depth_windows.bed
# -b gives one row per window, so window size sets the curve's resolution;
# the awk drops the PanSN prefix for K12's plain refName
in_pggb odgi depth -i "/data/$og" -b /data/depth_windows.bed |
awk -v p="K12#1#chr" -v OFS='\t' '$1 == p && $4 + 0 == $4 { print "chr", $2, $3, $4 }' |
sort -k1,1 -k2,2n > ecoli_pggb_depth.bedgraph
printf 'chr\t%s\n' "$reflen" > chrom.sizes
bedGraphToBigWig ecoli_pggb_depth.bedgraph chrom.sizes ecoli_pggb_depth.bwchrom.sizes is written by hand, since the .fai carries the PanSN path.
Load it as a QuantitativeTrack on
K12:
Goes in the tracks array of config.json. See Tracks.
{
"type": "QuantitativeTrack",
"trackId": "ecoli_pggb_depth",
"name": "pggb graph: pangenome depth (paths over K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "BigWigAdapter",
"uri": "ecoli_pggb_depth.bw"
}
}jbrowse add-track ecoli_pggb_depth.bw \
--trackId ecoli_pggb_depth \
--name "pggb graph: pangenome depth (paths over K12)" \
--assemblyNames K12 \
--load copyIn JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "QuantitativeTrack",
"trackId": "ecoli_pggb_depth",
"name": "pggb graph: pangenome depth (paths over K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "BigWigAdapter",
"uri": "ecoli_pggb_depth.bw"
}
}ecoli_pggb_depth.bw is relative to a config.json. Replace it with its URL or its path on this computer.
Zoomed out, the track shows the pangenome's core/accessory landscape:
- a plateau near the strain count
- spikes past it over the rRNA operons the graph collapses into one copy
- troughs at 1 over K12's private sequence
The depth lane is drawn at the end of this section, under the per-strain rows.
An unrelated isolate's long reads show the same thing without the graph:
nanopore reads from E. coli E146, mapped straight onto K12 with
minimap2 -ax map-ont. Reads long enough to cross the element carry it as a
single labelled deletion.
The Minigraph-Cactus tutorial draws this window from its own graph's MAF, breaking at the same prophage edges, and plots both graphs' curves over one of those operons.
Per-strain presence
odgi pav
splits depth per strain: it reports the fraction of each window that strain's
path traverses. Load the set as one
MultiQuantitativeTrack:
in_pggb odgi pav -i "/data/$og" -b /data/depth_windows.bed > pav.tsv
# K12 omitted: it is present over every window by construction
for strain in Sakai CFT073 NCTC86 IAI39; do
# column 5 is the PanSN path, column 6 the presence fraction
awk -F'\t' -v OFS='\t' -v g="${strain}#1#chr" \
'$5 == g && $6 + 0 == $6 { print "chr", $2, $3, $6 }' pav.tsv |
sort -k1,1 -k2,2n > "pav_${strain}.bedgraph"
bedGraphToBigWig "pav_${strain}.bedgraph" chrom.sizes "ecoli_pggb_pav_${strain}.bw"
doneGoes in the tracks array of config.json. See Tracks.
{
"type": "MultiQuantitativeTrack",
"trackId": "ecoli_pggb_pav",
"name": "pggb graph: per-strain presence (odgi pav, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "Sakai",
"uri": "ecoli_pggb_pav_Sakai.bw"
},
{
"type": "BigWigAdapter",
"name": "CFT073",
"uri": "ecoli_pggb_pav_CFT073.bw"
},
{
"type": "BigWigAdapter",
"name": "NCTC86",
"uri": "ecoli_pggb_pav_NCTC86.bw"
},
{
"type": "BigWigAdapter",
"name": "IAI39",
"uri": "ecoli_pggb_pav_IAI39.bw"
}
]
}
}jbrowse add-track-json '{
"type": "MultiQuantitativeTrack",
"trackId": "ecoli_pggb_pav",
"name": "pggb graph: per-strain presence (odgi pav, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "Sakai",
"uri": "ecoli_pggb_pav_Sakai.bw"
},
{
"type": "BigWigAdapter",
"name": "CFT073",
"uri": "ecoli_pggb_pav_CFT073.bw"
},
{
"type": "BigWigAdapter",
"name": "NCTC86",
"uri": "ecoli_pggb_pav_NCTC86.bw"
},
{
"type": "BigWigAdapter",
"name": "IAI39",
"uri": "ecoli_pggb_pav_IAI39.bw"
}
]
}
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "MultiQuantitativeTrack",
"trackId": "ecoli_pggb_pav",
"name": "pggb graph: per-strain presence (odgi pav, vs K12)",
"assemblyNames": ["K12"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "Sakai",
"uri": "ecoli_pggb_pav_Sakai.bw"
},
{
"type": "BigWigAdapter",
"name": "CFT073",
"uri": "ecoli_pggb_pav_CFT073.bw"
},
{
"type": "BigWigAdapter",
"name": "NCTC86",
"uri": "ecoli_pggb_pav_NCTC86.bw"
},
{
"type": "BigWigAdapter",
"name": "IAI39",
"uri": "ecoli_pggb_pav_IAI39.bw"
}
]
}
}ecoli_pggb_pav_Sakai.bw, ecoli_pggb_pav_CFT073.bw, ecoli_pggb_pav_NCTC86.bw, ecoli_pggb_pav_IAI39.bw are relative to a config.json. Replace each with its URL or its path on this computer.
Under the curve, these rows show which strain each dip is missing: one row goes white over ybaL, and a second over the rhsD Rhs element.
Compared to odgi viz
pggb rendered the graph in 1D with
odgi viz
(*.viz_*.png) and in 2D with
odgi layout
(*.lay.draw.png). The figure below is odgi viz re-run.
The JBrowse projections below keep one row per strain, redrawn on K12's coordinates; the Minigraph-Cactus page marks the same 100 kb on both axes.
Drawing the graph as a graph
JBrowse can also draw the graph as a graph, through the graph genome view plugin; Pangenome (hosting your own graph) covers installing it and the one command that indexes a graph.
Browsing the whole graph by locus
Walking a P line in step order gives every segment an interval on that path,
coordinates a plain GFA does not carry.
build_pangenome_graph.sh
does that walk and takes the graph's bubbles from the raw snarl VCF kept
above:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_pangenome_graph.sh
bash build_pangenome_graph.sh "$gfa" ecoli_pggb --reference K12 --snarls ecoli_pggb_snarls.vcf.gzThe script writes the tabix-indexed segments, links, a coarse tier, and an
ecoli_pggb.config.json. The uris below are our hosted copy; a local build
uses ecoli_pggb instead:
Goes in the tracks array of config.json. See Tracks.
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_segments",
"name": "pggb graph segments (whole graph, by locus)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb",
"coarse": {
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50",
"aboveBpPerPx": 1
}
},
"displayDefaults": { "showLabels": "none" },
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_segments-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_segments-LinearBasicDisplay"
}
]
}jbrowse add-track-json '{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_segments",
"name": "pggb graph segments (whole graph, by locus)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb",
"coarse": {
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50",
"aboveBpPerPx": 1
}
},
"displayDefaults": { "showLabels": "none" },
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_segments-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_segments-LinearBasicDisplay"
}
]
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_segments",
"name": "pggb graph segments (whole graph, by locus)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb",
"coarse": {
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50",
"aboveBpPerPx": 1
}
},
"displayDefaults": { "showLabels": "none" },
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_segments-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_segments-LinearBasicDisplay"
}
]
}A segment's name is its GFA id, cut every ~17 bp, so showLabels is off here
and below.
Turned on, the track draws the window as a graph on K12's coordinates; Display types → Feature display draws it as a lane. A ruler drag offers Launch → Linear synteny view; the same menu on a synteny row's scale bar lets Replace current view re-anchor the stack.
Adding the track itself, from an empty session, is Open track... → Add pangenome graph track:
In the force layout, a node's length is proportional to its sequence, so one long arm can swallow the drawing. Bubble spread → Compress lengths pulls the longest and shortest nodes towards the mean.
One node per bubble, when the window is wider than the graph can draw
The segments draw one node per GFA segment, about 17 bp each; the coarse tier draws one node per bubble instead, with the invariant reference as backbone. Its 50 bp threshold absorbs single-base alternatives and keeps every indel. The reference-position ramp colors agreeing stretches and paints charcoal where the strains differ.
The graph track names the tier coarse, handed over via aboveBpPerPx. Type
chr:1,250,000-1,350,000, past one bp per pixel; the tier also loads as a
separate track, whose Display types → Feature display draws it as a lane at
any width:
Goes in the tracks array of config.json. See Tracks.
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_tier50",
"name": "pggb graph bubbles (coarse tier, one node per bubble)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50"
},
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_tier50-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_tier50-LinearBasicDisplay"
}
]
}jbrowse add-track-json '{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_tier50",
"name": "pggb graph bubbles (coarse tier, one node per bubble)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50"
},
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_tier50-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_tier50-LinearBasicDisplay"
}
]
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_tier50",
"name": "pggb graph bubbles (coarse tier, one node per bubble)",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb.tier50"
},
"displays": [
{
"type": "LinearGraphDisplay",
"displayId": "ecoli_pggb_tier50-LinearGraphDisplay"
},
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_tier50-LinearBasicDisplay"
}
]
}Hover a node for the segments it collapsed. A tier node's Open in K12 takes the view to the span it stands for, and the graph track cuts the segments there.
Layout → Sample rows gives each strain a separate row: carriage on this
graph, build order (minigraph's SR) on an rGFA.
Sample rows reads each segment individually. Type chr:1,004,500-1,004,961; a
row's bar is drawn over the reference it replaces, so CFT073's row draws one
long bar labelled 7 kb del for a 75 bp segment.
In Sample rows the top row is K12's backbone; each strain's marks below are the segments it takes instead, in the MAF's row order.
Layout → Force-directed layout redraws the same nodes by their shape, with no reference axis, until Layout → Sample rows puts them back on K12's:
Who carries a segment
Clicking a node opens its details, including carriedBy: every haplotype
walking that segment, recorded as an SM:Z: tag.
Carriage as a linear lane
The SM:Z: tag reaches the segments track as attributes: samples is the
haplotype list and carriers its length, which the color paints by:
Goes in the tracks array of config.json. See Tracks.
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_carriage",
"name": "pggb graph: segment carriage",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb"
},
"displays": [
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_carriage-LinearBasicDisplay",
"displayMode": "collapsed",
"showLabels": false,
"color": {
"field": "carriers",
"domain": ["5", "4", "3", "2", "1"],
"range": ["#bdbdbd", "#fed976", "#feb24c", "#fd8d3c", "#e31a1c"],
"labels": [
"All 5 strains (core)",
"4 strains",
"3 strains",
"2 strains",
"1 strain (private)"
],
"title": "Strains carrying"
}
}
]
}jbrowse add-track-json '{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_carriage",
"name": "pggb graph: segment carriage",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb"
},
"displays": [
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_carriage-LinearBasicDisplay",
"displayMode": "collapsed",
"showLabels": false,
"color": {
"field": "carriers",
"domain": ["5", "4", "3", "2", "1"],
"range": ["#bdbdbd", "#fed976", "#feb24c", "#fd8d3c", "#e31a1c"],
"labels": [
"All 5 strains (core)",
"4 strains",
"3 strains",
"2 strains",
"1 strain (private)"
],
"title": "Strains carrying"
}
}
]
}'In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "FeatureTrack",
"trackId": "ecoli_pggb_carriage",
"name": "pggb graph: segment carriage",
"assemblyNames": ["K12"],
"adapter": {
"type": "RgfaTabixAdapter",
"uri": "https://jbrowse.org/demos/ecoli_pangenome/ecoli_pggb"
},
"displays": [
{
"type": "LinearBasicDisplay",
"displayId": "ecoli_pggb_carriage-LinearBasicDisplay",
"displayMode": "collapsed",
"showLabels": false,
"color": {
"field": "carriers",
"domain": ["5", "4", "3", "2", "1"],
"range": ["#bdbdbd", "#fed976", "#feb24c", "#fd8d3c", "#e31a1c"],
"labels": [
"All 5 strains (core)",
"4 strains",
"3 strains",
"2 strains",
"1 strain (private)"
],
"title": "Strains carrying"
}
}
]
}This lane is one box per segment, unlike the depth track's windowed mean.
Opening a node in the strain that carries it
A segment the reference never visits sits on the strain's own coordinates.
Right-click the 75 bp CFT073 segment and pick Open in CFT073: it opens
CFT073 at 1,048,515 with its gene track.
K12 carries several genes in the span; CFT073 runs ssuE straight into pyrD with none between.
The graph genome view guide covers the rest of the node's menu.
Limits of the locus index
Three limits on browsing a base-level graph by locus:
- The index is offline. It is rebuilt whenever the graph changes.
- Index size tracks total sequence. Prefer the SV-resolution minigraph graph for whole-genome browsing on a human pangenome, as the HPRC tutorial does.
- A cut of segments covers a small window. The track declines past its node budget; a wider window is the tier's.
The build script also runs minigraph -cxggs and
indexes its rGFA the same way, so the figure below puts one locus through both
graphs.
Browse the rGFA whole-genome, and open the pggb graph for every base.
A window as a file
With no index, Add → Graph genome view takes a GFA by file or URL. Three odgi commands cut one:
-E is the aggressive option; -c/-d expand by a bounded number of steps or
bp instead:
in_pggb bash -c "odgi extract -i /data/$og -r K12#1#chr:1004500-1004900 -E -o - \
| odgi sort -i - -o - -O \
| odgi view -i - -g" > ecoli_pggb_subgraph.gfaA plain GFA marks no path as the reference, so pick one under View menu → Settings → Reference path.
The same odgi extract/sort/view walk, run outside the browser, puts those nodes on a linear track:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/gfa_nodes_to_bed.py
python3 gfa_nodes_to_bed.py ecoli_pggb_subgraph.gfa K12#1#chr chr \
| sort -k1,1 -k2,2n | bgzip > ecoli_pggb_subgraph_nodes.bed.gz
tabix -p bed ecoli_pggb_subgraph_nodes.bed.gzThe BED's itemRgb is the view's own viridis Depth ramp, needing no color
configuration.
A cut graph has ends: the ringed node marks where CFT073 rejoins after
the same deletion, one end open since its
far link falls outside. -c 1 fetches only that anchor, skipping the nodes
between.
The same deletion is four nodes in the minigraph graph:
tabix https://jbrowse.org/demos/ecoli_pangenome/ecoli_minigraph.links.bed.gz \
'K12#1#chr:997000-1005000's378 → s379 → s380 is K12 through the deletion; s378 → s2025 → s380 is
CFT073 around it.
-d is the answer at a collapsed repeat, where -E walks out to every copy: at
the 16S rRNA gene rrsB, -d 500 keeps the cut to a handful of segments.
in_pggb bash -c "odgi extract -i /data/$og -r K12#1#chr:4166800-4167300 -d 500 -o - \
| odgi sort -i - -o - -O \
| odgi view -i - -g" > ecoli_pggb_rrna.gfaodgi paths -L lists two copies each in Sakai, CFT073, NCTC86 and IAI39 and one
in K12: the same collapse the depth curve reads as a spike and
untangle draws in coordinate space.
Drawing the haplotype paths
A P line is a path through the graph; a W line is the same in GFA 1.1's walk syntax. View menu → Settings → Draw paths splits every node into one lane per path. Set Color to Grey first.
Neither an rGFA nor a subgraph cut from the tabix index carries P or W lines. The file route keeps them: cut the IS5 bubble the coarse tier arrows as a file.
in_pggb bash -c "odgi extract -i /data/$og -r K12#1#chr:1299400-1300800 -E -o - \
| odgi sort -i - -o - -O \
| odgi view -i - -g" > ecoli_pggb_is5.gfaThe gene lane names the element, insH21, the IS5 transposase, and the loop in the drawing below is its node.
With the paths in the file, the view derives the bubble under Mark bubbles; the Walk dropdown lifts one strain's path out of the drawing.
Reproduce it end to end
build_ecoli_pangenome_graph.sh
runs everything above in one shot, fetching the helper scripts it needs:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_ecoli_pangenome_graph.sh
bash build_ecoli_pangenome_graph.sh # builds ./ecoli_pangenome_graph_build/jbrowse2
npx --yes serve ecoli_pangenome_graph_build/jbrowse2In one run it:
- downloads the RefSeq genomes and runs pggb
- converts the projections above from the wfmash PAF,
odgi untangle, both VCF tiers, the MAF,odgi similarity,odgi depthandodgi pav - downloads JBrowse and writes a
config.jsonwith the assemblies, gene tracks, graph-derived tracks, and a default session - writes the
odgi vizraster, every cut GFA this page opens as a file, and the rGFA tabix indexes behind the segments track
The config.json declares the graph genome view plugin; force the container
runtime with CONTAINER=singularity.
JBrowse Desktop opens the folder's config.json by
path, so npx serve is only for the web build.
Adding genomes to the strain table is the only edit an expanded pangenome needs; wfmash mapping cost grows with the square of the genome count.
See also
- Graph genome view
- Pangenome (Minigraph-Cactus)
- Pangenome (HPRC): browsing the graph
- Synteny visualization (all-vs-all minimap2)
- Synteny from gene symbols (44 E. coli genomes)
- User guide: MAF track
- Multi-sample variant display
- PIF (Pairwise Indexed Format)
- pggb
- odgi
Feedback on this tutorial is welcome: contact us.