A volcano plot is a scatter plot that displays the results of a differential expression analysis, plotting the magnitude of change on the x-axis against statistical significance on the y-axis. It gets its name from the shape that forms: genes with both large fold changes and high significance fan out at the top like an eruption, while non-significant genes cluster at the bottom. Building one requires a table of differential expression results and a basic understanding of two transformations: log2 fold change and negative log10 p-values.
What the Axes Represent
The x-axis shows the log2 fold change between two conditions. Fold change is simply the ratio of average expression in your experimental group to the average in your control group. Taking the log base 2 makes the scale symmetric: a gene expressed twice as much in the experimental group has a log2 fold change of +1, while a gene expressed at half the level has a log2 fold change of -1. Without the log transformation, upregulation and downregulation would be on completely different scales (2x vs. 0.5x), making the plot lopsided.
The y-axis shows the negative log10 of the p-value. This transformation does two useful things. It flips the scale so that more significant genes appear higher on the plot (a p-value of 0.001 becomes 3, while 0.05 becomes about 1.3). And it spreads out the tiny p-values that are common in genomics, where you might see values like 0.0000001, making them easier to distinguish visually.
Preparing Your Data
You need a table with one row per gene (or protein, metabolite, etc.) and at minimum three columns: a gene identifier, a fold change or log2 fold change value, and a p-value. Most differential expression tools like DESeq2, edgeR, and limma-voom produce output tables that already contain these columns, often including adjusted p-values (FDR) as well. If your tool outputs raw fold change rather than log2 fold change, you’ll need to convert it yourself by taking log base 2 of the value.
If you’re calculating log2 fold change manually from normalized counts, the standard approach is to add a small pseudocount before taking the log to avoid issues with zero values. A common formula looks like this: log2(experimental + 0.5) minus log2(control + 0.5). The pseudocount (0.5 or 1) prevents the log of zero, which is undefined.
Make sure your file is in a tabular format (TSV or CSV). Galaxy’s training materials note that the four key columns are: raw p-values, adjusted p-values (FDR), log fold change, and gene labels. Having all four gives you the flexibility to switch between raw and adjusted p-values depending on your analysis goals.
Raw P-Values vs. Adjusted P-Values
Most volcano plots use adjusted p-values (FDR-corrected using the Benjamini-Hochberg method) rather than raw p-values, because testing thousands of genes simultaneously inflates false positives. However, there’s a statistical subtlety worth knowing. A 2021 paper in Briefings in Bioinformatics showed that the classic “double filtering” approach, where you select genes by both FDR-adjusted p-value and fold change thresholds, can inflate the false discovery rate among the selected genes. This happens because FDR correction guarantees an error rate across the full set of discoveries, not across any arbitrary subset you filter from it.
For most practical purposes, plotting adjusted p-values on the y-axis and using standard thresholds is perfectly acceptable. Just be aware that the genes you highlight as “significant” after double filtering may carry a higher false discovery rate than the nominal FDR suggests. More advanced methods like closed testing with Simes tests can preserve FDR control under double filtering, but these aren’t yet standard in most pipelines.
Setting Significance Thresholds
The widely accepted defaults are an adjusted p-value cutoff of 0.05 and a log2 fold change cutoff of ±1 (which corresponds to a 2-fold change in expression). These thresholds divide your plot into distinct zones:
- Up-regulated: log2 fold change greater than +1 and adjusted p-value below 0.05. These appear in the upper right.
- Down-regulated: log2 fold change less than -1 and adjusted p-value below 0.05. These appear in the upper left.
- Non-significant: everything else, clustered in the middle and bottom of the plot.
These cutoffs aren’t sacred. Some studies use a fold change threshold of ±1.5 (log2 fold change of about ±0.58), and p-value cutoffs vary by field and dataset size. The important thing is to display the thresholds as dashed lines on the plot so readers can immediately see what you defined as significant.
Making a Volcano Plot in R
The most straightforward option in R is the EnhancedVolcano package from Bioconductor, which produces publication-ready plots with a single function call. After installing it, the basic usage looks like this:
EnhancedVolcano(res, lab = rownames(res), x = 'log2FoldChange', y = 'pvalue')
Here, res is your results data frame (such as the output from DESeq2), x points to the column containing log2 fold change values, and y points to the p-value column. The function automatically applies the negative log10 transformation to the y-axis, draws threshold lines, and colors points by significance category.
The customization options are extensive. You can adjust the p-value cutoff with pCutoff (for example, pCutoff = 10e-32 for very stringent filtering), the fold change cutoff with FCcutoff (for example, FCcutoff = 0.5), and control the appearance with parameters like pointSize, labSize, col for colors, colAlpha for transparency, and shape for point shapes. A typical color scheme uses grey for non-significant genes, green for genes meeting only the fold change cutoff, blue for genes meeting only the p-value cutoff, and red for genes meeting both.
If you prefer building from scratch with ggplot2, you have full control but need to handle the transformations and coloring manually. Create a new column in your data frame for the negative log10 p-value, assign a category column based on your thresholds, then use geom_point() with aes(color = category).
Making a Volcano Plot in Python
In Python, the bioinfokit package provides a dedicated volcano plot function. After installing it (pip install bioinfokit), you can generate a plot with:
from bioinfokit import visuz
visuz.GeneExpression.volcano(df="your_dataframe", lfc="log2FC", pv="pvalue")
This function expects a pandas DataFrame with columns for log2 fold change and p-values. It handles the negative log10 transformation internally and applies default thresholds and coloring.
For more control, you can build the plot manually with matplotlib or seaborn. Calculate a new column for negative log10 p-values, assign colors based on your threshold logic, then use plt.scatter() with color mapping. Adding horizontal and vertical dashed lines with plt.axhline() and plt.axvline() marks your significance cutoffs.
Labeling Genes Without Overlap
Labeling the most significant or interesting genes is what makes a volcano plot informative rather than just decorative. The challenge is that the genes you most want to label tend to cluster together in the upper corners of the plot, creating overlapping text.
In R, the ggrepel package solves this elegantly. Replace geom_text() with geom_text_repel() and it automatically pushes labels apart, drawing thin lines connecting each label to its data point. The geom_label_repel() variant adds a background box behind each label for better readability. EnhancedVolcano uses this approach internally, so labels are repelled by default.
In Python, matplotlib’s adjustText library provides similar functionality. A practical alternative is to label only a curated list of genes of interest rather than every significant hit, which keeps the plot readable.
Handling Zero and Extreme P-Values
Some differential expression tools report p-values of exactly zero for highly significant genes. When you take the negative log10 of zero, you get infinity, which breaks your plot. There are a few ways to handle this.
The simplest fix is to replace zero p-values with the smallest non-zero p-value in your dataset, or with an arbitrary floor like 1e-300. Another approach is to cap the y-axis at a reasonable maximum and place these genes at the cap. EnhancedVolcano handles this internally by placing extreme points at the top of the visible range.
Similarly, some tools report NA values for genes with very low counts or outlier-driven results. Filter these rows out before plotting or they’ll silently disappear from your visualization without you noticing how many genes were dropped.
Reading the Final Plot
Once your volcano plot is built, interpretation follows directly from the layout. The plot’s characteristic V or volcanic shape emerges because genes with large fold changes (far left or far right) are more likely to reach statistical significance (high on the y-axis). Points in the upper right corner are your upregulated genes: they’re expressed more in the experimental condition and the difference is statistically significant. Points in the upper left are downregulated. The dense cloud of points near the origin represents genes with small or non-significant changes.
The width of the volcano tells you about the overall magnitude of expression changes in your experiment, while the height reflects the statistical power. A very tall, narrow volcano suggests high statistical power but modest biological effect sizes. A wide, short volcano suggests large changes but noisy data. Genes that fall outside the vertical threshold lines but below the horizontal threshold line changed substantially but didn’t reach significance, which could indicate they’re worth investigating with more replicates.

