# How to Create a Circular Genome Plot in R with circlize

By the end of this article you will build a complete circular genome plot in R from scratch. The finished figure will show each chromosome as a sector around a circle, a track of genes colored by type, a scatter track of variant quality scores, a kb scale axis, and curved links connecting rearranged regions. You will also learn how to load real BED, GFF3, and VCF files, how to fix the errors that trip up almost everyone on their first plot, and how to check that the figure actually reflects your data.

You need R (version 4.0 or newer works well), the circlize package from CRAN, and optionally a few Bioconductor packages for reading real annotation files. A text editor or RStudio helps but is not required. Budget about 30 minutes for the worked example and another hour if you are adapting it to your own sequencing data.

## What a Circular Genome Plot Shows

A circular genome plot, often called a Circos plot after the original command line tool that popularized the style, arranges genomic features around the circumference of a circle. Each chromosome occupies one arc-shaped sector. Data tracks run outward from the center like tree rings: one ring might hold gene models, the next SNV density, the next copy number, and curved ribbons in the interior can connect duplicated or rearranged loci. The format packs an entire genome summary into a single figure, which is why it appears so often in genome paper figures and interactive browsers.

Interactive tools have pushed this format further. The yEvo Mutation Browser, an R Shiny application built for a high school experimental evolution program, centers on an interactive chromosome map of mutated genes paired with graphs that categorize mutation types and a gene viewer for individual mutation sites [1]. The same layout logic you will implement with circlize in this article is what powers such browser views, just with interactivity layered on top.

circlize is the standard R package for this task. It reimplements the circos paradigm using base R graphics, gives you pixel level control over every sector and track, and scales from a 16 kb plasmid to a multi gigabase genome. The tradeoff is that it is low level. You write a few dozen lines of R rather than a single ggplot2 call, and in exchange you get full control over track order, spacing, colors, and labels.

## What You Need Installed

Install circlize from CRAN with a single command:

```r
install.packages("circlize")
```

For reading real annotation and variant files later in the workflow, install Bioconductor and three packages. Bioconductor is the R ecosystem for genomic data structures and file parsers, and the BiocManager package is its standard installer:

```r
install.packages("BiocManager")
BiocManager::install(c("GenomicRanges", "rtracklayer", "vcfR"))
```

Confirm the installation and record your versions, because circlize behavior has been stable but not identical across major versions:

```r
packageVersion("circlize")
#> [1] '0.4.16'
packageVersion("rtracklayer")
#> [1] '1.66.0'
```

Your version numbers may differ. What matters is that circlize is at least 0.4.x, which is the current major line.

## Step-by-Step: How to Create a Circular Genome Plot in R

This walkthrough uses a small synthetic eight chromosome genome so the script runs standalone with no downloads. Every block of code builds on the previous one. Run them in order in a fresh R session.

### Step 1: Build a genomic coordinate table

circlize works from plain data frames where the first three columns describe genomic regions. The convention is chromosome name, start position, and end position, with positions in base pairs. Build a toy genome, a set of genes, a set of SNVs, and three rearrangement links:

```r
library(circlize)

set.seed(42)

chromosomes <- paste0("chr", 1:8)
chr.lengths <- c(600000, 550000, 480000, 450000, 400000, 350000, 300000, 250000)
names(chr.lengths) <- chromosomes

make_regions <- function(chr.lengths, n, min.width = 500, max.width = 5000) {
  chr   <- sample(names(chr.lengths), n, replace = TRUE)
  width <- sample(min.width:max.width, n, replace = TRUE)
  start <- runif(n, 1, chr.lengths[chr] - width)
  data.frame(chr = chr,
             start = as.integer(start),
             end = as.integer(start + width))
}

genes <- make_regions(chr.lengths, 400)
genes$type  <- sample(c("coding", "ncRNA"), 400, replace = TRUE, prob = c(0.7, 0.3))
genes$score <- rnorm(400)

snps <- data.frame(
  chr = sample(names(chr.lengths), 600, replace = TRUE),
  pos = as.integer(runif(600, 1, 600000))
)
snps$end  <- snps$pos + 1
snps$qual <- runif(600, 10, 90)

links <- data.frame(
  chr1   = c("chr1", "chr2", "chr4"),
  start1 = c(120000,  80000, 300000),
  end1   = c(125000,  85000, 305000),
  chr2   = c("chr3", "chr5", "chr6"),
  start2 = c(200000, 150000, 100000),
  end2   = c(205000, 155000, 105000)
)
```

Inspect the head of the genes table to confirm the structure:

```r
head(genes)
#>    chr   start     end   type   score
#> 1 chr5  102341  105841 coding  0.914
#> 2 chr2  418770  421102  ncRNA -1.207
#> 3 chr1  553029  554410 coding  0.432
```

Your random values will differ because of the seed interaction with R version, but the columns will match. The first three columns, chr, start, end, are the ones circlize reads for genomic placement.

### Step 2: Set global parameters and initialize the layout

The circos.par() function controls the geometry of the whole figure. Then circos.initialize() assigns an angular arc to each chromosome proportional to its length:

```r
circos.clear()

circos.par(
  gap.degree   = 3,
  start.degree = 90,
  canvas.xlim  = c(-1.4, 1.4),
  canvas.ylim  = c(-1.4, 1.4),
  track.margin = c(0.01, 0.01),
  cell.padding = c(0.02, 0, 0.02, 0)
)

circos.initialize(
  factors = factor(chromosomes, levels = chromosomes),
  xlim    = cbind(rep(0, 8), chr.lengths[chromosomes])
)
```

Three details in this block matter more than they look. First, `circos.clear()` resets any layout left over from a previous plot and prevents overlapping figures. Second, wrapping `chromosomes` in `factor()` with an explicit `levels` argument fixes the drawing order. Without it, R sorts chromosome names as strings and you get chr1, chr10, chr11, chr2, chr3. Third, `canvas.xlim` and `canvas.ylim` expand the drawing canvas beyond the circle radius of 1, which reserves blank margin around the figure for legends and outside labels.

### Step 3: Add a chromosome name track

Tracks are added from the outside inward, so the first circos.track() call is the outermost ring. Each track is one annular band, and the panel.fun function draws content inside each chromosome sector:

```r
circos.track(
  ylim = c(0, 1),
  track.height = 0.05,
  bg.border = NA,
  panel.fun = function(x, y) {
    circos.text(CELL_META$xcenter, CELL_META$ycenter,
                CELL_META$sector.index,
                cex = 0.8, facing = "bending.inside", niceFacing = TRUE)
  }
)
```

CELL_META is a convenience object that gives you the coordinates of the current sector cell. Here the chromosome name is placed at the center of each sector, bent to follow the arc so it stays readable.

### Step 4: Draw genes as colored rectangles

The gene track uses circos.rect() to draw one rectangle per gene. Genes on the forward and reverse strands are often drawn at different heights, but for a first plot a single band with two colors works:

```r
circos.track(
  ylim = c(0, 1),
  track.height = 0.18,
  bg.border = "grey85",
  panel.fun = function(x, y) {
    chr <- CELL_META$sector.index
    g   <- genes[genes$chr == chr, ]
    circos.rect(g$start, 0.15, g$end, 0.85,
                col = ifelse(g$type == "coding", "#2166AC", "#B2182B"),
                border = NA)
  }
)
```

The subsetting line `genes[genes$chr == chr, ]` is the key pattern. panel.fun runs once per sector, and you filter the full table down to the features on the current chromosome before drawing. This keeps every function call local to one cell of the layout.

### Step 5: Add a variant scatter track

Points are placed by genomic position on the x axis and by a value on the y axis. Here the y value is the variant quality score mapped into the 0 to 1 range of the track:

```r
snps$y <- snps$qual / 100

circos.track(
  ylim = c(0, 1),
  track.height = 0.18,
  bg.border = "grey85",
  panel.fun = function(x, y) {
    chr <- CELL_META$sector.index
    s   <- snps[snps$chr == chr, ]
    circos.points(s$pos, s$y, pch = 16, cex = 0.4,
                  col = adjustcolor("#4D4D4D", 0.5))
  }
)
```

The adjustcolor() call sets 50 percent transparency so dense clusters of variants remain readable rather than merging into solid ink.

### Step 6: Add a scale axis

The axis track puts tick marks and labels in kilobases along each chromosome. Because chromosome lengths differ, compute the tick positions inside panel.fun for the current sector:

```r
circos.track(
  ylim = c(0, 1),
  track.height = 0.05,
  bg.border = NA,
  panel.fun = function(x, y) {
    major.at <- seq(0, chr.lengths[CELL_META$sector.index], by = 100000)
    circos.axis(h = "top",
                major.at = major.at,
                labels = major.at / 1000,
                labels.cex = 0.45)
  }
)
```

This produces tick labels of 0, 100, 200 and so on, one label per 100 kb. Add the string "kb" once in your figure caption rather than repeating it on every tick.

### Step 7: Connect rearranged regions with links

circos.link() draws a curved ribbon between two genomic intervals, optionally on different chromosomes. Draw links last so they sit in the empty center of the circle:

```r
for (i in seq_len(nrow(links))) {
  circos.link(links$chr1[i], c(links$start1[i], links$end1[i]),
              links$chr2[i], c(links$start2[i], links$end2[i]),
              col = adjustcolor("#B2182B", 0.4),
              border = "#B2182B", lwd = 0.5)
}
```

The col argument fills the ribbon and border outlines it. Wide intervals produce wide ribbons, which is why you usually pass a narrow range such as a 5 kb window on each side rather than the full length of a large rearrangement.

### Step 8: Save the figure

circlize draws with base R graphics, so you control output by opening a graphics device first, drawing the complete plot, and closing the device. Always call circos.clear() before and after:

```r
png("circos_genome.png", width = 3000, height = 3000, res = 300)

circos.clear()
# re-run every circos.par(), circos.initialize() and circos.track() call here

circos.clear()
dev.off()
```

A cleaner way to keep this maintainable is to wrap the whole plotting sequence in a function named draw_genome_plot() and call it once on the screen and once inside png(). For vector output that scales in a manuscript, swap png() for pdf("circos_genome.pdf", width = 10, height = 10) or svg(). The width and height in pdf() and svg() are in inches, while png() takes pixels and a res value in dots per inch.

### What the finished plot looks like

When the script runs without errors, R's plotting pane shows a ring of eight labeled chromosome sectors, each sized in proportion to its length, with chr1 the largest and chr8 the smallest. Inside the label ring sits a band of blue rectangles for coding genes and red rectangles for noncoding RNAs. Below that, a gray scatter of 600 points rises and falls with the simulated quality scores, and a fine tick axis runs along the inner edge with labels in kilobases. Three translucent red ribbons sweep through the empty center, joining intervals on chr1 to chr3, chr2 to chr5, and chr4 to chr6. If you see all of those elements, the script worked.

## The Parameters That Matter

![Worked sum of four circlize track heights plus three margin gaps giving 0.52 of the radius, leaving room for links](/article-images/illustrations/circular-genome-plot-r-circlize-radius-budget.webp)

circlize exposes dozens of options, but a handful control almost all of the visual quality. This table lists the ones worth learning first.

| Parameter | Where it is set | What it controls | Practical value |
|--|--|--|--|
| gap.degree | circos.par | Gap between neighboring sectors, in degrees | 2 to 3 for many chromosomes, 8 to 12 for few |
| start.degree | circos.par | Rotates where the first sector begins | 90 to place the first chromosome at the top |
| canvas.xlim, canvas.ylim | circos.par | Size of the drawing canvas around the circle | c(-1.4, 1.4) to reserve space for legends |
| track.margin | circos.par and circos.track | Blank space between adjacent tracks, in fractions of the radius | 0.01 to 0.02 |
| cell.padding | circos.par | Padding inside each sector cell, as c(bottom, left, top, right) | c(0.02, 0, 0.02, 0) |
| track.height | circos.track | Radial thickness of a track, as a fraction of the full radius (1) | 0.05 for label and axis strips, 0.1 to 0.3 for data |
| bg.col, bg.border | circos.track | Sector background fill and outline | Alternate light grays to separate chromosomes |
| facing, niceFacing | circos.text | Text orientation along the arc | "bending.inside" with niceFacing = TRUE for names |
| col, border | Drawing functions | Fill and outline color of shapes | Use transparency for dense data |
| points.overflow.warning | circos.par | Whether to warn when points fall outside their cell | Leave TRUE while debugging, FALSE only for final output |

The track height arithmetic is worth doing once by hand because the full radius equals 1. The worked script uses a label strip of 0.05, a gene track of 0.18, a scatter track of 0.18, and an axis track of 0.05. Those four tracks sum to 0.46. Each adjacent pair of tracks is separated by the sum of their margins, 0.01 plus 0.01, so three internal gaps add 0.06. The total comes to 0.52, which leaves nearly half the radius empty and available for the central links. If you keep adding tracks past roughly 0.8 of the radius, circlize runs out of space and errors, which is the most common failure mode in ambitious first plots.

## A Shortcut: Genomic Convenience Functions

Everything above uses the low level interface. circlize also ships a genomic layer that understands coordinate tables directly. circos.genomicInitialize() builds the whole sector layout from a data frame whose first three columns are chromosome, start, and end:

```r
circos.clear()
circos.genomicInitialize(genes[, c("chr", "start", "end")])

circos.genomicTrack(snps[, c("chr", "pos", "end", "qual")],
  panel.fun = function(region, value) {
    circos.genomicPoints(region, value, pch = 16, cex = 0.4,
                         col = adjustcolor("#4D4D4D", 0.5))
  }
)
```

The genomic family includes circos.genomicRect for gene models, circos.genomicLines for coverage, circos.genomicHeatmap for layered scores, circos.genomicDensity for windowed feature density, circos.genomicLabels for text labels with leader lines, and circos.genomicLink for rearrangements. These functions handle the per-sector subsetting from Step 4 automatically, which removes the most error prone part of the low level interface. For human genomes, circos.initializeWithIdeogram() goes one step further and draws chromosome ideograms from cytoband data, using a built in hg19 cytoband table when you call it with no arguments. For nonhuman genomes you supply a cytoband format file with chromosome, start, end, and band designation columns.

## Loading Real Data: BED, GFF3, and VCF

Real projects feed circlize from standard file formats, and the Bioconductor parsers make that nearly painless.

A BED file of peaks or features loads directly, remembering that BED coordinates are 0 based half open:

```r
bed <- read.table("peaks.bed", header = FALSE,
                  col.names = c("chr", "start", "end", "name"))
```

A GFF3 or GTF annotation file imports cleanly through rtracklayer, and the resulting GRanges object converts to the data frame layout circlize expects:

```r
library(rtracklayer)
gff <- import("genes.gff3")
genes <- as.data.frame(gff)
genes <- genes[, c("seqnames", "start", "end", "type")]
colnames(genes)[1] <- "chr"
```

Variant call format files load with vcfR. XVCF, a Shiny application for summarizing and visualizing genomic variation, is itself built on the vcfR and maftools packages, which reflects how thoroughly those libraries have become the standard R path into VCF data [2]:

```r
library(vcfR)
vcf <- read.vcfR("variants.vcf.gz")
fix <- as.data.frame(getFIX(vcf))
snps <- data.frame(chr = as.character(vcf@fix[, 1]),
                   start = as.integer(vcf@fix[, 2]),
                   end = as.integer(vcf@fix[, 2]))
```

The genotype format question comes up often when people move beyond variant files. LDblockR, a pure R toolkit for linkage disequilibrium analysis, reads VCF, HapMap, and PLINK BED, BIM, FAM files directly in R without external binaries, which illustrates how much format friction you can eliminate by staying inside one language [3]. Whatever importer you choose, the last step is always the same: reduce the file to a data frame with chromosome, start, and end in the first three columns, then hand it to circlize.

Some genomes are circular in a literal sense. Chloroplast and most mitochondrial genomes are circular molecules, so a circular plot is not just a style choice but the natural representation of the molecule itself. The CGAS pipeline for chloroplast genomics runs raw reads through assembly, annotation, and comparative modules and integrates R based visualizations where graphical output is needed [4], and a circlize ring is a good match for that kind of small, gene dense genome.

## Common Mistakes and Limitations

These are the failures you are most likely to hit, with symptoms and fixes.

**Forgetting circos.clear().** circlize layouts persist in the graphics state. If you run a second circos.initialize() without clearing, R stops with an error telling you the layout has already been initialized, and if you suppress that and keep drawing, the new plot overlays the old one in unreadable ways. The fix is mechanical: call circos.clear() at the start of every plotting script and again before dev.off().

**Wrong chromosome order.** Without an explicit factor ordering, chromosomes sort alphabetically, so chr10 and chr11 appear between chr1 and chr2. Sector sizes will still be correct, which makes the error easy to miss on plots with many scaffolds. Fix it once at data preparation:

```r
chr.levels <- c(paste0("chr", 1:9), paste0("chr", 10:22))
genes$chr <- factor(genes$chr, levels = chr.levels)
```

For de novo assemblies with arbitrary scaffold names, order by length instead. If a track is added with a different set of factor levels than circos.initialize() received, circlize stops with an error saying the factors must match the ones from circos.initialize, which is usually a symptom of a subset table whose largest chromosome was filtered out.

**Mixing 0 based and 1 based coordinates.** BED files count from 0 and treat the end as exclusive. GFF3, GTF, and VCF files count from 1 and treat the end as inclusive. Passing BED coordinates through unconverted shifts every feature one base pair, which is invisible at genome scale but corrupts base level comparisons. Convert BED input with `start <- start + 1` on import, and document which convention your pipeline uses.

**Too many tracks for the radius.** Exceeding the available radial space raises an error about insufficient space for a new track. The fix is to reduce track.height values, trim margins, or drop the least informative track. Four to six data tracks is a practical ceiling for a readable figure.

**Plotting millions of raw points.** A whole genome variant file can contain millions of rows, and drawing each one as a point produces a slow, muddy figure. Aggregate instead. Window the genome into 10 to 100 kb bins and plot mean density or mean score per bin, or use circos.genomicDensity, which does the binning for you. A 3 Mb bacterial genome can plot every variant directly, a 3 Gb mammalian genome usually cannot.

**Opaque colors in dense regions.** Solid fills hide overlap structure. Give every dense layer some transparency with adjustcolor() or rgb() with an alpha argument, and reserve fully opaque color for the few elements you want to dominate.

**Text clipped at the canvas edge.** Labels around the outside disappear when the canvas is exactly the size of the circle. Expand canvas.xlim and canvas.ylim as in Step 2, or increase the device width and height.

**Saving before circos.clear().** Closing the device without clearing leaves the layout in memory, so the next plot in the session inherits garbage. Make circos.clear() the last line of every drawing function.

One structural limitation follows from the format itself. Circular plots excel at genome wide overview and at showing relationships between loci, but they are poor at reading exact positions, because arc length is harder to judge than Cartesian distance. Interactive chromosome maps such as the yEvo Mutation Browser pair the circular or linear genome view with drill down gene viewers for exactly this reason [1]. Use your circlize figure to communicate pattern, and use a linear genome browser to verify detail.

## Tips for Publication-Quality Circular Genome Plots

**Order sectors deliberately.** Sort chromosomes by biological convention, not alphabetical accident. For assemblies, putting the largest scaffold first and descending by length makes sector sizes intuitive.

**Budget your radius before you draw.** Write down each track.height, sum them with the margins, and confirm the total stays under about 0.8. Redoing the budget on paper takes two minutes and prevents the most common late stage redesign.

**Use a colorblind safe palette and encode type redundantly.** Pair color with shape or with a separate track so the figure survives grayscale printing. The viridis and RColorBrewer palettes are the usual safe choices.

**Keep one idea per track.** Reviewers read these figures ring by ring. A gene model ring, a variant density ring, and a linkage ring each answer one question. Cramming multiple encodings into one ring costs more comprehension than it saves.

**Label in kilobases or megabases, never raw base pairs.** An axis reading 0 to 600000 is unreadable. Divide tick positions by 1000 or 1000000 as in Step 6.

**Expand the canvas and place legends outside the circle.** With canvas.xlim set to c(-1.4, 1.4), the base legend() function can drop a legend in the empty corner without covering data.

**Record the environment.** Workflows that span preprocessing, analysis, and visualization lose reproducibility when provenance is not tracked, a problem that motivated SNPio to embed provenance tracking attributes directly in its data objects [5]. In R, the minimum version of this discipline is a saved sessionInfo() output and a fixed random seed next to your plotting script. Recent genomics papers routinely pair such plotting code with the tidyverse workhorses ggplot2, dplyr, ggrepel, and tidyr for the surrounding figures and tables [6], so your circlize script will live alongside those in a real project. Consistent style across all of them makes the figure set easier to review.

**Verify one feature by hand.** Pick a known gene, confirm its sector and angular position match a genome browser view of the same genome build. A single round trip check catches coordinate convention errors that no amount of visual inspection of the whole ring will reveal.

## How to Verify Your Plot Is Correct

Three checks cover most errors. First, ask circlize to report the layout it built:

```r
circos.info()
```

The output lists every sector with its data range and every track index, and looks roughly like this for the toy genome:

```r
#> All your sectors:
#>  chr1 chr2 chr3 chr4 chr5 chr6 chr7 chr8
#> All your tracks:
#>  1 2 3 4
```

Confirm that each sector's x range runs from 0 to the chromosome length you intended, and that the number of tracks matches the number you think you drew.

Second, check the arithmetic of sector sizes. In the toy genome the eight chromosomes sum to 3,380,000 bases, so chr1 at 600,000 bases should occupy about 600000 / 3380000 of the circle, roughly 64 degrees before gaps. Measure visually against the gap.degree setting of 3 degrees per junction. If sector proportions look wrong, the usual cause is a factor ordering mismatch or an xlim row that does not correspond to the right factor.

Third, count what was drawn against what you supplied. Drop a counter into a panel.fun during debugging:

```r
n_drawn <- 0
circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
  chr <- CELL_META$sector.index
  n_drawn <<- n_drawn + sum(snps$chr == chr)
})
n_drawn   # should equal nrow(snps), which is 600
```

If the count is short, a chromosome name spelling mismatch between your table and your factor levels is silently dropping rows, and `setdiff(unique(snps$chr), levels(genes$chr))` will name the offenders.

## Where This Fits in an R for Bioinformatics Workflow

A circular genome plot is a presentation layer, and it earns its place at the end of a pipeline that starts with raw data. The pattern looks like this: read and filter your variants or annotations, compute per window summaries, then render. Toolkits across genomics keep converging on this shape. CGAS wraps assembly through phylogeny in scripted modules with R visualization embedded where output is graphical [4], LDblockR unifies genotype parsing through visualization in one R object model [3], and XVCF exposes VCF summarization through R Shiny so users never touch the plotting code directly [2]. Building your circlize script as a function that takes a few data frames and a file path makes it a drop in module for the same kind of pipeline.

The natural next step after a static figure is interactivity. Because circlize is plain R, the same plotting logic ports into a Shiny application where users select chromosomes, zoom to loci, and click features, which is precisely the architecture of education and browsing tools like the yEvo Mutation Browser [1]. If your audience includes collaborators who will not run R, a Shiny wrapper around your plotting function is often more valuable than a prettier static figure.

## Frequently Asked Questions

### What is a circular genome plot used for?

It summarizes an entire genome in one figure by arranging chromosomes as arcs around a circle and stacking data tracks such as genes, variant density, expression, and structural rearrangements in rings. It is best for genome wide pattern and inter chromosomal relationships, not for reading exact coordinates.

### How do I install circlize in R?

Run install.packages("circlize") in the R console, which installs it from CRAN with all dependencies. For reading real genomic file formats, also install BiocManager and then the Bioconductor packages GenomicRanges, rtracklayer, and vcfR.

### Why are my chromosomes in the wrong order?

R sorts chromosome names as strings by default, so chr10 lands between chr1 and chr2. Convert the chromosome column to a factor with an explicit levels argument in the order you want, for example chr1 through chr22, and pass that factor to circos.initialize.

### How do I reset the plot between two circular figures?

Call circos.clear() before initializing the next layout and again before closing the graphics device. Without it, circlize either errors when you reinitialize or silently overlays the two figures.

### Can circlize read BED, GFF3, or VCF files directly?

circlize reads data frames, so you parse the file first. BED files read with read.table, GFF3 with rtracklayer::import, and VCF with vcfR, and each reduces to a data frame with chromosome, start, and end in the first three columns that circos.genomicInitialize accepts.

### Why is my saved circular plot cut off or blank?

![Yes and no path for a blank or clipped saved circlize figure, ending in drawing inside the device and clearing the layout](/article-images/illustrations/circular-genome-plot-r-circlize-saving-blank-plot.webp)

The plot must be drawn inside the graphics device, so open png() or pdf() first, draw the entire figure, call circos.clear(), and then dev.off(). Clipping means the canvas is too small, so increase width and height or expand canvas.xlim and canvas.ylim in circos.par.

### Is circlize better than ggplot2 for genome plots?

They solve different problems. circlize gives precise control over circular layouts and is the practical choice for circos style genome figures, while ggplot2 excels at Cartesian plots and integrates with the rest of the tidyverse. Many projects use both, as the genomic literature reflects.

### How many tracks can I add before the plot becomes unreadable?

Because track heights are fractions of the circle radius, budget them so the total stays under about 0.8. In practice four to six well separated data tracks is the ceiling for a figure a reader can absorb, and aggregation into windows beats adding more rings.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is a circular genome plot used for?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It summarizes an entire genome in one figure by arranging chromosomes as arcs around a circle and stacking data tracks such as genes, variant density, expression, and structural rearrangements in rings. It is best for genome wide pattern and inter chromosomal relationships, not for reading exact coordinates."
      }
    },
    {
      "@type": "Question",
      "name": "How do I install circlize in R?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Run install.packages(\"circlize\") in the R console, which installs it from CRAN with all dependencies. For reading real genomic file formats, also install BiocManager and then the Bioconductor packages GenomicRanges, rtracklayer, and vcfR."
      }
    },
    {
      "@type": "Question",
      "name": "Why are my chromosomes in the wrong order?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "R sorts chromosome names as strings by default, so chr10 lands between chr1 and chr2. Convert the chromosome column to a factor with an explicit levels argument in the order you want, for example chr1 through chr22, and pass that factor to circos.initialize."
      }
    },
    {
      "@type": "Question",
      "name": "How do I reset the plot between two circular figures?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call circos.clear() before initializing the next layout and again before closing the graphics device. Without it, circlize either errors when you reinitialize or silently overlays the two figures."
      }
    },
    {
      "@type": "Question",
      "name": "Can circlize read BED, GFF3, or VCF files directly?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "circlize reads data frames, so you parse the file first. BED files read with read.table, GFF3 with rtracklayer::import, and VCF with vcfR, and each reduces to a data frame with chromosome, start, and end in the first three columns that circos.genomicInitialize accepts."
      }
    },
    {
      "@type": "Question",
      "name": "Why is my saved circular plot cut off or blank?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The plot must be drawn inside the graphics device, so open png() or pdf() first, draw the entire figure, call circos.clear(), and then dev.off(). Clipping means the canvas is too small, so increase width and height or expand canvas.xlim and canvas.ylim in circos.par."
      }
    },
    {
      "@type": "Question",
      "name": "Is circlize better than ggplot2 for genome plots?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "They solve different problems. circlize gives precise control over circular layouts and is the practical choice for circos style genome figures, while ggplot2 excels at Cartesian plots and integrates with the rest of the tidyverse. Many projects use both, as the genomic literature reflects."
      }
    },
    {
      "@type": "Question",
      "name": "How many tracks can I add before the plot becomes unreadable?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Because track heights are fractions of the circle radius, budget them so the total stays under about 0.8. In practice four to six well separated data tracks is the ceiling for a figure a reader can absorb, and aggregation into windows beats adding more rings."
      }
    }
  ]
}
</script>

## Sources

1. [The yEvo Mutation Browser: Enhancing student understanding of experimental evolution and genomics through interactive data visualization.](https://pubmed.ncbi.nlm.nih.gov/42933842/)
2. [XVCF: Exquisite visualization of VCF data from genomic experiments.](https://pubmed.ncbi.nlm.nih.gov/42323052/)
3. [LDblockR: a cross-platform R toolkit for multi-format linkage disequilibrium analysis and integrated regional genomics visualization.](https://pubmed.ncbi.nlm.nih.gov/43005392/)
4. [CGAS (Chloroplast Genome Analysis Suite): An automated python pipeline for comprehensive comparative chloroplast genomics.](https://pubmed.ncbi.nlm.nih.gov/42890830/)
5. [SNPio: a Python interface for population genomic data processing.](https://pubmed.ncbi.nlm.nih.gov/42399766/)
6. [Synergistic role of molecular imaging and genomics in thyroid cancer management: from diagnosis to personalized treatment.](https://pubmed.ncbi.nlm.nih.gov/42806342/)