Author: Stefano Mangiola
Date: 2025-10-29

# Overview of the tidyHeatmap package

#### Stefano Mangiola

#### 2025-10-29

## Citation

Mangiola et al., (2020). tidyHeatmap: an R package for modular heatmap production based on tidy principles. Journal of Open Source Software, 5(52), 2472, https://doi.org/10.21105/joss.02472

`tidyHeatmap` is a package that introduces tidy principles to the creation of information-rich heatmaps. This package uses ComplexHeatmap as graphical engine. website: stemangiola.github.io/tidyHeatmap

Advantages:

- Modular annotation with just specifying column names
- Custom grouping of rows/columns is easy to specify with `annotation_group`, e.g. `heatmap(...) |> annotation_group(...)`
- Labels size adjusted by row and column total number
- Default use of Brewer and Viridis palettes

## Functions/utilities available

Function | Description
--- | ---
`heatmap` | Plots base heatmap
`annotation_group` | Adds group annotation strips and grouping to the heatmap (replaces group_by)
`annotation_tile` | Adds tile annotation to the heatmap
`annotation_point` | Adds point annotation to the heatmap
`annotation_bar` | Adds bar annotation to the heatmap
`annotation_numeric` | Adds bar + number annotation to the heatmap
`annotation_line` | Adds line annotation to the heatmap
`layer_text` | Add layer of text on top of the heatmap
`layer_point` | Adds layer of symbols on top of the heatmap
`layer_square` | Adds layer of symbols on top of the heatmap
`layer_diamond` | Adds layer of symbols on top of the heatmap
`layer_arrow_up` | Adds layer of symbols on top of the heatmap
`layer_arrow_down` | Add layer of symbols on top of the heatmap
`layer_star` | Add layer of symbols on top of the heatmap
`layer_asterisk` | Add layer of symbols on top of the heatmap
`split_rows` | Splits the rows based on the dendogram
`split_columns` | Splits the columns based on the dendogram
`save_pdf` | Saves the PDF of the heatmap
`+` | Integrate heatmaps side-by-side
`as_ComplexHeatmap` | Convert the tidyHeatmap output to ComplexHeatmap for non-standard “drawing”
`wrap_heatmap` | Allows the integration with the `patchwork` package

## Installation

To install the most up-to-date version

    devtools::install_github("stemangiola/tidyHeatmap")

To install the most stable version (however please keep in mind that this package is under a maturing lifecycle stage)

    install.packages("tidyHeatmap")

## Contribution

If you want to contribute to the software, report issues or problems with the software or seek support please open an issue here

## Input data frame

The heatmaps visualise a multi-element, multi-feature dataset, annotated with independent variables. Each observation is a element-feature pair (e.g., person-physical characteristics).

element | feature | value | independent_variables
--- | --- | --- | ---
`chr` or `fctr` | `chr` or `fctr` | `numeric` | …

Let’s transform the mtcars dataset into a tidy “element-feature-independent variables” data frame. Where the independent variables in this case are ‘hp’ and ‘vs’.

    mtcars_tidy <-
        mtcars |>
        as_tibble(rownames="Car name") |>

        # Scale
        mutate_at(vars(-`Car name`, -hp, -vs), scale) |>

        # tidyfy
        pivot_longer(cols = -c(`Car name`, hp, vs), names_to = "Property", values_to = "Value")

    mtcars_tidy

    ## # A tibble: 288 × 5
    ##    `Car name`       hp    vs Property Value[,1]
    ##    <chr>         <dbl> <dbl> <chr>        <dbl>
    ##  1 Mazda RX4       110     0 mpg          0.151
    ##  2 Mazda RX4       110     0 cyl         -0.105
    ##  3 Mazda RX4       110     0 disp        -0.571
    ##  4 Mazda RX4       110     0 drat         0.568
    ##  5 Mazda RX4       110     0 wt          -0.610
    ##  6 Mazda RX4       110     0 qsec        -0.777
    ##  7 Mazda RX4       110     0 am           1.19
    ##  8 Mazda RX4       110     0 gear         0.424
    ##  9 Mazda RX4       110     0 carb         0.735
    ## 10 Mazda RX4 Wag   110     0 mpg          0.151
    ## # ℹ 278 more rows

## Plotting

For plotting, you simply pipe the input data frame into heatmap, specifying:

- The rows, cols relative column names (mandatory)
- The value column name (mandatory)
- The annotations column name(s)

mtcars

    mtcars_heatmap <-
        mtcars_tidy |>
        heatmap(`Car name`, Property, Value,    scale = "row"   ) |>
        annotation_tile(hp)

    ## tidyHeatmap says: If you use tidyHeatmap for scientific research, please cite: Mangiola, S. and Papenfuss, A.T., 2020. 'tidyHeatmap: an R package for modular heatmap production based on tidy principles.' Journal of Open Source Software. doi:10.21105/joss.02472.
    ## This message is displayed once per session.

    mtcars_heatmap

## Saving

    mtcars_heatmap |> save_pdf("mtcars_heatmap.pdf")

## Clustering

Choose alternative clustering distance and methods.

    tidyHeatmap::pasilla |>

        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row",

            # Arguments passed to ComplexHeatmap
            clustering_distance_rows = "manhattan",
            clustering_distance_columns = "manhattan",
            clustering_method_rows = "ward.D",
            clustering_method_columns = "ward.D"
        )

## Grouping and splitting

We can easily group the data (one group per dimension maximum, at the moment only the vertical dimension is supported) with dplyr, and the heatmap will be grouped accordingly

    # Make up more groupings
    mtcars_tidy_groupings =
        mtcars_tidy |>
        mutate(property_group = if_else(Property %in% c("cyl", "disp"), "Engine", "Other"))

    mtcars_tidy_groupings |>
        heatmap(`Car name`, Property, Value, scale = "row") |>
        annotation_group(vs, property_group) |>
        annotation_tile(hp)

You can provide colour palettes and other aesthetics to groupings

    mtcars_tidy_groupings |>
        heatmap(
            `Car name`, Property, Value ,
            scale = "row"
        ) |>
        annotation_group(
            vs, property_group,
            palette_grouping = list(
                # For first grouping (vs)
                c("#66C2A5", "#FC8D62"),
                # For second grouping (property_group)
                c("#b58b4c", "#74a6aa")
            ),
            group_label_fontsize = 14,
            show_group_name = TRUE,
            group_strip_height = grid::unit(20, "pt")
        ) |>
        annotation_tile(hp)

We can split based on the cladogram

    mtcars_tidy |>
        heatmap(`Car name`, Property, Value,    scale = "row"   ) |>
        split_rows(2) |>
        split_columns(2)

We can split on kmean clustering (using ComplexHeatmap options, it is stochastic)

    mtcars_tidy |>
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            row_km = 2,
            column_km = 2
        )

## Custom palettes

We can easily use custom palette, using strings, hexadecimal color character vector,

    mtcars_tidy |>
        heatmap(
            `Car name`,
            Property,
            Value,
            scale = "row",
            palette_value = c("red", "white", "blue")
        )

A better-looking blue-to-red palette

    mtcars_tidy |>
        heatmap(
            `Car name`,
            Property,
            Value,
            scale = "row",
            palette_value = circlize::colorRamp2(
                seq(-2, 2, length.out = 11),
                RColorBrewer::brewer.pal(11, "RdBu")
            )
        )

Or a grid::colorRamp2 function for higher flexibility

    mtcars_tidy |>
        heatmap(
            `Car name`,
            Property,
            Value,
            scale = "row",
            palette_value = circlize::colorRamp2(c(-2, -1, 0, 1, 2), viridis::magma(5))
        )

We can use custom colors for tile annotation

    mtcars_tidy |>
        heatmap(
            `Car name`,
            Property,
            Value,
            scale = "row"
        ) |>
        annotation_tile(
            hp,
            palette = c("red", "white", "blue")
        )

We can use grid::colorRamp2 function for tile annotation for specific color scales

    mtcars_tidy |>
        heatmap(
            `Car name`,
            Property,
            Value,
            scale = "row"
        ) |>
        annotation_tile(
            hp,
            palette = circlize::colorRamp2(c(0, 100, 200, 300), viridis::magma(4))
        )

## Multiple groupings and annotations

    tidyHeatmap::pasilla |>
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        annotation_group(location, type) |>
        annotation_tile(condition) |>
        annotation_tile(activation)

Example with custom aesthetics for annotation_group

    tidyHeatmap::pasilla |>
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        annotation_group(
            location, type,
            group_label_fontsize = 12,
            group_strip_height = grid::unit(15, "pt"),
            show_group_name = FALSE
        ) |>
        annotation_tile(condition) |>
        annotation_tile(activation)

Remove legends, adding aesthetics to annotations in a modular fashion, using `ComplexHeatmap` arguments

    tidyHeatmap::pasilla |>
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row",
            show_heatmap_legend = FALSE
        ) |>
        annotation_tile(condition, show_legend = FALSE) |>
        annotation_tile(activation, show_legend = FALSE)

## Annotation types

“tile”, “point”, “bar”, “line” and “numeric” are available

    # Create some more data points
    pasilla_plus <-
        tidyHeatmap::pasilla |>
        dplyr::mutate(activation_2 = activation, activation_3 = activation) |>
        tidyr::nest(data = -sample) |>
        dplyr::mutate(size = rnorm(n(), 4,0.5)) |>
        dplyr::mutate(age = runif(n(), 50, 200)) |>
        tidyr::unnest(data)

    # Plot
    pasilla_plus |>
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        annotation_tile(condition) |>
        annotation_point(activation) |>
        annotation_numeric(activation_3) |>
        annotation_tile(activation_2) |>
        annotation_bar(size) |>
        annotation_line(age)

## Annotation size

We can customise annotation sizes using the `grid::unit()`, and the size of their names using in-built `ComplexHeatmap` arguments

    pasilla_plus |>
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        annotation_tile(condition, size = unit(0.3, "cm"),  annotation_name_gp= gpar(fontsize = 8)) |>
        annotation_point(activation, size = unit(0.3, "cm"),    annotation_name_gp= gpar(fontsize = 8)) |>
        annotation_tile(activation_2, size = unit(0.3, "cm"),   annotation_name_gp= gpar(fontsize = 8)) |>
        annotation_bar(size, size = unit(0.3, "cm"),    annotation_name_gp= gpar(fontsize = 8)) |>
        annotation_line(age, size = unit(0.3, "cm"),    annotation_name_gp= gpar(fontsize = 8))

# Layer symbol

Add a layer on top of the heatmap

    tidyHeatmap::pasilla |>

        # filter
        filter(symbol %in% head(unique(tidyHeatmap::pasilla$symbol), n = 10)) |>

        # Add dynamic size
        mutate(my_size = runif(n(), 1,5)) |>

        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        layer_point(
            `count normalised adjusted log` > 6 & sample == "untreated3"
        ) |>
        layer_square(
            `count normalised adjusted log` > 6 & sample == "untreated2",
            .size = my_size
        ) |>
        layer_arrow_up(
            `count normalised adjusted log` > 6 & sample == "untreated1",
            .size = 4
        )

## Complex layer symbol example with colors

Advanced example showing different symbols with custom colors and sizes

    tidyHeatmap::pasilla |>

        # filter
        filter(symbol %in% head(unique(tidyHeatmap::pasilla$symbol), n = 10)) |>

        # Add dynamic color and size columns
        mutate(
            my_color = case_when(
                `count normalised adjusted log` > 7 ~ "#57fb77",
                `count normalised adjusted log` > 6 ~ "darkorange",
                TRUE ~ "#fb7ef9"
            ),
            my_size = runif(n(), 3, 8)
        ) |>

        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        layer_point(
            `count normalised adjusted log` > 6 & sample == "untreated3",
            .color = "red",
            .size = 5
        ) |>
        layer_diamond(
            `count normalised adjusted log` > 6 & sample == "untreated1",
            .color = "green",
            .size = 6
        ) |>
        layer_star(
            `count normalised adjusted log` > 7,
            .color = "purple",
            .size = 4
        ) |>

        # Dynamically color and size
        layer_square(
            `count normalised adjusted log` > 6 & sample == "untreated2",
            .color = my_color,
            .size = my_size
        )

# Layer text

Add a text layer on top of the heatmap

    tidyHeatmap::pasilla |>

        # filter
        filter(symbol %in% head(unique(tidyHeatmap::pasilla$symbol), n = 10)) |>

        # Add dynamic text
        mutate(my_text = "mt", my_size = 7) |>

        # Plot
        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        layer_text(
            `count normalised adjusted log` > 6 & sample == "untreated3",
            .value = "a",
            .size = 15
        ) |>
        layer_text(
        `count normalised adjusted log` > 6 & sample == "untreated2",
        .value = my_text,
        .size = my_size
    )

## Complex layer text example with colors

Advanced example showing text layers with custom colors, sizes, and dynamic content

    tidyHeatmap::pasilla |>

        # filter
        filter(symbol %in% head(unique(tidyHeatmap::pasilla$symbol), n = 10)) |>

        # Add dynamic text content and styling
        mutate(
            text_content = case_when(
                `count normalised adjusted log` > 7 ~ "***",
                `count normalised adjusted log` > 6 ~ "**",
                `count normalised adjusted log` > 5 ~ "*",
                TRUE ~ ""
            ),
            text_color = case_when(
                `count normalised adjusted log` > 7 ~ "darkred",
                `count normalised adjusted log` > 6 ~ "darkorange",
                `count normalised adjusted log` > 5 ~ "darkgreen",
                TRUE ~ "darkgray"
            ),
            text_size = case_when(
                `count normalised adjusted log` > 7 ~ 12,
                `count normalised adjusted log` > 6 ~ 10,
                `count normalised adjusted log` > 5 ~ 8,
                TRUE ~ 6
            )
        ) |>

        heatmap(
            .column = sample,
            .row = symbol,
            .value = `count normalised adjusted`,
            scale = "row"
        ) |>
        layer_text(
            `count normalised adjusted log` > 6 & sample == "untreated3",
            .value = "HIGH",
            .color = "red",
            .size = 12
        ) |>
        layer_text(
            `count normalised adjusted log` > 5 & sample == "untreated1",
            .value = "↑",
            .color = "blue",
            .size = 10
        ) |>

        # Dynamically set text content, color and size
        layer_text(
            `count normalised adjusted log` > 6 & sample == "untreated2",
            .value = text_content,
            .color = text_color,
            .size = text_size
        )

# Adding heatmap side-by-side

    p_heatmap = heatmap(mtcars_tidy, `Car name`, Property, Value, scale = "row")

    p_heatmap + p_heatmap

Add more than two heatmaps

> Note:

> Due to limitations in R’s method dispatch, adding more than two tidyHeatmap heatmaps using the `+` operator (e.g. `p_heatmap + p_heatmap + p_heatmap`) may result in an error or unexpected behavior.

> To robustly combine more than two heatmaps, convert each to a ComplexHeatmap object first, this will use the `+` functionality of `ComplexHeatmap`. This ensures that all objects are compatible and can be combined using ComplexHeatmap’s native addition functionality.

    as_ComplexHeatmap(p_heatmap) +
      as_ComplexHeatmap(p_heatmap) +
      as_ComplexHeatmap(p_heatmap)

# Using patchwork to integrate across heatmaps and including ggplots

    library(ggplot2)

    ## Warning: package 'ggplot2' was built under R version 4.5.1

    library(patchwork)

    ## Warning: package 'patchwork' was built under R version 4.5.1

    p_heatmap =
        mtcars_tidy |>
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
                show_heatmap_legend = FALSE,
            row_names_gp = gpar(fontsize = 7)
        )

    p_ggplot = data.frame(value = 1:10) |> ggplot(aes(value)) + geom_density()

    wrap_heatmap(p_heatmap) +
        p_ggplot +

        # Add padding for better aesthetics
        wrap_heatmap(
            p_heatmap,
            padding = grid::unit(c(-30, -0, -0, -10), "points" ),
            clip = FALSE
        ) +
        plot_layout(width = c(1, 0.3, 1))

# ComplexHeatmap further styling

## Add cell borders

    mtcars_tidy |>
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            rect_gp = grid::gpar(col = "#161616", lwd = 0.5)
        )

## Drop row clustering

    mtcars_tidy |>
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            cluster_rows = FALSE
        )

## Reorder rows elements

    library(forcats)

    ## Warning: package 'forcats' was built under R version 4.5.1

    mtcars_tidy |>
        mutate(`Car name` = forcats::fct_reorder(`Car name`, `Car name`, .desc = TRUE)) %>%
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            cluster_rows = FALSE
        )

## Size of dendrograms

    mtcars_tidy |>
        mutate(`Car name` = forcats::fct_reorder(`Car name`, `Car name`, .desc = TRUE)) %>%
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            column_dend_height = unit(0.2, "cm"),
            row_dend_width = unit(0.2, "cm")
        )

## Size of rows/columns titles and names

    mtcars_tidy |>
        mutate(`Car name` = forcats::fct_reorder(`Car name`, `Car name`, .desc = TRUE)) %>%
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            row_names_gp = gpar(fontsize = 7),
            column_names_gp = gpar(fontsize = 7),
            column_title_gp = gpar(fontsize = 7),
            row_title_gp = gpar(fontsize = 7)
        )

## Remove row and column titles

You can completely remove the row and column titles by setting them to empty strings:

    mtcars_tidy |>
        mutate(`Car name` = forcats::fct_reorder(`Car name`, `Car name`, .desc = TRUE)) |>
        heatmap(
            `Car name`, Property, Value,
            scale = "row",
            row_title = "",      # Remove row title
            column_title = ""    # Remove column title
        )

## Align numeric annotation

This can be done only for `annotation_numeric` because of `ComplexHeatmap` requirements (?ComplexHeatmap::anno_numeric)

    mtcars_tidy |>
        mutate(`Car name` = forcats::fct_reorder(`Car name`, `Car name`, .desc = TRUE)) %>%
        heatmap(
            `Car name`, Property, Value,
            scale = "row"
        ) |>
      annotation_numeric(hp, align_to="right")

## External `ComplexHeatmap` functionalities

ComplexHeatmap has some graphical functionalities that are not included in the standard functional framework. We can use `as_ComplexHeatmap` to convert our output before applying drawing options.

### Chainging side of legends

    heatmap(mtcars_tidy, `Car name`, Property, Value, scale = "row" ) %>%
        as_ComplexHeatmap() %>%
        ComplexHeatmap::draw(heatmap_legend_side = "left"   )

### Add title using `draw` from `ComplexHeatmap`

    mtcars_tidy |>
        heatmap(`Car name`, Property, Value,    scale = "row"   ) |>
        as_ComplexHeatmap() |>
        ComplexHeatmap::draw(
            column_title = "TITLE",
            column_title_gp = gpar(fontsize = 16)
        )

### Add title using `ggtitle` from `ggplot2`

    mtcars_tidy |>
        heatmap(`Car name`, Property, Value,  scale = "row" ) |>
        wrap_heatmap() +
            ggplot2::ggtitle("TITLE")

# Session Info

    sessionInfo()

    ## R version 4.5.0 (2025-04-11)
    ## Platform: x86_64-apple-darwin20
    ## Running under: macOS Sonoma 14.6.1
    ##
    ## Matrix products: default
    ## BLAS:   /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRblas.0.dylib
    ## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
    ##
    ## locale:
    ## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
    ##
    ## time zone: Australia/Adelaide
    ## tzcode source: internal
    ##
    ## attached base packages:
    ## [1] grid      stats     graphics  grDevices utils     datasets  methods
    ## [8] base
    ##
    ## other attached packages:
    ## [1] forcats_1.0.1      patchwork_1.3.2    ggplot2_4.0.0      tidyHeatmap_1.13.1
    ## [5] tidyr_1.3.1        dplyr_1.1.4
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] viridis_0.6.5         utf8_1.2.6            sass_0.4.10
    ##  [4] generics_0.1.4        shape_1.4.6.1         digest_0.6.37
    ##  [7] magrittr_2.0.4        evaluate_1.0.5        RColorBrewer_1.1-3
    ## [10] iterators_1.0.14      circlize_0.4.16       fastmap_1.2.0
    ## [13] foreach_1.5.2         doParallel_1.0.17     jsonlite_2.0.0
    ## [16] GlobalOptions_0.1.2   gridExtra_2.3         ComplexHeatmap_2.25.2
    ## [19] purrr_1.1.0           viridisLite_0.4.2     scales_1.4.0
    ## [22] codetools_0.2-20      jquerylib_0.1.4       cli_3.6.5
    ## [25] rlang_1.1.6           crayon_1.5.3          withr_3.0.2
    ## [28] cachem_1.1.0          yaml_2.3.10           tools_4.5.0
    ## [31] parallel_4.5.0        colorspace_2.1-2      GetoptLong_1.0.5
    ## [34] BiocGenerics_0.55.4   vctrs_0.6.5           R6_2.6.1
    ## [37] png_0.1-8             magick_2.9.0          matrixStats_1.5.0
    ## [40] stats4_4.5.0          lifecycle_1.0.4       S4Vectors_0.47.4
    ## [43] IRanges_2.43.5        clue_0.3-66           cluster_2.1.8.1
    ## [46] dendextend_1.19.1     pkgconfig_2.0.3       gtable_0.3.6
    ## [49] pillar_1.11.1         bslib_0.9.0           Rcpp_1.1.0
    ## [52] glue_1.8.0            xfun_0.53             tibble_3.3.0
    ## [55] tidyselect_1.2.1      rstudioapi_0.17.1     knitr_1.50
    ## [58] farver_2.1.2          rjson_0.2.23          htmltools_0.5.8.1
    ## [61] labeling_0.4.3        rmarkdown_2.30        Cairo_1.6-5
    ## [64] compiler_4.5.0        S7_0.2.0