Introduction
Heatmaps are a visual representation that uses color gradients to illustrate the magnitude of values in a matrix or data frame. That's why In R, creating a heatmap is straightforward thanks to several dedicated packages, and the process can be mastered in just a few steps. On top of that, this guide explains how to make a heatmap in r, covering the most common methods—base R, ggplot2, and the popular pheatmap package. By the end, you will be able to generate clear, publication‑ready heatmaps for any dataset, whether you are visualizing correlation matrices, expression data, or custom numeric tables.
Worth pausing on this one.
Prerequisites
Before you can draw a heatmap, check that the necessary tools are installed and loaded.
-
Install R and RStudio (or any IDE you prefer).
-
Open the console and run the following commands to install the required packages:
install.packages("pheatmap") # for advanced heatmaps with clustering install.packages("ggplot2") # for ggplot‑based heatmaps -
Load the libraries in your session:
library(pheatmap) # advanced heatmap functions library(ggplot2) # grammar of graphics
Tip: If you are using a fresh R installation, the install.packages() step may take a few minutes depending on your internet connection Practical, not theoretical..
Basic Heatmap with Base R
The simplest way to create a heatmap is by using the built‑in heatmap() function. This method works directly on numeric matrices Simple, but easy to overlook..
Creating a Data Matrix
First, generate a matrix of numeric values. For demonstration, we’ll simulate a 10 × 10 matrix with random normal data And that's really what it comes down to..
set.seed(123) # for reproducibility
data_matrix <- matrix(rnorm(100), nrow = 10, ncol = 10)
colnames(data_matrix) <- paste0("Var", 1:10) # optional column names
rownames(data_matrix) <- paste0("Sample", 1:10) # optional row names
Plotting the Heatmap
heatmap(data_matrix,
col = colorRampPalette(c("blue", "white", "red"))(100), # custom color scale
scale = "none", # disable automatic scaling
margins = c(5, 5), # space for labels
labRow = TRUE, labCol = TRUE) # show row/col names
Key points
coldefines the color palette; you can supply any vector of colors.scalecontrols whether the data are transformed (e.g.,scale = "row"to z‑score each row).mar(margins) adds space for dendrograms and labels.
While this approach is quick, it lacks fine‑grained control over clustering and annotations, which are often needed for more complex visualizations And that's really what it comes down to..
Advanced Heatmap with ggplot2
ggplot2 enables a grammar‑of‑graphics approach, making it easy to customize layers such as titles, themes, and annotations.
Data Preparation
Convert your matrix to a long‑format data frame, which ggplot2 expects Not complicated — just consistent..
library(reshape2) # for melt()
df_long <- melt(data_matrix)
colnames(df_long) <- c("Row", "Column", "Value")
Plotting with geom_tile()
ggplot(df_long, aes(x = Column, y = Row, fill = Value)) +
geom_tile(color = "white") + # draws each cell with a border
scale_fill_gradient(low = "blue", high = "red") + # custom gradient
theme_minimal() +
theme(
axis.title = element_blank(),
axis.text = element_text(size = 10),
legend.title = element_blank(),
plot.title = element_text(hjust = 0.5)
) +
ggtitle("Heatmap of Simulated Data (ggplot2)")
Why use ggplot2?
- Full control over visual aesthetics (colors, fonts, themes).
- Easy integration with other ggplot layers (e.g., adding a correlation matrix heatmap).
- Produces publication‑ready graphics with minimal tweaking.
Using the pheatmap Package
pheatmap is the go‑to package for heatmaps with hierarchical clustering and annotation capabilities. It builds on base R graphics but adds many conveniences.
Installation & Loading
library(pheatmap)
Preparing Annotations
Annotations (e.g., row/column metadata) are supplied via annotation_* functions. For this example, we’ll create a simple data frame describing sample groups Not complicated — just consistent..
annotation_colors <- data.frame(
Row = rep(c("A", "B"), each = 5),
stringsAsFactors = FALSE
)
ann_colors <- list(Row = c(A = "darkorange", B = "steelblue"))
Plotting
pheatmap(data_matrix,
display_numbers = FALSE, # hide numbers
```r
cluster_rows = TRUE, # cluster rows
cluster_cols = TRUE, # cluster columns
show_rownames = TRUE, # display row names
show_colnames = TRUE, # display column names
annotation_col = annotation_colors, # add annotations
annotation_colors = ann_colors, # specify colors for annotations
main = "pheatmap with Clustering and Annotations",
color = colorRampPalette(rev(brewer.pal(9, "RdBu")))(100),
border_color = "white", # cell borders
cellwidth = 15, # adjust cell width
cellheight = 15, # adjust cell height
scale = "none") # no scaling
Advantages of pheatmap
- Integrated clustering: Dendrograms are drawn automatically when
cluster_rowsorcluster_colsareTRUE. - Annotation layers: Row and column metadata can be displayed directly beneath the heatmap.
- Customizable appearance: Fine‑tune colors, labels, cell sizes, and borders.
- Number display: Optionally show numeric values in cells with
display_numbers = TRUE.
Choosing the Right Method
| Method | Strengths | Weaknesses |
|---|---|---|
| Base R | Quick, minimal code | Limited customization, no clustering |
| ggplot2 | Highly flexible, publication‑ready | Steeper learning curve, slower for big data |
| pheatmap | Clustering + annotations + polished defaults | Less control over individual layers |
Use base R for exploratory analysis, ggplot2 when aesthetics matter, and pheatmap for complex datasets requiring both clustering and metadata.
Conclusion
Heatmaps in R can be created with base graphics, ggplot2, or the specialized pheatmap package. Day to day, for rapid visualization, base R suffices; for involved, publication‑grade figures, ggplot2 excels; and for heatmaps that must integrate clustering and annotations, pheatmap is the most convenient choice. Plus, each approach offers a balance between simplicity and control. Selecting the appropriate tool ensures that your data stories are both accurate and visually compelling Still holds up..
Further Resources
pheatmapvignette:vignette("pheatmap")— complete walkthrough to clustering options, annotation formatting, and saving high‑resolution output.ComplexHeatmappackage (Bioconductor) — for advanced layouts (multiple heatmaps side‑by‑side, on‑coplot annotations, and interactive Shiny integration).ggplot2extensions:geom_tile()combined withggdendroordendextendfor custom dendrogram–heatmap assemblies.- Color theory: ColorBrewer (via
RColorBrewer) andviridisfor perceptually uniform, color‑blind safe palettes. - Reproducible reporting: Wrap heatmap code in R Markdown or Quarto documents; use
knitr::kable()for accompanying summary tables.
Reproducibility
The examples above were generated with the following environment. Recording session information ensures that collaborators can recreate identical figures.
sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 22.04.4 LTS
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] pheatmap_1.0.12 ggplot2_3.5.1 RColorBrewer_1.1-3
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.6.5 cli_3.6.2 knitr_1.46 rlang_1.1.3
## [5] xfun_0.43 generics_0.1.3 jsonlite_1.8.8 glue_1.7.0
## [9] htmltools_0.5.8.1 scales_1.3.0 fansi_1.0.6 rmarkdown_2.26
## [13] grid_4.4.1 evaluate_0.23 tibble_3.2.1 fastmap_1.1.1
## [17] yaml_2.3.8 lifecycle_1.0.4 compiler_4.4.1 pillar_1.9.0
## [21] pkgconfig_2.0.3 munsell_0.5.1 cachem_1.0.8
By mastering the three core approaches—base R for speed, ggplot2 for design freedom, and pheatmap (or ComplexHeatmap) for integrated clustering and annotation—you equip yourself to transform any numeric matrix into an insightful, publication‑ready heatmap No workaround needed..
Practical Decision Framework
Choosing the right tool often comes down to the specific constraints of your project. Use the checklist below to match your requirements to the most efficient approach No workaround needed..
| Requirement | Recommended Tool | Why |
|---|---|---|
| Exploratory analysis < 5 min | heatmap() or image() |
Zero dependencies, instant feedback |
| Publication figure with custom legend, faceting, or non‑standard geoms | ggplot2 + geom_tile() |
Full grammar‑of‑graphics control |
| Hierarchical clustering + row/column annotations + dendrograms | pheatmap |
One‑function solution with sensible defaults |
| Multiple aligned heatmaps, on‑coplot annotations, or interactive Shiny apps | ComplexHeatmap |
Designed for complex multi‑panel layouts |
| Large matrices (> 10⁴ rows/cols) needing WebGL rendering | plotly::heatmaply() or ggplotly() |
Hardware‑accelerated browser output |
| Reproducible pipeline with parameter versioning | targets + ggplot2/pheatmap |
Tracks code, data, and output hashes |
Worth pausing on this one.
Quick heuristic: start with pheatmap::pheatmap(mat). If you hit a layout limitation (e.g., split heatmaps, custom annotation graphics), migrate to ComplexHeatmap. If you need total aesthetic freedom or want to embed the heatmap in a multi‑panel ggplot2 figure, rebuild with geom_tile().
Common Pitfalls & How to Avoid Them
| Pitfall | Symptom | Fix |
|---|---|---|
| Implicit factor ordering | Rows/columns appear in alphabetical rather than biological order | Convert to factor with explicit levels= before plotting |
| Diverging palette centered at zero | Zero-centered data (log‑fold changes) looks skewed | Use scale_fill_gradient2(midpoint = 0) or pheatmap(breaks = seq(-max, max, length = 101)) |
| Overplotting in dense annotations | Text labels overlap in pheatmap |
Reduce fontsize_row, rotate with angle_col = 45, or switch to ComplexHeatmap::HeatmapAnnotation with gp = gpar(fontsize = 6) |
| Color blindness unsafe palettes | Red–green clusters indistinguishable | Default to viridis::viridis(), RColorBrewer::brewer.pal("RdBu"), or colorRampPalette(c("#053061","#FFFFFF","#67001F")) |
Missing sessionInfo() in reports |
Collaborators cannot reproduce exact colors/clustering | Always render sessionInfo() in an appendix chunk (as shown above) |
| Clustering on un‑scaled data | High‑variance features dominate dendrograms | pheatmap(mat, scale = "row") or pre‑process with t(scale(t(mat))) |
Exporting Publication‑Ready Files
# Vector (editable in Illustrator/Inkscape)
pheatmap(mat, filename = "heatmap.svg", width = 7, height = 6)
# High‑resolution raster (for journals requiring TIFF @ 300 dpi)
pheatmap(mat, filename = "heatmap.tiff", width = 7, height = 6,
res = 300, units = "in", compression = "lzw")
# ggplot2 equivalent
ggsave("heatmap_ggplot.pdf", plot = p, width = 7, height = 6, device = cairo_pdf)
Tip: Set fonts = "Arial" (or your journal’s required font) via showtext::showtext_auto() before rendering to avoid font substitution warnings Less friction, more output..
Minimal Reproducible Template
Save the following as heatmap_template.Rmd; render with quarto::quarto_render() or rmarkdown::render() for a complete, version‑controlled report.
---
title: "Heatmap Analysis – {{date}}"
format: pdf
execute:
echo: false
warning: false
message: false
## Advanced Customization with **ComplexHeatmap**
When the basic `pheatmap` defaults no longer satisfy your design requirements, **ComplexHeatmap** offers a flexible, layer‑based approach. It integrates tightly with `grid` and `ggplot2` graphics systems, allowing you to:
- **Add custom annotation graphics** (e.g., logos, phylogenetic trees) as separate heatmap layers.
- **Control row/column grouping** with `HeatmapRowLabels` and `HeatmapColumnLabels`.
- **Overlay statistical tracks** such as p‑value heatmaps or consensus matrices.
```r
library(ComplexHeatmap)
library(circlize)
# Assume 'mat' is your normalized expression matrix
ha <- HeatmapAnnotation(
group = anno_block(
block_height = unit(10, "mm"),
gp = gpar(fill = "lightgray", col = NA),
label = "Sample Group"
),
stringsAsFactors = FALSE
)
Heatmap(mat,
top_annotation = ha,
cluster_columns = TRUE,
cluster_rows = TRUE,
show_row_dendogram = TRUE,
show_col_dendogram = TRUE,
border = TRUE,
rect_gp = gpar(fill = "white", col = NA, lwd = 2)
) + draw()
The resulting graphic can be saved directly as an SVG for vector‑based publishing:
gtsave("advanced_heatmap.svg", width = 8, height = 6)
Embedding Heatmaps in ggplot2 for Multi‑Panel Figures
If you need to combine heatmaps with other visual elements (e., bar plots, line graphs, or custom glyphs) inside a single ggplot2 canvas, converting the raster/matrix to geom_tile is the way to go. Still, g. This approach also lets you make use of ggplot2’s faceting (facet_grid/facet_wrap) for complex layouts.
Counterintuitive, but true.
library(ggplot2)
# Convert matrix to long format
library(reshape2)
mat_long <- melt(mat, variable.name = "Row", value.name = "Value")
# Choose a colour scale centered at zero for log‑fold changes
p <- ggplot(mat_long, aes(x = Row, y = as.numeric(Var1), fill = Value)) +
geom_tile(color = "white", size = 0.5) +
scale_fill_gradient2(
palette = "RdBu",
midpoint = 0,
limits = c(-max(abs(mat_long$Value)), max(abs(mat_long$Value))),
space = "Lab",
na.value = "grey50"
) +
theme_minimal() +
theme(
axis.title.x = element_blank(),
axis.title.y = element_blank(),
axis.text.x = element_text(size = 8, angle = 45, hjust = 1),
axis.text.y = element_text(size = 8),
legend.position = "right"
) +
labs(title = "Expression Heatmap (ggplot2)",
x = NULL, y
```r
y = NULL) + # close the label arguments
geom_rect(data = dendro_rows, mapping = aes(xmin = x1, xmax = x2,
ymin = y1, ymax = y2),
inherit.aes = FALSE, colour = "black", fill = NA, size = 0.5) +
geom_rect(data = dendro_cols, mapping = aes(xmin = x1, xmax = x2,
ymin = y1, ymax = y2),
inherit.aes = FALSE, colour = "black", fill = NA, size = 0.5) +
# Optional: overlay a silhouette or consensus matrix as a semi‑transparent layer
geom_tile(data = consensus_long,
aes(x = Row, y = as.numeric(Var1), fill = Value),
alpha = 0.3, show.legend = FALSE) +
scale_fill_gradient2(
palette = "YlOrRd", midpoint = 0,
limits = range(consensus_long$Value),
space = "Lab", na.value = "grey80"
) +
labs(
title = "Expression Heatmap (ggplot2) with Dendrogram Overlays",
subtitle = "Rows = genes, Columns = samples; coloured overlay = consensus matrix",
x = NULL, y = NULL
) +
theme_minimal() +
theme(
axis.title.x = element_blank(),
axis.title.y = element_blank(),
axis.text.x = element_text(size = 8, angle = 45, hjust = 1),
axis.text.y = element_text(size = 8),
legend.position = "right",
legend.title = element_blank(),
panel.grid.major = element_line(colour = "grey90"),
panel.grid.minor = element_blank()
)
Building Multi‑Panel Figures with Faceting
When you need to compare several heatmaps side‑by‑side (e.g.On the flip side, , different experimental conditions or data subsets), ggplot2’s faceting capabilities become invaluable. By converting each subset to the long format and using facet_grid or facet_wrap, you can generate a cohesive multi‑panel layout while preserving a shared colour scale And it works..
# Assume 'mat_condition1' and 'mat_condition2' are matrices for two conditions
mat_cond1_long <- melt(mat_condition1, variable.name = "Row", value.name = "Value")
mat_cond2_long <- melt(mat_condition2, variable.name = "Row", value.name = "Value")
# Combine into a single data frame with a condition factor
cond1 <- transform(mat_cond1_long, Condition = "Condition 1")
cond2 <- transform(mat_cond2_long, Condition = "Condition 2")
combined <- rbind(cond1, cond2)
# Create the faceted heatmap
p_faceted <- ggplot(combined, aes(x = Row, y = as.numeric(Var1), fill = Value)) +
geom_tile(color = "white", size = 0.3) +
scale_fill_gradient2(
palette = "RdBu", midpoint = 0,
limits = c(-max(abs(combined$Value)), max(abs(combined$Value))),
space = "Lab", na.value = "grey50"
) +
facet_grid(Condition ~ .) + # wrap across conditions, stack rows
labs(
title = "Comparative Heatmaps by Condition",
x = NULL, y = NULL, fill = "Log₂ Fold Change"
) +
theme_minimal() +
theme(
axis.text.x = element_text(size = 7
### Extending Faceted Heatmaps with Dendrograms and Refined Layout
The faceted plot shown above is a solid foundation, but real‑world analyses often benefit from visual cues that reveal the underlying structure of the data. Adding row‑ and column‑dendrograms, aligning the y‑axis across panels, and fine‑tuning the strip labels can transform a simple comparison into an informative multi‑panel figure.
This is the bit that actually matters in practice.
#### 1. Incorporating Dendrogram Overlays
If the matrices originate from a clustering step (e.Consider this: g. , hierarchical clustering performed with `pheatmap::pheatmap` or `stats::hclust`), you can reuse the corresponding dendrogram objects to guide the row ordering in each facet.
```r
# Assuming dend_row_cond1 and dend_col_cond1 are dendrogram objects for Condition 1
# and similarly for Condition 2
library(ggdendro)
# Prepare dendrogram data for each condition
dend_row_cond1 <- as.data.frame(as.dendrogram(dend_row_cond1))
dend_row_cond2 <- as.data.frame(as.dendrogram(dend_row_cond2))
# Bind them into a single data frame with a condition factor
dend_rows <- bind_rows(
mutate(dend_row_cond1, Condition = "Condition 1"),
mutate(dend_row_cond2, Condition = "Condition 2")
)
# Create a facet‑specific row order by merging the dendrogram order back into the long data
combined <- combined %>%
left_join(dend_rows, by = c("Row" = "member"))
# Reorder the rows within each facet according to the dendrogram order
combined <- combined %>%
arrange(Condition, member)
# Now rebuild the faceted heatmap with the reordered rows
p_faceted <- ggplot(combined, aes(x = Row, y = as.numeric(Var1), fill = Value)) +
geom_tile(color = "white", size = 0.3) +
scale_fill_gradient2(
palette = "RdBu", midpoint = 0,
limits = c(-max(abs(combined$Value)), max(abs(combined$Value))),
space = "Lab", na.value = "grey50"
) +
facet_grid(Condition ~ ., scales = "fixed", shrink = FALSE) +
# Add row dendrograms (drawn on the left of each facet)
geom_dendrogram(data = dend_rows, stats = "row", orientation = "left",
alpha = 0.6, size = 0.4, colour = "steelblue") +
labs(
title = "Comparative Heatmaps by Condition (with Row Dendrograms)",
x = NULL, y = NULL, fill = "Log₂ Fold Change"
) +
theme_minimal() +
theme(
axis.text.x = element_text(size = 7, angle = 45, hjust = 1),
axis.text.y = element_blank(),
panel.grid.major = element_line(colour = "grey90"),
panel.grid.minor = element_blank(),
strip.text = element_text(size = 9, face = "bold"),
strip.background = element_rect(fill = "lightyellow", colour = NA),
legend.position = "right",
legend.title = element_blank()
)
What the extra layer does
facet_grid(Condition ~ ., scales = "fixed", shrink = FALSE)forces all facets to share the same y‑axis limits, which is essential when you want the row dendrograms to line up vertically.geom_dendrogram()draws a compact, semi‑transparent dendrogram on the left side of each panel, allowing the viewer to see how rows cluster within each condition.- The
membercolumn created by `as.data