3 min read

Control labels in anno_block()

In ComplexHeatmap, labels and other related settings in anno_block() always correspond to the heatmap slices from top to bottom after the heatmap is generated, which means, you always need to first generate the heatmap, then to observe how the heatmap slices are ordered and finally to set a proper value for labels. It is difficult to map the labels in anno_block() to the heatmap slices if clustering is applied, or when the ordering is unknown before the heatmap is drawn.

Let’s see the following example. In the first example, the split variable has the order of "A", "B", "C", so when the row clustering is not applied, it is safe to also set labels = c("A", "B", "C") in anno_block().

library(ComplexHeatmap)
set.seed(123)
m = matrix(rnorm(200), nrow = 20)
split = c(rep("A", 12), rep("B", 6), rep("C", 2))
Heatmap(m, row_split = split, cluster_rows = FALSE,
    right_annotation = rowAnnotation(foo = anno_block(labels = c("A", "B", "C"))))

But if row clustering is applied, the labels in anno_block() correspond wrongly to the heatmap slices.

Heatmap(m, row_split = split, cluster_rows = TRUE,
    right_annotation = rowAnnotation(foo = anno_block(labels = c("A", "B", "C"))))

We need to manually change the value for labels to correctly correspond to heatmap slices.

Heatmap(m, row_split = split, cluster_rows = TRUE,
    right_annotation = rowAnnotation(foo = anno_block(labels = c("B", "A", "C"))))

This would be a problem if we want to programmatically set anno_block(). To solve this problem, in the new version of ComplexHeatmap, I added a new argument graphics in anno_block(). The value for graphics should be a self-defined function with two arguments: 1. row/column indices in current slice, and 2. the levels from the splitting variable in current slice. The graphics function will be executed to every slice.

Let’s implement the previous example with graphics:

Heatmap(m, row_split = split, cluster_rows = TRUE,
    right_annotation = rowAnnotation(foo = anno_block(
        graphics = function(index, levels) {
            grid.rect()
            grid.text(levels, rot = 90)
        }))
)

As you can see, the labels in anno_block() now always correspond to the correct heatmap slice if clustering is appled.

We can create more split variables. In the following example, the heatmap is split by a k-means clustering and a split variable, so there are two split variables. In this case, the value of levels in anno_block() will be a vector of length two: the correspond levels in the two split variables. Now it is also possible to customize according to the corresponding levels:

col = c("1" = "red", "2" = "blue", "A" = "green", "B" = "orange", "C" = "purple")
Heatmap(m, row_km = 2, row_split = split) + 
rowAnnotation(foo = anno_block(
    graphics = function(index, levels) {
        grid.rect(gp = gpar(fill = col[levels[2]], col = "black"))
        grid.text(paste(levels, collapse = ","), 0.5, 0.5, rot = 90,
            gp = gpar(col = col[levels[1]]))
    }
))

We can define a mapping variable, then the original labels of the splitting levels can be changed:

labels = c("1" = "one", "2" = "two", "A" = "Group_A", "B" = "Group_B", "C" = "Group_C")
Heatmap(m, row_km = 2, row_split = split) + 
rowAnnotation(foo = anno_block(
    graphics = function(index, levels) {
        grid.rect(gp = gpar(fill = col[levels[2]], col = "black"))
        grid.text(paste(labels[levels], collapse = ","), 0.5, 0.5, rot = 90,
            gp = gpar(col = col[levels[1]]))
    }
))

We can also construct more complex labels:

Heatmap(m, row_km = 2, row_split = split) + 
rowAnnotation(foo = anno_block(
    graphics = function(index, levels) {
        grid.rect(gp = gpar(fill = col[levels[2]], col = "black"))
        txt = paste(labels[levels], collapse = ",")
        txt = paste0(txt, "\n", length(index), " rows")
        grid.text(txt, 0.5, 0.5, rot = 0,
            gp = gpar(col = col[levels[1]]))
    },
    width = unit(4, "cm")
))

One drawback of setting graphics is now anno_block() does not know what graphics are going to be drawn, so it won’t automatically calculate the space for the graphics, which means, if the graphics are too large, you need to manually set width or height argument.