14  A Single Heatmap

In this thread, we’ll use ggalign to draw all the heatmap in https://jokergoo.github.io/ComplexHeatmap-reference/book/a-single-heatmap.html

library(ggalign)
#> Loading required package: ggplot2
set.seed(123)
nr1 <- 4
nr2 <- 8
nr3 <- 6
nr <- nr1 + nr2 + nr3
nc1 <- 6
nc2 <- 8
nc3 <- 10
nc <- nc1 + nc2 + nc3
mat <- cbind(
    rbind(
        matrix(rnorm(nr1 * nc1, mean = 1, sd = 0.5), nrow = nr1),
        matrix(rnorm(nr2 * nc1, mean = 0, sd = 0.5), nrow = nr2),
        matrix(rnorm(nr3 * nc1, mean = 0, sd = 0.5), nrow = nr3)
    ),
    rbind(
        matrix(rnorm(nr1 * nc2, mean = 0, sd = 0.5), nrow = nr1),
        matrix(rnorm(nr2 * nc2, mean = 1, sd = 0.5), nrow = nr2),
        matrix(rnorm(nr3 * nc2, mean = 0, sd = 0.5), nrow = nr3)
    ),
    rbind(
        matrix(rnorm(nr1 * nc3, mean = 0.5, sd = 0.5), nrow = nr1),
        matrix(rnorm(nr2 * nc3, mean = 0.5, sd = 0.5), nrow = nr2),
        matrix(rnorm(nr3 * nc3, mean = 1, sd = 0.5), nrow = nr3)
    )
)
mat <- mat[sample(nr, nr), sample(nc, nc)]
rownames(mat) <- paste0("row", seq_len(nr))
colnames(mat) <- paste0("column", seq_len(nc))

14.1 Colors

It is important to note that the ComplexHeatmap package reorders the dendrogram by default, while align_dendro() in ggalign does not modify the tree layout.

Another key difference is in how the two packages treat the starting point. ggalign considers the left-bottom as the starting point, whereas ComplexHeatmap starts from the left-top. When reordering the dendrogram, ComplexHeatmap does so in decreasing order, while ggalign uses an ascending order.

To modify colors in the heatmap, you can use the scale_fill_*() function from ggplot2, which provides a flexible way and enriched pallete to adjust color schemes.

dim(mat)
ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

# ComplexHeatmap::Heatmap(mat)
#> [1] 18 24
ggheatmap(mat) +
    scale_fill_gradient2(low = "green", high = "red") +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro() +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

oob argument in the scale_fill_* function can be used to deal with the outliers.

mat2 <- mat
mat2[1, 1] <- 100000
ggheatmap(mat2) +
    scale_fill_gradient2(
        low = "green", high = "red",
        limits = c(-2, 2),
        oob = scales::squish
    ) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

We can use align_plots() to arrange them.

h1 <- ggheatmap(mat) +
    scale_fill_gradient2(name = "mat", low = "green", high = "red") +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())

h2 <- ggheatmap(mat / 4) +
    scale_fill_gradient2(
        name = "mat/4", limits = c(-2, 2L),
        oob = scales::squish,
        low = "green", high = "red"
    ) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())

h3 <- ggheatmap(abs(mat)) +
    scale_fill_gradient2(name = "abs(mat)", low = "green", high = "red") +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
align_plots(h1, h2, h3, ncol = 2L)
#> → heatmap built with `geom_tile()`
#> → heatmap built with `geom_tile()`
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    scale_fill_gradientn(colors = rev(rainbow(10))) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

For character matrix, you can use ggplot2 discrete filling scales.

discrete_mat <- matrix(sample(1:4, 100, replace = TRUE), 10, 10)
colors <- structure(1:4, names = c("1", "2", "3", "4")) # black, red, green, blue
ggheatmap(discrete_mat, aes(fill = factor(value))) +
    scale_fill_manual(values = colors) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

discrete_mat <- matrix(sample(letters[1:4], 100, replace = TRUE), 10, 10)
colors <- structure(1:4, names = letters[1:4])
ggheatmap(discrete_mat) +
    scale_fill_manual(values = colors)
#> → heatmap built with `geom_tile()`

mat_with_na <- mat
na_index <- sample(c(TRUE, FALSE),
    nrow(mat) * ncol(mat),
    replace = TRUE, prob = c(1, 9)
)
mat_with_na[na_index] <- NA
ggheatmap(mat_with_na) +
    scale_fill_gradient2(low = "blue", high = "red", na.value = "black") +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

We won’t compare the LAB and RGB space. If you want to convert color between different color space, try to use farver pacakge.

In ggplot2, you can use panel.border argument in theme() function to control the Heatmap body border.

ggheatmap(mat) +
    theme(
        axis.text.x = element_text(angle = -60, hjust = 0),
        panel.border = element_rect(linetype = "dashed", fill = NA)
    ) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

You can use the filling argument to turn off the heatmap cell filling, allowing you to customize the heatmap body geoms. Use the color aesthetic to specify the cell border color and the linewidth aesthetic to set the border width.

ggheatmap(mat, filling = NULL) +
    geom_tile(aes(fill = value), color = "white") +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) &
    theme(plot.margin = margin())

To draw a blank heatmap body:

ggheatmap(mat, filling = NULL) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) &
    theme(plot.margin = margin())

14.2 Titles

We can use patch_titles() to add titles around each border of the plot. You can use theme() to control the text appearance.

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    patch_titles(right = "I am a row title") +
    theme(plot.patch_title.right = element_text(face = "bold", size = 16)) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    patch_titles(top = "I am a column title") +
    theme(plot.patch_title.top = element_text(face = "bold", size = 16)) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.3 Clustering

14.3.1 Distance methods

# ComplexHeatmap::Heatmap(mat,
#   name = "mat", clustering_distance_rows = "pearson",
#   column_title = "pre-defined distance method (1 - pearson)"
# )
ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(distance = "pearson", reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    patch_titles(top = "pre-defined distance method (1 - pearson)") +
    theme(plot.patch_title.top = element_text(face = "bold", size = 16)) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

# ComplexHeatmap::Heatmap(mat,
#     name = "mat", clustering_distance_rows = function(m) dist(m),
#     column_title = "a function that calculates distance matrix"
# )
ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(distance = dist, reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    patch_titles(top = "a function that calculates distance matrix") +
    theme(plot.patch_title.top = element_text(face = "bold", size = 16)) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.3.2 Clustering methods

Method to perform hierarchical clustering can be specified by method argument, Possible methods are those supported in hclust() function.

# ComplexHeatmap::Heatmap(mat,
#     name = "mat",
#     clustering_method_rows = "single"
# )
ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(method = "single", reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

Use distance = NULL if you don’t want to calculate the distance.

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(
        distance = NULL, method = cluster::diana,
        reorder_dendrogram = TRUE
    ) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(
        distance = NULL, method = cluster::agnes,
        reorder_dendrogram = TRUE
    ) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.3.3 Render dendrograms

It’s easy for ggalign to color the branches by setting the color mapping, since ggalign will add the cutree() results into the underlying data.

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_dendro(aes(color = branch), k = 2L, reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.4 Set row and column orders

We can use align_order() to set the order.

# ComplexHeatmap::Heatmap(mat,
#     name = "mat",
#     row_order = order(as.numeric(gsub("row", "", rownames(mat)))),
#     column_order = order(as.numeric(gsub("column", "", colnames(mat)))),
#     column_title = "reorder matrix"
# )
ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_order(as.numeric(gsub("row", "", rownames(mat)))) +
    anno_top(size = unit(15, "mm")) +
    align_order(as.numeric(gsub("column", "", colnames(mat)))) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.5 Seriation

align_reorder() can directly take the seriate() function as the input and extract the ordering information.

mat2 <- max(mat) - mat
ggheatmap(mat2) +
    scale_fill_gradient2(low = "blue", high = "red", midpoint = 2L) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_reorder(seriation::seriate, method = "BEA_TSP") +
    anno_top(size = unit(15, "mm")) +
    align_reorder(seriation::seriate, method = "BEA_TSP") &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

The above code will execute seriate() twice—once for each dimension. However, since a single run of seriate() can provide the ordering for both dimensions, we can manually extract the ordering indices to avoid redundancy.

o <- seriation::seriate(mat2, method = "BEA_TSP")
ggheatmap(mat2) +
    scale_fill_gradient2(low = "blue", high = "red", midpoint = 2L) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(20, "mm")) +
    align_order(seriation::get_order(o, 1L)) +
    anno_top(size = unit(15, "mm")) +
    align_order(seriation::get_order(o, 2L)) &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

For more use of the seriate() function, please refer to the seriation package.

14.6 Dimension labels

ggplot2 use scales and theme to control the axis labels, Please see chapter for more details.

# ComplexHeatmap::Heatmap(mat,
#     name = "mat", row_names_side = "left", row_dend_side = "right",
#     column_names_side = "top", column_dend_side = "bottom"
# )
ggheatmap(mat) +
    scale_x_continuous(position = "top") +
    scale_y_continuous(position = "right") +
    theme(axis.text.x = element_text(angle = 60, hjust = 0)) +
    anno_left(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    scale_x_continuous(position = "top") +
    anno_bottom(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    scale_y_continuous(position = "right") +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    scale_y_continuous(breaks = NULL) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(
        axis.text.x = element_text(angle = -60, hjust = 0),
        axis.text.y = element_text(face = "bold", size = 16)
    ) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(
        axis.text.x = element_text(angle = -60, hjust = 0),
        axis.text.y = element_text(
            face = "bold", size = 16,
            colour = c(rep("red", 10), rep("blue", 8))
        )
    ) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> Warning: Vectorized input to `element_text()` is not officially supported.
#> ℹ Results may be unexpected or may change in future versions of ggplot2.
#> → heatmap built with `geom_tile()`

14.7 Heatmap split

14.7.1 Split by k-means clustering

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_kmeans(2L) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_kmeans(3L) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_kmeans(2L) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_kmeans(3L) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

The dendrogram was calculated in each group defined by kmeans.

14.7.2 Split by categorical variables

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_group(rep(c("A", "B"), 9)) +
    align_dendro(reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_group(rep(c("C", "D"), 12)) +
    align_dendro(reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.7.3 Split by dendrogram

When you splitted by a dendrogram, the cutted height will be indicated with a dashed line.

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(k = 3L, reorder_dendrogram = TRUE) +
    anno_top(size = unit(15, "mm")) +
    align_dendro(k = 2L, reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_dendro(aes(color = branch), k = 3L, reorder_dendrogram = TRUE) +
    scale_color_brewer(palette = "Dark2") +
    anno_top(size = unit(15, "mm")) +
    align_dendro(k = 2L, reorder_dendrogram = TRUE) +
    quad_active() &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.7.4 Order of slices (panels)

The order of the panels always follow the factor level. Note: the merging of dendrogram between ComplexHeatmap and ggalign is a little different.

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_group(rep(LETTERS[1:3], 6)) +
    align_dendro(aes(color = branch),
        reorder_dendrogram = TRUE,
        reorder_group = TRUE,
        merge_dendrogram = TRUE
    ) +
    scale_color_brewer(palette = "Dark2") +
    anno_top(size = unit(15, "mm")) +
    align_group(rep(letters[1:6], 4)) +
    align_dendro(aes(color = branch),
        reorder_dendrogram = TRUE,
        reorder_group = TRUE,
        merge_dendrogram = TRUE
    ) +
    quad_active() -
    with_quad(theme(strip.text = element_text()), "tr") &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

ggheatmap(mat) +
    theme(axis.text.x = element_text(angle = -60, hjust = 0)) +
    anno_right(size = unit(15, "mm")) +
    align_group(rep(LETTERS[1:3], 6)) +
    align_dendro(aes(color = branch), reorder_dendrogram = TRUE) +
    scale_color_brewer(palette = "Dark2") +
    anno_top(size = unit(15, "mm")) +
    align_group(rep(letters[1:6], 4)) +
    align_dendro(aes(color = branch), reorder_dendrogram = TRUE) +
    quad_active() -
    with_quad(theme(strip.text = element_text()), "tr") &
    theme(plot.margin = margin())
#> → heatmap built with `geom_tile()`

14.7.5 Titles for splitting (facet strip text)

By default, the facet strip text is removed. You can override this behavior with theme(strip.text = element_text()). Since align_group() does not create a new plot, the panel title can only be added to the heatmap plot.

waiting for complete …

14.7.6 Graphic parameters for splitting

ggh4x::facet_grid2(strip = ggh4x::strip_themed(
    background_x = list(
        element_rect(fill = "red"),
        element_rect(fill = "blue"),
        element_rect(fill = "green")
    )
))
#> <ggproto object: Class FacetGrid2, FacetGrid, Facet, gg>
#>     attach_axes: function
#>     compute_layout: function
#>     draw_back: function
#>     draw_front: function
#>     draw_labels: function
#>     draw_panels: function
#>     finish_data: function
#>     finish_panels: function
#>     init_scales: function
#>     map_data: function
#>     params: list
#>     setup_aspect_ratio: function
#>     setup_axes: function
#>     setup_data: function
#>     setup_panel_table: function
#>     setup_params: function
#>     shrink: TRUE
#>     strip: <ggproto object: Class StripElemental, Strip, gg>
#>         assemble_strip: function
#>         build_strip: function
#>         clip: inherit
#>         draw_labels: function
#>         elements: list
#>         finish_strip: function
#>         get_strips: function
#>         given_elements: list
#>         incorporate_grid: function
#>         incorporate_wrap: function
#>         init_strip: function
#>         params: list
#>         setup: function
#>         setup_elements: function
#>         strips: list
#>         super:  <ggproto object: Class StripElemental, Strip, gg>
#>     train_scales: function
#>     vars: function
#>     vars_combine: function
#>     super:  <ggproto object: Class FacetGrid2, FacetGrid, Facet, gg>