ggchord: chord, collinear and plasmid tutorial

ggchord

This paired tutorial is generated from the same sources as the website. Code blocks are shown without executing during vignette rendering; the shared documentation script verifies the examples directly. Examples prefer bundled or source-backed data; deliberate diagnostic errors are labelled. Inputs marked as repository files require the checkout or input bundle. Each chapter provides its own preparation; start with the first-plot lesson.

Install and prepare the environment

Install and confirm the environment, then choose one progressive module. Reading the entire reference is not a prerequisite.

Install and inspect the environment

R ≥4.1.0 and ggplot2 ≥4.0.0 are required. Installation and examples target v0.16.6.

install.packages(c("remotes", "dplyr", "purrr", "tidyr"))
remotes::install_github("DangJem/[email protected]")
library(ggchord)
packageVersion("ggchord")

Which version are you using?

This tutorial documents ggchord 0.16.6. The installation above pins its formal tag; see GitHub Releases for release assets. For a source checkout, run from its repository root:

install.packages("pkgload")
pkgload::load_all(".")
packageVersion("ggchord")

Use the loading route appropriate to your source; you do not need both installation paths. Record the version when reproducing a plot. This lesson needs neither optional Bioconductor packages nor Biopython.

Support evidence

Declared lower bounds are R 4.1.0 and ggplot2 4.0.0. Release checks and actual platform versions are recorded in the release acceptance report; these do not certify every dependency combination. Optional readers require their documented dependencies.

Choose a learning path

  1. Module I · Chord: learn the three tables, then import, backbones, layers and geometry.
  2. Module II · Collinear: reuse the chord inputs, then switch coordinates, route links and focus locally.
  3. Module III · Plasmid: start with one sequence and DNA, then add layers and explore 13 reference cases.

Starting with chord follows ggchord’s development order. See the learning map. The two-sequence exercise below is an optional environment check rather than a substitute for the modules.

Which columns come first?

Sequences need accver/length; links need two IDs and four endpoints; genes need accver/start/end/strand. Module I’s first lesson explains real tables, optional fields and BLAST naming.

Optional: check one complete plot

Run the whole folded block, then inspect the figure. Bundled source-backed records need no external files or BLAST run.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(gene_data_example)
data(ribbon_data_example)
ids <- c("MT108731.1", "MT118296.1")
first_sequences <- seq_data_example |>
  filter(accver %in% ids) |>
  select(accver, length)
first_genes <- gene_data_example |>
  filter(accver %in% ids) |>
  transmute(accver, start, end, strand = source_strand)
first_links <- ribbon_data_example |>
  filter(qaccver %in% ids, saccver %in% ids) |>
  select(qaccver, saccver, qstart, qend, sstart, send)

first_colours <- setNames(c("#E64B35", "#233E4A"), ids)
p_first <- ggchord(
  seq_data = first_sequences, ribbon_data = first_links,
  gene_data = first_genes
) +
  geom_link_ribbon(fill = "#82B6AD", alpha = .55) +
  geom_seq(colour = "#233E4A", show.legend = FALSE) +
  geom_gene(aes(gene_fill = accver), position = position_strand(.10), width = .045) +
  geom_seq_label(size = 4) +
  coord_collinear() +
  scale_gene_fill_manual(
    values = first_colours, name = "Sequence",
    guide = guide_ggchord_legend(nrow = 1, theme = theme(
      legend.title = element_text(size = 13, face = "bold"), legend.text = element_text(size = 11),
      legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"), legend.margin = margin(l = 7, unit = "mm")
    ))
  ) +
  scale_seq_position_continuous(n.breaks = 3, minor_breaks = NULL) +
  theme_ggchord_publication(axis.text = element_text(size = 11)) +
  theme(
    legend.position = "bottom",
    plot.background = element_rect(fill = "#FFFCF5", colour = NA),
    panel.background = element_blank()
  )
p_first
Your first plot: two bundled sequences, gene arrows and observed alignment intervals
Your first plot: two bundled sequences, gene arrows and observed alignment intervals · Click to enlarge

Read the quick result

Arrows retain biological strands; colours identify sequences only. Ribbons are existing comparison intervals, not new homology results. This small linear example makes positions easy to read; the full chord module starts with backbones.

Optional: save the quick result

Run this only after the block above has created p_first:

ggsave("first-comparison.png", p_first, width = 10, height = 5, units = "in", dpi = 144)

Use the same 10×5 in dimensions for preview and export; see output.

Common execution problems

For missing objects, run the preparation block. For missing functions, check the package version. getwd() determines the save directory. Optional readers need dependencies only in their own topics; basic modules do not require Bioconductor or Biopython.

Choose your learning module

Choose a layout for your question. The examples below compare three views using source-backed bundled records.

Which view should you choose?

Task Preferred view Avoid this misreading
Overall relationships among several sequences Chord Crossings alone are not evidence of inversion
Annotations on one plasmid or circular sequence Circular Multiple sequences do not fit a single-sequence coordinate
Positions, gene order and local neighbourhoods Collinear Display alignment or flipping is not a biological conclusion

All three consume existing data; none computes a new BLAST alignment. Try the minimal combinations below, then follow the raw-record workflow, complete plasmid map or local homolog comparison.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
feature_palette <- c(
  CDS = "#993366", promoter = "#31849B", rep_origin = "#F1E51D",
  primer_bind = "#A020F0", misc_feature = "#A6ACB3", protein_bind = "#31849B"
)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
feature_source <- find_common_features(plasmid_example_pUC19)
features <- feature_source |>
  select(accver, start, end, strand, type, anno)

Run the preparation above once. Each object below is a normal composable ggplot; print it or use ggsave().

Chord: compare multiple sequences

p_chord <- ggchord(seq_data = seqs, ribbon_data = links, gene_data = genes) +
  geom_link_ribbon(aes(ribbon_fill = qaccver), alpha = .7) +
  scale_ribbon_fill_manual(values = seq_palette, guide = "none") +
  geom_seq(show.legend = FALSE) +
  geom_gene(position = position_strand(.1), width = .04) +
  geom_seq_label(size = 4, seq_label_radius = .84, seq_label_hjust = .5) +
  coord_chord() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  doc_theme
p_chord
Bundled sequences and alignment intervals in a chord view
Bundled sequences and alignment intervals in a chord view · Click to enlarge

Links are coloured by query sequence ID using the backbone category palette; this does not encode identity or homolog groups. See the raw-record workflow for colour mapped to reported identity. coord_chord() controls global rotation and fitting. Sequence-local radius, curvature, gap and orientation belong to geom_seq(). A NULL link control point uses the historical origin-based chord route; it does not mean a universal straight link. Ribbon avoidance defaults to uniform, line avoidance to none.

Collinear: align local comparisons

anchors <- genes |>
  group_by(accver) |>
  slice_max(end - start, n = 1, with_ties = FALSE) |>
  ungroup() |>
  select(accver, start, end)
p_collinear <- ggchord(seq_data = seqs, ribbon_data = links, gene_data = genes) +
  geom_link_ribbon(aes(ribbon_fill = qaccver), alpha = .7) +
  scale_ribbon_fill_manual(values = seq_palette, guide = "none") +
  geom_seq() +
  geom_gene(position = position_strand(.1)) +
  geom_seq_label(size = 4) +
  coord_collinear(anchor = anchors, align = "center", orientation = "auto") +
  doc_axis() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  doc_theme
p_collinear
Bundled records aligned to explicitly selected annotation anchors
Bundled records aligned to explicitly selected annotation anchors · Click to enlarge

Each sequence is anchored to its own longest known gene for display; this does not assert homology. anchor aligns specified intervals; orientation = "auto" chooses supported orientations using available evidence. Inspect orientation diagnostics rather than assuming biological inversion. Ribbons connect adjacent displayed rows by default; use adjacent_only = FALSE or explicit pairs to retain others. Non-adjacent outlines default to dashed; explicit styles take priority. These rules differ from chord mode. Focus and segments preserve source coordinates for cropped or locally reversed views.

Circular: annotate one sequence

circle_features <- features |>
  filter(type %in% c("CDS", "rep_origin"))
p_circular <- ggchord(seq_data = plasmid_seq) +
  geom_seq() +
  geom_feature(aes(feature_fill = type), data = circle_features, position = position_plasmid()) +
  geom_feature_label_repel(data = circle_features, position = position_plasmid(), size = 4) +
  geom_seq_center_label() +
  scale_feature_fill_manual(values = feature_palette, breaks = unique(circle_features$type)) +
  coord_circular() +
  doc_theme
p_circular
pUC19 annotations in a circular map
pUC19 annotations in a circular map · Click to enlarge

This example selects CDS and the replication origin for display, retaining the full sequence bp axis. coord_circular() requires a single sequence. General features can carry unknown strand. geom_feature_plasmid() is a convenience preset with its own defaults; choose it explicitly when those defaults fit the task. Primer and restriction layers are described in recognition.

Check your result

First count sequences: use circular for one annotated sequence, chord for the overall link pattern, and collinear for positions, distances or neighbourhoods.

1.1 Tables and field meanings

Learn the tables before selecting layers. Chord comparison was ggchord’s original core view. Its early BLAST workflows explain the compact field names retained today. A multi-sequence chord view displays existing interval comparisons among sequences; drawing does not run a multiple-sequence alignment algorithm.

Minimal shapes of three tables

Table Required columns One row represents
seq_data accver, length One sequence and its full length in bp
ribbon_data qaccver, saccver, qstart, qend, sstart, send One observed pair of aligned intervals
gene_data accver, start, end, strand One directed gene interval

Backbones alone need neither links nor genes. Use geom_feature() for annotations with unknown strand rather than assigning a direction.

Names and optional columns

accver originated from accession.version, but any stable contig identifier is allowed; versions are not added or removed. q means query and s means subject. Both IDs must occur in the sequence table.

pident is percentage identity, needed only for identity mapping/filtering. Alignment length differs from full sequence length. Keep evalue, bitscore, qlen and slen when required by an analysis. Gene anno is optional annotation text; supply it for labels. Additional category columns can feed aes() without becoming mandatory inputs.

Inspect real tables

library(ggchord)
library(dplyr)
seq_data_example |> select(accver, length) |> slice_head(n = 3)
ribbon_data_example |> select(qaccver, saccver, qstart, qend, sstart, send) |> slice_head(n = 3)
gene_data_example |> transmute(accver, start, end, strand = source_strand, anno) |> slice_head(n = 3)

These are bundled source-backed records. This historical teaching fixture stores biological strand separately in source_strand; restore it explicitly. Newly read GFF3 annotations use their ordinary strand column.

Coordinates and directions

Use one-based inclusive bp coordinates. Genes have ordered bounds with a separate strand. Endpoint order can represent reverse alignments; do not sort every link. Maximum feature endpoints do not establish full sequence lengths. See the coordinate contract.

Continue to 1.2 From analysis results to input tables.

1.2 Produce and import comparisons

Start with sequences and analysis results, then read them with ggchord. Missing comparisons must not be invented to populate a figure.

Start with FASTA and GFF3

Extract the tutorial bundle so the working directory contains examples/. FASTA supplies full lengths; GFF3 supplies intervals and strands. Retain source records separately from display tables.

Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}
library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

show_alignments retains 32 observed HSPs and show_links selects 12. show_genes retains 266 CDS; show_features selects CDS of at least 500 bp from four groups across annotation order per sequence, additionally retaining source-backed negative-strand endpoints. Selection neither reruns alignment nor establishes functional homology.

For coordinate inspection, reshape the four endpoint columns with tidyr while retaining the original link table:

link_endpoints <- show_links |>
  select(qaccver, saccver, qstart, qend, sstart, send) |>
  pivot_longer(
    cols = c(qstart, qend, sstart, send),
    names_to = c("side", "endpoint"), names_pattern = "([qs])(start|end)",
    values_to = "position"
  )
head(link_endpoints)

Produce your own BLAST results

These producer commands are for analysts with BLAST installed and their own input files; the website builder does not execute them.

blastn -query query.fna -subject subject.fna -outfmt 6 -out result.tsv
library(ggchord)
blast_source <- read_blast(file = "result.tsv")

Without a custom field list, outfmt 6 uses the standard 12-column table; 7 adds comments. Omitting outfmt entirely requests another format. Query/subject IDs map to qaccver/saccver. Consult the NCBI field documentation and nucleotide/protein guide. Historical tutorial HSPs are converted records, not newly run BLAST results.

When another format already exists

Source Entry point Check
Existing table as_ggchord_seq/feature/link Explicit role mapping; retain the original
BED, PAF read_bed, read_paf Convert zero-based half-open coordinates only once
GenBank read_genbank Biopython required; preserve record and segmented-location provenance
GRanges, Seqinfo as_ggchord_feature, as_ggchord_seq Full lengths from Seqinfo; unknown strand stays unknown
GInteractions as_ggchord_link, as_ggchord_seq First anchor is query; full length context is still required

These adapters address the request in issue #2. Current code provides the entries; the historical discussion does not certify every current combination. Missing optional packages are explicitly skipped locally. See format conversion for runnable optional-object examples and limitations.

Continue to 1.3 Build from backbones.

1.3 Build the layers step by step

Complete each small step before the flagship example. All four views reuse the same records, making the effect of each added layer visible.

Prepare this page

Extract the input bundle first.
Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

Step 1: sequence backbones

p_learn_seq <- ggchord(seq_data = show_sequences) +
  geom_seq() +
  geom_seq_label(size = 4) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000), minor_breaks = NULL) +
  coord_chord() +
  doc_theme
p_learn_seq
Chord step1: sequence backbones
Chord step1: sequence backbones · Click to enlarge

geom_seq() draws backbones, geom_seq_label() identifies sequences, and coord_chord() selects the global view. No links or genes are required yet.

Step 3: selected genes

p_learn_genes <- p_learn_links +
  geom_gene(data = show_features, position = position_strand(0.08)) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  guides(gene_fill = strand_guide)
p_learn_genes
Chord step3: selected genes
Chord step3: selected genes · Click to enlarge

position_strand() separates positive and negative strands. Display selected real annotations while retaining every source CDS separately.

Step 4: label every displayed feature

p_learn_labels <- p_learn_genes +
  geom_gene_label_repel(
    data = show_features, label_layout = "arc", label_wrap = 24,
    label_max_lines = 3, position = position_strand(0.08), size = 3
  )
p_learn_labels
Chord step4: labels for selected features
Chord step4: labels for selected features · Click to enlarge

The arrow and repel-label layers receive the same feature table without another label filter. Renaming, deleting or reordering records can change meaning; retain clear intermediate objects.

Continue to 1.4 Direction, radius, curvature and clearance.

1.4 Direction, curvature and clearance

These controls have different owners: geoms configure sequence geometry, Positions place local features, coords select the global view, and scales/guides control encodings.

Inspect directional evidence first

Extract the input bundle first.
Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

Inspect directional evidence

show_optimization optimizes display orientation only, preserving sequence order and weighting observed alignment lengths. Inspect seq_orientation and score_before/score_after. A display flip is not a new biological inversion result; the heuristic does not guarantee a global optimum.

Combine capabilities into a readable view

show_curvature <- c(
  "MT108731.1" = 0, "MT118296.1" = 0.85,
  "OQ646790.1" = 1.1, "OR222515.1" = -0.4
)
show_radius <- c(
  "MT108731.1" = 1, "MT118296.1" = 1.05,
  "OQ646790.1" = 0.95, "OR222515.1" = 1.08
)
p_show_chord <- ggchord(show_sequences, show_links, show_features) +
  geom_link_ribbon(aes(ribbon_fill = pident), link_avoid = "smooth", alpha = 0.58, linewidth = 0.18) +
  geom_seq(
    seq_orientation = show_optimization$seq_orientation,
    seq_curvature = show_curvature, seq_radius = show_radius,
    seq_gap = 0.06, linewidth = 1.1
  ) +
  geom_gene(position = position_strand(0.07), width = 0.035) +
  geom_gene_label_repel(
    data = show_labels, label_layout = "radial", label_wrap = 24,
    size = 3.1, label_max_lines = 3, position = position_strand(0.07)
  ) +
  geom_seq_label(size = 4, seq_label_radius = 0.84, seq_label_hjust = 0.5) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  guides(gene_fill = strand_guide) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000), minor_breaks = NULL) +
  coord_chord(rotation = 50) +
  doc_theme +
  labs(
    title = "Four genomes, one connected view",
    subtitle = "Signed curvature and radius · selected strand-aware CDS · local ribbon clearance"
  )
p_show_chord
Four-genome chord comparison with mixed backbone curvature and measured identity
Four-genome chord comparison with mixed backbone curvature and measured identity · Click to enlarge

Named vectors match sequence IDs. Curvature 0 is straight, signed values bend in opposite directions, and magnitude changes bow depth. Radius changes sequence placement. The result combines automatic orientation, varying radius, zero/positive/negative curvature and local clearance without changing source coordinates.

Why clearance modes look different

link_avoid = "uniform" clears the whole endpoint interval. "smooth" retreats locally near feature polygons; "none" disables feature avoidance. Automatic clearance requires link_gap = NULL; a fixed number overrides it. Clearance considers feature geometry, not text, and does not automatically route around intervening sequence rows.

Guides and export

Identity uses a long left-side colourbar; sequence and strand keys remain below. Explicit strand overrides mirror the arrow glyphs. See styling for guide responsibilities. Save at 12×10 in, then review 1.7 The complete showcase.

1.5 Filter and summarize links

Reduce link clutter while retaining provenance: filtering, deduplication, merging and bundling have different meanings.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed. For file-based examples, first extract the input bundle so the working directory contains examples/.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
sequence_source <- read_fasta_lengths(files = "examples/fasta/*.fna", source_file = TRUE)
seq_real <- sequence_source |>
  select(accver, length)
annotation_source <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep", source_file = TRUE)
feature_real <- annotation_source |>
  select(accver, start, end, strand, type, anno)
alignment_source <- read_blast(files = "examples/blastn-standard/*.tsv", source_file = TRUE)
link_real <- alignment_source |>
  select(qaccver, saccver, qstart, qend, sstart, send, pident, length)
real_validation <- validate_ggchord_data(seq_real, link_real)
real_validation

real_prepared <- prepare_ggchord_data(seq_real, link_real,
  features = list(annotation = feature_real)
)
real_filtered <- filter_ggchord_ribbons(link_real, min_pident = 80, min_length = 500)
# Explicit display subset: the six longest CDS per sequence; raw annotations stay intact.
real_display_features <- feature_real |>
  group_by(accver) |>
  slice_max(end - start + 1, n = 6, with_ties = FALSE) |>
  ungroup()
real_labels <- real_display_features |>
  filter(anno != "hypothetical protein") |>
  group_by(accver) |>
  slice_head(n = 2) |>
  ungroup()
real_filtered$report

Begin with the original link table

Use the real-record workflow to create seq_real, link_real and real_filtered. Keep its original tables and filter report.

unique_links <- deduplicate_ggchord_ribbons(real_filtered$data,
  by = "overlap",
  min_reciprocal_overlap = .9
)
merged_links <- merge_ggchord_ribbons(unique_links$data,
  max_gap = 100,
  min_pident_difference = 2
)
bundled_links <- bundle_ggchord_ribbons(link_real, seq_real, bins = 30)
p_density <- ggchord(seq_real, link_real, validate = "none") +
  geom_seq(show.legend = FALSE) +
  geom_seq_label(size = 4, seq_label_radius = .84, seq_label_hjust = .5) +
  stat_ribbon_density(aes(ribbon_alpha = after_stat(density)), fill = "#269B82") +
  scale_ribbon_alpha_continuous(
    name = "Relative density", range = c(.1, .6),
    guide = guide_ggchord_legend(position = "bottom", nrow = 1, theme = doc_legend_theme)
  ) +
  scale_seq_colour_manual(values = seq_palette) +
  coord_chord() +
  doc_theme
p_bundle <- ggchord(seq_real, link_real, validate = "none") +
  geom_seq() +
  stat_ribbon_bundle(bins = 30, fill = "#269B82") +
  coord_chord() +
  doc_theme
p_density
Metric Observed output
Filtered HSPs 8
After overlap deduplication 8
Merged display intervals 8
Bundle output rows 28

Understand each transformation

Filtering keeps observations meeting declared thresholds. Reciprocal-overlap deduplication selects a representative existing observation; it does not sum identities. Merging combines nearby compatible intervals into a display envelope. Bundling groups nearby query/subject locations into an aggregate. Inspect each $report and source-row membership, especially when extra metadata conflict. The merged or bundled envelope is not a newly computed nucleotide alignment. Estimated lengths retain their flags; unobserved identity is never invented.

Density is an explicit statistic

Interval density from the original alignment observations
Interval density from the original alignment observations · Click to enlarge

stat_ribbon_density() maps a computed density, and stat_ribbon_bundle() performs its explicit aggregation during build. The ordinary ribbon geom still draws one ribbon per row. Interpret count, reported length and identity weighting in their own units; more opaque ribbons need not mean stronger independently validated homology. A point anchor without direction evidence cannot acquire an aggregate orientation merely because its endpoints are equal.

Choose interval ribbons or point links

After this page’s preparation code:

# Midpoints summarize existing HSPs for display; they are not new alignments.
points <- ribbon_data_example |>
  transmute(
    qaccver, saccver,
    qpos = floor((qstart + qend) / 2),
    spos = floor((sstart + send) / 2),
    pident
  )
p_points <- ggchord(seq_data = seqs) +
  geom_seq() +
  geom_seq_label(size = 4) +
  geom_link_line(aes(link_colour = pident),
    data = points,
    arrow = arrow(length = unit(2, "mm")), linewidth = .7
  ) +
  scale_link_colour_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks,
    name = "Identity (%)", guide = guide_ggchord_colourbar(
      available_aes = "link_colour",
      position = "bottom", direction = "horizontal",
      theme = doc_legend_theme + theme(legend.key.width = unit(65, "mm"), legend.key.height = unit(5, "mm"))
    )
  ) +
  scale_seq_colour_manual(values = seq_palette) +
  coord_chord() +
  doc_theme
p_points
Midpoint links derived from existing HSP intervals
Midpoint links derived from existing HSP intervals · Click to enlarge

Point positions are integer midpoints of real bundled HSPs; identity remains the reported pident. Lines use qpos/spos, not interval ends, and an arrow points from query to subject. The line default avoidance is none; ribbon avoidance is uniform. Global chord rotation belongs to the coordinate system. Pair selection and adjacent-row defaults in linear plots are covered in three modes.

The point-link colour scale supplies available_aes = "link_colour" to its colourbar explicitly. Ordinary ggplot2 colourbars and the wrapper default do not accept every extended role; choose the matching role when making a custom guide.

Check your result

Inspect each report and member rows before reading the plot. Aggregated bands are not new alignments; point links summarize midpoints of existing HSPs.

1.6 Full source-file workflow

Combine original FASTA, GFF3 and standard BLAST tables into a traceable comparison. Inspect inputs, choose a display subset, then compare chord and full-length views.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed. For file-based examples, first extract the input bundle so the working directory contains examples/.

Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

This complete workflow uses four sequence records already present in the source repository. It requires the matching development checkout or the tutorial input bundle, not new alignments or external gene prediction. Download the inputs and unpack them so examples/ is under the working directory. Record the package version and build manifest with the exported figure.

1. Read and inspect original records

library(dplyr)
sequence_source <- read_fasta_lengths(files = "examples/fasta/*.fna", source_file = TRUE)
seq_real <- sequence_source |>
  select(accver, length)
annotation_source <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep", source_file = TRUE)
feature_real <- annotation_source |>
  select(accver, start, end, strand, type, anno)
alignment_source <- read_blast(files = "examples/blastn-standard/*.tsv", source_file = TRUE)
link_real <- alignment_source |>
  select(qaccver, saccver, qstart, qend, sstart, send, pident, length)
real_validation <- validate_ggchord_data(seq_real, link_real)
real_validation

The FASTA headers supply exact accession versions and lengths. GFF3 contributes CDS intervals and their original strands. BLAST output contributes existing alignments; the reader does not run BLAST. Inspect real_validation before continuing: decreasing link endpoints can be legitimate reverse-direction hits. Never turn unknown annotation strands into + to make a gene shortcut accept them.

2. Make the display selection explicit

real_prepared <- prepare_ggchord_data(seq_real, link_real,
  features = list(annotation = feature_real)
)
real_filtered <- filter_ggchord_ribbons(link_real, min_pident = 80, min_length = 500)
# Explicit display subset: the six longest CDS per sequence; raw annotations stay intact.
real_display_features <- feature_real |>
  group_by(accver) |>
  slice_max(end - start + 1, n = 6, with_ties = FALSE) |>
  ungroup()
real_labels <- real_display_features |>
  filter(anno != "hypothetical protein") |>
  group_by(accver) |>
  slice_head(n = 2) |>
  ungroup()
real_filtered$report
Metric Observed output
Sequences 4
Annotation rows 266
CDS selected for the chord view 24
Original HSPs 32
HSPs retained by the display filter 8
Selected text labels 6

The link display retains identity ≥80% and reported alignment length ≥500 bp. The chord view selects the six longest imported CDS per sequence, labelling at most two selected CDS with informative annotations (excluding hypothetical protein). These are display rules, not biological findings or a claim that omitted genes are absent. Raw feature_real and link_real remain intact; real_prepared$report and real_filtered$report retain preparation/filter evidence. No display strands are recoded. This differs from the small packaged teaching fixture whose documented generation includes display-strand recoding.

3. Build a chord comparison

p_real_chord <- ggchord(seq_data = seq_real, ribbon_data = real_filtered$data, validate = "none") +
  geom_link_ribbon(aes(ribbon_fill = pident), alpha = .65) +
  geom_seq(show.legend = FALSE) +
  geom_feature(aes(feature_fill = strand),
    data = real_display_features,
    width = .045, position = position_strand(.08)
  ) +
  geom_feature_label_repel(
    data = real_labels, label_layout = "radial",
    label_wrap = 20, size = 4, position = position_strand(.08)
  ) +
  geom_seq_label(size = 4, seq_label_radius = .84, seq_label_hjust = .5) +
  coord_chord() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_feature_fill_manual(values = gene_palette, breaks = c("+", "-")) +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  doc_theme
p_real_chord
Original sequences with explicitly selected CDS and alignment links
Original sequences with explicitly selected CDS and alignment links · Click to enlarge

Read sequence labels first, then source-strand feature arrows and interval links. Ribbons encode existing aligned intervals and mapped identity; their crossing is not itself evidence of a rearrangement. The selected features remain visibly separate from the much wider alignment spans. Use the full input tables when scientific interpretation needs annotations outside this display subset.

4. Inspect all annotations in a linear view

p_real_linear <- ggchord(seq_real, real_filtered$data, validate = "none") +
  geom_link_ribbon(fill = "#6BAFA6", alpha = .45) +
  geom_seq() +
  geom_seq_label(size = 4) +
  geom_feature(data = feature_real, feature_fill = "#CE1536", show.legend = FALSE, position = position_strand(.18)) +
  geom_seq_scale(length = 10000) +
  coord_collinear() +
  doc_axis() +
  scale_seq_colour_manual(values = seq_palette) +
  doc_theme
p_real_linear
Original sequences, all imported CDS and filtered links
Original sequences, all imported CDS and filtered links · Click to enlarge

This view draws all imported annotations. Ribbons connect adjacent displayed sequences by default; retaining every comparison is an explicit layer decision. The bp scale bar belongs to collinear coordinates. See local views for cropping, source-coordinate axes and independent windows, and output to preview and save at the gallery’s stated dimensions.

5. Keep the evidence with the figure

The input archive contains unmodified FASTA/GFF3/BLAST files, a SHA256 manifest and the shared example script. The plot manifest records the code hash, PNG device, width, height and dpi. The accession names identify source records; a drawing is not a replacement for the original annotations or alignment evidence.

Select fields before passing data

The *_source objects keep original fields and provenance; seq_real has two columns, feature_real only geometry/type/text, and link_real adds identity and reported length only because this workflow filters and colours by them. Standard 12-column BLAST conversions are used, not a requirement for the historical 17 fields. The bundled gene fixture preserves its original strand in source_strand; the short examples restore that biological strand before selecting four gene columns. See alignment input for exact format and unit boundaries.

Check your result

Check that IDs agree across the tables, inspect filtering reports and verify that guides encode identity and strand. Use the overview to locate regions, then focus a neighbourhood to study a CDS.

Save this result

Save the plot just created to the working directory; use matching dimensions for preview.

ggsave("phage-comparison.png", p_real_chord, width = 10, height = 8, units = "in", dpi = 144)

1.7 Complete chord showcase

Bring four genomes into one connected view. Combine straight and curved backbones, strand-aware genes and a continuous identity scale without hiding where the data came from.

What this view shows

Four-genome chord comparison with mixed backbone curvature and measured identity
Four-genome chord comparison with mixed backbone curvature and measured identity · Click to enlarge

This is the end of Module I. Begin with tables, import, layer construction and geometry/clearance. 18 selected real CDS each receive repel labels; the complete 266-row annotation source remains separate. The four backbones combine zero, positive and negative curvature, varying radii and optimized display directions.

Prepare original records

Extract the tutorial input bundle into the working directory, so it contains examples/. Install the optional example packages with install.packages(c("dplyr", "purrr", "tidyr")); they prepare tables, map lists and inspect endpoint columns.

Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}
library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

The input contains four unchanged FASTA records, their GFF3 annotations and 32 original HSPs. The display keeps HSPs at least 900 bp long and at least 75% identical, with at most three per sequence pair. This reduces clutter without merging or inventing alignments.

Compose the layers

show_curvature <- c(
  "MT108731.1" = 0, "MT118296.1" = 0.85,
  "OQ646790.1" = 1.1, "OR222515.1" = -0.4
)
show_radius <- c(
  "MT108731.1" = 1, "MT118296.1" = 1.05,
  "OQ646790.1" = 0.95, "OR222515.1" = 1.08
)
p_show_chord <- ggchord(show_sequences, show_links, show_features) +
  geom_link_ribbon(aes(ribbon_fill = pident), link_avoid = "smooth", alpha = 0.58, linewidth = 0.18) +
  geom_seq(
    seq_orientation = show_optimization$seq_orientation,
    seq_curvature = show_curvature, seq_radius = show_radius,
    seq_gap = 0.06, linewidth = 1.1
  ) +
  geom_gene(position = position_strand(0.07), width = 0.035) +
  geom_gene_label_repel(
    data = show_labels, label_layout = "radial", label_wrap = 24,
    size = 3.1, label_max_lines = 3, position = position_strand(0.07)
  ) +
  geom_seq_label(size = 4, seq_label_radius = 0.84, seq_label_hjust = 0.5) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  guides(gene_fill = strand_guide) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000), minor_breaks = NULL) +
  coord_chord(rotation = 50) +
  doc_theme +
  labs(
    title = "Four genomes, one connected view",
    subtitle = "Signed curvature and radius · selected strand-aware CDS · local ribbon clearance"
  )
p_show_chord

geom_seq(seq_curvature = ...) applies named values to stable sequence identifiers. 0 is a straight backbone. Curvature changes the display geometry; it does not alter sequence lengths, biological strands or alignment endpoints.

Read and adapt the result

Start with the sequence colours and gene directions, then trace the larger aligned intervals. A ribbon crossing is not evidence of a rearrangement by itself. The colour bar uses the same identity scale as the collinear view.

Use link filtering to change the display threshold, labels to choose annotation density, or sequence parameters to change radius and curvature. For a file-by-file introduction, start with original sequence comparisons.

Save at its intended size

ggsave("four-genome-chord.png", p_show_chord, width = 12, height = 10, units = "in", dpi = 144)

2.1 Reuse chord input tables

Collinearity extends ggchord’s original chord grammar into a linear view. The design goal is to retain shared data and layers, switching coordinates wherever the contracts permit.

The same three tables

Input Required columns Linear interpretation
seq_data accver, length One sequence per row with a shared bp x scale
ribbon_data qaccver, saccver, qstart, qend, sstart, send An observed interval correspondence between rows
gene_data accver, start, end, strand Genes separated above/below rows by a Position

Optional fields and the historical BLAST naming follow 1.1. Identity needs pident; labels need anno. Source fields preserve coordinates in local windows; display gaps do not represent invented DNA lengths.

Obtain the inputs

Reuse the FASTA/GFF3/BLAST workflow in 1.2, or import PAF, GRanges and GInteractions through adapters. Protein alignments require explicit CDS projection; aa endpoints must not be presented as bp.

Current conversion boundaries

The next page demonstrates replacing coord alone. Explicit circular radius, curvature, ring and radial sequence-label placement are rejected by the current linear contract rather than silently ignored. Keep coordinate-specific styling in the appropriate showcase. Perfect conversion of every possible composition remains a design goal, not a current guarantee.

Linear views default to adjacent displayed rows. Set adjacent_only = FALSE in shared layers to retain the chord link set. Choose manual sequence directions or coordinate-owned automatic orientation, not both at once.

2.2 Switch coordinates and add layers

Verify the shared grammar before tuning linear composition. These objects replace only the coordinates on the chord teaching objects: no table edits or new alignments.

Prepare this page

Extract the input bundle first.
Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)
Preparation code — run once on this page
p_learn_seq <- ggchord(seq_data = show_sequences) +
  geom_seq() +
  geom_seq_label(size = 4) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000), minor_breaks = NULL) +
  coord_chord() +
  doc_theme
p_learn_seq

p_learn_links <- p_learn_seq +
  geom_link_ribbon(
    aes(ribbon_fill = pident),
    data = show_links,
    adjacent_only = FALSE, link_avoid = "smooth"
  ) +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values, limits = c(0, 100),
    breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  )
p_learn_links

p_learn_genes <- p_learn_links +
  geom_gene(data = show_features, position = position_strand(0.08)) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  guides(gene_fill = strand_guide)
p_learn_genes

p_learn_labels <- p_learn_genes +
  geom_gene_label_repel(
    data = show_features, label_layout = "arc", label_wrap = 24,
    label_max_lines = 3, position = position_strand(0.08), size = 3
  )
p_learn_labels

Replace coord

# Reuse exactly the same data and layers; replace only the coordinate system.
p_linear_seq <- p_learn_seq + coord_collinear()
p_linear_links <- p_learn_links + coord_collinear()
p_linear_labels <- p_learn_labels + coord_collinear(ratio = 0.65)
p_linear_labels

Read backbone lengths

The same backbones with linear coordinates
The same backbones with linear coordinates · Click to enlarge

Rows share one bp scale. Different sequence lengths remain different rather than being stretched to equal width.

Add strand-separated genes and labels

The same genes and labels with linear coordinates
The same genes and labels with linear coordinates · Click to enlarge

Arrows and labels use the same selected table. Display flips do not rewrite source strands. Continue to 2.3 Route cross-row links.

2.3 Routes, clearance and the ruler

Adjacent and cross-row links impose different reading costs. Separate their data and controls while preserving observed HSP endpoints. Control points change display paths only.

Prepare this page

Extract the input bundle first.
Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

Specify controls for long links

# Omit two short, crowded candidates from this linear display only.
# Both source rows remain in show_features and in the chord display.
show_linear_features <- show_features |>
  filter(!anno %in% c("tail assembly protein", "gp4 protein"))
# Separate adjacent and cross-row links, retaining original endpoint values.
show_pair_rows <- show_links |>
  mutate(
    query_row = match(qaccver, show_sequences$accver),
    subject_row = match(saccver, show_sequences$accver),
    span = abs(query_row - subject_row)
  )
show_adjacent <- show_pair_rows |> filter(span == 1)
# One longest observed HSP per non-neighbour pair keeps the long routes legible.
show_cross <- show_pair_rows |>
  filter(span > 1) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 1, with_ties = FALSE) |>
  ungroup()
# Inspect a backbone-only layout using the same alignment and gap as the final view.
show_linear_coord <- coord_collinear(align = "center", orientation = "auto", ratio = 0.7)
show_routing_base <- ggchord(show_sequences, show_links) +
  geom_seq(seq_gap = 0.42) + show_linear_coord
show_reference <- get_chord_layout(show_routing_base)$sequence_reference
# Shared controls sit just beyond the intervening backbones, rather than far away.
show_cross_controls <- map2(show_cross$query_row, show_cross$subject_row, function(query_row, subject_row) {
  between <- seq(min(query_row, subject_row) + 1L, max(query_row, subject_row) - 1L)
  middle_ids <- show_sequences$accver[between]
  right_edge <- max(show_reference$x_offset[middle_ids] + show_reference$lens[middle_ids] * show_reference$x_scale)
  middle_y <- mean(show_reference$y[show_sequences$accver[c(query_row, subject_row)]])
  c(right_edge + if (abs(query_row - subject_row) == 3) 0.85 else 0.65, middle_y)
})
p_show_collinear <- ggchord(show_sequences, gene_data = show_linear_features) +
  geom_link_ribbon(
    aes(ribbon_fill = pident),
    data = show_adjacent,
    link_avoid = "smooth", alpha = 0.48, linewidth = 0.22
  ) +
  geom_link_ribbon(
    aes(ribbon_fill = pident),
    data = show_cross,
    adjacent_only = FALSE, link_ctrl_point = show_cross_controls,
    link_avoid = "smooth", alpha = 0.28, linewidth = 0.3
  ) +
  geom_seq(seq_gap = 0.42, linewidth = 1) +
  geom_gene(position = position_strand(0.09), width = 0.045) +
  geom_gene_label_repel(
    data = show_linear_features |> arrange(desc(nchar(anno)), accver, start),
    label_layout = "auto", label_wrap = 24,
    label_side = "auto", position = position_strand(0.09), size = 3.3
  ) +
  geom_seq_label(size = 4) +
  geom_seq_scale(length = 10000, offset = unit(c(0, 10), "mm")) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  guides(gene_fill = strand_guide) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000, 60000, 80000), minor_breaks = NULL) +
  show_linear_coord +
  doc_theme +
  labs(
    title = "Sequence architecture, across four genomes",
    subtitle = "Selected labelled CDS · strand-separated arrows · routed cross-row comparisons"
  )
p_show_collinear
Four-genome collinearity: strand-separated features and routed cross-row links
Four-genome collinearity: strand-separated features and routed cross-row links · Click to enlarge

The linear showcase omits the crowded tail assembly protein and gp4 protein candidates, retaining them in the source and chord view; the remaining 16 features are individually labelled. Keep the selected adjacent HSPs and one longest HSP per non-neighbour pair. show_cross_controls matches show_cross row order; rebuild both after sorting/filtering. Controls use exported layout x/y units, not bp. Each link accepts two or four numbers, shaping its two ribbon edges rather than invoking a global route planner.

Shorter sequences are horizontally centred. A backbone-only layout provides actual row positions, lengths and offsets; controls are placed just beyond the intervening right ends rather than far from the data. Row spacing uses seq_gap and the longest backbone width, while controls do not accept bp coordinates. Changing spacing, order or orientation requires inspecting the paths again; arbitrary controls are not guaranteed free of self-intersection.

Local clearance and dashed outlines

position_strand() separates strands. link_avoid = "smooth" with the default NULL gap retreats locally near arrows. Non-adjacent outlines are dashed by default; a larger linewidth makes them visible without overriding linetype. Adjacency follows display order, not biological alignment direction.

Why the ruler was offset

geom_seq_scale() defaults to a 2 mm horizontal and 6 mm vertical offset. The earlier inset was configuration, not incorrect bp conversion. offset = unit(c(0, 10), "mm") aligns the start with the left sequence envelope and reserves vertical label space. With centred rows, source coordinate0 is no longer aligned across sequences; the ruler uses the left envelope, and reversed rows have their source origin at the other end. A scale bar measures distance rather than a particular source interval.

Continue in depth

Use focus/segments for local questions and CDS neighbourhoods for homology evidence; review the 2.6 Showcase. GC remains available in tracks, while the flagship reserves space for labels and links.

2.4 Compare CDS neighbourhoods

Compare AmpR neighbourhoods in two plasmids. Check real coding DNA, establish correspondence, then focus to each CDS plus 200 bp on either side.

Prepare this page

Only bundled data is needed; no download or new alignment. Load the package and optional display settings first.

Preparation code — run once on this page
library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

1. Extract actual CDS and coding DNA

Select AmpR annotations from bundled pUC19 and pETDuet-1, retaining IDs, coordinates, strand and text. Reverse-complement negative-strand DNA so comparisons use the same biological orientation rather than annotation names alone.

library(dplyr)
data(plasmid_example_pUC19)
data(plasmid_example_pETDuet_1)
homolog_source <- bind_rows(plasmid_example_pUC19, plasmid_example_pETDuet_1)
homolog_seq <- homolog_source |>
  select(accver, length)
cluster_features <- homolog_source |>
  find_common_features() |>
  filter(type == "CDS", anno == "AmpR") |>
  select(accver, start, end, strand, anno) |>
  arrange(match(accver, homolog_source$accver)) |>
  mutate(feature_id = paste(accver, start, end, sep = ":"))
# Membership is computed from exact strand-corrected CDS DNA equality, not gene names.
reverse_complement <- function(sequence) {
  bases <- strsplit(sequence, "")[[1]]
  chartr("ACGT", "TGCA", paste(rev(bases), collapse = ""))
}
coding_sequences <- cluster_features |>
  left_join(select(homolog_source, accver, sequence), by = "accver") |>
  rowwise() |>
  mutate(
    coding_dna = substr(sequence, start, end),
    coding_dna = if (strand == "-") reverse_complement(coding_dna) else coding_dna
  ) |>
  ungroup()
cds_dna <- pull(coding_sequences, coding_dna)

cluster_features should contain two actual CDS; cds_dna retains their oriented coding sequences. This strict example checks exact DNA equality. General homology analysis can use external alignments and clustering; homologs need not be identical.

3. Focus on comparable neighbourhoods

Full plasmid lengths would shrink the individual CDS. Construct windows from each real CDS source interval and add 200 bp on either side, retaining the original sequence and annotation tables.

# Compare equivalent local windows instead of shrinking one gene on full plasmids.
homolog_windows <- cluster_features |>
  select(accver, start, end)
local_homologs <- focus_ggchord_data(homolog_seq, homologs$data, cluster_features,
  loci = homolog_windows, expand = 200
)
p_homologs <- ggchord(
  seq_data = local_homologs$seq_data,
  ribbon_data = local_homologs$ribbon_data, gene_data = local_homologs$gene_data
) +
  geom_link_ribbon(fill = "#269B82", alpha = .65) +
  geom_seq(colour = "#233E4A", show.legend = FALSE) +
  geom_feature(
    data = local_homologs$gene_data, feature_fill = "#F1D334",
    position = position_strand(.10), width = .07
  ) +
  geom_feature_label_repel(data = local_homologs$gene_data, size = 4, label_fit = "shrink") +
  geom_seq_label(size = 4) +
  geom_seq_break() +
  coord_collinear() +
  doc_axis("source") +
  doc_theme
p_homologs
AmpR neighbourhoods: identical CDS, 200 bp flanks and source coordinates
AmpR neighbourhoods: identical CDS, 200 bp flanks and source coordinates · Click to enlarge

Gold arrows identify the checked identical coding sequences; the purple band connects their CDS ranges and internal text names AmpR. Axes use each original sequence’s positions, so one horizontal location can have different source labels. Break markers indicate that only local windows remain.

local_homologs$report records retention and clipping. Genes outside the windows are omitted, not absent. The band represents declared interval correspondence, not a base alignment; envelopes of segmented annotations likewise do not establish exon-level correspondence.

4. Project supported protein intervals to both CDS

# Exact CDS DNA equality supplies evidence for these selected ungapped residues.
matching <- homologs$data |>
  slice_head(n = 1)
paired_coding <- cluster_features |>
  mutate(phase = 0)
paired_cds <- build_ggchord_cds(paired_coding)
protein_link <- tibble(
  qfeature_id = matching$qfeature_id, sfeature_id = matching$sfeature_id,
  qstart = 1, qend = 20, sstart = 1, send = 20
)
projected_links <- project_ggchord_links(protein_link, paired_cds, ungapped = TRUE)
projected_links
Metric Observed output
Projected genomic feature rows 1
Projected nucleotide link rows 1

The first 20 residues of identical CDS provide sequence evidence for ungapped = TRUE. Equal spans alone do not justify that claim for ordinary BLASTp HSPs. The utility retains protein statistics rather than presenting them as nucleotide identity.

Check your result

Both internal labels should read AmpR and arrows retain the actual negative strand. Coding DNA supports correspondence; source axes and focus reports explain the local display. Save at explicit 10 × 5 in dimensions.

Next: read real BLASTp query intervals, understand CDS projection and segments, or create independent local windows.

Save this result

Save the plot just created to the working directory; use matching dimensions for preview.

ggsave("ampR-neighbourhood.png", p_homologs, width = 10, height = 5, units = "in", dpi = 144)

2.5 Focus, anchors and segments

Select local windows or segments while keeping source positions distinct from display transformations.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

Focus without losing origins

After this page’s preparation code:

focus_loci <- tibble(
  accver = seqs$accver,
  start = floor(seqs$length * .25), end = floor(seqs$length * .75)
)
focused <- focus_ggchord_data(seqs, links, genes, loci = focus_loci)
p_focus <- ggchord(
  seq_data = focused$seq_data, ribbon_data = focused$ribbon_data,
  gene_data = focused$gene_data
) +
  geom_seq() +
  geom_link_ribbon(fill = "#6BAFA6", alpha = .5) +
  geom_gene() +
  geom_seq_break() +
  geom_seq_label(size = 4) +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  doc_axis("source") +
  coord_collinear() +
  doc_theme
p_focus
Focused sequence windows with source-coordinate axes
Focused sequence windows with source-coordinate axes · Click to enlarge

Focus relocates retained positions to local one-based coordinates and retains source origins, truncation flags and a report. boundary = "trim" can clip links by a linear display projection; it does not realign sequences. A scaled alignment length is flagged as estimated. coordinates = "source" restores source axis labels, including after repeated focusing.

Independent windows and local reversal

windows <- tibble(
  accver = rep(seqs$accver[1], 2),
  start = c(1, floor(seqs$length[1] * .65)), end = c(floor(seqs$length[1] * .4), seqs$length[1]),
  locus_id = c("left_window", "right_window"), row_id = seqs$accver[1], orientation = c(1, -1)
)
segmented <- segment_ggchord_data(slice_head(seqs, n = 1), windows = windows)
p_segments <- ggchord(seq_data = segmented$seq_data) +
  geom_seq() +
  coord_collinear(segments = segmented$segments, segment_gap = 2000)
p_segments

Each window has a reversible source mapping; omitted gaps have no mapping. map_ggchord_position() maps explicit source positions into a plot for ordinary ggplot2 annotations. Supply the window ID when windows share a source sequence. Do not interpret display orientation as a change to the original strand.

Explicit biological relationships

find_ggchord_loci() selects declared feature/name queries. link_ggchord_clusters() requires an explicit membership table and sequence pairs; equal gene names do not establish homology. filter_ggchord_ribbons(), deduplicate_ggchord_ribbons() and merge_ggchord_ribbons() prepare existing alignments, preserving source membership and estimated-length flags. Bundle and density statistics summarize provided links rather than creating new sequence alignments.

Map an annotation back to source coordinates

source_position <- focus_loci$start[1] + 100
source_mark <- map_ggchord_position(p_focus, seqs$accver[1], source_position, source = TRUE)
p_marked <- p_focus +
  ggplot2::geom_point(
    data = source_mark, aes(x, y),
    inherit.aes = FALSE, colour = "#D59B17", size = 3
  )
p_marked

Run the focus example first. The helper returns Cartesian x/y for a source position 100 bp inside the first selected window. It does not make all ordinary ggplot2 coordinates become genomic bp. In segmented views choose the window ID, and check positions in omitted gaps rather than extrapolating across them. Use explicit homologs when the locus selection comes from declared feature/cluster evidence.

Check your result

Inspect cropping reports, break markers and source coordinates. Omitted gaps have no mapping; a display flip alone is not evidence of inversion.

2.6 Complete collinear showcase

The complete Module II result: four real genomes, strand-separated features and outward-routed cross-row links.

Complete result

Four-genome collinearity: strand-separated features and routed cross-row links
Four-genome collinearity: strand-separated features and routed cross-row links · Click to enlarge

Collinearity extends the chord coordinate grammar; reusing data and layers is the goal. Read inputs, coordinate replacement and routes/ruler, then run the complete example. The identity scale and feature candidates are shared with the chord example; this linear composition omits two crowded short candidates, displaying 16 CDS with labels, with positive and negative strands above/below backbones.

Prepare original records

Extract the input bundle and install dplyr, purrr and tidyr first:

Preparation code — run once on this page
# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}
library(ggchord)
library(dplyr)
library(purrr)
library(tidyr)
# Original repository records; extract the tutorial input bundle first.
show_sequences <- read_fasta_lengths(files = "examples/fasta/*.fna") |>
  select(accver, length)
show_annotations <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep")
show_genes <- show_annotations |>
  filter(type == "CDS") |>
  select(accver, start, end, strand, anno)
show_alignments <- read_blast(files = "examples/blastn-standard/*.tsv")
# At most three substantial observed HSPs per pair; no coordinates are invented.
show_links <- show_alignments |>
  filter(length >= 900, pident >= 75) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 3, with_ties = FALSE) |>
  ungroup()
# Select up to four source CDS per sequence, spread across annotation order.
# No feature is moved or invented; keep complete source tables separately.
show_features <- show_genes |>
  filter(end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  mutate(display_bin = ntile(row_number(), 4), informative = !is.na(anno) & anno != "hypothetical protein") |>
  group_by(accver, display_bin) |>
  arrange(desc(informative), desc(end - start), .by_group = TRUE) |>
  slice_head(n = 1) |>
  ungroup() |>
  select(accver, start, end, strand, anno)
# Also retain well-separated negative-strand CDS, if not already represented.
show_reverse <- show_genes |>
  filter(strand == "-", end - start + 1 >= 500) |>
  group_by(accver) |>
  arrange(start, .by_group = TRUE) |>
  slice(c(1, n())) |>
  ungroup()
show_features <- bind_rows(show_features, show_reverse) |>
  distinct(accver, start, end, strand, .keep_all = TRUE) |>
  arrange(accver, start)
show_labels <- show_features
show_optimization <- optimize_ggchord_layout(
  show_sequences, show_links,
  optimize = "orientation", weight = "length"
)

Retain four full FASTA records, 266 CDS and 32 original HSPs. Adjacent rows use the explicitly selected observations; each non-neighbour pair retains only its longest HSP to reduce overlap.

Reserve space for connections and labels

The flagship no longer includes GC tracks; composition analysis remains in tracks. This keeps two different reading tasks from competing in the entry figure.

Compose the layers

# Omit two short, crowded candidates from this linear display only.
# Both source rows remain in show_features and in the chord display.
show_linear_features <- show_features |>
  filter(!anno %in% c("tail assembly protein", "gp4 protein"))
# Separate adjacent and cross-row links, retaining original endpoint values.
show_pair_rows <- show_links |>
  mutate(
    query_row = match(qaccver, show_sequences$accver),
    subject_row = match(saccver, show_sequences$accver),
    span = abs(query_row - subject_row)
  )
show_adjacent <- show_pair_rows |> filter(span == 1)
# One longest observed HSP per non-neighbour pair keeps the long routes legible.
show_cross <- show_pair_rows |>
  filter(span > 1) |>
  group_by(qaccver, saccver) |>
  slice_max(length, n = 1, with_ties = FALSE) |>
  ungroup()
# Inspect a backbone-only layout using the same alignment and gap as the final view.
show_linear_coord <- coord_collinear(align = "center", orientation = "auto", ratio = 0.7)
show_routing_base <- ggchord(show_sequences, show_links) +
  geom_seq(seq_gap = 0.42) + show_linear_coord
show_reference <- get_chord_layout(show_routing_base)$sequence_reference
# Shared controls sit just beyond the intervening backbones, rather than far away.
show_cross_controls <- map2(show_cross$query_row, show_cross$subject_row, function(query_row, subject_row) {
  between <- seq(min(query_row, subject_row) + 1L, max(query_row, subject_row) - 1L)
  middle_ids <- show_sequences$accver[between]
  right_edge <- max(show_reference$x_offset[middle_ids] + show_reference$lens[middle_ids] * show_reference$x_scale)
  middle_y <- mean(show_reference$y[show_sequences$accver[c(query_row, subject_row)]])
  c(right_edge + if (abs(query_row - subject_row) == 3) 0.85 else 0.65, middle_y)
})
p_show_collinear <- ggchord(show_sequences, gene_data = show_linear_features) +
  geom_link_ribbon(
    aes(ribbon_fill = pident),
    data = show_adjacent,
    link_avoid = "smooth", alpha = 0.48, linewidth = 0.22
  ) +
  geom_link_ribbon(
    aes(ribbon_fill = pident),
    data = show_cross,
    adjacent_only = FALSE, link_ctrl_point = show_cross_controls,
    link_avoid = "smooth", alpha = 0.28, linewidth = 0.3
  ) +
  geom_seq(seq_gap = 0.42, linewidth = 1) +
  geom_gene(position = position_strand(0.09), width = 0.045) +
  geom_gene_label_repel(
    data = show_linear_features |> arrange(desc(nchar(anno)), accver, start),
    label_layout = "auto", label_wrap = 24,
    label_side = "auto", position = position_strand(0.09), size = 3.3
  ) +
  geom_seq_label(size = 4) +
  geom_seq_scale(length = 10000, offset = unit(c(0, 10), "mm")) +
  scale_seq_colour_manual(
    values = seq_palette, name = "Sequence",
    guide = sequence_guide
  ) +
  scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand") +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  guides(gene_fill = strand_guide) +
  scale_seq_position_continuous(breaks = c(0, 20000, 40000, 60000, 80000), minor_breaks = NULL) +
  show_linear_coord +
  doc_theme +
  labs(
    title = "Sequence architecture, across four genomes",
    subtitle = "Selected labelled CDS · strand-separated arrows · routed cross-row comparisons"
  )
p_show_collinear

Dashed outlines identify non-adjacent displayed rows, with sufficient linewidth to read. Right-side controls separate long routes; smooth local clearance retreats near arrows. Identity has the same left-side colourbar as the chord view, and the bottom strand keys use arrows. The ruler’s horizontal offset is 0; its length still uses the shared bp conversion.

Adjustment and limits

Rebuild controls when changing seq_gap, order, orientation or link selection. Local arrow clearance is not an automatic route around intervening whole sequences; ribbons may still cross legitimately. Use focus/segments for local questions. Explicit circular radius/curvature cannot currently be passed to linear coordinates; see conversion boundaries.

Save at the intended size

ggsave("four-genome-collinear.png", p_show_collinear, width = 12, height = 8, units = "in", dpi = 144)

3.1 DNA, annotations and 13 references

A single-sequence circular map starts with two columns. Feature, primer and restriction searches additionally need real DNA; existing annotations can be drawn directly without running recognition again.

Tables and optional information

Input Required content Optional content
Backbone accver, length; exactly one sequence label for display; sequence for searches
Features accver, start, end and declared directionality type, anno/label, feature_id and shape categories
Primers Computed binding interval, strand/name roles required by the geom Recognition and provenance fields
Restriction sites Position/name roles required by the geom Enzyme catalogue, methylation assumption and filter provenance

Backbones do not require DNA. Prefer search-result tables for the latter layers and consult their help for detailed fields. Unknown-strand annotations belong in generic feature layers.

Inspect bundled real records

library(ggchord)
library(dplyr)
plasmid_example_pUC19 |> select(accver, length)
find_common_features(plasmid_example_pUC19) |> select(accver, start, end, strand, type, anno) |> slice_head(n = 5)

The compact names retain ggchord’s early BLAST workflow history. accver need not be a database accession. Full DNA and length must agree. Recognition is computational matching, not experimental validation.

Import your own plasmid

read_fasta_lengths() reads IDs and full lengths only. Searches additionally need actual DNA, for example read with optional Biostrings readDNAStringSet() and as.character(), retaining matching IDs. GFF3/BED/GenBank or GRanges annotations follow the format guide, retaining provenance. Handle circular-origin and segmented records according to the reader contract; do not invent one continuous interval by merging every segment.

A progressive case sequence

3.2 builds pUC19 layer by layer; 3.3 explains recognition, primers and restriction sites; 3.4 uses all 13 unique repository plasmids; 3.5 completes the dense pETDuet-1 map. pUC19c is a compatibility alias, not a 14th case.

3.2 From a circle to annotation layers

Focus on the visual information added at each step. Preparation computes inputs; independent layers still control drawing.

Prepare this page

Preparation code — run once on this page
library(ggchord)
feature_palette <- c(
  CDS = "#993366", promoter = "#31849B", rep_origin = "#F1E51D",
  primer_bind = "#A020F0", misc_feature = "#A6ACB3", protein_bind = "#31849B"
)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
plasmid_feature_source <- find_common_features(plasmid_example_pUC19)
# Dedicated primer layers already show bindings; avoid duplicate primer annotations.
plasmid_features <- plasmid_feature_source |>
  filter(type != "primer_bind") |>
  select(accver, start, end, strand, type, anno)
plasmid_primers <- find_primer_bindings(plasmid_example_pUC19, set = "universal")
plasmid_sites <- find_restriction_sites(plasmid_example_pUC19)
display_sites <- filter_restriction_sites(plasmid_sites, enzymes = c("EcoRI", "BamHI", "HindIII"))
primer_names <- primer_catalog(set = "universal")
plasmid_position <- position_feature_stack(spacing = 0.085, base_position = position_plasmid())
p_plasmid_backbone <- ggchord(seq_data = plasmid_seq) +
  geom_seq(seq_style = "double", colour = "#252525") +
  geom_seq_center_label() +
  coord_circular(rotation = 90) +
  theme_ggchord_plasmid() +
  theme(
    panel.background = element_blank(),
    plot.background = element_rect(fill = doc_background, colour = NA)
  )
p_plasmid_features <- p_plasmid_backbone +
  geom_feature_plasmid(data = plasmid_features, position = plasmid_position)
p_plasmid_text <- p_plasmid_features +
  geom_feature_label_repel(
    data = plasmid_features, position = plasmid_position,
    external = TRUE, label_fit = "shrink", label_min_size = 2.5
  )
p_plasmid_text

Step 1: double backbone and centre name

pUC19 step1: double backbone
pUC19 step1: double backbone · Click to enlarge

Check name and full length first. coord_circular() accepts one sequence; the geom owns the double-line style.

Step 2: feature shapes

pUC19 step2: features
pUC19 step2: features · Click to enlarge

position_feature_stack() separates overlaps; geom_feature_plasmid() selects shapes and colours by real category. Compact short-feature shapes do not imply missing data.

Step 3: fitted feature labels

pUC19 step3: labels
pUC19 step3: labels · Click to enlarge

Geometry and labels share a Position. Allow shrinking only above an explicit minimum and external placement when necessary; recheck after changing dimensions.

Step 4: primers and selected restriction sites

plasmid_position <- position_feature_stack(spacing = .10, base_position = position_plasmid())
p_plasmid <- ggchord(seq_data = plasmid_seq) +
  geom_seq(seq_style = "double") +
  geom_feature_plasmid(aes(feature_fill = type), data = plasmid_features, position = plasmid_position) +
  geom_feature_label_repel(
    data = plasmid_features, position = plasmid_position,
    size = 4
  ) +
  geom_primer(data = plasmid_primers, colour = "#A020F0") +
  geom_primer_label_repel(data = plasmid_primers, colour = "#A020F0", max_overlaps = 0, size = 3) +
  geom_restriction_site(data = display_sites, colour = "#29364B", label_size = 3, leader = "radial") +
  geom_seq_center_label() +
  scale_feature_fill_manual(values = feature_palette) +
  scale_seq_position_continuous() +
  coord_circular(rotation = 90) +
  theme_ggchord_plasmid() +
  theme(
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank()
  )
p_plasmid
pUC19 features, exact primer hits and three selected restriction sites
pUC19 features, exact primer hits and three selected restriction sites · Click to enlarge

Only three selected enzyme sites are displayed; the full search table remains available. See 3.3 for recognition provenance and assumptions. Compare annotation densities across 13 references, then finish the dense pETDuet-1 showcase.

3.3 Features, primers and restriction

Search known DNA for primers, restriction sites and common features, then choose a display. Computational matches and experimental validation are different.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

library(ggchord)
library(dplyr)

Restriction interpretation

find_restriction_sites() returns unfiltered matches and cleavage coordinates; filter_restriction_sites() selects a display set. Enzyme site counts refer to the full supplied sequence and remain stable after filtering. methylation = "auto" uses audited context only for exactly recognized bundled references; it does not claim unknown samples are unblocked. This example explicitly uses "none"; this is a search assumption, not a measurement of methylation in a physical sample. geom_restriction_site(leader = "radial") is the default route.

General feature recognition

find_common_features() supports curated exact-reference annotations, DNA and record-specific matching modes. Protein searches use standard-code six-frame translation; they do not implement splicing, alternative initiation or origin-crossing protein matches. Source genetic-code metadata does not select another translation table. For nonstandard codes use DNA matching or external code-aware analysis. Confidence is a heuristic label, not a probability. Use explicit CDS projection for known protein/genome maps.

Follow an annotated reference workflow

This short workflow uses the same source-backed plasmid as the complete map. The complete pUC19 workflow demonstrates catalogue annotations, exact reference matching, site display filtering, primer labels and feature stacking together. Changing only a sequence name does not make it a recognized reference; sequence content determines source-backed recognition. Read the result methods and methylation provenance instead of assigning reference claims to an unknown sample.

Check your result

Inspect match positions, methods and provenance. Restriction filtering is a display choice; methylation search conditions are not a measurement of an unknown sample.

3.4 Thirteen plasmid references

This gallery contains all 13 unique repository FASTA reference records, redrawn with current code. Historical PNGs and .dna files are not these outputs. Vector roles provide learning prompts rather than experimental validation. Bundled records and their generation rules retain source identity and lengths.

Prepare all cases once

Preparation code — run once on this page
library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}
library(ggchord)
library(dplyr)
library(purrr)
# Thirteen unique records; the compatibility spelling pUC19c is not an extra plasmid.
catalog_objects <- c(
  "plasmid_example_pSB1C3", "plasmid_example_pUC19", "plasmid_example_pBR322",
  "plasmid_example_pBluescript_II_SK_plus", "plasmid_example_pET_28a_plus",
  "plasmid_example_pETDuet_1", "plasmid_example_pDONR221",
  "plasmid_example_pcDNA3_1_plus", "plasmid_example_pTRE_Tight_BI",
  "plasmid_example_pTRIPZ", "plasmid_example_pCAMBIA1300",
  "plasmid_example_pEarleyGate_201", "plasmid_example_pSpCas9_BB_2A_GFP_PX458"
)
read_catalog_record <- function(object) {
  target <- new.env(parent = emptyenv())
  data(list = object, package = "ggchord", envir = target)
  target[[object]]
}
catalog_sequences <- map(catalog_objects, read_catalog_record)
catalog_features <- map(catalog_sequences, find_common_features)
catalog_summary <- map2(catalog_sequences, catalog_features, function(sequence, features) {
  tibble(accver = sequence$accver, length = sequence$length, features = nrow(features))
}) |>
  bind_rows()
make_catalog_plot <- function(sequence, features) {
  placement <- position_feature_stack(spacing = 0.085, base_position = position_plasmid())
  ggchord(seq_data = sequence |> select(accver, length)) +
    geom_seq(seq_style = "double", colour = "#252525") +
    geom_feature_plasmid(data = features, position = placement) +
    geom_feature_label_repel(
      data = features, position = placement, external = FALSE,
      label_fit = "shrink", label_min_size = 2.5, max_overlaps = 0
    ) +
    geom_seq_center_label() +
    coord_circular(rotation = 90, fit = "manual", xlim = c(-1.35, 1.35), ylim = c(-1.35, 1.35)) +
    theme_ggchord_plasmid() +
    theme(
      panel.background = element_blank(),
      plot.background = element_rect(fill = doc_background, colour = NA)
    )
}
catalog_plots <- map2(catalog_sequences, catalog_features, make_catalog_plot)
names(catalog_plots) <- catalog_summary$accver
# Save any record at the same size used in the gallery.
# ggsave("pBR322.png", catalog_plots[["pBR322"]], width = 10, height = 8, dpi = 144)

One small function composes independent layers, retaining every recognition result in catalog_features. Actual counts appear below. These maps retain all feature geometry and fit labels inside, allowing labels to hide when space is insufficient; primers and dense restriction annotations are combined in the complete case. Save a named plot with the commented ggsave call.

Inputs and recognition results

Reference Length (bp) Features
pSB1C3 2070 7
pUC19 2686 9
pBR322 4361 6
pBluescript II SK(+) 2961 14
pET-28a(+) 5369 16
pETDuet-1 5420 20
pDONR221 4761 11
pcDNA3.1(+) 5428 13
pTRE-Tight-BI 2856 15
pTRIPZ 13320 33
pCAMBIA1300 8959 17
pEarleyGate 201 11779 21
pSpCas9(BB)-2A-GFP (PX458) 9288 19

pSB1C3: Small vector and basic structure

pSB1C3: source-backed feature map
pSB1C3: source-backed feature map · Click to enlarge

pUC19: Common features and replication origin

pUC19: source-backed feature map
pUC19: source-backed feature map · Click to enlarge

pBR322: Multiple resistance regions and direction

pBR322: source-backed feature map
pBR322: source-backed feature map · Click to enlarge

pBluescript II SK(+): Local regulation and short features

pBluescript II SK(+): source-backed feature map
pBluescript II SK(+): source-backed feature map · Click to enlarge

pET-28a(+): Expression regions and tags

pET-28a(+): source-backed feature map
pET-28a(+): source-backed feature map · Click to enlarge

pETDuet-1: Dual expression units; transition to the dense case

pETDuet-1: source-backed feature map
pETDuet-1: source-backed feature map · Click to enlarge

pDONR221: Recombination-related architecture

pDONR221: source-backed feature map
pDONR221: source-backed feature map · Click to enlarge

pcDNA3.1(+): Annotation hierarchy in an expression vector

pcDNA3.1(+): source-backed feature map
pcDNA3.1(+): source-backed feature map · Click to enlarge

pTRE-Tight-BI: Bidirectional regulatory structure

pTRE-Tight-BI: source-backed feature map
pTRE-Tight-BI: source-backed feature map · Click to enlarge

pTRIPZ: Display density on a longer sequence

pTRIPZ: source-backed feature map
pTRIPZ: source-backed feature map · Click to enlarge

pCAMBIA1300: Architecture of a larger plant vector

pCAMBIA1300: source-backed feature map
pCAMBIA1300: source-backed feature map · Click to enlarge

pEarleyGate 201: Category comparison with another plant vector

pEarleyGate 201: source-backed feature map
pEarleyGate 201: source-backed feature map · Click to enlarge

pSpCas9(BB)-2A-GFP (PX458): Large expression vector and label fitting

pSpCas9(BB)-2A-GFP (PX458): source-backed feature map
pSpCas9(BB)-2A-GFP (PX458): source-backed feature map · Click to enlarge

Next

After comparing densities, continue to 3.5 The complete case, adding separate primer and restriction layers.

3.5 Complete plasmid cases

Build a readable plasmid map from bundled pUC19 DNA, with separate annotation, primer and restriction layers. Keep display choices separate from complete results.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
feature_palette <- c(
  CDS = "#993366", promoter = "#31849B", rep_origin = "#F1E51D",
  primer_bind = "#A020F0", misc_feature = "#A6ACB3", protein_bind = "#31849B"
)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

The preparation above declares the palette and shared appearance.

Use a source-backed sequence

The bundled plasmid_example_pUC19 is copied from the repository’s original examples/plasmid/pUC19.fna; bases and sequence identity are not edited for display. The workflow uses the package’s source-backed recognition catalogues. It needs no network request or experimental measurements fabricated for the figure.

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
plasmid_feature_source <- find_common_features(plasmid_example_pUC19)
# Dedicated primer layers already show bindings; avoid duplicate primer annotations.
plasmid_features <- plasmid_feature_source |>
  filter(type != "primer_bind") |>
  select(accver, start, end, strand, type, anno)
plasmid_primers <- find_primer_bindings(plasmid_example_pUC19, set = "universal")
plasmid_sites <- find_restriction_sites(plasmid_example_pUC19)
display_sites <- filter_restriction_sites(plasmid_sites, enzymes = c("EcoRI", "BamHI", "HindIII"))
primer_names <- primer_catalog(set = "universal")

Inspect plasmid_features, plasmid_primers and display_sites to distinguish complete search results from display choices.

Compose independent layers

Add the backbone, annotations, labels, primers and restriction sites in that order. Features and their labels share one Position; each layer reads its own data.

plasmid_position <- position_feature_stack(spacing = .10, base_position = position_plasmid())
p_plasmid <- ggchord(seq_data = plasmid_seq) +
  geom_seq(seq_style = "double") +
  geom_feature_plasmid(aes(feature_fill = type), data = plasmid_features, position = plasmid_position) +
  geom_feature_label_repel(
    data = plasmid_features, position = plasmid_position,
    size = 4
  ) +
  geom_primer(data = plasmid_primers, colour = "#A020F0") +
  geom_primer_label_repel(data = plasmid_primers, colour = "#A020F0", max_overlaps = 0, size = 3) +
  geom_restriction_site(data = display_sites, colour = "#29364B", label_size = 3, leader = "radial") +
  geom_seq_center_label() +
  scale_feature_fill_manual(values = feature_palette) +
  scale_seq_position_continuous() +
  coord_circular(rotation = 90) +
  theme_ggchord_plasmid() +
  theme(
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank()
  )
p_plasmid
pUC19 features, exact primer hits and three selected restriction sites
pUC19 features, exact primer hits and three selected restriction sites · Click to enlarge
Metric Observed output
Full sequence length (bp) 2686
Annotation rows 7
Exact primer hit records 6
Enzyme match records (not distinct positions) 49845
Restriction records selected for display 3

Mixed exterior annotations share root-ordered placement, including sparse restriction labels beside primers. This example uses the dedicated plasmid theme and default feature fitting: internal text is tried before adjacent/external placement. Inspect at the final export dimensions instead of hiding crossings by shrinking text or forcing offsets.

The original annotation table is retained in plasmid_feature_source. Dedicated primer layers replace duplicate primer_bind annotations in the displayed feature table; only geometry, type and annotation text are passed to feature layers.

Read the annotation layers

The double backbone establishes one circular sequence. General features use a plasmid preset and explicit stacking Position; labels use the same Position. Primer arcs indicate binding ranges, restriction ticks indicate cleavage/display positions, and the centre label identifies the sequence and length. A cropped or filtered display does not change the full-sequence restriction count.

Choose the displayed sites deliberately

find_restriction_sites() returns all matches. This figure explicitly selects EcoRI, BamHI and HindIII so that a compact restriction cluster remains readable; plasmid_sites retains the complete search result. The reference filter uses a saved reference display set only when the complete supplied sequence matches that reference. Other tasks can select unique, six_plus or explicit enzyme names. Inspect display_sites and the site’s methylation/source columns before drawing. The reference set is a presentation choice, not a claim about an unknown sample.

Understand catalogue and label limits

primer_names exposes catalogue entries. This example searches the universal catalogue exactly; reference primer annotations and exact oligonucleotide searches have distinct provenance. Near-exact protein feature recognition has the limitations in recognition; it does not validate biological function or a primer experiment. Text can move, shrink within its explicit minimum, or hide. Keep the geometry and inspect exported label reasons at the final size rather than assuming every annotation must have text.

Adapt the workflow

For your own named DNA sequence, replace the input and inspect search results before adding geoms. For externally curated annotations, pass your feature table instead of running recognition. Use stacking and shapes for geometry, labels for fitting, and output for devices.

geom_primer_label_repel(side = "inside") keeps callouts inside the backbone, even alongside exterior restriction and feature labels. This is corrected in the 0.16.6 release; v0.16.5 can move those requested inside labels outside. The default outside placement and existing source coordinates are unchanged.

Inside restriction labels also retain their inner geometry and now report the inner side in the exported annotation registry; earlier versions mislabelled those diagnostic rows as exterior resources.

The full inside scene can still place long primer text over other feature regions. The default max_overlaps = Inf does not certify zero overlap across feature layers. Prefer the default outside field when space is tight, or use show_location = FALSE to shorten the display; inspect the result at its final size.

Check your result

Start with the centre name and length, then read positions, category colours, arrow directions and outer labels. Check enzyme names in display_sites; selecting a display subset leaves plasmid_sites intact.

Save this result

Save the plot just created to the working directory; use matching dimensions for preview.

ggsave("pUC19-map.png", p_plasmid, width = 10, height = 9, units = "in", dpi = 144)

Complete example: a dense pETDuet-1 map

Use the pUC19 example above to learn the layers, then move to the richer pETDuet-1 map. Inputs come from the bundled sequence, feature and reference-primer catalogues. The restriction subset retains unique six-base-or-longer recognition sites from commercial_all; it is not every cut made by every enzyme.

library(ggchord)
library(dplyr)
data(plasmid_example_pETDuet_1)
show_plasmid <- plasmid_example_pETDuet_1 |>
  select(accver, length)
show_plasmid_features <- find_common_features(plasmid_example_pETDuet_1)
show_plasmid_primers <- find_primer_bindings(plasmid_example_pETDuet_1, set = "reference")
show_plasmid_all_sites <- find_restriction_sites(plasmid_example_pETDuet_1)
show_plasmid_sites <- filter_restriction_sites(
  show_plasmid_all_sites,
  set = "unique_6plus", parent_set = "commercial_all"
)
show_plasmid_position <- position_feature_stack(
  spacing = 0.085, base_position = position_plasmid()
)
p_show_plasmid <- ggchord(show_plasmid, validate = "none") +
  geom_seq(seq_style = "double", colour = "#252525") +
  geom_feature_plasmid(data = show_plasmid_features, position = show_plasmid_position) +
  geom_feature_label_repel(
    data = show_plasmid_features, position = show_plasmid_position,
    external = TRUE, max_overlaps = 0,
    label_fit = "shrink", label_min_size = 2.5
  ) +
  geom_primer(data = show_plasmid_primers, colour = "#A020F0") +
  geom_primer_label_repel(
    data = show_plasmid_primers, colour = "#A020F0", max_overlaps = 0
  ) +
  geom_restriction_site(data = show_plasmid_sites, colour = "#252525") +
  geom_seq_center_label() +
  coord_circular(
    rotation = 90, fit = "manual",
    xlim = c(-3.4, 4.6), ylim = c(-1.6, 2.4)
  ) +
  theme_ggchord_plasmid() +
  theme(
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank()
  )
p_show_plasmid
Dense pETDuet-1 map: features, reference primers and grouped restriction sites
Dense pETDuet-1 map: features, reference primers and grouped restriction sites · Click to enlarge

Inputs contain 20 features, five reference primers and 58 grouped cut positions. The feature preset retains plum coding regions, mint resistance genes, yellow origins and blue regulatory features; primers are purple, consistently across this site. Dense labels depend on export size; open the full image to read them.

The explicit viewport keeps the dense map at the documented export size. label_fit = "shrink" allows short-feature labels to fit down to 2.5 mm; labels that do not fit remain callouts. Preview and save at the same 16.5 × 8 in size.

ggsave("pETDuet-1-map.png", p_show_plasmid, width = 16.5, height = 8, units = "in", dpi = 144)

Prepare tables and coordinates

Turn file observations into plotting roles. Check minimal fields and coordinates, then use validation reports to decide on cleaning; do not guess missing lengths or strands.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed. For file-based examples, first extract the input bundle so the working directory contains examples/.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

Tables

Input Required fields Meaning
Sequence accver, length Unique ID; positive full length in bp
Interval link qaccver, saccver, qstart, qend, sstart, send Query and subject anchors
Gene accver, start, end, strand Known +/-; optional anno
Feature accver, start, end, strand or declared direction May retain unknown strand; optional type, label, feature_id

Identifiers may be arbitrary strings. Accession versions are preserved exactly. pident and alignment length are optional unless a mapping, filter or statistic needs them. A feature interval does not imply a sequence’s full length.

Coordinate contract

Genomic coordinates are one-based, inclusive bp. BED/PAF readers convert their source conventions; do not convert twice. Gene endpoints are ordered with strand stored separately; link endpoint order may encode direction. A one-bp link without direction evidence stays unknown. Unknown strand is never silently changed to +; use geom_feature() instead of the strict gene layer.

Amino-acid coordinates require explicit CDS projection. Native Cartesian layout units, genomic bp, angles in degrees, physical mm and track value units are different domains. See the full data contract (help("ggchord_data_contract", package = "ggchord")).

Import original records

Run this repository-only example from its root. Original files are not rewritten.

seq_files <- read_fasta_lengths(files = "examples/fasta/*.fna", source_file = TRUE)
link_files <- read_blast(files = "examples/blastn-standard/*.tsv", source_file = TRUE)
feature_files <- read_gff3(files = "examples/gff3/*.gff3", unstranded = "keep", source_file = TRUE)

read_genbank() retains record/feature provenance and segmented locations. read_bed() and read_paf() retain their conversion metadata. as_ggchord_seq(), as_ggchord_feature() and as_ggchord_link() adapt supported S4 objects; provide full lengths and IDs explicitly when the object lacks them. file and files both accept path vectors or globs; repeated resolved paths are read once in first-occurrence order. Choose one clear entry for a workflow. Missing sequence or homology data must be supplied by the analyst, not invented by the plotting layer.

Validate, clean, prepare

After this page’s preparation code:

validation <- validate_ggchord_data(seqs, links, genes)
cleaned <- clean_ggchord_data(seqs, links, genes)
prepared <- prepare_ggchord_data(seqs, links, features = list(genes = genes))

Validation reports issues; cleaning makes explicit repairs with a report. Preparation combines workflow steps without inferring biological membership. Inspect validation, cleaned$report and prepared$report before plotting. source_row in geometry refers to the resolved layer input, while .source_line means a physical file line; GenBank has separate record/feature provenance. Aggregates may have source_rows and .length_estimated flags. Preserve these when filtering or merging links.

Inspect a problematic input and its repair report

# Deliberately corrupt copies to demonstrate diagnostics; original rows remain intact.
problem_links <- ribbon_data_example |>
  slice_head(n = 2) |>
  select(qaccver, saccver, qstart, qend, sstart, send, pident) |>
  mutate(
    qaccver = replace(qaccver, 2, "absent"),
    pident = replace(pident, 1, 103)
  )
problem_report <- validate_ggchord_data(seqs, problem_links)
repaired <- clean_ggchord_data(seqs, problem_links, unknown_id = "drop", invalid_pident = "clip")
Metric Observed output
Input rows 2
Rows retained after explicit cleaning 1

The demonstration intentionally includes an unknown ID and identity outside 0–100 in copied source rows. These explicit repairs are not a recommendation to clip scientific observations silently. Inspect problem_report and repaired$report, retain the original table, and choose policies appropriate to actual evidence. A legitimate reverse alignment can keep endpoint order instead of being sorted. prepare_ggchord_data() therefore retains its workflow-specific default policy; standalone cleaning and preparation should not be assumed identical.

Declare genome and contig keys

# Each source record represents a separate complete phage genome.
contigs <- seqs |>
  mutate(genome = accver)
genomes <- tibble(genome = seqs$accver)
hierarchy <- validate_ggchord_hierarchy(contigs, genomes, strict = TRUE)

hierarchy$contigs summarizes declared membership without concatenating contigs. No membership is inferred from an accession prefix. Provide globally unique IDs when separate samples reuse local contig names. Use file formats and adapters for format/optional-object details, and the real-record tutorial to carry reports into a complete figure.

Minimal input and protein alignments

Metric Observed output
Sequence columns passed to the plot 2
Link columns passed to the plot 6
Gene columns passed to the plot 4

The second positional constructor argument is ribbon_data, not gene_data. Use explicit argument names as in the shared examples. Optional identity, length and coverage fields are needed only by operations that consume them. See standard BLAST/protein input and parameter formats.

Check your result

Input IDs must match complete sequence records. Keep original records separate from repaired copies. Decreasing endpoints can be legitimate reverse hits; warnings do not all mean rows must be removed.

Import and convert data

Convert tables, BED, PAF or GenBank into ggchord inputs while retaining record identity and converting coordinate conventions only once.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed. For file-based examples, first extract the input bundle so the working directory contains examples/.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
feature_source <- find_common_features(plasmid_example_pUC19)
features <- feature_source |>
  select(accver, start, end, strand, type, anno)

Adapt an existing table

The preparation above loads the input tables. These examples rename columns of bundled source-backed sequences, links and genes; they do not invent alignments.

renamed_sequences <- seqs |>
  rename(chrom = accver, size = length)
sequence_input <- as_ggchord_seq(renamed_sequences, mapping = c(accver = "chrom", length = "size"))
renamed_features <- genes |>
  rename(chrom = accver, left = start, right = end)
feature_input <- as_ggchord_feature(renamed_features,
  mapping = c(accver = "chrom", start = "left", end = "right")
)
link_input <- as_ggchord_link(links)

Adapters return ordinary frames with ggchord_conversion provenance. IDs remain unchanged. Supply reliable full lengths rather than using maximum feature ends. See alignment input for minimal fields and coordinate units.

BED and PAF: convert coordinates once

The BED example writes known pUC19 intervals as minimal BED3 and reads them back. PAF comes directly from real minimap2 output in the public tutorial, with original optional tags/values. The display retains mapq ≥30 and receives only six endpoint roles.

# BED3 carries intervals only; the source annotations remain in features.
bed_path <- tempfile(fileext = ".bed")
bed_rows <- tibble(accver = features$accver, start0 = features$start - 1L, end = features$end)
write.table(bed_rows, bed_path, sep = "\t", quote = FALSE, row.names = FALSE, col.names = FALSE)
bed_features <- read_bed(bed_path)
unlink(bed_path)
# Unchanged PAF rows from the public EMALE tutorial, not a simulated alignment.
paf_source <- read_paf("examples/format-inputs/emales-pair.paf")
paf_selected <- paf_source |>
  filter(!is.na(mapq), mapq >= 30)
paf_links <- paf_selected |>
  select(qaccver, saccver, qstart, qend, sstart, send)
paf_seqs <- bind_rows(
  transmute(paf_source, accver = qaccver, length = qlen),
  transmute(paf_source, accver = saccver, length = slen)
) |>
  distinct()
p_paf <- ggchord(seq_data = paf_seqs, ribbon_data = paf_links) +
  geom_link_ribbon(fill = "#6BAFA6", alpha = .5) +
  geom_seq(colour = "#233E4A") +
  geom_seq_label(size = 4) +
  coord_collinear() +
  doc_axis() +
  doc_theme
p_paf
A real minimap2 PAF comparison between two EMALE genomes
A real minimap2 PAF comparison between two EMALE genomes · Click to enlarge

The reader converts BED zero-based half-open intervals to one-based inclusive coordinates; do not add one again. BED3 has no strand, so it returns unknown strand; use a strand-bearing format when needed. Negative PAF relative direction becomes decreasing subject endpoints. These are valid reverse hits, not values to sort indiscriminately. Quality/identity use producer fields, not new alignments.

GenBank: retain record identity

# The original NCBI L09137.2 record identifies pUC19c; keep that identity.
genbank_result <- NULL
if (nzchar(Sys.which("python3")) && identical(suppressWarnings(system2("python3",
  c("-c", shQuote("from Bio import SeqIO")),
  stdout = FALSE, stderr = FALSE
)), 0L)) {
  genbank_result <- read_genbank("examples/format-inputs/pUC19c-L09137.2.gb")
}

This is the original NCBI L09137.2 record, named pUC19c; do not rename it to the bundled pUC19. Results include seq_data, features$annotation and report, retaining segmented locations and record/feature provenance. The selected Python needs Biopython already installed; no automatic installation occurs. unsupported = "report" records unsupported locations rather than guessing bounds.

Optional interval objects

range_inputs <- NULL
if (requireNamespace("GenomicRanges", quietly = TRUE)) {
  unknown <- features |>
    filter(strand == ".") |>
    slice_head(n = 1)
  ranges <- GenomicRanges::GRanges(
    seqnames = unknown$accver,
    ranges = IRanges::IRanges(unknown$start, unknown$end), strand = "*",
    seqlengths = setNames(plasmid_seq$length, plasmid_seq$accver)
  )
  range_inputs <- list(seq_data = as_ggchord_seq(ranges), features = as_ggchord_feature(ranges))
}
interaction_inputs <- NULL
if (requireNamespace("GenomicRanges", quietly = TRUE) &&
  requireNamespace("InteractionSet", quietly = TRUE)) {
  source_links <- ribbon_data_example |>
    slice_head(n = 2)
  lengths <- setNames(seq_data_example$length, seq_data_example$accver)
  query <- GenomicRanges::GRanges(
    seqnames = source_links$qaccver,
    ranges = IRanges::IRanges(
      pmin(source_links$qstart, source_links$qend),
      pmax(source_links$qstart, source_links$qend)
    ),
    strand = ifelse(source_links$qstart <= source_links$qend, "+", "-"),
    seqlengths = lengths
  )
  subject <- GenomicRanges::GRanges(
    seqnames = source_links$saccver,
    ranges = IRanges::IRanges(
      pmin(source_links$sstart, source_links$send),
      pmax(source_links$sstart, source_links$send)
    ),
    strand = ifelse(source_links$sstart <= source_links$send, "+", "-"),
    seqlengths = lengths
  )
  interactions <- InteractionSet::GInteractions(query, subject)
  interaction_inputs <- list(
    seq_data = as_ggchord_seq(interactions),
    ribbon_data = as_ggchord_link(interactions)
  )
}
Metric Observed output
GenBank optional reader SKIP: Biopython unavailable
Optional interval adapter PASS
Optional GInteractions adapter PASS

An originally unknown-strand pUC19 annotation remains unknown as *. Known full lengths populate Seqinfo. GInteractions can become links but still needs explicit sequence/length context. Missing dependencies are marked skipped, not validated by source presence. See the data contract (help("ggchord_data_contract", package = "ggchord")). SAM/BAM CIGAR is not parsed directly: use an alignment-aware tool before table adaptation; renaming columns alone cannot resolve splicing.

Check your result

Inspect returned fields and conversion reports before selecting plotting fields. Preserve endpoint direction for reverse PAF hits; do not rename a GenBank record to pretend it is a different bundled plasmid.

Import nucleotide and protein alignments

Use standard BLAST tabular output without the historical custom 17-column layout. Distinguish file fields, minimal plotting roles and nucleotide versus protein units.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed. For file-based examples, first extract the input bundle so the working directory contains examples/.

library(ggchord)
library(dplyr)

Use the default tabular columns

The historical custom list of 17 fields is unnecessary. Use standard tabular output:

blastn -query query.fna -subject subject.fna -outfmt 6 -out result.tsv
blastp -query proteins.faa -subject reference.faa -outfmt 7 -out result.tsv

Format 6 is a table; 7 adds comments. Without a custom field list both use 12 default fields. Omitting -outfmt entirely produces pairwise text, which this tabular reader does not parse. Fields are query ID, subject ID, pident, length, mismatch, gapopen, qstart, qend, sstart, send, evalue, bitscore. Their identifier roles become qaccver/saccver; values are preserved and need not be accession versions. See the NCBI format documentation.

File layout and minimal plotting fields differ

Twelve file columns do not require twelve plotting columns. After the preparation code above, try the source-backed records:

blast_source <- read_blast("examples/blastn-standard/MT108731.1__MT118296.1.tsv")
blast_links <- blast_source |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# A six-column table is sufficient for drawing; extra fields stay in blast_source.
custom_path <- tempfile(fileext = ".tsv")
write.table(blast_links, custom_path, sep = "\t", quote = FALSE, row.names = FALSE, col.names = FALSE)
custom_source <- read_blast(custom_path, format = "custom", col_names = names(blast_links))
custom_links <- custom_source |>
  select(all_of(names(blast_links)))
unlink(custom_path)

blast_source retains statistics and provenance; blast_links has six endpoint roles. Identity requires pident only when mapped/filtered; alignment-length filters or weights require length. qlen/slen support the corresponding coverage calculations. They are not general plotting requirements. A custom headerless layout requires explicit col_names; arbitrary six-column meanings are not guessed. Use as_ggchord_link(x, mapping = ...) for an already-read non-BLAST table; see format conversion.

Files in examples/blastn-standard/ retain historical HSP rows and their first 12 values. They are column conversions, not new BLAST runs. Old .o7 inputs are unchanged; the conversion script and hashes record that distinction.

Read BLASTp with explicit coordinate units

This real BLASTP 2.9.0+ output and query CDS annotation come from the public EMALE tutorial. Retain the original table, then select sufficiently long, low-evalue query HSPs with complete known CDS mappings.

protein_source <- read_blast("examples/format-inputs/emales-mavirus-blastp.tsv", source_file = TRUE)
emale_source <- read_gff3("examples/format-inputs/emales-BVI_023A.gff3", source_file = TRUE)
# The source declares complete Prodigal CDS and translation table 11.
emale_coding <- emale_source |>
  filter(grepl("partial=00", attributes)) |>
  select(feature_id, accver, start, end, strand, phase)
emale_cds <- build_ggchord_cds(emale_coding, genetic_code = 11)
# Keep strong HSPs whose query protein has a known complete CDS.
# Gaps are retained: projecting one query span does not assert an ungapped pair.
protein_kept <- protein_source |>
  filter(qaccver %in% emale_coding$feature_id, evalue <= 1e-10, length >= 50) |>
  arrange(desc(bitscore))
selected_hsp <- protein_kept |>
  slice_head(n = 1)
query_interval <- tibble(
  feature_id = "query_HSP", parent_id = selected_hsp$qaccver,
  start = selected_hsp$qstart, end = selected_hsp$qend
)
projected_query <- project_ggchord_features(query_interval, emale_cds)
# Subject projection additionally requires its own CDS map; it is not guessed from an accession.
Metric Observed output
Original BLASTp HSPs 2948
Complete query CDS records 24
Query HSPs retained by the explicit filter 3
Selected query start (aa) 50
Selected query end (aa) 289
Projected query-span rows 1

BLASTp endpoints are amino-acid coordinates. This example projects the query HSP span into the known CDS’s genomic coordinates, retaining strand and phase. A gapped HSP still has a projectable query span; that envelope is not a base-by-base nucleotide correspondence. Weak hits, gap counts and statistics remain in the source.

Projecting both ends requires the subject’s own CDS map. It cannot be inferred from an accession, maximum endpoint or annotation name. The ungapped = TRUE assertion for project_ggchord_links() needs evidence. The projection tutorial uses two real plasmid CDS with identical oriented DNA to demonstrate a justified ungapped two-ended case.

Native protein backbones with aa axes are outside this version. For blastx and tblastn, check each endpoint’s units independently. Parsing does not establish that a table can be drawn directly in genomic bp coordinates.

Sources and limits

Original EMALE records come from the public gggenomes data/tutorial. Only data records are reused; all figures are drawn with ggchord, without copying upstream figures or plotting code. The input bundle’s sources.json records URLs, archive members, line ranges, hashes and selection. Parsing neither reruns alignments nor establishes biological function.

Check your result

A basic link table needs six fields. BLASTp reading retains aa coordinates; use a supported CDS mapping before drawing on this version’s bp axes.

Project protein intervals to genomic coordinates

Project known protein intervals to genomic positions through real CDS, retaining phase, strand and segmentation explicitly.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
data(plasmid_example_pETDuet_1)
homolog_source <- bind_rows(plasmid_example_pUC19, plasmid_example_pETDuet_1)
homolog_seq <- homolog_source |>
  select(accver, length)
cluster_features <- homolog_source |>
  find_common_features() |>
  filter(type == "CDS", anno == "AmpR") |>
  select(accver, start, end, strand, anno) |>
  arrange(match(accver, homolog_source$accver)) |>
  mutate(feature_id = paste(accver, start, end, sep = ":"))
# Membership is computed from exact strand-corrected CDS DNA equality, not gene names.
reverse_complement <- function(sequence) {
  bases <- strsplit(sequence, "")[[1]]
  chartr("ACGT", "TGCA", paste(rev(bases), collapse = ""))
}
coding_sequences <- cluster_features |>
  left_join(select(homolog_source, accver, sequence), by = "accver") |>
  rowwise() |>
  mutate(
    coding_dna = substr(sequence, start, end),
    coding_dna = if (strand == "-") reverse_complement(coding_dna) else coding_dna
  ) |>
  ungroup()
cds_dna <- pull(coding_sequences, coding_dna)

membership <- tibble(
  feature_id = cluster_features$feature_id,
  cluster_id = paste0("exact_CDS_", match(cds_dna, unique(cds_dna)))
)
homologs <- link_ggchord_clusters(cluster_features, membership,
  pairs = tibble(qaccver = homolog_seq$accver[1], saccver = homolog_seq$accver[2])
)
loci <- find_ggchord_loci(homolog_seq, cluster_features,
  targets = cluster_features$feature_id[cluster_features$anno == "AmpR"][1], upstream = 100, downstream = 100
)

# Compare equivalent local windows instead of shrinking one gene on full plasmids.
homolog_windows <- cluster_features |>
  select(accver, start, end)
local_homologs <- focus_ggchord_data(homolog_seq, homologs$data, cluster_features,
  loci = homolog_windows, expand = 200
)
p_homologs <- ggchord(
  seq_data = local_homologs$seq_data,
  ribbon_data = local_homologs$ribbon_data, gene_data = local_homologs$gene_data
) +
  geom_link_ribbon(fill = "#269B82", alpha = .65) +
  geom_seq(colour = "#233E4A", show.legend = FALSE) +
  geom_feature(
    data = local_homologs$gene_data, feature_fill = "#F1D334",
    position = position_strand(.10), width = .07
  ) +
  geom_feature_label_repel(data = local_homologs$gene_data, size = 4, label_fit = "shrink") +
  geom_seq_label(size = 4) +
  geom_seq_break() +
  coord_collinear() +
  doc_axis("source") +
  doc_theme
p_homologs

Start with source-backed CDS

Run the identical-CDS comparison first. It uses real bundled plasmid annotations and DNA. Complete CDS use explicit phase 0 and translation table 11.

# Run the exact-CDS comparison first; these are complete source-backed CDS.
coding <- cluster_features |>
  mutate(phase = 0)
cds <- build_ggchord_cds(coding, genetic_code = 11)
# This selects residues for a display example; it does not claim a detected domain.
domains <- tibble(feature_id = "selected_residues", parent_id = coding$feature_id[1], start = 2, end = 20)
genomic <- project_ggchord_features(domains, cds)
genomic

This example selects residues 2–20 for display; it does not claim a detected protein domain. Projection returns genomic intervals with strand/segment context. Actual domains require exact parent CDS IDs and amino-acid ranges; annotation names or maximum feature endpoints cannot supply missing evidence.

Segments, phase and partial regions

build_ggchord_cds() constructs a segment map; project_ggchord_features() then splits amino-acid intervals through it. Crossing a boundary can return several rows; multiplying coordinates by three is insufficient. Inspect policies/reports for partial CDS, phase and truncation. DNA matching, interval projection and actual translation/alignment are distinct operations.

Check your result

Check parent CDS IDs, projection reports and returned segments. The selected residue range is a display example, not a detected domain.

Features, stacking and genomic decoration

Draw real annotations as independent layers, choose suitable shapes and share stacking positions between overlapping intervals and text.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
feature_palette <- c(
  CDS = "#993366", promoter = "#31849B", rep_origin = "#F1E51D",
  primer_bind = "#A020F0", misc_feature = "#A6ACB3", protein_bind = "#31849B"
)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
feature_source <- find_common_features(plasmid_example_pUC19)
features <- feature_source |>
  select(accver, start, end, strand, type, anno)

Preserve feature semantics

The preparation above loads the actual pUC19 annotations; the circular example introduces their roles. This table retains actual overlapping pUC19 annotations and unknown strands. Geometry and fill map separately from the same category; an explicit fixed shape would override shape mapping.

stack_features <- features |>
  select(accver, start, end, strand, type, anno)
stack_position <- position_feature_stack(spacing = .12)
feature_shapes <- setNames(ifelse(names(feature_palette) == "CDS", "arrow", "block"), names(feature_palette))
p_stack <- ggchord(seq_data = plasmid_seq) +
  geom_seq(show.legend = FALSE) +
  geom_feature(aes(feature_fill = type, feature_shape = type),
    data = stack_features,
    width = .08, position = stack_position
  ) +
  geom_feature_label_repel(data = stack_features, position = stack_position, size = 3.8, label_fit = "shrink", label_min_size = 2.8) +
  scale_feature_shape_manual(values = feature_shapes) +
  scale_feature_fill_manual(values = feature_palette) +
  coord_collinear() +
  doc_axis() +
  doc_theme
p_stack
Stacked overlapping annotations from the pUC19 reference
Stacked overlapping annotations from the pUC19 reference · Click to enlarge

position_feature_stack() resolves overlapping annotation lanes. The label layer uses the same Position, so text anchors follow those lanes. Prefer unique feature_id values for subset/reordered label inputs. The strict gene shortcut requires known +/-; general features retain unknown/bidirectional semantics.

Choose shapes by meaning

Geometry Typical use Important distinction
arrow, compact_arrow Directional coding/feature interval Known direction is evidence, not a display guess
block, capped_line Region or unfilled interval Shape choice does not infer biological function
promoter_arrow, primer_arrow, primer_arc Declared promoter/primer-style annotation Computational matching and drawing are separate
marker, chevron, lollipop Custom interval/point-like emphasis Keep real interval metadata; display shape is a mapping

geom_gene() and geom_feature_plasmid() are useful presets, not aliases with identical defaults. Use position_strand() for signed offsets and position_plasmid() as a circular placement preset; the latter is not a fourth coordinate mode.

Highlight an interval without rewriting observations

regions <- tibble(
  accver = links$qaccver[1], start = min(links$qstart[1], links$qend[1]),
  end = max(links$qstart[1], links$qend[1]), category = "alignment interval"
)
p_decorated <- ggchord(seq_data = seqs, ribbon_data = links) +
  geom_seq_region(aes(region_fill = category), data = regions, colour = NA) +
  geom_link_ribbon(fill = "#6BAFA6", alpha = .4) +
  geom_seq() +
  geom_seq_label(size = 4) +
  scale_region_fill_manual(values = c("alignment interval" = "#F4D35E")) +
  scale_seq_colour_manual(values = seq_palette) +
  geom_seq_scale(length = 10000) +
  coord_collinear() +
  doc_axis() +
  doc_theme
p_decorated
Highlighting the query interval of an observed alignment
Highlighting the query interval of an observed alignment · Click to enlarge

A region overlay uses its own table and region_fill scale. geom_seq_label() names sequences, geom_seq_center_label() supplies name/length in a single circle, and geom_seq_break() marks cropped/segmented ends. geom_seq_scale() is an independent bp reference bar for collinear plots; it differs from scale_seq_position_continuous(), which controls genomic axes on sequences. An ordinary ggplot2 annotation should use position mapping instead of treating bp as Cartesian x/y.

Check your result

Check intervals and strands before shapes and labels. Unknown strands remain in general features; stacking changes display placement, not genomic coordinates.

Labels and local placement

Arrange text while retaining annotation geometry: choose export dimensions and a label subset before choosing automatic or fixed placement.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

Select a layout

Gene labels offer "auto", "radial" and "arc" layouts. General feature labels additionally support the internal-first "feature" default and external "callout" layout. Choose the documented values for the layer you use; they are not interchangeable presets.

After this page’s preparation code:

label_data <- gene_data_example |>
  filter(anno != "hypothetical protein") |>
  transmute(accver, start, end, strand = source_strand, anno)
p_labels <- ggchord(seq_data = seqs, gene_data = genes) +
  geom_seq() +
  geom_gene() +
  geom_gene_label_repel(data = label_data, label_layout = "auto", size = 4, label_wrap = 22) +
  geom_seq_label(size = 4) +
  coord_collinear() +
  doc_axis() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  doc_theme
p_labels
Automatic placement of informative source gene annotations
Automatic placement of informative source gene annotations · Click to enlarge

Fixed and automatic placement

Use geom_gene_label() or geom_feature_label() for manual label_rotation, radial/circumferential offsets and orientation controls. Automatic layers reject these manual controls: choose label_layout or remove the manual argument. Use size, colour and family for supported fixed text styles.

label_fit, label_wrap, label_max_lines and label_min_size control fitting where supported. Shrinking is explicit and bounded. Unresolved labels may move or hide; the feature geometry remains. Inspect label_fit_diagnostics and label_diagnostics at the final export size.

Match anchors across layers

In collinear mode, an omitted feature-label Position inherits an unambiguous matching feature layer. Unique feature_id permits reordering or subsets, but accession, interval and strand must agree. Multiple matching layers need an explicit Position or unambiguous IDs. Preview after selecting the final device size; resizing the Plots pane changes the available space.

Compare the three gene layouts

# Compare layouts with at most the first and last informative label per sequence.
comparison_labels <- label_data |>
  group_by(accver) |>
  filter(row_number() %in% c(1L, n())) |>
  ungroup()
label_plots <- lapply(c("auto", "radial", "arc"), function(layout) {
  ggchord(seq_data = seqs, gene_data = genes) +
    geom_seq() +
    geom_gene() +
    geom_gene_label_repel(data = comparison_labels, label_layout = layout, size = 4, label_wrap = 22) +
    geom_seq_label(size = 4) +
    scale_seq_colour_manual(values = seq_palette, name = "Sequence",
      guide = guide_ggchord_legend(nrow = 2, theme = doc_legend_theme)) +
    scale_gene_fill_manual(values = gene_palette, breaks = c("+", "-"), name = "Strand", guide = strand_guide) +
    coord_chord(xlim = c(-1.8, 1.8), ylim = c(-1.6, 1.6)) +
    theme_ggchord_publication(axis = element_blank()) +
    doc_legend_theme +
    theme(
      legend.position = "bottom",
      plot.background = element_rect(fill = doc_background, colour = NA),
      panel.background = element_blank()
    ) +
    labs(title = layout)
})
names(label_plots) <- c("auto", "radial", "arc")
p_manual_labels <- ggchord(seq_data = seqs, gene_data = genes) +
  geom_seq() +
  geom_gene() +
  geom_gene_label(data = label_data, label_rotation = 30) +
  coord_collinear()
label_plots$auto
Informative gene annotations with automatic side placement
Informative gene annotations with automatic side placement · Click to enlarge
The same gene annotations with radial placement
The same gene annotations with radial placement · Click to enlarge
The same gene annotations with arc-oriented text
The same gene annotations with arc-oriented text · Click to enlarge

Keep at most the first and last informative labels per sequence, retaining all gene arrows, to compare text orientation. Print label_plots$auto, $radial or $arc to inspect a candidate. The inputs and requested font size are identical; only the layout choice changes. Auto uses shared columns where applicable, radial follows local contours and arc follows the sequence tangent. p_manual_labels shows the separate fixed-label route; its explicit rotation does not belong in an automatic repel layer. General feature labels have additional internal-first/callout choices; see the complete circular workflow for dense mixed annotation and stacking for inherited or explicit Position anchors.

Check your result

The three layout comparisons share the same input. Assess text direction, leaders and hiding reasons together; missing text does not mean a feature was lost.

Quantitative tracks

Compute GC fractions from real DNA, then add bars, a moving mean and an annotation band. Each track has an explicit unit, window and baseline.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
feature_palette <- c(
  CDS = "#993366", promoter = "#31849B", rep_origin = "#F1E51D",
  primer_bind = "#A020F0", misc_feature = "#A6ACB3", protein_bind = "#31849B"
)

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
feature_source <- find_common_features(plasmid_example_pUC19)
features <- feature_source |>
  select(accver, start, end, strand, type, anno)

Compute a track from actual DNA

After this page’s preparation code, compute GC fraction in 100 bp windows of bundled pUC19. The final shorter window uses its actual base count.

library(dplyr)
data(plasmid_example_pUC19)
track_seq <- plasmid_example_pUC19 |>
  select(accver, length)
starts <- seq.int(1L, track_seq$length, by = 100L)
gc_fraction <- function(sequence) {
  bases <- strsplit(sequence, "")[[1]]
  mean(bases %in% c("G", "C"))
}
bins <- tibble(
  accver = track_seq$accver,
  start = starts,
  end = pmin(starts + 99L, track_seq$length)
) |>
  rowwise() |>
  mutate(gc = gc_fraction(substr(plasmid_example_pUC19$sequence, start, end))) |>
  ungroup()
head(bins)

Each row of bins is an actual sequence window. Check its endpoints and gc, then map values to track height and colour.

Draw GC fractions

p_tracks <- ggchord(seq_data = track_seq) +
  geom_seq() +
  geom_seq_center_label() +
  geom_track_bar(aes(track_value = gc, track_fill = gc),
    data = bins,
    position = position_track("gc", width = unit(10, "mm"))
  ) +
  scale_track_value_continuous("gc", limits = c(0, 1), unit = "GC fraction") +
  scale_track_fill_gradientn("gc", colours = c("#E4F3EA", "#65B79B", "#27775C")) +
  coord_circular() +
  doc_theme
p_tracks
GC fractions computed from the bundled pUC19 DNA
GC fractions computed from the bundled pUC19 DNA · Click to enlarge

The domain is 0–1, in fractions. Replace it with the actual unit/range of other measurements. position_track() controls placement/physical width; scale_track_value_continuous() controls numerical mapping.

Share an ID deliberately

Same-ID layers share a numerical domain, including bar baselines, and must agree on width/placement. Colour scales are scoped independently by track ID. Tiles show interval categories, lines connect observations, bars encode interval values relative to a baseline. Sharing a domain does not require sharing a spatial band.

Inspect retention and limits

The export’s track_registry and track_diagnostics record domains, physical sizes, native offsets and clipping. Omitted/clipped rows do not mean zero values. Focus tracks with focus_ggchord_track() using the same windows.

Combine bars, a moving mean and annotation bands

Run the circular example to obtain real features first.

line_bins <- bins |>
  transmute(
    accver,
    pos = floor((start + end) / 2),
    gc_mean = as.numeric(stats::filter(gc, rep(1 / 3, 3), sides = 2))
  ) |>
  filter(is.finite(gc_mean))
categories <- features |>
  select(accver, start, end, type)
p_multitrack <- ggchord(seq_data = track_seq) +
  geom_seq() +
  geom_seq_center_label() +
  geom_track_bar(aes(track_value = gc, track_fill = gc),
    data = bins,
    position = position_track("gc", width = unit(10, "mm"))
  ) +
  geom_track_line(aes(track_value = gc_mean),
    data = line_bins,
    position = position_track("gc", width = unit(10, "mm")), colour = "#233E4A"
  ) +
  scale_track_value_continuous("gc", limits = c(0, 1), unit = "GC fraction") +
  scale_track_fill_gradientn("gc", colours = c("#E4F3EA", "#65B79B", "#27775C")) +
  geom_track_tile(aes(track_fill = type),
    data = categories,
    position = position_track("features", order = 2, width = unit(2, "mm"))
  ) +
  scale_track_fill_manual("features", values = feature_palette) +
  coord_circular() +
  doc_theme +
  theme(legend.box = "vertical", legend.box.just = "left")
focused_bins <- focus_ggchord_track(bins, track_seq,
  loci = tibble(accver = track_seq$accver, start = 500, end = 1500), type = "interval"
)
p_multitrack
pUC19 GC fractions, a three-bin moving mean and annotation bands
pUC19 GC fractions, a three-bin moving mean and annotation bands · Click to enlarge

Bars and line use the same actual GC measurements; the line is a three-window moving mean. Incomplete end windows are omitted, not filled with invented values. Line positions use integer bin midpoints. Annotation bands have an independent ID and palette. Bands are explicitly ordered outside; overly wide inner bands at small radii can fold and are rejected.

Check your result

GC in bins lies between 0 and 1; each row is an actual window and the last may be shorter than 100 bp. Continuous colour expresses values; the category band expresses feature types, not GC magnitude.

Save this result

Save the plot just created to the working directory; use matching dimensions for preview.

ggsave("pUC19-tracks.png", p_multitrack, width = 9, height = 8, units = "in", dpi = 144)

Mappings, scales, guides and themes

Give colours a clear meaning: identify categories or continuous quantities, then choose mappings, scales and guides. Website accents do not dictate data colours.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

Build styles from data roles

The preparation above declares the inputs and shared appearance. The example declares categories explicitly: ribbon fill maps query ID; sequence colour maps sequence ID; gene fill maps strand. Manual scale names match those values exactly.

p_styled <- ggchord(seq_data = seqs, ribbon_data = links, gene_data = genes) +
  geom_seq() +
  geom_link_ribbon(aes(ribbon_fill = qaccver), alpha = .5) +
  geom_gene() +
  geom_seq_label(size = 4) +
  coord_collinear() +
  doc_axis() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  scale_ribbon_fill_manual(values = seq_palette) +
  guides(gene_fill = guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)) +
  doc_theme
theme_variants <- list(minimal = theme_ggchord_minimal(), dark = theme_ggchord_dark(), plasmid = theme_ggchord_plasmid())
p_styled
Explicit sequence, strand and query-category colour scales
Explicit sequence, strand and query-category colour scales · Click to enlarge
Role Mapped data Useful scale family
seq_colour, seq_ring Sequence/category Sequence colour or ring manual scales
ribbon_fill, ribbon_alpha, ribbon_colour, ribbon_linetype Alignment metadata or statistics Manual, gradient/steps/identity and alpha scales
feature_fill, feature_shape, gene_fill, region_fill Annotation category or strand Role-specific manual/preset scales
link_colour, link_alpha, link_linewidth, link_linetype Point-link metadata Continuous or manual link scales
track_value, track_fill, track_colour Values/categories in one named track Named value domain and colour/fill scales

Fixed styles and user scale priority

Put a constant style outside aes(). Map variable styles inside it and supply an appropriate scale. An explicitly fixed geom value takes precedence; omitting that argument keeps mapping active. Your scale takes priority over automatic defaults. Repeated same-kind layers keep independent data/mapping/parameters. A geometric vector named by accver does not mean a scale also accepts per-sequence lists.

Make legends readable

guide_ggchord_legend() controls discrete role legends; guide_ggchord_colourbar() controls continuous colour guides. The colour/color spelling aliases are retained, with colour preferred in this documentation. Use show.legend on a layer, guides() for guide choices and theme(legend.position = ...) for placement. A wide guide may need multiple rows or a wider export, not smaller biological data.

Select a theme for the scene

The example stores minimal, dark and plasmid alternatives in theme_variants. Add the appropriate object to the matching plot. theme_ggchord_publication() is used above; the plasmid preset belongs with the circular workflow. These objects style non-data appearance. They do not choose a coordinate system or replace data-driven fill scales. Use theme(text = element_text(...)) for text appearance; removed base_size constructor-style shortcuts have a migration route.

Check your result

Links from the same query share a colour; continuous identity needs a continuous scale. Put fixed colours outside aes() and make each guide explain its mapping.

Set values by sequence and strand

Adjust geometry by sequence or strand, retaining useful flexible parameter formats while making their scope explicit.

Previously described as “flexible parameter formats”, this feature configures geometry per sequence or strand.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

The capability remains supported

Sequence-local geometry often needs one value per sequence or strand; duplicating whole layers would obscure that intent. Keep scalars as the default, use vectors named by accver when values differ, and name strand pairs +/-.

# Sequence IDs remain stable when the data is reordered; positional lists do not.
radii <- setNames(c(1.1, 1, 1, 1), seqs$accver)
widths <- c("+" = .045, "-" = .065)
p_parameters <- ggchord(seq_data = seqs, gene_data = genes) +
  geom_seq(seq_radius = radii) +
  geom_gene(
    width = widths,
    position = position_strand(c("+" = .08, "-" = -.1))
  ) +
  geom_seq_label(size = 4) +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  coord_chord() +
  doc_theme
p_parameters

Run this page’s preparation code first. Named radii survive sequence reordering; unnamed entries follow current positions. These are layout/geometry values, not a universal extension to colours or themes.

Supported parameters

Interface Parameters Matching scope
geom_seq() seq_radius, seq_curvature, seq_gap, seq_orientation Sequence; radius/curvature effects depend on coordinates
geom_seq_label() labels, seq_label_radius, seq_label_rotation, size, seq_label_hjust, seq_label_vjust Sequence; seq_label_orientation is one mode choice
geom_link_ribbon() / geom_link_line() link_gap Endpoint sequence; Cartesian control points are not per-sequence scalars
geom_gene() / geom_feature() width Sequence and/or +/- strand
position_strand() / position_plasmid() offset Sequence/strand; prefer Positions over compatibility offset inputs
Fixed geom_gene_label() / geom_feature_label() label_radial_offset, label_circum_offset, label_circum_limit, label_rotation Sequence/strand, not every text setting
position_track() Numeric width, gap Native numeric placement by sequence; physical unit() is a scalar absolute size

seq_order is itself an ordering. Automatic label_layout is a mode; manual angles and offsets belong to fixed label layers rather than automatic-label .... Axes/themes, aesthetics, scales and guides do not inherit the list extension. Manual scales do accept vectors named by data categories, under ordinary ggplot2 scale semantics; see styling.

Compatibility and design tradeoffs

Equal-length unnamed vectors, sequence-named lists, position-named lists and unnamed lists remain compatible. Gene/feature entries can additionally contain strand pairs. Each parameter still validates its own type, length, range and units. This does not make a list a valid container for any argument.

The v0.16.6 RC audit retains these equivalent forms: no deletion or deprecation is announced. Positional forms follow the current displayed sequence order after filtering; names take precedence when they match accession IDs. In width/fixed-label lists, a per-sequence entry is a strand pair; list(value) is the scalar recycling form. Position offsets also accept scalar entries per sequence. Explicit strand-named Position offsets are signed; an omitted strand or sequence uses zero. position_plasmid() uses one shared offset and rejects strand pairs. Use unique keys: accession IDs "1", "+" or "-" can be ambiguous with index/strand shorthand, so prefer sequence-named nested lists for strand values. Native track width/gap must cover all displayed sequences. These rules do not extend to themes.

Numeric values do not all share one unit: seq_gap is a proportion, rotations are degrees, and physical track sizes use explicit unit() values. seq_backbone_gap and seq_backbone_width are scalar style controls, not members of the list rule. Automatic collinear orientation belongs to coord_collinear(orientation = "auto"); geom_seq(seq_orientation = ...) supplies numerical +1/-1 sequence directions.

Check your result

Named-vector keys must match actual accver values or +/− strands. Recheck matching after reordering; list rules do not extend to themes, scales or aesthetics.

Common adjustments and grammar

Put common adjustments in the appropriate ggplot2 component: mappings, geometry, positions, coordinates and themes have distinct roles.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

Assign each responsibility

Task Interface
Map a column to an aesthetic aes()
Define mapping and guide scale_*(), guides()
Geometry and fixed style geom_*()
Local placement position_*()
Global coordinate system coord_*()
Non-data appearance theme_*(), theme()
Titles and captions labs()

Fixed styles may be supplied directly to geoms. Data-driven variation needs a mapping and scale. An explicit fixed value overrides its mapping; omitting the fixed argument preserves the mapping. User scales override inferred defaults. shape is the fixed feature geometry parameter; feature_shape remains the mapped role. Explicit shape = NULL is invalid, not a request to use a mapping.

Map link identity

After this page’s preparation code:

identity_links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send, pident)
p_mapped <- ggchord(seq_data = seqs, ribbon_data = identity_links) +
  geom_seq() +
  geom_link_ribbon(aes(ribbon_fill = pident), alpha = .6) +
  scale_ribbon_fill_gradientn(
    colours = identity_palette, values = identity_values,
    limits = c(0, 100), breaks = identity_breaks, name = "Identity (%)", guide = identity_guide
  ) +
  scale_seq_colour_manual(values = seq_palette) +
  coord_chord(rotation = 20) +
  labs(title = "Sequence comparison") +
  doc_theme
p_mapped

Place features

Use position_strand(c("+" = .1, "-" = -.2)) for signed strand offsets. position_feature_stack() handles overlapping features; position_track() allocates a named quantitative lane. Geometric sequence parameters accept one value, a matching unnamed vector, or a vector named by accver; named vectors are recommended. This syntax does not extend to scales, guides or themes.

Each layer owns its own data, mapping and parameters. Repeated feature or link layers do not replace one another. Use ordinary ggplot2 show.legend and inherit.aes; role input mappings precede geometry, while staged visual mappings follow ggplot2 evaluation. See the API contract (help("ggchord_api_contract", package = "ggchord")).

Check your result

Use a coordinate for global rotation, named values for per-sequence geometry, and mappings plus scales for data-dependent colour.

Export, preview and diagnostics

Inspect figures at their actual export dimensions, save with standard ggsave(), and understand what layout tables and diagnostics establish.

Prepare this page

In a fresh R session, run the code below, then follow this page in order. Colours and layout are optional display choices; inputs select only the fields needed.

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

# Optional appearance choices; none of these columns or scales is required input.
library(ggchord)
seq_palette <- setNames(
  c("#E64B35", "#233E4A", "#269B82", "#D59B17"),
  c("MT108731.1", "MT118296.1", "OQ646790.1", "OR222515.1")
)
gene_palette <- c("+" = "#CE1536", "-" = "#346B9D", "." = "#8291A6")

library(ggchord)
doc_background <- "#FFFCF5"
identity_palette <- c("#303B71", "#36537D", "#269B82", "#A5D52C", "#ECEB31")
identity_values <- c(0, 0.5, 0.8, 0.9, 1)
identity_breaks <- c(0, 50, 80, 90, 100)
doc_legend_theme <- theme(
  legend.title = element_text(size = 13, face = "bold"),
  legend.text = element_text(size = 11),
  legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
  legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
  legend.margin = margin(l = 4, r = 4, unit = "mm")
)
sequence_guide <- guide_ggchord_legend(nrow = 1, theme = doc_legend_theme)
strand_guide <- guide_ggchord_legend(
  nrow = 1, override.aes = list(strand = c("+", "-")), theme = doc_legend_theme
)
identity_guide <- guide_ggchord_colourbar(
  position = "left", direction = "vertical",
  theme = theme(
    legend.key.height = unit(75, "mm"), legend.key.width = unit(5, "mm"),
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11), legend.margin = margin(r = 7, unit = "mm")
  )
)
doc_theme <- theme_ggchord_publication(
  axis.text = element_text(size = 11),
  axis.text.check.overlap = TRUE
) +
  theme(
    legend.position = "bottom",
    legend.title = element_text(size = 13, face = "bold"),
    legend.text = element_text(size = 11),
    legend.key.width = unit(9, "mm"), legend.key.height = unit(5, "mm"),
    legend.spacing.x = unit(9, "mm"), legend.spacing.y = unit(7, "mm"),
    legend.box.spacing = unit(6, "mm"),
    plot.background = element_rect(fill = doc_background, colour = NA),
    panel.background = element_blank(),
    plot.title = element_text(face = "bold", size = 18),
    plot.subtitle = element_text(size = 10, colour = "#5C625A"),
    plot.margin = margin(14, 18, 14, 18)
  )
doc_axis <- function(coordinates = "relative") {
  scale_seq_position_continuous(
    coordinates = coordinates, n.breaks = 3, minor_breaks = NULL,
    labels = function(x) format(x, scientific = FALSE, big.mark = ",", trim = TRUE)
  )
}

anchors <- genes |>
  group_by(accver) |>
  slice_max(end - start, n = 1, with_ties = FALSE) |>
  ungroup() |>
  select(accver, start, end)
p_collinear <- ggchord(seq_data = seqs, ribbon_data = links, gene_data = genes) +
  geom_link_ribbon(aes(ribbon_fill = qaccver), alpha = .7) +
  scale_ribbon_fill_manual(values = seq_palette, guide = "none") +
  geom_seq() +
  geom_gene(position = position_strand(.1)) +
  geom_seq_label(size = 4) +
  coord_collinear(anchor = anchors, align = "center", orientation = "auto") +
  doc_axis() +
  scale_seq_colour_manual(values = seq_palette) +
  scale_gene_fill_manual(values = gene_palette) +
  doc_theme
p_collinear

Save the intended size

After creating p_collinear in three modes:

ggsave("comparison.png", p_collinear, width = 8, height = 5, units = "in", dpi = 144)

RStudio’s Plots pane uses the current panel size and can rearrange labels and guides. Preview at the same explicit dimensions before final ggsave():

view_ggchord(p_collinear, width = 8, height = 5, units = "in")
diagnosis <- diagnose_ggchord(p_collinear, width = 8, height = 5, units = "in")

view_ggchord() returns a temporary image path; it does not keep a final export. The website, README and vignettes share PNG assets exported through standard ggsave() at sizes listed in the gallery. They do not capture an interactively resized panel. Use your intended PDF/SVG device explicitly for final vector output; font availability and embedding remain device-dependent.

Consume geometry by name

geometry <- export_ggchord_layout(p_collinear, include = "seq")
rows <- geometry$seq |>
  distinct(layer_id, component, source_row)

export_ggchord_layout() is the recommended layer-aware table interface. get_chord_layout() retains the complete documented layout; neither is deprecated. Select named fields, not fixed column positions. source_row indexes resolved layer input; IDs are build-local resources, not permanent biological identifiers. See the complete layout contract (help("ggchord_layout_contract", package = "ggchord")).

Interpret evidence

diagnose_ggchord() separates measured panel size from geometry convergence. A measured panel does not certify collision-free text, font embedding or all applications. Inspect warnings, text/guide measurements and placement reasons. optimize_ggchord_layout() applies documented opt-in adjustments; retain a copy and inspect the result. Previews and diagnostics use independent render copies. Direct mutation of shared ggproto layer environments retains ordinary ggplot2 reference semantics; it is not an automatically isolated edit.

Complete layout, tables and opt-in ordering

full_layout <- get_chord_layout(p_collinear)
layer_tables <- export_ggchord_layout(p_collinear, include = c("seq", "gene"), original_data = TRUE)
layout_choice <- optimize_ggchord_layout(seqs, links, optimize = c("order", "orientation"), weight = "count")

Create p_collinear in three modes first. full_layout retains the complete documented getter result. layer_tables selects sequence/gene tables and resolved original data; it is not a raw-file archive. Layer IDs and source-row roles distinguish independent same-kind layers. Empty components can lack a fixed column prototype. Select fields by name.

layout_choice optimizes declared order/orientation using count weighting because the minimal plotting table deliberately omits alignment-length statistics. It returns a choice for explicit use in sequence settings; it does not mutate the plot or establish biological orientation. Save the choice and compare the result before adopting it. The export asset manifest (website/assets/plots.csv in the source repository) records exact documentation sizes.

Check your result

Use matching dimensions for preview and saving. Inspect text, guides and warnings; a successful measurement does not certify fonts or release quality across devices.

Articles and complete workflows

Learn progressively inside a module, then consult shared techniques as needed. Each path starts with tables and ends with a complete showcase.

Capabilities and runnable evidence

Capability Read Executable source / output
Three coordinate modes Choose your learning module chord, circular, collinear · chord, circular, collinear
Original-record comparisons 1.6 Full source-file workflow realimport, realprepare, realchord, reallinear · realchord, reallinear
Complete circular annotation 3.5 Complete plasmid cases plasmid · plasmid
Validation, cleaning and hierarchy Prepare tables and coordinates validation, cleaning, hierarchy
Formats and adapters Import and convert data, Import nucleotide and protein alignments adapters, formats, genbank, optionalranges, standardblast, protein · paf
Link preparation and statistics 1.5 Filter and summarize links ribbons · density
Interval and point links 1.5 Filter and summarize links, Choose your learning module pointlinks, collinear · pointlinks
Features and Position Features, stacking and genomic decoration featurestack · featurestack
Automatic and fixed labels Labels and local placement, 3.5 Complete plasmid cases labellayouts, labels, plasmid · labelauto, labelradial, labelarc
Axes, scale bars and regions Features, stacking and genomic decoration, 2.5 Focus, anchors and segments decorations, focus · decorations
Quantitative tracks and domains Quantitative tracks tracks, multitrack · multitrack
Primers, sites and feature search 3.3 Features, primers and restriction, 3.5 Complete plasmid cases recognition, plasmid · plasmid
CDS and link projection Project protein intervals to genomic coordinates, 2.4 Compare CDS neighbourhoods projection, linkprojection
Explicit clusters and loci 2.4 Compare CDS neighbourhoods homologs · homologs
Focus, segments and source mapping 2.5 Focus, anchors and segments focus, segment, mappingposition · focus
Scales, guides and themes Mappings, scales, guides and themes, Common adjustments and grammar, Set values by sequence and strand styling, adjustments, parameters · styling
Preview, results and ordering Export, preview and diagnostics save, preview, provenance, layoutresults
Migration and FAQ Migrate to the candidate API, Frequently asked questions migration

Frequently asked questions

Why are labels different after resizing?

The available physical space changed. Use explicit-size preview and saving. Check hidden-label diagnostics; do not infer missing input rows from absent text.

Why did an old parameter stop working?

The API cleanup removed execution of old aliases. Use the migration table. Do not mix old and new spellings for one semantic setting. The colour/color function spelling aliases remain, with colour preferred in documentation.

Why is a feature rejected as a gene?

The gene shortcut requires known +/- strand. Keep unknown strand in a general feature layer; do not guess +. Amino-acid intervals require explicit projection.

Why is a ribbon missing in linear mode?

By default ribbons connect adjacent displayed sequences. Inspect pair selection, clipping and validation. Use explicit pairs or adjacent_only = FALSE deliberately. This default does not remove original input records.

Can the package compute alignments or validate primer experiments?

It visualizes supplied alignments and computational search results. It does not run BLAST, infer clusters from names or validate wet-lab outcomes. Search confidence is not a probability. See recognition limitations.

Is a measured diagnostic a release certification?

No. It describes a requested render. Platform/device/font checks, full vignettes and package checks belong to an explicitly authorized release. See the release acceptance record for the actual scope of verification.

Where should I report a problem?

Use the issue tracker with a small input, package/ggplot2 versions, code, intended width/height, device and diagnostic output. Avoid attaching private sequence data; a synthetic reproduction is usually enough.

Migrate to the candidate API

Scope and timing

These are current candidate replacements, not a v1.0.0 compatibility promise. The 0.16.0–0.16.4 phases were delivered together in v0.16.5. The current installation tag is v0.16.6. The RC audit retains flexible parameter formats, spelling aliases, reexports and getter/export entry points without a new deprecation. Old calls in this table explain migration and must not be executed as current examples. Exact first-removal tags are not guessed where historical evidence gives only an upper bound.

Replacement table

Old (do not run) Current Removal / guard Meaning
geom_ribbon() geom_link_ribbon() ≤ 0.16.1 Renamed link layer; not ggplot2::geom_ribbon().
ggchord(..., rotation = 20) ggchord(...) + coord_chord(rotation = 20) ≤ 0.16.1 Global coordinates are composed with +.
geom_feature(feature_shape = "block", feature_width = .05) geom_feature(shape = "block", width = .05) 0.16.1 Mapped feature_shape is retained.
geom_gene(gene_offset = c("+" = .1, "-" = .2)) geom_gene(position = position_strand(c("+" = .1, "-" = -.2))) 0.16.1 Explicit Position values are signed.
geom_feature(feature_offset = .1) geom_feature(position = position_strand(.1)) 0.16.1 Use a Position object.
geom_gene(gene_width = .05) geom_gene(width = .05) 0.16.1 Fixed geometry uses the shared width name.
geom_link_ribbon(ribbon_gap = .01) geom_link_ribbon(link_gap = .01) 0.16.1 Use link_gap; likewise use link_ctrl_point.
geom_gene_label_repel(gene_label_layout = "aligned") geom_gene_label_repel(label_layout = "auto") 0.16.1 Migrate both the name and value.
geom_gene_label_repel(label_rotation = 90) geom_gene_label(label_rotation = 90) 0.16.4 guard Previously ignored manual control now errors; alternatively remove it.
geom_feature_label_repel(label_orientation = "horizontal") geom_feature_label(label_orientation = "horizontal") 0.16.4 guard Use the fixed feature-label layer for unknown-strand features.
geom_restriction_site(leader = "trunk") geom_restriction_site(leader = "radial") 0.16.1 The radial default remains.
theme_ggchord(base_size = 10) theme_ggchord() + theme(text = element_text(size = 10)) ≤ 0.16.1 Text appearance belongs to theme elements.

Run the replacement

In a fresh session, run this preparation before the current replacements:

Preparation code — run once on this page
library(ggchord)
library(dplyr)
data(seq_data_example)
data(ribbon_data_example)
data(gene_data_example)
seqs <- seq_data_example |>
  select(accver, length)
links <- ribbon_data_example |>
  select(qaccver, saccver, qstart, qend, sstart, send)
# The fixture preserves biological strands separately from its old display strands.
genes <- gene_data_example |>
  transmute(accver, start, end, strand = source_strand)

library(dplyr)
data(plasmid_example_pUC19)
plasmid_seq <- plasmid_example_pUC19 |>
  select(accver, length)
feature_source <- find_common_features(plasmid_example_pUC19)
features <- feature_source |>
  select(accver, start, end, strand, type, anno)
p_migrated <- ggchord(plasmid_seq) +
  geom_seq() +
  geom_feature(data = features, shape = "block", width = .05) +
  geom_feature_label(data = features, label_rotation = 0) +
  coord_circular()
p_offsets <- ggchord(seqs, gene_data = genes) +
  geom_seq() +
  geom_gene(position = position_strand(c("+" = .1, "-" = -.2)))

Old minus-strand offset magnitudes need a negative sign in explicit Position values. Mapping still uses aes(feature_shape = type) with a shape scale; the fixed parameter is shape. Automatic labels use label_layout; manual geometry uses the corresponding fixed-label layer. label_layout = "aligned" becomes "auto". Removed names are errors, not silently retained execution paths.

What remains supported

The five colour/color spelling aliases, all upstream reexports, gene/feature/plasmid shortcuts and both geometry getter/export interfaces remain. Coordinate-specific defaults and already documented result fields are retained. See the API contract (help("ggchord_api_contract", package = "ggchord")) and current reference for NULL/NA/missing semantics; NULL does not universally mean disabled.

Terminology and design sources

Shared terminology

English 现代汉语 API / units
sequence 序列 accver / length
chord diagram 弦图 coord_chord()
circular sequence map 单序列圆图 coord_circular()
collinearity plot 共线性图 coord_collinear()
interval link / ribbon 区间连接 / 连接带 qaccver / saccver
feature 通用注释特征(feature) geom_feature()
gene 已知链的基因 geom_gene()
strand 链方向 + / - / unknown
aesthetic mapping 美学属性映射 aes()
scale 标度 scale_*()
guide 图例 / 标度指南 guides()
position 局部位置调整 position_*()
track 定量轨道 position_track()
genomic position 基因组位置 one-based inclusive bp / 一基闭区间
layout units 原生布局单位 not bp or mm / 不等于 bp 或 mm
source row 已解析输入来源行 source_row
physical file line 物理文件行 .source_line
focus 局部聚焦 focus_ggchord_data()
segment 分段窗口 segment_ggchord_data()
leader 标签引导线 label / connection
CDS projection 编码序列坐标投影 build_ggchord_cds()
measured panel 已测量面板 not visual certification / 不等于视觉认证

Design and asset provenance

The grammar follows ggplot2: mappings, scales, geometry, positions, coordinates and themes retain distinct responsibilities. Compact query/subject field names follow BLAST-style tables; ggchord does not run an alignment tool during drawing. Circular hierarchy and feature readability are compared with Circos, clinker and DNA Features Viewer, as recorded in the repository design documents. These are conceptual comparisons, not copied APIs.

The detailed ggchord artwork was supplied by the user on 2026-11-01 for this website and documentation. Full-size PNG and WebP preserve every decoded RGBA pixel. Only the small navigation thumbnail is resized. See the image manifest (website/assets/icon.json in the source repository) for source hashes and exact sizes. The compact hexagonal study mark below, site CSS/JS and documentation diagrams are original project assets under the repository MIT terms. The study mark is supplementary, not a replacement for the supplied artwork.

Compact hexagonal study mark System fonts are used; no webfont, icon library or third-party image is downloaded. Plot colours come from the package’s existing defaults or explicit teaching code; those literal colour choices are listed with the source. The site uses the existing Pandoc and R documentation converters at build time without bundling their binaries or stylesheets.