3 min read

Block annotation over several slices

In ComplexHeatmap package, anno_block() function draws rectangles for row/column slices, like in the following plot. Then what if we want to draw the rectangles over several slices to show they belong to certain groups?

set.seed(123)
mat = matrix(rnorm(50*50), nrow = 50)
library(ComplexHeatmap)
ha = HeatmapAnnotation(foo = anno_block(gp = gpar(fill = 2:6), labels = LETTERS[1:5]))
split = rep(1:5, each = 10)
Heatmap(mat, name = "mat", column_split = split, top_annotation = ha, 
    column_title = NULL)

Currently, it is difficult to directly support it in anno_block(), however, there is workaround for it. Actually, to draw rectangles across several slices, we need to know two things: 1. the positions of the slices in the plot, and 2. space to draw the rectangles. Luckily, the positions can be obtained by directly go to the correspoding viewport and the space can be allocated by anno_empty() function.

In the following code, we use anno_empty() to create an empty annotation:

ha = HeatmapAnnotation(
    empty = anno_empty(border = FALSE),
    foo = anno_block(gp = gpar(fill = 2:6), labels = LETTERS[1:5])
)
Heatmap(mat, name = "mat", column_split = split, top_annotation = ha, 
    column_title = NULL)

Let’s say, we want to put the first three column slices as a group and the last two slices as the second group.

The positions of the first and the third slicesm for annotation "empty" can be obtained by:

seekViewport("annotation_empty_1")
loc1 = deviceLoc(x = unit(0, "npc"), y = unit(0, "npc"))
seekViewport("annotation_empty_3")
loc2 = deviceLoc(x = unit(1, "npc"), y = unit(1, "npc"))
loc2
## $x
## [1] 4.07350393700788inches
## 
## $y
## [1] 6.50787401574803inches

The viewport name "annotation_empty_1" correspond to the first slice for annotation empty, and we take the left bottom of the first “empty” annotation slice and the top right of the third slice, saved in loc1 and loc2 variables.

Here what is important is the use of grid::deviceLoc() function. It directly converts a location measured in a certain viewport to the position in the graphics device.

In the end, we go to the "global" viewport because the size of "global" viewport is the size of the graphics device, and draw the rectangle and add label.

seekViewport("global")
grid.rect(loc1$x, loc1$y, width = loc2$x - loc1$x, height = loc2$y - loc1$y, 
    just = c("left", "bottom"), gp = gpar(fill = "red"))
grid.text("group 1", x = (loc1$x + loc2$x)*0.5, y = (loc1$y + loc2$y)*0.5)

The viewport names for the annotations are in a fixed format, which is annotation_{annotation_name}_{slice_index}. The full set of viewport names can be obtained by list_components() function.

list_components()
##  [1] "ROOT"                        "global"                     
##  [3] "global_layout"               "global-heatmaplist"         
##  [5] "main_heatmap_list"           "heatmap_mat"                
##  [7] "mat_heatmap_body_wrap"       "mat_heatmap_body_1_1"       
##  [9] "mat_heatmap_body_1_2"        "mat_heatmap_body_1_3"       
## [11] "mat_heatmap_body_1_4"        "mat_heatmap_body_1_5"       
## [13] "mat_dend_row_1"              "mat_dend_column_1"          
## [15] "mat_dend_column_2"           "mat_dend_column_3"          
## [17] "mat_dend_column_4"           "mat_dend_column_5"          
## [19] "annotation_empty_1"          "annotation_foo_1"           
## [21] "annotation_empty_2"          "annotation_foo_2"           
## [23] "annotation_empty_3"          "annotation_foo_3"           
## [25] "annotation_empty_4"          "annotation_foo_4"           
## [27] "annotation_empty_5"          "annotation_foo_5"           
## [29] "global-heatmap_legend_right" "heatmap_legend"

If more than one group-level rectangles are to be added, we can wrap the code into a simple function group_block_anno():

ha = HeatmapAnnotation(
    empty = anno_empty(border = FALSE, height = unit(8, "mm")),
    foo = anno_block(gp = gpar(fill = 2:6), labels = LETTERS[1:5])
)
Heatmap(mat, name = "mat", column_split = split, top_annotation = ha, 
    column_title = NULL)

group_block_anno = function(group, empty_anno, gp = gpar(), 
    label = NULL, label_gp = gpar()) {

    seekViewport(qq("annotation_@{empty_anno}_@{min(group)}"))
    loc1 = deviceLoc(x = unit(0, "npc"), y = unit(0, "npc"))
    seekViewport(qq("annotation_@{empty_anno}_@{max(group)}"))
    loc2 = deviceLoc(x = unit(1, "npc"), y = unit(1, "npc"))

    seekViewport("global")
    grid.rect(loc1$x, loc1$y, width = loc2$x - loc1$x, height = loc2$y - loc1$y, 
        just = c("left", "bottom"), gp = gp)
    if(!is.null(label)) {
        grid.text(label, x = (loc1$x + loc2$x)*0.5, y = (loc1$y + loc2$y)*0.5, gp = label_gp)
    }
}

group_block_anno(1:3, "empty", gp = gpar(fill = "red"), label = "group 1")
group_block_anno(4:5, "empty", gp = gpar(fill = "blue"), label = "group 2")