5 min read

Recent improvements on legends

In this post, I will demonstrate several improvements on the legends in ComplexHeatmap package (version 2.7.1).

First I load the ComplexHeatmap package.

library(ComplexHeatmap)

Discrete legends

Now it works with multi-line labels:

lgd = Legend(labels = c("aaaaa\naaaaa", "bbbbb\nbbbbb", "c", "d"),
    legend_gp = gpar(fill = 1:4))

When the multi-line legend labels are in different rows:

lgd = Legend(labels = c("aaaaa\naaaaa", "c", "d", "bbbbb\nbbbbb"),
    legend_gp = gpar(fill = 1:4), nrow = 2)

Legend() function has a new argument graphics where users can self-define the graphics drawn in the legend. Check the following example:

lgd = Legend(labels = letters[1:4],
    graphics = list(
        function(x, y, w, h) grid.rect(x, y, w*0.33, h, gp = gpar(fill = "red")),
        function(x, y, w, h) grid.rect(x, y, w, h*0.33, gp = gpar(fill = "blue")),
        function(x, y, w, h) grid.text("A", x, y, gp = gpar(col = "darkgreen")),
        function(x, y, w, h) grid.points(x, y, gp = gpar(col = "orange"), pch = 16)
    ))

This new feature in mainly used to draw complex legends in oncoPrint.

Continuous legends

I first define a color mapping function by circlize::colorRamp2() function.

library(circlize)
col_fun = colorRamp2(c(-2, 0, 2), c("green", "white", "red"))

By default the legend looks like:

lgd = Legend(col_fun = col_fun, title = "foo")

If at is set in the decreasing order, the legend is reversed, i.e. the smallest value is on the top of the legend.

lgd = Legend(col_fun = col_fun, title = "foo", at = c(2, 1, 0, -1, -2))

Most continuous legends have legend breaks with equal distance, which I mean, e.g. the distance between the first and the second breaks are the same as the distance between the second and the third breaks. However, there are still special cases where users want to set legend breaks with unequal distances.

In the following example, the color mapping function col_fun_prop visualizes proportion values with breaks in c(0, 0.05, 0.1, 0.5, 1). The legend breaks with unequal distance might reflect the different importance of the values in c(0, 1). For example, maybe we want to see more details in the interval c(0, 0.1).

Following is the default style of the legend where the breaks are selected from 0 to 1 with equal distance.

col_fun_prop = colorRamp2(c(0, 0.05, 0.1, 0.5, 1), 
    c("green", "white", "red", "black", "blue"))
lgd = Legend(col_fun = col_fun_prop, title = "Prop")

You cann’t see the details in the interval c(0, 0.1), right? This also reminds us that the breaks set in colorRamp2() only defines the color mapping while not determine the breaks in the legend.

If we manually select the break values, the color bar keeps the same. The labels are shifted and lines connect them to the original positions. In this case, the distance in the color bar is still proportional to the real difference in the break values, i.e., the distance between 0.5 and 1 is five times longer than 0 and 0.1.

col_fun_prop = colorRamp2(c(0, 0.05, 0.1, 0.5, 1), 
    c("green", "white", "red", "black", "blue"))
lgd = Legend(col_fun = col_fun_prop, title = "Prop",
    at = c(0, 0.05, 0.1, 0.5, 1))

From version 2.7.1, Legend() function has a new argument break_dist that controls the distance between two neighbouring break values in the legend. It might be confusing, but from here, when I mention “break distance”, it always means the visual distance in the legend.

The value of break_dist should have length either one which means all break values have equal distance in the legend, or length(at) - 1.

lgd = Legend(col_fun = col_fun_prop, title = "Prop", break_dist = 1)

And in the following example, the top two break intervals are three times longer than the bottom two intervals.

lgd = Legend(col_fun = col_fun_prop, title = "Prop", break_dist = c(1, 1, 3, 3))

If we increase the legend height by legend_height argument, there will be enough space for the labels and their positions are not adjusted any more.

lgd = Legend(col_fun = col_fun_prop, title = "Prop", break_dist = c(1, 1, 3, 3),
    legend_height = unit(4, "cm"))

Multi-scheme colors

Imaging following user case, we want to use one color scheme for the values in c(0, 0.1) and a second color schema for the values in c(0.1, 1), maybe for the reason that we want to emphasize the two intervals are very different. The color mapping can be defined as:

col_fun2 = colorRamp2(c(0, 0.1, 0.1+1e-6, 1), c("white", "red", "yellow", "blue"))

So here I just added a tiny shift (1e-6) to 0.1 and set it as the lower bound for the second color scheme. The legend looks like:

lgd = Legend(col_fun = col_fun2, title = "Prop", at = c(0, 0.05, 0.1, 0.5, 1),
    break_dist = c(1, 1, 3, 3), legend_height = unit(4, "cm"))

Now you can see the colors are not changed smoothly from 0 to 1 and there are two disticnt color schemes.

Work with heatmaps and annotations

As explained in the ComplexHeatmap book, the new arguments be set either via argument heatmap_legend_param in Heatmap() function or argument annotation_legend_param in HeatmapAnnotation() function.

Example with a heatmap

Here I made a simplified version of the measles vaccine heatmap. Check how I set the heatmap_legend_param argument.

mat = readRDS(system.file("extdata", "measles.rds", package = "ComplexHeatmap"))
col_fun = colorRamp2(c(0, 800, 1000, 127000), c("white", "cornflowerblue", "yellow", "red"))
Heatmap(mat, name = "cases", col = col_fun, rect_gp = gpar(col= "white"),
    cluster_columns = FALSE, show_row_dend = FALSE, show_column_names = FALSE,
    row_names_side = "left", row_names_gp = gpar(fontsize = 8),
    heatmap_legend_param = list(
        at = c(0, 400, 800, 50000, 100000, 150000), 
        labels = c("0", "400", "800", "50k", "100k", "150k"),
        break_dist = c(1, 1, 3, 3, 3), 
        legend_gp = gpar(col = "black"),
        legend_height = unit(5, "cm")
    )
)

I can also adjust the legend to put it at the bottom of the heatmap.

ht = Heatmap(mat, name = "cases", col = col_fun, rect_gp = gpar(col= "white"),
    cluster_columns = FALSE, show_row_dend = FALSE, show_column_names = FALSE,
    row_names_side = "left", row_names_gp = gpar(fontsize = 8),
    heatmap_legend_param = list(
        at = c(0, 400, 800, 50000, 100000, 150000), 
        labels = c("0", "400", "800", "50k", "100k", "150k"),
        break_dist = c(1, 1, 3, 3, 3), 
        legend_gp = gpar(col = "black"),
        legend_width = unit(9, "cm"),
        direction = "horizontal",
        title_position = "lefttop"
    )
)
draw(ht, heatmap_legend_side = "bottom")