ggvolc packageInstall the package using the following commands 
# from CRAN
install.packages("ggvolc")
# or the development version from GitHub
# install.packages("remotes")
# remotes::install_github("loukesio/ggvolc")
install.packages("ggvolc", repos = c('https://loukesio.r-universe.dev', 'https://cloud.r-project.org'))
library(ggvolc)
ggvolc turns the results of a differential-expression analysis — from DESeq2,
edgeR, or limma — into a clean, publication-ready volcano plot. From there
you can highlight genes, auto-label the top hits, attach a gt table, or make the
whole thing interactive. Start by loading the library and exploring the two example
datasets that ship with the package:
library(ggvolc)
#> Welcome to ggvolc version 0.3.0 !
#>
#> 888
#> 888
#> 888
#> .d88b. .d88b. 888 888 .d88b. 888 .d8888b
#> d88P"88b d88P"88b 888 888 d88""88b 888 d88P"
#> 888 888 888 888 Y88 88P 888 888 888 888
#> Y88b 888 Y88b 888 Y8bd8P Y88..88P 888 Y88b.
#> "Y88888 "Y88888 Y88P "Y88P" 888 "Y8888P
#> 888 888
#> Y8b d88P Y8b d88P
#> "Y88P" "Y88P"
#>
data(all_genes) # data.frame that contains the output of differentially expressed genes
head(all_genes,5) # have a look at the first 5 rows
#> genes baseMean log2FoldChange lfcSE stat pvalue
#> 1 GCR1 7201.5782 2.244064 0.2004959 11.192564 4.434241e-29
#> 2 OPI10 1009.4171 -2.257454 0.2096469 -10.767889 4.880607e-27
#> 3 AGA2 249.1173 3.829474 0.3623263 10.569132 4.143136e-26
#> 4 FIM1_1376 5237.5035 2.550409 0.2560379 9.961059 2.256459e-23
#> 5 HMG1 10838.1037 2.214300 0.2229065 9.933763 2.968371e-23
#> padj
#> 1 2.153711e-25
#> 2 1.185255e-23
#> 3 6.707736e-23
#> 4 2.739905e-20
#> 5 2.883475e-20
data(attention_genes) # here is a data.frame with genes that I want to mention to the volcano plot
head(attention_genes,5) # have a look at the first five rows
#> genes baseMean log2FoldChange lfcSE stat pvalue
#> 1 THI13 480.5194 1.585811 0.5219706 3.038122 2.380572e-03
#> 2 FBP1 22710.8428 -2.366733 0.3533032 -6.698871 2.100354e-11
#> 3 TRA1 4491.1343 -1.410696 0.4384316 -3.217595 1.292700e-03
#> 4 YDR222W 591.2289 -4.045918 0.9133881 -4.429572 9.442026e-06
#> 5 BRL1 4434.7712 2.375919 0.5037264 4.716686 2.397176e-06
#> padj
#> 1 1.371582e-02
#> 2 3.290780e-09
#> 3 8.565681e-03
#> 4 1.819838e-04
#> 5 5.850796e-05
Created on 2023-08-11 with reprex v2.0.2
Pass a single data frame and every gene is coloured by significance — down, up, or not significant.
ggvolc(all_genes)

Supply a second data frame to outline and label the genes you care about.
ggvolc(all_genes, attention_genes)

Turn on add_seg to draw the fold-change and significance thresholds.
ggvolc(all_genes, attention_genes, add_seg = TRUE) +
labs(title="Add segments of significance")

Scale point size by effect size with size_var = "log2FoldChange".
ggvolc(all_genes, attention_genes, size_var = "log2FoldChange", add_seg = TRUE)

Or scale it by significance with size_var = "pvalue".
ggvolc(all_genes, attention_genes, size_var = "pvalue", add_seg = TRUE)

plot <- ggvolc(all_genes, attention_genes, add_seg = TRUE) +
labs(title="Add a table with the genes of interest")
plot %>%
genes_table(attention_genes)

The gene table is rendered with gt and composed with
patchwork, so the result is a single
object you can style further or save with ggplot2::ggsave().
Instead of curating your own set of genes, you can let genes_table() pick the
most significant ones automatically with top_n. Pass the full DE table and it
selects the top genes by sig_col (defaults to padj, falling back to
pvalue). Use dir = "each" to take the top N up- and down-regulated genes.
plot_top <- ggvolc(all_genes, label_top = 10, add_seg = TRUE) +
labs(title = "Top 10 most significant genes")
plot_top %>%
genes_table(all_genes, top_n = 10)

ggvolc() and genes_table() accept the output of all three major
differential-expression pipelines directly — column names are auto-detected and
mapped internally, so no manual renaming is needed. Gene identifiers held in row
names (as edgeR/limma often do) are promoted to a genes column automatically.
| Pipeline | Fold change | p-value | adjusted p | expression |
|---|---|---|---|---|
DESeq2 (results()) |
log2FoldChange |
pvalue |
padj |
baseMean |
edgeR (topTags()) |
logFC |
PValue |
FDR |
logCPM |
limma (topTable()) |
logFC |
P.Value |
adj.P.Val |
AveExpr |
You don’t even need to convert to a table first: pass the result objects themselves and ggvolc extracts every gene for you.
# DESeq2
ggvolc(DESeq2::results(dds)) # or just ggvolc(dds) after DESeq()
# edgeR
ggvolc(edgeR::glmQLFTest(fit, coef = 2))
# limma: choose the coefficient for multi-coefficient fits
ggvolc(limma::eBayes(fit), coef = "treatment")
Supported: DESeqResults / DESeqDataSet (DESeq2), TopTags / DGELRT /
DGEExact (edgeR), and MArrayLM (limma). de_table() does the conversion if
you want the data frame yourself.
The package also ships edger_genes, an example topTags()-style table:
data(edger_genes) # an edgeR topTags()-style table (genes in the rownames)
head(edger_genes, 3)
#> logFC logCPM PValue FDR
#> GCR1 2.244064 12.8143 4.434241e-29 2.153711e-25
#> OPI10 -2.257454 9.9807 4.880607e-27 1.185255e-23
#> AGA2 3.829474 7.9665 4.143136e-26 6.707736e-23
ggvolc(edger_genes, label_top = 8, add_seg = TRUE, title = "edgeR input")

ggvolc() calls significance on the adjusted p-value (FDR) by default — the
right cutoff for most DE workflows — and the y-axis and the significance line
follow along, so the plot stays consistent. Prefer the raw p-value? Set
sig_col = "pvalue".
ggvolc(all_genes, add_seg = TRUE) # significance on padj (FDR) by default

If your table has no adjusted-p column, ggvolc automatically falls back to the raw p-value.
ggvolc() is also robust to p-values of exactly 0 (which DESeq2/edgeR can emit
for the strongest genes): instead of silently dropping them, their -log10
value is capped so they stay pinned near the top of the plot.
No need to build a separate data frame — label_top highlights and labels the N
most significant genes for you, and label_dir lets you pick the direction.
# top 10 overall
ggvolc(all_genes, label_top = 10, add_seg = TRUE, title = "Top 10 hits")

# top 8 up- and top 8 down-regulated
ggvolc(all_genes, label_top = 8, label_dir = "each", add_seg = TRUE,
title = "Top 8 up + 8 down")

label_dir accepts "both" (default), "up", "down", or "each".
Set interactive = TRUE to get an ggiraph
widget — hover any point to see the gene name and its statistics. ggiraph is an
optional dependency (install it with install.packages("ggiraph")).
ggvolc(all_genes, attention_genes, interactive = TRUE)
Note: GitHub can’t run the widget, so the image below is a static preview. The live, hover-to-inspect version runs in RStudio and on the package website.

Pick a color preset with palette. ggvolc_palettes() lists them all, including
colorblind-safe options and presets taken from the
ltc palettes (ltc does not need to be installed).
ggvolc_palettes()
#> palette down not_sig up colorblind_safe source
#> 1 default #00798c #808080 #d1495b FALSE ltc: minou
#> 2 okabe_ito #0072B2 #999999 #D55E00 TRUE Okabe & Ito (2008)
#> 3 rdbu #0571b0 #999999 #ca0020 TRUE ColorBrewer RdBu
#> 4 pantone23 #1F2C43 #B8B8B8 #FFB000 TRUE ltc: pantone23
#> 5 seafarer #013D5A #BDBDBD #E4A25B TRUE ltc: seafarer
#> 6 midnight #075056 #B0B0B0 #FF5B04 FALSE ltc: midnight
#> 7 maya #3d5a80 #A9A9A9 #ee6c4d FALSE ltc: maya
#> 8 trio1 #0E7175 #B0B0B0 #FD7901 FALSE ltc: trio1
ggvolc(all_genes, label_top = 8, label_dir = "each", palette = "okabe_ito",
title = "Colorblind-safe: Okabe-Ito")

ggvolc(all_genes, label_top = 8, label_dir = "each", palette = "pantone23",
title = "ltc: pantone23")

Or pass your own colors, including any ltc palette:
ggvolc(all_genes, palette = c(down = "navy", up = "firebrick"))
pal <- ltc::ltc("maya")
ggvolc(all_genes, palette = c(down = pal[1], up = pal[4]))
genes_table() colors the log2FC column to match the plot automatically. Light
colors (such as the gold of pantone23) are darkened just enough to stay
readable as table text.
ggvolc(all_genes, label_top = 6, show_counts = TRUE, add_seg = TRUE)

Pass a named list of gene sets (a pathway, a GO term, your own list) to
highlight. Each set gets its own color and legend entry; by default the
colors come from the same ltc palette as the up/down colors.
genes <- all_genes$genes
ribosomal <- grep("^RP[LS][0-9]", genes, value = TRUE) # ribosomal proteins
heat_shock <- grep("^HSP[0-9]|^SSA[0-9]", genes, value = TRUE) # heat-shock proteins
ggvolc(all_genes, highlight = list(Ribosomal = ribosomal, `Heat shock` = heat_shock))

Choose the colors with highlight_colors and the transparency with
highlight_alpha, either one value for all sets or one per set:
ggvolc(all_genes,
highlight = list(Ribosomal = ribosomal, `Heat shock` = heat_shock),
highlight_alpha = c(Ribosomal = 0.5, `Heat shock` = 1))

data2 also accepts a plain vector of gene names: ggvolc(all_genes, c("GCR1", "HMG1")).
Give ggvolc() a named list of results to get one panel per contrast on shared
axes. label_top and show_counts are applied in each panel, and any mix of
DESeq2, edgeR and limma results works.
ggvolc(list("Day 1" = res_day1, "Day 7" = res_day7), label_top = 5, show_counts = TRUE)
The package ships stress_timecourse, real DESeq2 results from fission yeast
under osmotic stress (Leong et al. 2014):
30, 60 and 180 minutes after stress, each compared with the unstressed cells at
0 minutes. In every panel the x-axis is the log2 fold change against 0 minutes
and the y-axis is the significance of that change.
ggvolc(stress_timecourse, label_top = 5, show_counts = TRUE, add_seg = TRUE)

The fold-change axis is shared, so effect sizes compare directly, while each
panel uses its own height for significance; the counts in the corners show the
response peaking at 30 minutes and fading by 180. Use facet_scales = "fixed"
to share the significance axis as well.
Prefer the fold change on the y-axis? flip = TRUE swaps the axes, so the
fold-change axis lines up across the panels and you can follow genes from one
time point to the next:
ggvolc(stress_timecourse, label_top = 5, show_counts = TRUE, add_seg = TRUE,
flip = TRUE)

With flip = TRUE the significance (x) axis is free per panel by default;
facet_scales = "fixed" shares it.
Genes that DESeq2 leaves without an adjusted p-value (low-count or outlier genes) are left out of the plot, with a message saying how many.
docker pull ghcr.io/loukesio/ggvolc:latest for a ready-to-run RStudio
environment with ggvolc pre-installed.