Heatmap Layout

The heatmap_layout()/ggheatmap() function provides a powerful way to create customizable heatmaps in R using ggplot2. This vignette will guide you through its usage.

input data

The data input can be a numeric or character vector, a data frame, and any other data which can be converted into a matrix.

set.seed(123)
small_mat <- matrix(rnorm(81), nrow = 9)
rownames(small_mat) <- paste0("row", seq_len(nrow(small_mat)))
colnames(small_mat) <- paste0("column", seq_len(ncol(small_mat)))
library(ggalign)
ggheatmap(small_mat)
#> → heatmap built with `geom_tile()`

heatmap body

For ggplot2 usage, the matrix input will be converted into a long formated data frame when drawing. The default mapping will use aes(.data$.x, .data$.y), but can be controlled using mapping argument. The data in the underlying ggplot object contains following columns:

  • .xpanel and .ypanel: the column and row panel

  • .x and .y: the x and y coordinates

  • .row_names and .column_names: A factor of the row and column names of the original matrix (only applicable when names exist).

  • .row_index and .column_index: the row and column index of the original matrix.

  • value: the actual matrix value.

You can treat the heatmap_layout() object as a standard ggplot2 object with default mapping and data. This means you can add ggplot2 layers or elements just like in any ggplot object.

ggheatmap(small_mat) + geom_point() + scale_fill_viridis_c()
#> → heatmap built with `geom_tile()`

By default, ggheatmap() adds a heatmap layer. If the matrix has more than 20,000 cells (nrow * ncol > 20000), it uses geom_raster() for performance efficiency; for smaller matrices, geom_tile() is used. You can explicitly choose the layer by providing a single string ("raster" or "tile") in the filling argument.

ggheatmap(small_mat, filling = "raster")

ggheatmap(small_mat, filling = "tile")

If you set filling = NULL, a blank heatmap will be drawn, allowing for customized filling geoms. In this way, you must set fill mapping manually.

ggheatmap(small_mat, filling = NULL) +
  geom_tile(aes(fill = value), color = "black", width = 0.9, height = 0.9)

You can use external packages like ggrastr or ggfx to rasterize the heatmap body. Additionally, ggfx offers many image filters that can be applied to ggplot2 layers.

ggheatmap(small_mat, filling = FALSE) +
  ggrastr::rasterise(geom_tile(aes(fill = value)), dev = "ragg")

You can rasterize all plots in the layout directly with ggrastr::rasterise(). This method works for both ggheatmap() and ggstack() objects.

ggrastr::rasterise(ggheatmap(small_mat), dev = "ragg")
#> → heatmap built with `geom_tile()`

heatmap annotations

A heatmap typically has two observation axes, you can reorder the observations, or provide additional information for them. An annotation is a stack_layout() object internally, capable of holding multiple plots, and can be placed at the top, left, bottom, or right.

Note that ggalign use the concept of "number of observations" in the vctrs package or NROW() function. When aligning the observations, you must ensure the number of observations is equal. For column annotations, the layout data will be transposed before use. This is necessary because column annotations use heatmap columns as observations, but we need rows.

By default, heatmap_layout()/ggheatmap() does not initialize an active context, so all additions are placed within the heatmap body. we can use hmanno() to set the active context, directing all subsequent additions to this position. The active context allows for custom layout adjustments and the addition of various plot types.

In the following example, align_kmeans() is used to group the columns into three panels. It doesn’t matter if this is added to the top or bottom since it won’t add a plot area:

ggheatmap(small_mat) +
  hmanno("t") +
  align_kmeans(3L)
#> → heatmap built with `geom_tile()`

We can add any align_*() function to the annotation. For more details on align_*() functions, refer to vignette("layout-customize") and vignette("layout-plot").

ggheatmap(small_mat) +
  theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
  hmanno("r") +
  align_dendro(k = 3L) +
  geom_point(aes(color = factor(branch)))
#> → heatmap built with `geom_tile()`

In this example:

  • hmanno("r") change the active context to the right of the heatmap.
  • align_dendro(k = 3L) adds a dendrogram to this right-side annotation context and sets itself as the active plot in the annotation stack.
  • geom_point(aes(color = factor(branch))) is then added to this active plot within the annotation stack, here, it means the align_dendro() plot.

Plot Size

Heatmap Body Size

You can specify the relative sizes of the heatmap body using the width and height arguments in the ggheatmap() function.

ggheatmap(small_mat, height = 2) +
  scale_fill_viridis_c() +
  hmanno("t") +
  align_dendro()
#> → heatmap built with `geom_tile()`

Alternatively, the hmanno() function allows you to control the heatmap body sizes when position is NULL.

ggheatmap(small_mat) +
  hmanno(position = NULL, height = 2) +
  scale_fill_viridis_c() +
  hmanno("t") +
  align_dendro()
#> → heatmap built with `geom_tile()`

Annotation Stack Size

The hmanno() function allows you to control the total annotation stack size when position is a string. The size argument controls the relative width (for left and right annotations) or height (for top and bottom annotations) of the whole annotation stack.

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  hmanno("t", size = 1) +
  align_dendro()
#> → heatmap built with `geom_tile()`

You can also specify an exact size using unit():

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  hmanno("t", size = unit(30, "mm")) +
  align_dendro()
#> → heatmap built with `geom_tile()`

Single Plot Size

All align_*() functions that add plots in the annotation stack have a size argument to control the relative width (for left and right annotations) or height (for top and bottom annotations) of the single plot in the annotation stack.

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  hmanno("l", size = 0.2) +
  ggalign(data = rowSums, aes(x = value), size = unit(10, "mm")) +
  geom_bar(
    aes(y = .y, fill = factor(.y)),
    stat = "identity",
    orientation = "y"
  ) +
  scale_fill_brewer(palette = "Set1", guide = "none")
#> → heatmap built with `geom_tile()`

Plot actions

The plot_action() function defines the behavior of plots within a layout. It can be used in the action argument of layout functions like hmanno() or stack_active() to set global actions for all plots in the layout. Additionally, plot_action() can be applied directly to specific plots through the action argument in the align_*() functions, or it can be added directly to a plot.

guides

By default, ggheatmap() will collect all guide legends on the side from which they originate.

heatmap_collect_all_guides <- ggheatmap(small_mat, width = 2, height = 2L) +
  scale_fill_viridis_c(name = "I'm from heatmap body") +
  theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
  hmanno("t") +
  align_dendro(aes(color = branch), k = 3L) +
  scale_color_brewer(
    name = "I'm from top annotation", palette = "Dark2",
    guide = guide_legend(position = "right")
  ) +
  hmanno("l") +
  align_dendro(aes(color = branch), k = 3L) +
  scale_color_brewer(
    name = "I'm from left annotation", palette = "Dark2",
    guide = guide_legend(position = "top", direction = "vertical")
  ) +
  hmanno("b") +
  align_dendro(aes(color = branch), k = 3L) +
  scale_color_brewer(
    name = "I'm from bottom annotation", palette = "Dark2",
    guide = guide_legend(position = "left")
  ) +
  hmanno("r") +
  align_dendro(aes(color = branch), k = 3L) +
  scale_color_brewer(
    name = "I'm from right annotation", palette = "Dark2",
    guide = guide_legend(position = "bottom", direction = "vertical")
  ) &
  theme(plot.margin = margin())
heatmap_collect_all_guides
#> → heatmap built with `geom_tile()`

Use the guides argument control which side of guide legends should be gathered. In the following example, we’ll collect the guide legends only on the top and bottom sides:

heatmap_collect_all_guides +
  # we only collect guides in the top and bottom side
  hmanno(action = plot_action(guides = "tb"))
#> → heatmap built with `geom_tile()`

You can also apply the plot_action() function directly to specific plots:

heatmap_collect_all_guides +
  # we only collect guides in the top and bottom side
  hmanno(action = plot_action(guides = "tb")) +
  # we ensure the heatmap body is the active context
  hmanno() +
  # for the heatmap body, we collect guide in the right side
  plot_action(guides = "r")
#> → heatmap built with `geom_tile()`

As previously mentioned, the annotation is represented as a stack_layout() object. The guide legends within the annotation stack are first collected by the stack_layout() itself and then passed to the overall heatmap layout for further integration. By default, it inherits the guides arguments from the heatmap layout. See following example:

heatmap_collect_all_guides +
  # we only collect guides in the top and bottom side in the heatmap layout
  hmanno(action = plot_action(guides = "tb")) +
  # we ensure the active context is in the bottom annotation
  # By default, it inherits "guides" argument from the heamtap layout, which
  # means it'll collect "guides" in the top and bottom side
  hmanno("b") +
  # for the dendrogram in the bottom annotation, we collect guide in the left side
  plot_action(guides = "l")
#> → heatmap built with `geom_tile()`

Here, the guide legend is collected by the bottom annotation but will not be collected by the heatmap layout since the heatmap layout only gathers guides from the top and bottom. In this way, the guide legends of the annotation stack will be put around the annotation stack layout.

To override this guide collection behavior for the heatmap annotation, you can use the free_guides argument of the hmanno() function. This differs from the guides argument in plot_action(), which controls the behavior for the plots in the layout. The free_guides argument specifies which guide legends from the annotation stack layout should be collected by the heatmap layout.

heatmap_collect_all_guides +
  # we only collect guides in the top and bottom side in the heatmap layout
  hmanno(action = plot_action(guides = "tb")) +
  # we also collect guides in the left side for the bottom annotation stack
  # layout in the heatmap layout
  hmanno("b", free_guides = "l") +
  # for the dendrogram in the bottom annotation, we collect guide in the left side
  plot_action(guides = "l")
#> → heatmap built with `geom_tile()`

Note: The heatmap layout will only collect guide legends from the annotation stack if the stack layout collects its own guides first.

free_spaces

By default, ggheatmap() will align all elements of the plot, which can sometimes lead to unwanted spacing. Consider the following example:

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  hmanno("t", size = unit(30, "mm")) +
  align_dendro() +
  scale_y_continuous(
    expand = expansion(),
    labels = ~ paste("very very long labels", .x)
  ) +
  hmanno("l", unit(20, "mm")) +
  align_dendro()
#> → heatmap built with `geom_tile()`

In this case, the left annotation stack is positioned far from the heatmap body due to the wide axis labels in the top annotation stack. This occurs because the top annotation stack is aligned with the heatmap body. To fix this, you can remove the left borders around the panel of the top annotation stack by setting free_spaces = "l".

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  hmanno("t", size = unit(30, "mm"), action = plot_action(free_spaces = "l")) +
  align_dendro() +
  scale_y_continuous(
    expand = expansion(),
    labels = ~ paste("very long labels", .x)
  ) +
  hmanno("l", unit(20, "mm")) +
  align_dendro()
#> → heatmap built with `geom_tile()`

One useful way to utilize free_spaces is to position the guide legends next to the annotations. (Note the guide legend from the bottom annotation):

heatmap_collect_all_guides +
  # we only collect guides in the top and bottom side
  hmanno(action = plot_action(guides = "tb")) +
  # 1. in the bottom annotation stack layout, we collect the legends in the
  #    left side
  # 2. we remove the spaces of the left border in the annotation
  hmanno("b", action = plot_action(guides = "l", free_spaces = "l"))
#> → heatmap built with `geom_tile()`

In the ggheatmap() layout, the behavior of free_spaces and free_labs arguments is distinct from other arguments in plot_action():

  • If position is NULL: These arguments control the behavior of the heatmap body and set the global values for the heatmap layout. Setting to waiver() inherits from the parent layout.

  • If position is a string: These arguments set the global values for the annotation stack layout. If set to waiver(), it inherits from specific heatmap layout axes:

    • For top and bottom annotations, it inherits from the left (“l”) and right (“r”) axes.
    • For left and right annotations, it inherits from the top (“t”) and bottom (“b”) axes.

free_labs

By default, ggheatmap() won’t align the axis titles.

ggheatmap(small_mat) +
  scale_fill_viridis_c() +
  ylab("Heatmap title") +
  hmanno("t", size = unit(30, "mm")) +
  align_dendro() +
  ylab("Annotation title")
#> → heatmap built with `geom_tile()`

To align axis titles, you can set free_labs = NULL. Alternatively, A single string containing one or more of axis positions (“t”, “l”, “b”, “r”) to indicate which axis titles should be free from alignment.

ggheatmap(small_mat, action = plot_action(free_labs = NULL)) +
  scale_fill_viridis_c() +
  ylab("Heatmap title") +
  hmanno("t", size = unit(30, "mm")) +
  align_dendro() +
  ylab("Annotation title")
#> → heatmap built with `geom_tile()`

Session information

sessionInfo()
#> R version 4.4.1 (2024-06-14)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=C              
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] bookdown_0.41      ggalign_0.0.4.9000 ggplot2_3.5.1      rmarkdown_2.28    
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.5       jsonlite_1.8.9     compiler_4.4.1     highr_0.11        
#>  [5] crayon_1.5.3       ggbeeswarm_0.7.2   jquerylib_0.1.4    textshaping_0.4.0 
#>  [9] systemfonts_1.1.0  scales_1.3.0       yaml_2.3.10        fastmap_1.2.0     
#> [13] R6_2.5.1           labeling_0.4.3     knitr_1.48         tibble_3.2.1      
#> [17] maketools_1.3.1    munsell_0.5.1      bslib_0.8.0        pillar_1.9.0      
#> [21] RColorBrewer_1.1-3 rlang_1.1.4        utf8_1.2.4         cachem_1.1.0      
#> [25] xfun_0.48          sass_0.4.9         sys_3.4.3          viridisLite_0.4.2 
#> [29] cli_3.6.3          withr_3.0.1        magrittr_2.0.3     digest_0.6.37     
#> [33] grid_4.4.1         beeswarm_0.4.0     lifecycle_1.0.4    vipor_0.4.7       
#> [37] ggrastr_1.0.2      vctrs_0.6.5        evaluate_1.0.1     glue_1.8.0        
#> [41] farver_2.1.2       ragg_1.3.3         buildtools_1.0.0   fansi_1.0.6       
#> [45] colorspace_2.1-1   tools_4.4.1        pkgconfig_2.0.3    htmltools_0.5.8.1