Creating customized heatmaps and combining them in a single figure

Author

Charlotte Soneson, Michael Stadler

Summary

This document illustrates how to use ggplot2 to:

  • create customized heatmaps with nested axis label layers
  • combine many multiple such heatmaps in a grid

Prepare data

Run the following code to prepare the data used in this document:

suppressPackageStartupMessages({
    library(tibble)
    library(dplyr)
})

aa <- rev(c("A", "L", "I", "V", "F", "W", "Y", "H", 
            "S", "T", "Q", "N", "D", "E", "K", "R", 
            "M", "C", "G", "P", "*"))
pos_levels <- expand.grid(letters[seq_len(7)], seq_len(5)) %>%
    tidyr::unite(col = "final_position", 
                 c("Var2", "Var1"), sep = "") %>%
    dplyr::pull(final_position) %>% unique()
set.seed(123)
ppis <- data.frame(
    aminoacid = rep(aa, 5 * 7),
    heptad = rep(seq_len(5), each = 7 * length(aa)),
    position = rep(rep(letters[seq_len(7)], each = length(aa)), 5),
    ppi = runif(n = 5 * 7 * length(aa), min = 0.4, max = 1.1)
) %>%
    dplyr::mutate(final_position = factor(paste0(heptad, position), 
                                          levels = pos_levels))

tibble(ppis)
# A tibble: 735 × 5
   aminoacid heptad position   ppi final_position
   <chr>      <int> <chr>    <dbl> <fct>         
 1 *              1 a        0.601 1a            
 2 P              1 a        0.952 1a            
 3 G              1 a        0.686 1a            
 4 C              1 a        1.02  1a            
 5 M              1 a        1.06  1a            
 6 R              1 a        0.432 1a            
 7 K              1 a        0.770 1a            
 8 E              1 a        1.02  1a            
 9 D              1 a        0.786 1a            
10 N              1 a        0.720 1a            
# ℹ 725 more rows

Create figure

Load packages

Code
library(ggplot2)
library(cowplot)
library(scales)
library(ggh4x)

Plot

We start by generating a single heatmap of PPI values for each amino acid mutation at each position. We illustrate two ways of generating ‘hierarchical’ labels for the x-axis, indicating both the heptad number and the relative position within a heptad.

Code
ggplot(ppis,
       aes(x = position, y = aminoacid, fill = ppi)) + 
    geom_tile() + 
    scale_fill_gradientn(colours = c("#811c11", "white", "#aecccb"), 
                         values = rescale(c(0.4, 1, 1.1)),
                         guide = "colorbar", limits = c(0.4, 1.1), 
                         na.value = "white", name = "PPI") + 
    facet_nested(. ~ heptad + position, space = "free_x", 
                 scales = "free_x", switch = "x") +
    scale_x_discrete(expand = expansion(mult = c(0, 0))) + 
    labs(x = "Position", y = "Amino acid") + 
    theme_bw() + 
    theme(axis.title = element_text(size = 16), 
          axis.text.x = element_blank(),
          axis.ticks.x = element_blank(),
          strip.text = element_text(size = 14),
          panel.spacing.x = unit(0, "line"), 
          panel.border = element_blank(),
          panel.grid.major = element_blank(), 
          panel.grid.minor = element_blank(),
          strip.background = element_rect(fill = "white"))

Code
ggplot(ppis,
       aes(x = position, y = aminoacid, fill = ppi)) + 
    geom_tile() + 
    scale_fill_gradientn(colours = c("#811c11", "white", "#aecccb"), 
                         values = rescale(c(0.4, 1, 1.1)),
                         guide = "colorbar", limits = c(0.4, 1.1), 
                         na.value = "white", 
                         name = "PPI") + 
    facet_grid(. ~ heptad, space = "free_x", scales = "free_x", 
               switch = "x") +
    scale_x_discrete(expand = expansion(mult = c(0, 0))) + 
    labs(x = "Position", y = "Amino acid") + 
    theme_bw() + 
    theme(axis.title = element_text(size = 16), 
          axis.ticks.x = element_blank(),
          strip.text = element_text(size = 14),
          panel.spacing.x = unit(0, "line"), 
          panel.border = element_blank(),
          panel.grid.major = element_blank(), 
          panel.grid.minor = element_blank(),
          strip.background = element_rect(fill = "white"))

Next we generate a grid of many such heatmaps. The two plots below illustrate two different ways of displaying the faceting information in the plot.

Code
## Generate random data for nbr x nbr pairs
nbr <- 10
set.seed(123)
## ... random names
nms <- vapply(seq_len(nbr), function(i) paste(sample(LETTERS, 5, replace = TRUE), 
                                              collapse = ""), "")
allpairs <- expand.grid(nms, nms)
## ... generate data
alldat <- do.call(bind_rows, lapply(seq_len(nrow(allpairs)), function(i) {
    tmp <- ppis
    tmp$ppi <- runif(nrow(ppis), min = 0.4, max = 1.1)
    tmp$p1 <- allpairs[i, 1]
    tmp$p2 <- allpairs[i, 2]
    tmp
}))

ggplot(alldat,
       aes(x = final_position, y = aminoacid, fill = ppi)) + 
    geom_tile() + 
    scale_fill_gradientn(colours = c("#811c11", "white", "#aecccb"), 
                         values = rescale(c(0.4, 1, 1.1)),
                         guide = "colorbar", limits = c(0.4, 1.1), 
                         na.value = "white", 
                         name = "PPI") + 
    facet_grid(p1 ~ p2, scales = "free", switch = "y") + 
    scale_x_discrete(expand = expansion(mult = c(0, 0))) + 
    labs(x = "", y = "") + 
    theme_bw() + 
    theme(axis.text = element_blank(),
          axis.ticks = element_blank(),
          panel.border = element_blank(),
          panel.grid.major = element_blank(), 
          panel.grid.minor = element_blank(),
          legend.position = "none",
          strip.background = element_blank())

Code
## Alternative display, without strip titles but adding the labels to each panel
axis_titles <- alldat %>%
    dplyr::select(p1, p2) %>%
    dplyr::distinct()
ggplot(alldat) + 
    geom_tile(aes(x = final_position, y = aminoacid, fill = ppi)) + 
    scale_fill_gradientn(colours = c("#811c11", "white", "#aecccb"), 
                         values = rescale(c(0.4, 1, 1.1)),
                         guide = "colorbar", limits = c(0.4, 1.1), 
                         na.value = "white", 
                         name = "PPI") + 
    facet_grid(p1 ~ p2, scales = "free", switch = "y") + 
    labs(x = NULL, y = NULL) + 
    geom_text(data = axis_titles, aes(label = p2), 
              hjust = 0.5, x = 35 / 2, y = -1, color = "grey10", size = 2) + 
    geom_text(data = axis_titles, aes(label = p1), angle = 90, 
              vjust = 0.5, x = -1, y = 21 / 2, color = "grey10", size = 2) + 
    coord_cartesian(clip = "off") + 
    theme(plot.margin = margin(b = 10, l = 10, r = 5, t = 5)) + 
    scale_x_discrete(expand = expansion(mult = c(0, 0))) + 
    theme_bw() + 
    theme(axis.text = element_blank(),
          axis.ticks = element_blank(),
          panel.border = element_blank(),
          panel.grid.major = element_blank(), 
          panel.grid.minor = element_blank(),
          panel.spacing = unit(0.6, "line"), 
          legend.position = "none",
          strip.background = element_blank(),
          strip.text = element_blank())

Remarks

  • The combined plot could also have been generated with cowplot, which would give a bit more flexibility in the display of each panel - however, with many panels, it is a lot more resource intensive.

Session info

Code
sessioninfo::session_info()
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.5.2 (2025-10-31)
 os       macOS Sequoia 15.7.1
 system   aarch64, darwin20
 ui       X11
 language (EN)
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       Europe/Zurich
 date     2025-11-03
 pandoc   3.6.4 @ /usr/local/bin/ (via rmarkdown)
 quarto   1.7.28 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package      * version date (UTC) lib source
 cli            3.6.5   2025-04-23 [1] CRAN (R 4.5.0)
 cowplot      * 1.2.0   2025-07-07 [1] CRAN (R 4.5.0)
 dichromat      2.0-0.1 2022-05-02 [1] CRAN (R 4.5.0)
 digest         0.6.37  2024-08-19 [1] CRAN (R 4.5.0)
 dplyr        * 1.1.4   2023-11-17 [1] CRAN (R 4.5.0)
 evaluate       1.0.5   2025-08-27 [1] CRAN (R 4.5.1)
 farver         2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
 fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
 generics       0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
 ggh4x        * 0.3.1   2025-05-30 [1] CRAN (R 4.5.0)
 ggplot2      * 4.0.0   2025-09-11 [1] CRAN (R 4.5.0)
 glue           1.8.0   2024-09-30 [1] CRAN (R 4.5.0)
 gtable         0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
 htmltools      0.5.8.1 2024-04-04 [1] CRAN (R 4.5.0)
 htmlwidgets    1.6.4   2023-12-06 [1] CRAN (R 4.5.0)
 jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
 knitr          1.50    2025-03-16 [1] CRAN (R 4.5.0)
 labeling       0.4.3   2023-08-29 [1] CRAN (R 4.5.0)
 lifecycle      1.0.4   2023-11-07 [1] CRAN (R 4.5.0)
 magrittr       2.0.4   2025-09-12 [1] CRAN (R 4.5.1)
 pillar         1.11.1  2025-09-17 [1] CRAN (R 4.5.0)
 pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
 purrr          1.1.0   2025-07-10 [1] CRAN (R 4.5.1)
 R6             2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
 RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
 rlang          1.1.6   2025-04-11 [1] CRAN (R 4.5.0)
 rmarkdown      2.30    2025-09-28 [1] CRAN (R 4.5.0)
 rstudioapi     0.17.1  2024-10-22 [1] CRAN (R 4.5.0)
 S7             0.2.0   2024-11-07 [1] CRAN (R 4.5.0)
 scales       * 1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
 sessioninfo    1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
 tibble       * 3.3.0   2025-06-08 [1] CRAN (R 4.5.0)
 tidyr          1.3.1   2024-01-24 [1] CRAN (R 4.5.0)
 tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
 utf8           1.2.6   2025-06-08 [1] CRAN (R 4.5.0)
 vctrs          0.6.5   2023-12-01 [1] CRAN (R 4.5.0)
 withr          3.0.2   2024-10-28 [1] CRAN (R 4.5.0)
 xfun           0.54    2025-10-30 [1] CRAN (R 4.5.0)
 yaml           2.3.10  2024-07-26 [1] CRAN (R 4.5.0)

 [1] /Users/stadler/Library/R/arm64/4.5/library/__bioc322
 [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────