Volcano plots are the standard visualization for differential expression data — RNA-seq, proteomics, ATAC-seq — displaying fold change against statistical significance for every measured feature simultaneously. A well-made volcano plot communicates the story of your experiment at a glance.
What a volcano plot shows
A volcano plot places every gene (or protein, or peak) as a point with:
- X-axis: Log₂ fold change (L2FC) — positive = upregulated, negative = downregulated
- Y-axis: −log₁₀(p-value) or −log₁₀(adjusted p-value/q-value) — higher = more significant
The "volcano" shape emerges because highly significant changes (high y-axis values) tend to also have large fold changes (extreme x-axis positions), creating two wings.
Step 1 — Prepare your data
You need a table with at minimum:
- Feature identifier (gene name, protein accession)
- Log₂ fold change
- Nominal p-value
- Adjusted p-value (FDR-corrected: Benjamini-Hochberg is standard for RNA-seq)
If you are working from DESeq2, edgeR, or limma output, these columns are present by default.
Critical pre-processing decisions:
- Which p-value to plot: nominal p-value or adjusted (FDR)? Use adjusted p-value for the y-axis if you have it — it is the scientifically correct choice
- How to handle genes with p = 0 (computational zero in some tools): cap at a maximum −log₁₀ value (e.g., 300) or note in the legend
Step 2 — Set your significance thresholds
Standard thresholds, but justify your choice in the methods:
- FDR threshold: 0.05 (5%) — standard; some journals or analyses use 0.1 or 0.01
- Fold change threshold: 1 (so |L2FC| > 1, meaning ≥2-fold change) — standard; biologically driven choices are valid
Mark genes as:
- Significant and upregulated: FDR < 0.05 AND L2FC > 1 → typically colored red or orange
- Significant and downregulated: FDR < 0.05 AND L2FC < −1 → typically colored blue
- Not significant or low fold change: gray
Step 3 — Construct the plot
Axis labels:
- X: "Log₂ fold change" (include condition comparison: e.g., "Log₂FC: Treatment vs Control")
- Y: "−log₁₀(adjusted p-value)" — be explicit about which p-value
Threshold lines:
- Horizontal dashed line at your FDR threshold (y = −log₁₀(0.05) = 1.30)
- Vertical dashed lines at ±L2FC threshold (x = ±1)
Point size and transparency:
- For >5000 genes, use small points (0.5–1 pt) with transparency (alpha = 0.4–0.6) to reduce overplotting
- Significant points can be slightly larger (1–1.5 pt) to stand out
Color scheme:
- Significant upregulated: coral/red (#E05252 or similar)
- Significant downregulated: blue (#4B9CD3 or similar)
- Not significant: medium gray (#AAAAAA)
- Ensure the palette is distinguishable in grayscale
Step 4 — Label key genes
Label the top significant genes by −log₁₀(p-value), or specific genes of interest. Rules:
- Label maximum 10–20 genes on a busy volcano plot
- Use directional text labels (not overlapping the points)
- Use italics for gene names following standard nomenclature (Mus musculus: italic lowercase; Homo sapiens: italic uppercase)
- Use
ggrepel(R) oradjustText(Python) to avoid label overlap automatically
Step 5 — Report statistics in the figure legend
The figure legend must state:
- What the experiment compared (e.g., "Treatment X vs DMSO, n = 3 biological replicates")
- How differential expression was calculated (DESeq2, edgeR, etc.)
- Which p-value is plotted (adjusted/FDR using which method)
- The significance thresholds used for coloring
- Number of significant genes in each category
Code examples
R (ggplot2 + ggrepel):
library(ggplot2)
library(ggrepel)
df$color <- "NS"
df$color[df$padj < 0.05 & df$log2FC > 1] <- "Up"
df$color[df$padj < 0.05 & df$log2FC < -1] <- "Down"
ggplot(df, aes(x = log2FC, y = -log10(padj), color = color)) +
geom_point(alpha = 0.5, size = 0.8) +
scale_color_manual(values = c(NS = "gray70", Up = "#E05252", Down = "#4B9CD3")) +
geom_hline(yintercept = -log10(0.05), linetype = "dashed", color = "gray40") +
geom_vline(xintercept = c(-1, 1), linetype = "dashed", color = "gray40") +
geom_text_repel(data = subset(df, padj < 1e-10), aes(label = gene),
size = 2.5, max.overlaps = 15) +
labs(x = "Log₂ fold change", y = "-log₁₀(adjusted p-value)") +
theme_classic(base_size = 10)
Python (matplotlib):
import matplotlib.pyplot as plt
colors = ["gray"] * len(df)
colors = ["#E05252" if (r.padj < 0.05 and r.log2FC > 1) else
"#4B9CD3" if (r.padj < 0.05 and r.log2FC < -1) else
"gray" for _, r in df.iterrows()]
plt.figure(figsize=(3.5, 3.5)) # single-column Nature width
plt.scatter(df["log2FC"], -np.log10(df["padj"]), c=colors, s=4, alpha=0.5)
plt.axhline(-np.log10(0.05), ls="--", c="gray", lw=0.8)
plt.axvline(-1, ls="--", c="gray", lw=0.8)
plt.axvline(1, ls="--", c="gray", lw=0.8)
plt.xlabel("Log₂ fold change", fontsize=8)
plt.ylabel("-log₁₀(adjusted p-value)", fontsize=8)
plt.savefig("volcano.tiff", dpi=1200, bbox_inches="tight")
Common mistakes to avoid
- Plotting nominal p-values instead of FDR — this overstates the number of "significant" hits
- No threshold lines — reviewers cannot identify your cutoffs without them
- Overplotting — on dense plots, use transparency and smaller points
- Unlabeled axes — always state which test and which p-value adjustment method
- Not italicizing gene names — a common formatting error picked up in peer review
- Forgetting negative sign on y-axis — the y-axis is −log₁₀, making larger positive values = more significant
Building volcano plots in FigureGuild
FigureGuild's Graph Builder creates publication-quality volcano plots from your differential expression CSV. Paste your data, set your FDR and fold change thresholds, and the plot generates with correct axis labels, threshold lines, and color coding. Export directly as TIFF or PDF at your journal's required DPI.
FAQ
Should I use nominal p-value or FDR-adjusted p-value for the volcano plot? Use the FDR-adjusted p-value (q-value) on the y-axis. This is statistically correct and increasingly required by reviewers. State clearly in the legend that it is adjusted.
How many genes should I label on a volcano plot? Typically 5–20 genes of interest or the top hits by statistical significance. More labels create an unreadable plot. Use gene names rather than IDs where possible.
What is a good fold change threshold? |L2FC| > 1 (≥2-fold) is the conventional threshold in most transcriptomics work. Proteomics sometimes uses |L2FC| > 0.5. Choose based on biological context and state your rationale.
My volcano plot has points at p=0 (infinite -log10). What do I do? This happens with some statistical packages that round to zero. Cap these at a maximum value (e.g., −log₁₀ = 300) and note in the legend that "p-values of 0 were plotted at −log₁₀(p) = 300."
Can I make a volcano plot for proteomics or ATAC-seq data? Yes. Volcano plots are appropriate for any high-throughput comparison producing fold changes and p-values — proteomics, metabolomics, ATAC-seq, ChIP-seq, CRISPR screens.