How to visualize complex heatmaps interactively

Zuguang Gu ( z.gu@dkfz.de )

2024-05-01

Install

Package InteractiveComplexHeatmap is available on Bioconductor, you can install it by:

if (!requireNamespace("BiocManager", quietly=TRUE))
    install.packages("BiocManager")
BiocManager::install("InteractiveComplexHeatmap")

If you want the latest version, install it directly from GitHub:

library(devtools)
install_github("jokergoo/InteractiveComplexHeatmap")

A printer-friendly version of the documentation is available at bioRxiv.

Usage

The InteractiveComplexHeatmap package is very straightforward to use. For any heatmap (or list of heatmaps as a Heatmap or HeatmapList object) produced from ComplexHeatmap package, you just use the function htShiny() to export it as a Shiny app.

You can copy and paste the following code:

library(ComplexHeatmap)
library(InteractiveComplexHeatmap)
m = matrix(rnorm(100*100), nrow = 100)
ht = Heatmap(m)
ht = draw(ht) # not necessary, but recommended

htShiny(ht)

A link will be automatically opend in your web browser (or a pop-up window in RStudio IDE).

To use htShiny(), the heatmap object is recommended to be updated with the function draw(). If it has not been updated, it will be applied inside htShiny() automatically. Updating by draw() speeds up loading the Shiny application because draw() applies clusterings which is normally the most time-consuming step in heatmap generation. After draw() is executed, clustering results are saved in the returned heatmap object so that repeatedly drawing heatmap can directly use the saved clustering results. If the heatmap includes randomness, such as k-means clustering by setting row_km or column_km argument or using random colors for annotations, it is necessary to execute draw() before sending the heatmap object to htShiny() to get rid of obtaining different heatmaps when executing htShiny() multiple times.

Following screenshot demonstrates the Shiny app on rather complex heatmaps. The data is from here and the app can be generated by running htShinyExample(4.2).

In this Shiny app, users can click on the orignal heatmap or select an area from it. The information of the cell or the area selected by users can be found in the text below the heatmaps. If an area is selected, a sub-heatmap is drawn on the right side of the app. Both heatmaps can be resized by dragging from the bottom right.

There are several tools under both heatmaps. For the original heatmap, there are following tools:

  1. Search heatmap

It allows to search the heatmap labels to obtain a subset of rows and columns. Once rows or columns are found in the heatmaps, the sub-heatmaps will be drawn in the right panel.

  1. Configure brush

Border and backgroud of the brush can be configured here:

  1. Save image

The main heatmap can be saved into a file with one of the three formats (png, pdf and svg).

  1. Resize image

The size of the original heatmap can be controlled by dragging the box, however, it can be precisely controlled by entering the width and height, as shown in the following figure:

For the sub-heatmap, there are following tools:

  1. Configure sub-heatmap

There are three sections of controls. 1. Basic controls such as whether to show row or column names, 2. Brushing on the original heatmap might not precisely capture rows or columns users expected. You can manually remove a certain number of rows or columns from the four dimensions of the selected sub-heatmap. 3. Selected sub-heatmap can be further converted into a second interactive heatmap application.

  1. Export table

The values in the sub-heatmaps can be viewed and exported as a text table:

  1. Save sub-heatmap as an image

Similar as in the main heatmap.

  1. Resize sub-heatmap

Similar as in the main heatmap.

There are other vignettes focusing on more specific topics:

Use last generated heatmaps

ComplexHeatmap is broadly used in many other scripts and packages where they do not directly return the Heatmap/HeatmapList object. This is of no problem to make these heatmaps interactive because the last generated heatmap object is automatically saved and calling htShiny() without the heatmap object will automatically use the last one (by internally using the function ComplexHeatmap:::get_last_ht()). I demonstrate this functionality with the cola pacakge.

cola package heavily uses ComplexHeatmap to implement various customized heatmaps to visualize consensus clustering results as well as downstream analysis. As an example, the function get_signatures() extracts signatures that are significantly different between the predicted subgroups. get_signatures() makes one heatmap and returns a data frame of the signatures as well as various statistics for the test. When get_signatures() draws the heatmap, the heatmap object is saved internally, then directly calling htShiny() without the heatmap object will convert the static signature heatmap into an interactive one:

# the following code is runable
library(cola)  # cola is from Bioconductor
data(golub_cola)
get_signatures(golub_cola["ATC:skmeans"], k = 2) # this makes the heatmap
htShiny()

Note the functionality of automatically saving the last heatmap is only turned on when InteractiveComplexHeatmap packags is loaded, which means, library(InteractiveComplexHeatmap) should be called before making the heatmap, or you need to explicitly set ComplexHeatmap::ht_opt(save_last = TRUE).

Examples are in htShinyExample(1.6) and htShinyExample(1.7).

Nevertheless, if possible, I still suggest to explictly put the heatmap object in htShiny() which makes it clear which heatmap is becoming interactive.

Recursive interactive heatmap widgets

In the right panel there is the sub-heatmap that is selected from the left panel. When the original heatmap is very huge, even a tiny selection rectangle still generates a dense sub-heatmap where single cells are very hard to identify and read. In this case, the sub-heatmap can be continually exported as another independent interactive heatmap widget which is in a new layer above current one, just by clicking the button “Interactivate sub-heatmap” under the sub-heatmap. This process can be recursively applied untill you are satisfied with the details you can see in the sub-heatmap. An example is as follows:

Float output to mouse positions

The output which contains information of the clicked cell or the sub-heatmap by default is placed below the heatmaps. Argument output_ui_float can be set to TRUE so that the output is floating to the positions of mouse.

htShiny(ht, output_ui_float = TRUE)

There are two examples: htShinyExample(9.1) and htShinyExample(9.2). The demonstration is as follows:

As will be explained in the vignette “Functions for Shiny app development”, the output can be self-defined to put customized information. The self-defined output can also be floated:

Layout

There are three main components in the interactive heatmap UI, i.e., the orignal heatmap, the sub-heatmap and an output that shows information of the clicked cell or the selected sub-heatmap. The layout of the three components are controlled via layout argument, see the vignette “Functions for Shiny app development” for detailed explanation of the layout argument.

Only respond to one event

The argument response can be set as one of "click", "hover", "dblclick", "brush" and "brush-output" to only respond to one event on heatmap. E.g. if response is set to "click", there will be no response for the “brush event” in the interactive heatmap, also the sub-heatmap component is removed from the app.

Brushing on heatmap by default triggers two reponses: update in the sub-heatmap and update in the output. If response is set to "brush-output", brushing will only trigger changes in the output and there will be no sub-heatmap component in the app.

The following figure demonstrates only responding to "click" or "brush" where the output floats at mouse positions. Runnable examples are in htShinyExample(1.9) and htShinyExample(9.3).

Compact mode

By default all the three UI components are includedin the app. In htShiny(), the argument compact can be set to TRUE so that the sub-heatmap component is removed and the output floats at mouse positions:

htShiny(ht, compact = TRUE)

Other arguments in htShiny()

htShiny() is just a simple wrapper on the other two basic functions InteractiveComplexHeatmapOutput() and makeInteractiveComplexHeatmap(). Users can go to the vignette “Functions for Shiny app development” for more customized controls.

Live examples

The following code demostrates two horizontally concatenated heatmaps. Please visit https://jokergoo.shinyapps.io/interactive_complexheatmap/ for a live demo.

set.seed(123)
mat1 = matrix(rnorm(100), 10)
rownames(mat1) = colnames(mat1) = paste0("a", 1:10)
mat2 = matrix(sample(letters[1:10], 100, replace = TRUE), 10)
rownames(mat2) = colnames(mat2) = paste0("b", 1:10)

ht_list = Heatmap(mat1, name = "mat_a", row_km = 2, column_km = 2) +
    Heatmap(mat2, name = "mat_b")
htShiny(ht_list)

The following code demostrates two vertically concatenated heatmaps. Check https://jokergoo.shinyapps.io/interactive_complexheatmap_vertical/ for a live demo.

ht_list = Heatmap(mat1, name = "mat_a", row_km = 2, column_km = 2) %v%
    Heatmap(mat2, name = "mat_b")
htShiny(ht_list)

Integrate with other plots and packages

The functionality of the interactivity is general. It can be applied to any heatmap as long as it is generated from the ComplexHeatmap package. Thus, it can turn many other plots into an interactive app.

Interactive density heatmap

ComplexHeatmap::densityHeatmap() returns a Heatmap object, so it can be exported as a Shiny app. Check https://jokergoo.shinyapps.io/interactive_densityheatmap/ for a live demo. The example can also be run locally by executing htShinyExample(2.1). The usage of htShinyExample() will be introduced later and in the webpage opened by htShinyExample() there is also the source code for generating this Shiny app.

ht = densityHeatmap(...)
htShiny(ht)

Interactive oncoPrint

ComplexHeatmap::oncoPrint() returns a Heatmap object, thus, the oncoPrint can be interactive. Example code is as follows. Check https://jokergoo.shinyapps.io/interactive_oncoprint/ for a live demo. The example can be run locally by executing htShinyExample(2.2).

ht = oncoPrint(...)
htShiny(ht)

ht = oncoPrint(...) + Heatmap(...) + rowAnnotation(...)
htShiny(ht)

Interactive UpSet plot

ComplexHeatmap::UpSet() returns a Heatmap object, thus, the UpSet plot generated by ComplexHeatmap can be interactive. It might be useful when you visualize many sets at the same time. Example code is as follows. Check https://jokergooo.shinyapps.io/interactive_upset/ for a live demo. The example can be run locally by executing htShinyExample(2.3).

cm = make_comb_mat(...)
ht = UpSet(cm, ...)
htShiny(ht)

Interactive enriched heatmap

EnrichedHeatmap inherits ComplexHeatmap and it outputs Heatmap objects, thus, an “enriched heatmap” can be exported as a Shiny app as well. Check https://jokergoo.shinyapps.io/interactive_enrichedheatmap/ for a live demo. The example can be run locally by executing htShinyExample(3.1).

mat = normalizeToMatrix(...)
ht = EnrichedHeatmap(mat, ...)
htShiny(ht)

ht = EnrichedHeatmap(mat, ...) + EnrichedHeatmap(...) + Heatmap(...)
htShiny(ht)

Interactive pheatmap

Since ComplexHeatmap can seamlessly integrate pheatmap, this means your pheatmap can be interactive! Check https://jokergooo.shinyapps.io/interactive_pheatmap/. The example can be run locally by executing htShinyExample(2.4).

ComplexHeatmap::pheatmap() and pheatmap::pheatmap() have the same set of arguments and generate almost the same heatmaps, but be careful when you directly use pheatmap() without the namespace prefix (the package name), you need to make sure it is from ComplexHeatmap if you want to use the interactive functionality.

ht = pheatmap(...) # ComplexHeatmap::pheatmap should overwrite pheatmap::pheatmap
htShiny(ht)

Interactive heatmap

To facilitate the users who are still using heatmap() and heatmap.2() functions, to make the output of these two functions can be exported as interactive Shiny apps as well, from ComplexHeatmap version 2.7.2, two similar translation functions ComplexHeatmap:::heatmap() and ComplexHeatmap:::heatmap.2() which use the same set of arguments as the original functions and generate almost identical heatmaps. Then an interactive heatmap() will look like:

ht = ComplexHeatmap:::heatmap(...)
htShiny(ht)

As you see, heatmap() function is not exported from ComplexHeatmap (so that it will not conflict with stats package) and it should be explicitly specifid with :::.

An live example is at https://jokergooo.shinyapps.io/interactive_heatmap/. The example can be run locally by executing htShinyExample(2.5).

Interactive heatmap.2

Similarly, an interactive heatmap.2() looks like:

ht = ComplexHeatmap:::heatmap.2(...)
htShiny(ht)

Similarlly, heatmap.2() is not exported from ComplexHeatmap (so that it will not conflict with glots package) and it should be specified with :::.

An live example is at https://jokergooo.shinyapps.io/interactive_heatmap_2/. The example can be run locally by executing htShinyExample(2.6).

Interactive tidyHeatmap

The tidyHeatmap package basically wraps ComplexHeatmap and provides “a tidy way” for generating heatmaps. Since the final heatmap is actually generated by ComplexHeatmap, it can be directly exported as an interactive app. You can directly apply htShiny() on the object generated from tidyHeatmap. Check https://jokergooo.shinyapps.io/interactive_tidyheatmap/. The example can be run locally by executing htShinyExample(2.7).

library(tidyverse)
library(tidyHeatmap)
mtcars_tidy <- 
    mtcars %>% 
    as_tibble(rownames="Car name") %>% 
    mutate_at(vars(-`Car name`, -hp, -vs), scale) %>%
    pivot_longer(cols = -c(`Car name`, hp, vs), names_to = "Property", values_to = "Value")

mtcars_heatmap <- 
    mtcars_tidy %>% 
        heatmap(`Car name`, Property, Value ) %>%
        add_tile(hp)

htShiny(mtcars_heatmap)

Other tips

As explained before, for all these functions and packages that generate customized heatmaps, if the heatmaps are already generated in the interactive graphics device, the heatmap objects can be ommited when calling htShiny(). E.g.:

ComplexHeatmap::pheatmap(...)
htShiny()

oncoPrint(...)
htShiny()

Examples shipped with the package

There are many other examples shipped with the InteractiveComplexHeatmap package. The list of examples can be obtained by executing htShinyExample() with no argument.

htShinyExample()
## There are following examples. Individual example can be run by e.g. htShinyExample(1.1).
## 
## ──────── 1. Simple examples ───────────────────────────────────────────────────────── 
##  1.1 A single heatmap with minimal arguments.
##  1.2 A single heatmap from a character matrix.
##  1.3 A single heatmap with annotations on both rows and columns.
##  1.4 A single heatmap where rows and columns are split.
##  1.5 A list of two heatmaps.
##  1.6 A list of two vertically concatenated heatmaps
##  1.7 Use last generated heatmap, an example from cola package.
##  1.8 Use last generated heatmap, an app with three interactive heatmaps
##  1.9 Demonstrate hover, click and dblclick actions to select cells.
##  1.10 Only response to one of click/hover/dblclick/hover events. Please use
##       htShinyExample('1.10') to get this example (quote the index, or else
##       htShinyExample(1.10) will be treated as the same as htShinyExample(1.1)).
##  1.11 Interactive heatmap under compact mode.
## 
## ──────── 2. On other plots and packages ───────────────────────────────────────────── 
##  2.1 A density heatmap.
##  2.2 An oncoPrint.
##  2.3 A UpSet plot.
##  2.4 An interactive heatmap from pheatmap().
##  2.5 An interactive heatmap from heatmap().
##  2.6 An interactive heatmap from heatmap.2().
##  2.7 A heatmap produced from tidyHeatmap package.
##  2.8 Genome-scale heatmap.
##  2.9 A package-dependency heatmap. You can try to control "Fill figure region"
##      and "Remove empty rows and columns" in the tools under the sub-heatmap.
## 
## ──────── 3. Enriched heatmaps ─────────────────────────────────────────────────────── 
##  3.1 A single enriched heatmap.
##  3.2 A list of enriched heatmaps.
##  3.3 An enriched heatmap with discrete signals.
## 
## ──────── 4. On public datasets ────────────────────────────────────────────────────── 
##  4.1 An example from Lewis et al 2019.
##  4.2 Visualize cell heterogeneity from single cell RNASeq.
##  4.3 Correlations between methylation, expression and other genomic features.
## 
## ──────── 5. Shiny app development ─────────────────────────────────────────────────── 
##  5.1 A single Shiny app with two interactive heatmap widgets.
##  5.2 Self-define the output. The selected sub-matrix is shown as a text table.
##  5.3 Self-define the output. Additional annotations for the selected genes are
##      shown.
##  5.4 Visualize Gene Ontology similarities. A list of selected GO IDs as well as
##      their descriptions are shown in the output.
##  5.5 Interactive correlation heatmap. Clicking on the cell generates a
##      scatterplot of the two corresponding variables.
##  5.6 A heatmap on Jaccard coefficients for a list of genomic regions. Clicking
##      on the cell generates a Hilbert curve of how the two sets of genomic
##      regions overlap.
##  5.7 Implement interactivity from scratch. Instead of generating the whole
##      interactive heatmap widget, it only returns the information of rows and
##      columns that user have selected on heatmap and users can use this
##      information to build their own interactive heatmap widgets.
##  5.8 Implement interactivity from scratch. A visualization of 2D density
##      distribution. Brushing on heatmap triggers a new 2D density estimation
##      only on the subset of data.
## 
## ──────── 6. Dynamically generate heatmap widget in Shiny app ──────────────────────── 
##  6.1 The matrix with different dimensions is dynamically generated.
##  6.2 Reorder by a column that is specified by user.
##  6.3 Dynamically generate the widget with InteractiveComplexHeatmapModal(). The
##      modal is triggered by an action button.
##  6.4 Dynamically select interactive heatmaps. The modal is triggered by radio
##      buttons.
##  6.5 Dynamically generate the widget. A customized Javascript code is inserted
##      after the UI to change the default behavior of the action button.
##  6.6 The widget is generated by InteractiveComplexHeatmapWidget() where the UI
##      is directly put in the place defined by htmlOutput().
##  6.7 The widget is generated by InteractiveComplexHeatmapWidget() and a
##      customized Javascript code is inserted after the UI.
## 
## ──────── 7. Interactive R markdown document ───────────────────────────────────────── 
##  7.1 Integrate in an interactive R Markdown document.
##  7.2 Integrate in an interactive R Markdown document where the heatmap widgets
##      are dynamically generated.
## 
## ──────── 8. Interactivate heatmaps indirectly generated by heatmap()/heatmap.2()/pheatmap()  
##  8.1 Indirect use of pheatmap().
##  8.2 Indirect use of heatmap.2().
##  8.3 Two interactive heatmap widgets from indirect use of pheatmap().
## 
## ──────── 9. Float output UI along with mouse positions ────────────────────────────── 
##  9.1 A simple example that demonstrates output UI floating with the three
##      actions: hover, click and dblclick.
##  9.2 Floating self-defined outputs.
##  9.3 Floating output only from one event on heatmap, i.e.
##      hover/click/dblclick/brush-output.
## 
## ──────── 10. Work with shinydashboard ──────────────────────────────────────────────── 
##  10.1 Separate the three UI components into three boxes.
##  10.2 The three UI components are draggable.
##  10.3 A Shiny dashboard with two tabs.
##  10.4 Only contain the original heatmap where output floats.
##  10.5 A complex dashboard that visualizes a DESeq2 results.

You can select one specific example by sending the corresponding index to htShinyExample(), e.g. to run the example “1.4 A single heatmap where rows and columns are split.”, simply run:

htShinyExample(1.4)

Then the interactive heatmaps as well as the source code for generating the app will be available in the webpage.

Have fun!

SessionInfo

sessionInfo()
## R version 4.4.0 beta (2024-04-15 r86425)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 22.04.4 LTS
## 
## Matrix products: default
## BLAS:   /home/biocbuild/bbs-3.19-bioc/R/lib/libRblas.so 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: America/New_York
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] grid      stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
## [1] GOexpress_1.38.0                 Biobase_2.64.0                  
## [3] BiocGenerics_0.50.0              SC3_1.32.0                      
## [5] GetoptLong_1.0.5                 InteractiveComplexHeatmap_1.12.0
## [7] ComplexHeatmap_2.20.0            knitr_1.46                      
## 
## loaded via a namespace (and not attached):
##   [1] RColorBrewer_1.1-3          rstudioapi_0.16.0          
##   [3] jsonlite_1.8.8              shape_1.4.6.1              
##   [5] magrittr_2.0.3              magick_2.8.3               
##   [7] rmarkdown_2.26              GlobalOptions_0.1.2        
##   [9] zlibbioc_1.50.0             vctrs_0.6.5                
##  [11] ROCR_1.0-11                 memoise_2.0.1              
##  [13] Cairo_1.6-2                 RCurl_1.98-1.14            
##  [15] htmltools_0.5.8.1           S4Arrays_1.4.0             
##  [17] progress_1.2.3              curl_5.2.1                 
##  [19] SparseArray_1.4.0           sass_0.4.9                 
##  [21] KernSmooth_2.23-22          bslib_0.7.0                
##  [23] fontawesome_0.5.2           httr2_1.0.1                
##  [25] cachem_1.0.8                commonmark_1.9.1           
##  [27] mime_0.12                   lifecycle_1.0.4            
##  [29] iterators_1.0.14            pkgconfig_2.0.3            
##  [31] Matrix_1.7-0                R6_2.5.1                   
##  [33] fastmap_1.1.1               GenomeInfoDbData_1.2.12    
##  [35] MatrixGenerics_1.16.0       shiny_1.8.1.1              
##  [37] clue_0.3-65                 digest_0.6.35              
##  [39] colorspace_2.1-0            AnnotationDbi_1.66.0       
##  [41] S4Vectors_0.42.0            GenomicRanges_1.56.0       
##  [43] RSQLite_2.3.6               filelock_1.0.3             
##  [45] WriteXLS_6.5.0              randomForest_4.7-1.1       
##  [47] fansi_1.0.6                 httr_1.4.7                 
##  [49] abind_1.4-5                 compiler_4.4.0             
##  [51] rngtools_1.5.2              proxy_0.4-27               
##  [53] bit64_4.0.5                 doParallel_1.0.17          
##  [55] DBI_1.2.2                   highr_0.10                 
##  [57] gplots_3.1.3.1              biomaRt_2.60.0             
##  [59] rappdirs_0.3.3              DelayedArray_0.30.0        
##  [61] rjson_0.2.21                caTools_1.18.2             
##  [63] gtools_3.9.5                tools_4.4.0                
##  [65] rrcov_1.7-5                 httpuv_1.6.15              
##  [67] glue_1.7.0                  promises_1.3.0             
##  [69] gridtext_0.1.5              cluster_2.1.6              
##  [71] generics_0.1.3              gtable_0.3.5               
##  [73] class_7.3-22                hms_1.1.3                  
##  [75] xml2_1.3.6                  utf8_1.2.4                 
##  [77] XVector_0.44.0              foreach_1.5.2              
##  [79] pillar_1.9.0                markdown_1.12              
##  [81] stringr_1.5.1               later_1.3.2                
##  [83] robustbase_0.99-2           circlize_0.4.16            
##  [85] dplyr_1.1.4                 BiocFileCache_2.12.0       
##  [87] lattice_0.22-6              bit_4.0.5                  
##  [89] tidyselect_1.2.1            SingleCellExperiment_1.26.0
##  [91] Biostrings_2.72.0           IRanges_2.38.0             
##  [93] SummarizedExperiment_1.34.0 svglite_2.1.3              
##  [95] stats4_4.4.0                xfun_0.43                  
##  [97] matrixStats_1.3.0           DEoptimR_1.1-3             
##  [99] pheatmap_1.0.12             stringi_1.8.3              
## [101] UCSC.utils_1.0.0            yaml_2.3.8                 
## [103] kableExtra_1.4.0            evaluate_0.23              
## [105] codetools_0.2-20            tibble_3.2.1               
## [107] cli_3.6.2                   xtable_1.8-4               
## [109] systemfonts_1.0.6           munsell_0.5.1              
## [111] jquerylib_0.1.4             Rcpp_1.0.12                
## [113] GenomeInfoDb_1.40.0         dbplyr_2.5.0               
## [115] png_0.1-8                   parallel_4.4.0             
## [117] ggplot2_3.5.1               blob_1.2.4                 
## [119] prettyunits_1.2.0           doRNG_1.8.6                
## [121] bitops_1.0-7                viridisLite_0.4.2          
## [123] mvtnorm_1.2-4               scales_1.3.0               
## [125] e1071_1.7-14                pcaPP_2.0-4                
## [127] crayon_1.5.2                clisymbols_1.2.0           
## [129] rlang_1.1.3                 KEGGREST_1.44.0