This tutorial uses a real-world dataset to show how
gggenomes can be used to explore synteny among viral
genomes. We start with six viral genomes and progressively add gene
annotations, terminal repeats, pairwise genome alignments, GC content,
protein clusters, and functional annotations.
Along the way, we introduce the main data types and plotting layers
used by gggenomes and build the final synteny map one step
at a time. The final figure will look like this:

All processed data files are provided in the data/
directory, so you can run the complete R workflow without installing any
external bioinformatics tools. For reproducibility, we also show the
command-line steps used to generate these files, but running them is
optional.
The example is based on endogenous mavirus-like elements (EMALEs) from the marine heterotrophic flagellate Cafeteria burkhardae, described by Hackl et al. 2021.
The data used in this example is also bundled as R objects in the
package; see data(package = "gggenomes").
Download and unpack the tutorial data
To follow the tutorial locally, download and unpack the tutorial data from R:
download.file(
"https://github.com/thackl/gggenomes/releases/download/v1.1.3/vignettes.tar.gz",
"gggenomes-tutorials.tar.gz",
mode = "wb"
)
untar("gggenomes-tutorials.tar.gz", exdir = "gggenomes-tutorials")
setwd("gggenomes-tutorials")You can now copy the R code below into an interactive R session, or
open the included emales.Rmd file and run the tutorial from
there.
Read in the genomes
We start with a FASTA file containing six viral genomes.
read_seqs() reads the sequence lengths and metadata from
the FASTA headers into a table that can be passed directly to
gggenomes.
library(gggenomes)
# Read one row of sequence information per genome
emale_seqs <- read_seqs("data/emales.fna")
# Initialize a plot with one genome ("bin") per sequence
p1 <- gggenomes(seqs = emale_seqs) +
geom_seq() + # draw the sequence backbone
geom_bin_label() # label each genome
p1
ggsave("emales-p1.png", p1, width = 10, height = 3, dpi = 100)
At this stage, the plot contains only the genome sequences and their labels. Next, we add information about the genes encoded by each genome.
Annotate genes
The supplied gene annotations were generated with Prodigal-GV. This preprocessing step is shown for reproducibility; the resulting GFF file is already included with the tutorial data.
read_feats() reads the GFF annotations as genomic
features. Here we color genes by their GC content to add a first layer
of information to the genome map.
emale_genes <- read_feats("data/emales.gff") |>
# GC content is stored as text in the GFF attributes; convert it to numeric
dplyr::mutate(gc_cont = as.numeric(gc_cont))
p2 <- gggenomes(genes = emale_genes, seqs = emale_seqs) +
geom_seq() +
geom_bin_label() +
geom_gene(aes(fill = gc_cont)) +
scale_fill_distiller(palette = "Spectral")
p2
ggsave("emales-p2.png", p2, width = 10, height = 3, dpi = 100)
The genome backbones are now accompanied by gene arrows, with color showing variation in GC content among genes.
Find terminal inverted repeats
These types of viruses often have linear genomes with terminal inverted repeats (TIRs), so we look for matching regions at opposite ends of each genome. The commands below split the genomes and self-align their opposite strands; the resulting PAF file is already provided.
# Split into one genome per file | https://bioinf.shenwei.me/seqkit/
bin/seqkit split --force -i data/emales.fna
# Self-align opposite strands
for fna in `ls data/emales.fna.split/*.fna`; do
bin/minimap2 -c -B5 -O6 -E3 --rev-only $fna $fna > $fna.paf;
done;
cat data/emales.fna.split/*.paf > data/emales-tirs.pafWe read the alignments as features and retain only sufficiently long,
closely matching hits. geom_feat() then adds these regions
to the genome tracks.
# Keep candidate TIRs of at least 100 bp and less than 10% divergence
emale_tirs <- read_links("data/emales-tirs.paf") |>
dplyr::filter(map_length > 99 & de < 0.1)
p3 <- gggenomes(
genes = emale_genes,
seqs = emale_seqs,
feats = emale_tirs
) +
geom_seq() +
geom_bin_label() +
geom_feat(linewidth = 5) +
geom_gene(aes(fill = gc_cont)) +
scale_fill_distiller(palette = "Spectral")
p3
ggsave("emales-p3.png", p3, width = 10, height = 3, dpi = 100)
The thick feature segments mark the candidate terminal repeats, adding structural information beyond the gene annotations.
Compare genome synteny
To compare genome organization, we align all genomes against each
other with minimap2. gggenomes represents these pairwise
alignments as links between homologous regions.
# All-vs-all genome alignment | https://github.com/lh3/minimap2
bin/minimap2 -X -N 50 -p 0.1 -c data/emales.fna data/emales.fna > data/emales.paf
emale_links <- read_paf("data/emales.paf")
p4 <- gggenomes(
genes = emale_genes,
seqs = emale_seqs,
feats = emale_tirs,
links = emale_links
) +
geom_seq() +
geom_bin_label() +
geom_feat(linewidth = 5) +
geom_gene(aes(fill = gc_cont)) +
geom_link() +
scale_fill_distiller(palette = "Spectral")
# Flip genomes 4-6 so that shared regions are easier to compare visually
p4 <- p4 |> flip(4:6)
p4
ggsave("emales-p4.png", p4, width = 10, height = 3, dpi = 100)
The links reveal shared regions and their relative orientation across
genomes. flip() changes only how selected genomes are
displayed, making the syntenic structure easier to follow.
Add genome-wide GC content
So far, GC content is shown only for individual genes. We can also calculate it across the entire genome in fixed windows and display the result as a continuous track.
# Compute GC content in non-overlapping 50 bp windows | https://bioinf.shenwei.me/seqkit/
bin/seqkit sliding -s 50 -W 50 data/emales.fna |
bin/seqkit fx2tab --gc -ni > data/emales-gc.tsvThe output is a simple table of window IDs and GC content. We extract
the sequence ID and coordinates from each window ID to turn it into a
feature table understood by gggenomes.
emale_gc <- readr::read_tsv(
"data/emales-gc.tsv",
col_names = c("window_id", "gc_content")
) |>
# Window IDs have the form "seq_id_sliding:start-end"
dplyr::mutate(
seq_id = stringr::str_remove(window_id, "_sliding:.*"),
start = as.integer(stringr::str_extract(window_id, "(?<=_sliding:)\\d+")),
end = as.integer(stringr::str_extract(window_id, "\\d+$"))
)
# Register the GC windows as an additional feature set
p5 <- p4 |> add_feats(emale_gc)
# Select this feature set for a wiggle track
p5 <- p5 +
geom_wiggle(
aes(z = gc_content, linetype = "GC-content"),
feats(emale_gc),
fill = "blue",
alpha = .5
)
p5
ggsave("emales-p5.png", p5, width = 10, height = 3, dpi = 100)
The GC-content profile is now shown as an additional quantitative track along each genome. This illustrates how arbitrary position-based measurements can be added as feature sets and plotted alongside genes and synteny links.
Cluster proteins into orthogroups
The next step groups similar proteins across genomes. We use DIAMOND to cluster the predicted proteins; the cluster assignments are again included with the tutorial data.
bin/diamond makedb --in data/emales.faa --db data/emales.dmnd
bin/diamond cluster --db data/emales.dmnd -o data/emales-clusters.tsvWe count the number of genes in each cluster and create labels only for clusters represented by at least six genes. These shared clusters can then be used to color homologous genes consistently across genomes.
# Read cluster assignments and count genes per cluster
emale_clusters <- readr::read_tsv(
"data/emales-clusters.tsv",
col_names = c("cluster_id", "feat_id")
) |>
dplyr::group_by(cluster_id) |>
dplyr::add_count(name = "cluster_n") |>
dplyr::ungroup()
emale_clusters <- emale_clusters |>
dplyr::mutate(
cluster_label = paste0(cluster_id, " (", cluster_n, ")"),
# Highlight only clusters with at least six members
cluster_label = forcats::fct_lump_min(
cluster_label, 6, other_level = NA_character_
)
)
# Prodigal-GV uses generic gene IDs in the GFF (1_1, 3_5, ...), whereas
# its protein FASTA uses IDs such as "seq_id_1". Harmonize the IDs so the
# gene annotations can be joined to results from the protein analyses.
emale_genes <- emale_genes |>
dplyr::mutate(
feat_id = paste0(seq_id, "_", stringr::str_remove(feat_id, ".*_"))
)
p6 <- gggenomes(
genes = emale_genes,
seqs = emale_seqs,
feats = emale_tirs,
links = emale_links
) |>
add_feats(emale_gc) |>
# Associate genes through their shared protein-cluster IDs
add_clusters(emale_clusters) |>
flip(4:6) +
geom_wiggle(
aes(z = gc_content, linetype = "GC-content"),
feats(emale_gc),
fill = "black",
alpha = .3
) +
geom_seq() +
geom_bin_label() +
geom_feat(size = 5, data = feats()) +
geom_gene(aes(fill = cluster_label)) +
geom_link()
p6
ggsave("emales-p6.png", p6, width = 10, height = 3.5, dpi = 100)
Genes belonging to the same abundant protein cluster now share a color. This makes conserved gene content visible together with larger-scale genome synteny.
Add functional annotations
Finally, we add functional information based on similarity to proteins from mavirus. DIAMOND is used for the similarity search; the resulting table is included with the tutorial data.
# Search predicted EMALE proteins against published mavirus proteins
bin/diamond makedb --in data/mavirus.faa --db data/mavirus.dmnd
bin/diamond blastp --db data/mavirus.dmnd -q data/emales.faa --very-sensitive --outfmt 6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send evalue bitscore stitle > data/emales_mavirus.o6We extract gene names and product descriptions from significant hits and attach these annotations to the corresponding genes and protein clusters.
# Extract functional annotations from significant mavirus protein hits
emale_blast <- read_blast("data/emales_mavirus.o6") |>
dplyr::rename(feat_id = seq_id, blast_desc = X13) |>
dplyr::filter(evalue < 1e-3) |>
dplyr::mutate(
gene = stringr::str_extract(blast_desc, "(?<=gene=)\\S+"),
product = stringr::str_extract(blast_desc, "(?<=product=).*")
)
# Add functional annotations to the protein-cluster table
emale_clusters_blast <- emale_clusters |>
dplyr::left_join(
dplyr::select(emale_blast, feat_id, gene, product)
)
# Manual transposon annotations can be added as another feature set if desired:
# emale_transposons <- read_gff("emales-manual.gff", types = c("mobile_element"))
p7 <- gggenomes(
genes = emale_genes,
seqs = emale_seqs,
feats = emale_tirs,
links = emale_links
) |>
add_feats(emale_gc) |>
add_clusters(emale_clusters_blast) |>
# add_features(emale_transposons) |>
# Associate BLAST hits with their parent genes
add_subfeats(emale_blast) |>
# Reorder the genomes for the final figure
pick(6, 4, 3, 2, 1, 5) |>
# Use the synteny links to orient genomes consistently
sync() +
geom_seq() +
geom_bin_label() +
# Leave some extra space around genome tracks for the annotations below
geom_link(offset = c(0.3, 0.2)) +
# geom_feat(aes(color = "integrated transposon"),
# feats(emale_transposons), size = 7) +
geom_gene(aes(fill = product)) +
geom_gene_tag(aes(label = gene), size = 3, nudge_y = 0.1) +
# Mark genes with significant mavirus protein hits
geom_feat(
data = feats(emale_blast),
size = 2,
position = position_nudge(y = -.2),
color = "skyblue4"
) +
# Add the genome-wide GC-content track below the genes
geom_wiggle(
aes(z = gc_content, linetype = "GC-content"),
feats(emale_gc),
fill = "lavenderblush4",
position = position_nudge(y = -.2),
height = .2
) +
scale_fill_brewer(
"Conserved genes", palette = "Dark2", na.value = "cornsilk3"
) +
scale_color_viridis_d("Blast hits & Features", direction = -1) +
scale_linetype("Graphs") +
ggtitle(expression(paste(
"Endogenous mavirus-like elements of ",
italic("C. burkhardae")
)))
p7
ggsave("emales-p7.png", p7, width = 12, height = 4, dpi = 200)
The final figure combines genome structure, pairwise synteny, conserved gene content, functional annotations, and GC content in a single view. More importantly for this tutorial, each of these layers was added independently: the same pattern can be used to build genome comparison figures from many different combinations of sequence, feature, link, and quantitative data.