6 min read

Easily convert static heatmaps to interactive heatmaps in Shiny apps

Some people might have already implemented ComplexHeatmap in their Shiny apps, where a static heatmap is automatically generated by adjusting parameters for heatmaps. For example, in the following example, in the left panel, users can control three types of parameters for heatmaps:

  • Whether to apply clustering?
  • Whether to order heatmaps by row and column names?
  • Whether to perform k-means clustering with 2-group on both rows and columns?

The following code can be directly copied and pasted to an R session. It is runnable.

m = scale(as.matrix(mtcars))  # the matrix is scaled by columns

library(shiny)
library(ComplexHeatmap)

ui = fluidPage(
    sidebarLayout(
        sidebarPanel(
            radioButtons("order_method", "How to order rows and columns:", 
                c("Clustering" = "clustering", "Order by names" = "by_name"), 
                selected = "clustering"),
            checkboxInput('split', "Split rows and columns into 2 groups?", FALSE),
            width = 3
        ),
        mainPanel(
            plotOutput('heatmap', height = "600px", width = "400px"),
        ),
    )
)

server = function(input, output, session) {
    output$heatmap = renderPlot({
        cluster_rows = FALSE
        cluster_columns = FALSE
        row_order = NULL
        column_order = NULL
        row_km = NULL
        column_km = NULL

        if(input$order_method == "clustering") {
            cluster_rows = TRUE
            cluster_columns = TRUE
        } else {
            row_order = order(rownames(m))
            column_order = order(colnames(m))
        }

        if(input$split) {
            row_km = 2
            column_km = 2
        }

        ht = Heatmap(m, name = "mtcars",
            cluster_rows = cluster_rows, cluster_columns = cluster_columns,
            row_order = row_order, column_order = column_order, 
            row_km = row_km, column_km = column_km)
        draw(ht)
    })
}

shinyApp(ui = ui, server = server)

In the Shiny app above, it is “interactive” from the perspective of the app, which is, it allows users to interactively adjust the parameters and heatmaps are generated accordingly and automatically. But, the heatmap itself is static, which means, users cannot directly interact with the heatmap.

Nevertheless, with the help of InteractiveComplexHeatmap package, it is very easy to “interactivate” the static heatmap with only a few lines of code.

In the previous code, in the UI of the Shiny app, the heatmap is wrapped in a plotOutput(). Now to “interactivate” the static heatmap, we use InteractiveComplexHeatmapOutput() from InteractiveComplexHeatmap instead. Note in the following code chunk, the lines that I have changed are marked with “# <- this line is changed”.

Also, in the server function in the previous Shiny app, the static heatmap is generated as output$heatmap = renderPlot({...}), now we change it to observe({...; makeInteractiveComplexHeatmap(input, output, session, ht)}) so that changes of heatmap parameters can automatically refresh the interactive heatmap.

Please check the following code where there are only three lines of change compared to the previous “static heatmap” Shiny app. Also, the following code is runnable that it can be directly copied and pasted to an R session.

library(InteractiveComplexHeatmap)
ui = fluidPage(
    sidebarLayout(
        sidebarPanel(
            radioButtons("order_method", "How to order rows and columns:", 
                c("Clustering" = "clustering", "Order by names" = "by_name"), 
                selected = "clustering"),
            checkboxInput('split', "Split rows and columns into 2 groups?", FALSE),
            width = 3
        ),
        mainPanel(
            InteractiveComplexHeatmapOutput(),  # <- this line is changed
        ),
    )
)

server = function(input, output, session) {
    observe({       # <- this line is changed
        cluster_rows = FALSE
        cluster_columns = FALSE
        row_order = NULL
        column_order = NULL
        row_km = NULL
        column_km = NULL

        if(input$order_method == "clustering") {
            cluster_rows = TRUE
            cluster_columns = TRUE
        } else {
            row_order = order(rownames(m))
            column_order = order(colnames(m))
        }

        if(input$split) {
            row_km = 2
            column_km = 2
        }

        ht = Heatmap(m, name = "mtcars",
            cluster_rows = cluster_rows, cluster_columns = cluster_columns,
            row_order = row_order, column_order = column_order, 
            row_km = row_km, column_km = column_km)
        makeInteractiveComplexHeatmap(input, output, session, ht)    # <- this line is changed
    })
}

shinyApp(ui = ui, server = server)

In the previous “interactive heatmap” example, since the number of rows and columns are not too many that individual cell can actually be easily identified from the heatmap, thus, maybe it is not necessary to support the “brush” response which allows to select an area from the heatmap. Here we set two arguments compact = TRUE, response = "click" in InteractiveComplexHeatmapOutput() so that the interactive heatmap only responses to the click event and it pops a box around the mouse click that contains detailed information of the clicked cell.

ui = fluidPage(
    sidebarLayout(
        sidebarPanel(
            radioButtons("order_method", "How to order rows and columns:", 
                c("Clustering" = "clustering", "Order by names" = "by_name"), 
                selected = "clustering"),
            checkboxInput('split', "Split rows and columns into 2 groups?", FALSE),
            width = 3
        ),
        mainPanel(
            InteractiveComplexHeatmapOutput(compact = TRUE, response = "click"),  # <- this line is changed
        ),
    )
)

server = function(input, output, session) {
    observe({
        cluster_rows = FALSE
        cluster_columns = FALSE
        row_order = NULL
        column_order = NULL
        row_km = NULL
        column_km = NULL

        if(input$order_method == "clustering") {
            cluster_rows = TRUE
            cluster_columns = TRUE
        } else {
            row_order = order(rownames(m))
            column_order = order(colnames(m))
        }

        if(input$split) {
            row_km = 2
            column_km = 2
        }

        ht = Heatmap(m, name = "mtcars",
            cluster_rows = cluster_rows, cluster_columns = cluster_columns,
            row_order = row_order, column_order = column_order, 
            row_km = row_km, column_km = column_km)
        makeInteractiveComplexHeatmap(input, output, session, ht)
    })
}

shinyApp(ui = ui, server = server)

The response of the click event can be self-defined. Note, the matrix m for heatmap visualization is column-scaled on the original mtcars. In the next example, when a cell is click, the response will include a point chart of the distribution of the corresponding column in mtcars.

ui = fluidPage(
    sidebarLayout(
        sidebarPanel(
            radioButtons("order_method", "How to order rows and columns:", 
                c("Clustering" = "clustering", "Order by names" = "by_name"), 
                selected = "clustering"),
            checkboxInput('split', "Split rows and columns into 2 groups?", FALSE),
            width = 3
        ),
        mainPanel(
            InteractiveComplexHeatmapOutput(response = "click",
                output_ui = plotOutput("plot", width = 400, height = 400)),  # <- this line is changed
        ),
    )
)

# This function defines the response of the click event
click_action = function(df, output) {
    output$plot = renderPlot({
        if(is.null(df)) {
            grid.text("You should click on heatmap cells.")
        } else {
            i = df$row_index
            j = df$column_index
            od = order(mtcars[, j], rownames(mtcars))
            plot(1:nrow(mtcars), mtcars[od, j], xlab = "", ylab = colnames(mtcars)[j],
                main = paste0("Selected: ", rownames(mtcars)[i], ", ", colnames(mtcars)[j], " = ", mtcars[i, j]))
            map = structure(1:32, names = od)
            points(map[as.character(i)], mtcars[i, j], pch = 16, col = "red", cex = 1.2)
        }
    })
}

server = function(input, output, session) {
    observe({ 
        cluster_rows = FALSE
        cluster_columns = FALSE
        row_order = NULL
        column_order = NULL
        row_km = NULL
        column_km = NULL

        if(input$order_method == "clustering") {
            cluster_rows = TRUE
            cluster_columns = TRUE
        } else {
            row_order = order(rownames(m))
            column_order = order(colnames(m))
        }

        if(input$split) {
            row_km = 2
            column_km = 2
        }

        ht = Heatmap(m, name = "mtcars",
            cluster_rows = cluster_rows, cluster_columns = cluster_columns,
            row_order = row_order, column_order = column_order, 
            row_km = row_km, column_km = column_km)
        makeInteractiveComplexHeatmap(input, output, session, ht,
            click_action = click_action)    # <- this line is changed
    })
}

shinyApp(ui = ui, server = server)

Session info

sessionInfo()
## R version 4.1.2 (2021-11-01)
## Platform: x86_64-apple-darwin17.0 (64-bit)
## Running under: macOS Big Sur 10.16
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.1/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.1/Resources/lib/libRlapack.dylib
## 
## locale:
## [1] C/UTF-8/C/C/C/C
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] knitr_1.37
## 
## loaded via a namespace (and not attached):
##  [1] bookdown_0.24   digest_0.6.29   R6_2.5.1        jsonlite_1.7.2 
##  [5] magrittr_2.0.1  evaluate_0.14   blogdown_0.19   stringi_1.7.6  
##  [9] rlang_0.4.12    jquerylib_0.1.4 bslib_0.3.1     rmarkdown_2.11 
## [13] tools_4.1.2     stringr_1.4.0   xfun_0.29       yaml_2.2.1     
## [17] fastmap_1.1.0   compiler_4.1.2  htmltools_0.5.2 sass_0.4.0