Home | Blog | Software | Publications | GitHub


Add legends to circlize plot

circlize package provides complete freedom for users to design their own graphics by implementing the self-defined function panel.fun. However one drawback arises that circlize is completely blind to users' data so that one important thing is missing for the visualization which is the legend.

Although legends cannot be automatically generated by circlize package, by using functionality from other R packages, it is just a few more work to really implement it. In this post, I will demonstrate how to customize legends and arrange to the circular plot.

As an example, I generated a circular plot which contains two tracks and links inside the circle. The first track will have a legend that contains points, the second track will have a legend that contains lines, and the links correspond to a continuous color mapping. The code is wrapped into a function so that it can be used repeatedly.

library(circlize)

set.seed(123)
col_fun = colorRamp2(c(-1, 0, 1), c("green", "yellow", "red"))
circlize_plot = function() {
    circos.initializeWithIdeogram(plotType = NULL)

    bed = generateRandomBed(nr = 300)
    bed = generateRandomBed(nr = 300, nc = 2)
    circos.genomicTrackPlotRegion(bed,
        panel.fun = function(region, value, ...) {
            circos.genomicPoints(region, value, cex = 0.5, pch = 16, col = 2:3, ...)
    })

    bed = generateRandomBed(nr = 500, nc = 2)
    circos.genomicTrackPlotRegion(bed,
        panel.fun = function(region, value, ...) {
            circos.genomicLines(region, value, col = 4:5, ...)
    })

    bed1 = generateRandomBed(nr = 100)
    bed1 = bed1[sample(nrow(bed1), 20), ]
    bed2 = generateRandomBed(nr = 100)
    bed2 = bed2[sample(nrow(bed2), 20), ]

    circos.genomicLink(bed1, bed2, col = col_fun(bed1[[4]]))

    circos.clear()
}

In recently version of ComplexHeatmap package, there is a Legend() function which customizes legends with various styles. In following code, legends for the two tracks and links are constructed. In the end the three legends are packed vertically by packLegend(). For more detailed usage of Legend() and packLegend(), please refer to their help page.

library(ComplexHeatmap)
# discrete
lgd_points = Legend(at = c("label1", "label2"), type = "points", legend_gp = gpar(col = 2:3), 
    title_position = "topleft", title = "Track1")
# discrete
lgd_lines = Legend(at = c("label3", "label4"), type = "lines", legend_gp = gpar(col = 4:5, lwd = 2), 
    title_position = "topleft", title = "Track2")
# continuous
lgd_links = Legend(at = c(-1, -0.5, 0, 0.5, 1), col_fun = col_fun, title_position = "topleft",
    title = "Links")

lgd_list_vertical = packLegend(lgd_points, lgd_lines, lgd_links)
lgd_list_vertical
## frame[GRID.frame.65]

lgd_points, lgd_lines, lgd_links and lgd_list_vertical are all grob objects (graphical objects), which you can think as boxes which contain all graphical elements for legends and they can be added to the plot by grid.draw().

circlize is implemented by the base graphic system while ComplexHeatmap is implemented by grid graphic system. However, these two system can be mixed somehow. We can directly add grid graphics to the base graphics. (Actually they are two independent layers but drawn on a same graphic devide.)

circlize_plot()
pushViewport(viewport(x = unit(2, "mm"), y = unit(4, "mm"), width = grobWidth(lgd_list_vertical), 
    height = grobHeight(lgd_list_vertical), just = c("left", "bottom")))
grid.draw(lgd_list_vertical)
upViewport()

plot of chunk unnamed-chunk-4

In above plot, the whole image region corresponds to the circular plot and the legend layer is drawn just on top of it. If the legends are many and the size for the legends is too big, they may overap to the circle. In this case, it is better to split the image region into two parts where one part for the circular plot and the other part for legends.

Thanks to grid package that it is quite easy to split the image region and thanks to gridBase package that we can easily mix base graphics and grid graphics.

Following code is straightforward to understand. Only one line needs to be noticed: par(omi = gridOMI(), new = TRUE) that gridOMI() calculates the outer margins for the base graphics so that the base graphics can be put at the correct place and new = TRUE to ensure the base graphics are added to current graphic device instead of opening a new one.

Here I use plot.new() to open a new graphic device. In interactive session, it seems ok if you also use grid.newpage(), but grid.newpage() gives error when building a knitr document.

library(gridBase)
plot.new()
circle_size = unit(1, "snpc") - unit(1, "inches")

pushViewport(viewport(x = 0, y = 0.5, width = circle_size, height = circle_size,
    just = c("left", "center")))
par(omi = gridOMI(), new = TRUE)
circlize_plot()
upViewport()

pushViewport(viewport(x = circle_size, y = 0.5, width = grobWidth(lgd_list_vertical), 
    height = grobHeight(lgd_list_vertical), just = c("left", "center")))
grid.draw(lgd_list_vertical)
upViewport()

plot of chunk unnamed-chunk-5

The legends can also be put at the bottom of the circular plot and it is just a matter how users arrange the grid viewports. In this case, all legends are changed to horizontal style, and three legends are packed horizontally as well.

lgd_points = Legend(at = c("label1", "label2"), type = "points", legend_gp = gpar(col = 2:3), 
    title_position = "topleft", title = "Track1", nrow = 1)

lgd_lines = Legend(at = c("label3", "label4"), type = "lines", legend_gp = gpar(col = 4:5, lwd = 2), 
    title_position = "topleft", title = "Track2", nrow = 1)

lgd_links = Legend(at = c(-1, -0.5, 0, 0.5, 1), col_fun = col_fun, title_position = "topleft",
    title = "Links", direction = "horizontal")

lgd_list_horizontal = packLegend(lgd_points, lgd_lines, lgd_links, direction = "horizontal")

plot.new()
pushViewport(viewport(x = 0.5, y = 1, width = circle_size, height = circle_size,
    just = c("center", "top")))
par(omi = gridOMI(), new = TRUE)
circlize_plot()
upViewport()

pushViewport(viewport(x = 0.5, y = unit(1, "npc") - circle_size, 
    width = grobWidth(lgd_list_horizontal), height = grobHeight(lgd_list_horizontal), 
    just = c("center", "top")))
grid.draw(lgd_list_horizontal)
upViewport()

plot of chunk unnamed-chunk-6

Session info

sessionInfo()
## R version 3.3.2 (2016-10-31)
## Platform: x86_64-apple-darwin13.4.0 (64-bit)
## Running under: macOS Sierra 10.12.3
## 
## locale:
## [1] C/en_US.UTF-8/C/C/C/C
## 
## attached base packages:
## [1] grid      stats     graphics  grDevices utils     datasets  base     
## 
## other attached packages:
## [1] circlize_0.3.11       gridBase_0.4-7        ComplexHeatmap_1.13.2
## [4] digest_0.6.12         htmltools_0.3.5       GetoptLong_0.1.7     
## [7] markdown_0.7.7        knitr_1.15.1         
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.9          highr_0.6            DEoptimR_1.0-8      
##  [4] RColorBrewer_1.1-2   plyr_1.8.4           viridis_0.3.4       
##  [7] methods_3.3.2        class_7.3-14         tools_3.3.2         
## [10] prabclus_2.2-6       dendextend_1.4.0     mclust_5.2.2        
## [13] evaluate_0.10        tibble_1.2           gtable_0.2.0        
## [16] lattice_0.20-34      mvtnorm_1.0-5        gridExtra_2.2.1     
## [19] trimcluster_0.1-2    stringr_1.1.0        cluster_2.0.5       
## [22] GlobalOptions_0.0.11 fpc_2.1-10           stats4_3.3.2        
## [25] diptest_0.75-7       nnet_7.3-12          robustbase_0.92-7   
## [28] flexmix_2.3-13       kernlab_0.9-25       ggplot2_2.2.1       
## [31] magrittr_1.5         whisker_0.3-2        scales_0.4.1        
## [34] modeltools_0.2-21    MASS_7.3-45          assertthat_0.1      
## [37] shape_1.4.2          colorspace_1.3-2     stringi_1.1.2       
## [40] lazyeval_0.2.0       munsell_0.4.3        rjson_0.2.15