# How to visualize complex heatmaps interactively

## 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.

2. Configure brush

Border and backgroud of the brush can be configured here:

3. Save image

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

4. 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.

2. Export table

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

3. Save sub-heatmap as an image

Similar as in the main heatmap.

4. Resize sub-heatmap

Similar as in the main heatmap.

There are other vignettes focusing on more specific topics:

- How interactive ComplexHeatmap is implemented
- Functions for Shiny app development
- Decorations on the heatmaps
- Interactivate heatmaps indirectly generated by pheatmap(), heatmap.2() and heatmap()

## 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.5.2 (2025-10-31)
    ## Platform: x86_64-pc-linux-gnu
    ## Running under: Ubuntu 24.04.4 LTS
    ##
    ## Matrix products: default
    ## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
    ## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
    ##
    ## locale:
    ##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
    ##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=C
    ##  [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: Etc/UTC
    ## tzcode source: system (glibc)
    ##
    ## attached base packages:
    ## [1] grid      stats     graphics  grDevices utils     datasets  methods
    ## [8] base
    ##
    ## other attached packages:
    ## [1] InteractiveComplexHeatmap_1.18.1 ComplexHeatmap_2.26.1
    ## [3] knitr_1.51                       rmarkdown_2.30
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] sass_0.4.10         generics_0.1.4      xml2_1.5.2
    ##  [4] shape_1.4.6.1       stringi_1.8.7       digest_0.6.39
    ##  [7] magrittr_2.0.4      evaluate_1.0.5      RColorBrewer_1.1-3
    ## [10] iterators_1.0.14    circlize_0.4.17     fastmap_1.2.0
    ## [13] foreach_1.5.2       doParallel_1.0.17   jsonlite_2.0.0
    ## [16] GlobalOptions_0.1.3 promises_1.5.0      viridisLite_0.4.3
    ## [19] scales_1.4.0        textshaping_1.0.4   codetools_0.2-20
    ## [22] jquerylib_0.1.4     cli_3.6.5           shiny_1.13.0
    ## [25] rlang_1.1.7         crayon_1.5.3        cachem_1.1.0
    ## [28] yaml_2.3.12         otel_0.2.0          tools_4.5.2
    ## [31] parallel_4.5.2      colorspace_2.1-2    httpuv_1.6.16
    ## [34] kableExtra_1.4.0    GetoptLong_1.1.0    BiocGenerics_0.56.0
    ## [37] mime_0.13           buildtools_1.0.0    R6_2.6.1
    ## [40] png_0.1-8           matrixStats_1.5.0   stats4_4.5.2
    ## [43] lifecycle_1.0.5     stringr_1.6.0       S4Vectors_0.48.0
    ## [46] IRanges_2.44.0      clue_0.3-67         clisymbols_1.2.0
    ## [49] fontawesome_0.5.3   cluster_2.1.8.2     bslib_0.10.0
    ## [52] later_1.4.7         glue_1.8.0          Rcpp_1.1.1
    ## [55] systemfonts_1.3.1   xfun_0.56           rstudioapi_0.18.0
    ## [58] sys_3.4.3           farver_2.1.2        xtable_1.8-8
    ## [61] rjson_0.2.23        htmltools_0.5.9     svglite_2.2.2
    ## [64] maketools_1.3.2     compiler_4.5.2