How To Make A Heatmap In R

13 min read

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.

  1. Install R and RStudio (or any IDE you prefer).

  2. 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
    
  3. 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

  • col defines the color palette; you can supply any vector of colors.
  • scale controls 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_rows or cluster_cols are TRUE.
  • 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

  • pheatmap vignette: vignette("pheatmap") — complete walkthrough to clustering options, annotation formatting, and saving high‑resolution output.
  • ComplexHeatmap package (Bioconductor) — for advanced layouts (multiple heatmaps side‑by‑side, on‑coplot annotations, and interactive Shiny integration).
  • ggplot2 extensions: geom_tile() combined with ggdendro or dendextend for custom dendrogram–heatmap assemblies.
  • Color theory: ColorBrewer (via RColorBrewer) and viridis for 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 member column created by `as.data
Newest Stuff

Just Posted

Curated Picks

Related Reading

Thank you for reading about How To Make A Heatmap In R. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home