---
title: "A Bioconductor-Native, Composable Heatmap"
author: "SummarizedHeatmap developers"
output:
  BiocStyle::html_document:
    toc: true
    toc_depth: 2
vignette: >
  %\VignetteIndexEntry{A Bioconductor-Native, Composable Heatmap}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    warning = FALSE,
    message = FALSE,
    dev = "png",
    fig_crop = FALSE,
    fig.width = 7,
    fig.height = 5
)
options(bitmapType = "cairo")
library(SummarizedHeatmap)
```

# Introduction

Heatmaps are often built from a matrix and a collection of separate annotation
vectors. Keeping those pieces synchronized becomes difficult when data are
subsetted, reordered, or reused in a downstream analysis. `SummarizedHeatmap`
stores the assay, `rowData`, `colData`, display order, and any hierarchical
clusterings in one `SummarizedExperiment`-derived object. It separates that
data representation from rendering: each visual component is a regular
ggplot2 plot, and patchwork handles layout and composition.

This makes the package a natural fit for Bioconductor workflows, where assays
and their feature and sample metadata are already represented by
`SummarizedExperiment`. It also lets users continue to work with familiar
ggplot2 scales, themes, and geoms after making a heatmap component.

This approach complements other heatmap packages.
[ComplexHeatmap](https://bioconductor.org/packages/ComplexHeatmap/) provides a
comprehensive system for arranging heatmaps and annotations. The
CRAN package [ggheatmapper](https://csgroen.github.io/ggheatmapper/) also uses
ggplot2 and patchwork for tile-based heatmaps. `SummarizedHeatmap` focuses on a
different boundary: persistent assay, metadata, order, and clustering state
inside a Bioconductor class, with individually accessible ggplot components
for users who want to compose their own layouts.

# Installation

For a released version, install with BiocManager:

```{r install, eval=FALSE}
if (!requireNamespace("BiocManager", quietly = TRUE)) {
    install.packages("BiocManager")
}
BiocManager::install("SummarizedHeatmap")
```

# Create a heatmap object

The small deterministic example below resembles a normalized gene-expression
assay. Feature and sample annotations are passed using the usual
`SummarizedExperiment` metadata classes.

```{r create}
set.seed(2026)
mat <- matrix(
    rnorm(48),
    nrow = 8,
    dimnames = list(paste0("gene", seq_len(8)), paste0("sample", seq_len(6)))
)
row_info <- S4Vectors::DataFrame(
    feature_class = rep(c("marker", "background"), each = 4),
    row.names = rownames(mat)
)
column_info <- S4Vectors::DataFrame(
    condition = rep(c("control", "treated"), each = 3),
    batch = rep(c("batch 1", "batch 2"), 3),
    row.names = colnames(mat)
)

hm <- SummarizedHeatmap(
    mat,
    rowData = row_info,
    colData = column_info
)
hm
```

Rows and columns are clustered independently using Euclidean distance and
average linkage by default. `rowOrder()` and `colOrder()` return integer
indices in display order; the corresponding dendrograms are available through
`rowDendro()` and `colDendro()`.

```{r state}
rowOrder(hm)
colOrder(hm)
class(rowDendro(hm))
class(colDendro(hm))
```

Annotations remain ordinary `rowData` and `colData` and can be read or
modified through the inherited SummarizedExperiment accessors.

## Manual column ordering

Columns don't have to be ordered by clustering. Passing `colOrder` to the
constructor -- as column names or integer indices, covering every column
exactly once -- fixes the display order directly, leaves the column
dendrogram unset, and takes precedence over `clusterCols`. This suits columns
with a meaningful order of their own, such as a time course or a predefined
experimental grouping, rather than one derived from assay similarity.
`rowOrder` accepts the same two forms for manually ordering rows.

```{r manual-col-order}
hm_manual_cols <- SummarizedHeatmap(
    mat,
    rowData = row_info,
    colData = column_info,
    colOrder = rev(colnames(mat))
)
colOrder(hm_manual_cols)
is.null(colDendro(hm_manual_cols))
```

Rows are still clustered by default here, since `rowOrder`/`clusterRows`
weren't touched. See [Plot components] for how this shows up in
`plotHeatmap()`'s layout.

## Constructing from a SummarizedExperiment

A `SummarizedHeatmap` always holds exactly one assay, but the constructor also
accepts an existing `SummarizedExperiment` directly, which is convenient when
one is already produced upstream (for example, by an RNA-seq quantification
or normalization workflow). When the input has a single assay, it is used
automatically and `rowData`, `colData`, and `metadata` carry over unchanged.
The selected assay's original name is preserved as well.

```{r from-se-single}
se <- SummarizedExperiment::SummarizedExperiment(
    assays = list(scaled = mat),
    rowData = row_info,
    colData = column_info
)
hm_from_se <- SummarizedHeatmap(se)
hm_from_se
SummarizedExperiment::assayNames(hm_from_se) # "scaled", preserved from se
```

When the input has more than one assay, supply `assay` to select one by name
or by position; the others are dropped from the resulting object.

```{r from-se-multi}
se_multi <- SummarizedExperiment::SummarizedExperiment(
    assays = list(raw = mat, scaled = mat / max(abs(mat))),
    rowData = row_info,
    colData = column_info
)
hm_scaled <- SummarizedHeatmap(se_multi, assay = "scaled")
SummarizedExperiment::assayNames(hm_scaled)
```

Omitting `assay` for a multi-assay input, or supplying an unknown name or
out-of-range index, raises an informative error rather than silently picking
an assay.

```{r from-se-error, error=TRUE}
SummarizedHeatmap(se_multi)
```

# Plot components

`plotHeatmapMain()` returns a regular ggplot. Annotation functions create one
ggplot per variable, labelled with its variable name as axis text next to the
tiles; selecting multiple variables returns a patchwork stacking the
independent plots. Dendrogram functions also return ggplots.

```{r components}
main <- plotHeatmapMain(hm) +
    ggplot2::labs(fill = "Scaled expression") +
    ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))
column_annotation <- plotColData(hm, vars = c("condition", "batch"))
row_annotation <- plotRowData(hm, vars = "feature_class")
column_tree <- plotColDendro(hm)
row_tree <- plotRowDendro(hm)

class(main)
class(column_annotation)
```

`plotHeatmap()` provides a complete starting layout with annotations,
dendrograms, and a shared guide area. Column annotations are stacked vertically;
on the left of the heatmap, the row dendrogram is followed by row annotations,
so the annotations sit beside the heatmap body. The row dendrogram's leaves
face the heatmap. The component functions remain available when a custom layout
is preferred.

```{r complete, height = 9}
complete <- plotHeatmap(hm)
complete
```

`hm_manual_cols` (see [Manual column ordering]) has no stored column
dendrogram, since its column order was supplied manually rather than
clustered. `plotHeatmap()` omits that panel entirely -- rather than leaving
it blank -- while the row dendrogram, which is still clustered, draws as
usual, and the column annotation and heatmap columns simply follow the
manual order:

```{r manual-col-order-plot}
plotHeatmap(hm_manual_cols)
```

# Compose and customize with patchwork

The components can be arranged with patchwork operators, `wrap_plots()`, and
`plot_layout()`. `free()` releases a panel's alignment on selected sides, and
`guide_area()` reserves a location for guides when `guides = "collect"` is
used. 

```{r custom-layout}
stacked <- patchwork::wrap_plots(
    list(column_tree, main),
    ncol = 1, heights = c(1, 5), guides = "collect"
)
stacked
```

`column_annotation` is itself a two-panel patchwork (one panel per selected
variable), so combining it directly with `main` using an operator like `/`
would flatten both objects' panel lists together, leaving three panels where
`heights = c(1, 5)` only accounts for two. Use `wrap_plots()` with a list
instead: each list element is kept as one area regardless of how many panels
it contains internally, so the annotation strip stays aligned with the
heatmap and its guides are still collected normally.

Each annotation panel also labels its variable as axis text next to the
tiles. Wrapping the annotation in `patchwork::free(type = "space")` lets that
text occupy space without reserving room for it in the shared grid, so it
packs tightly against the heatmap instead of forcing extra column/row width
(`plotHeatmap()` does this internally for every annotation strip).

```{r free-layout}
custom <- patchwork::wrap_plots(
    list(patchwork::free(column_annotation, type = "space"), main),
    ncol = 1, heights = c(1, 5), guides = "collect"
)
custom
```

Arbitrary ggplots can be combined with heatmap components using the same
patchwork operations. Here a small sample-level summary plot sits above the
heatmap.

```{r extra-plot}
display_samples <- colnames(mat)[colOrder(hm)]
qc <- ggplot2::ggplot(
    data.frame(
        sample = factor(display_samples, levels = display_samples),
        mean = colMeans(mat)[display_samples]
    ),
    ggplot2::aes(x = sample, y = mean)
) +
    ggplot2::geom_point() +
    ggplot2::labs(x = NULL, y = "Mean\nassay\nvalue") +
    ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))

(qc / main) + patchwork::plot_layout(heights = c(1, 5))
```

Component functions also compose directly with `wrap_plots()`'s named-argument
form, letting a custom `design` string place each panel (and `guide_area()`)
anywhere in the grid. `palette` on `plotColData()`/`plotRowData()` maps
annotation levels to specific colours (via `scale_fill_manual()`); a single
call's `palette` applies to every variable it plots.

```{r custom-grid}
row_palette <- c(marker = "#8DA0CB", background = "#E5C494")
col_palette <- c(
    control = "#66C2A5",
    treated = "#FC8D62",
    `batch 1` = "#386CB0",
    `batch 2` = "#F0027F"
)

pColDendro <- plotColDendro(hm)
pColData <- plotColData(hm, palette = col_palette)
pRowDendro <- plotRowDendro(hm)
pRowData <- patchwork::free(
    plotRowData(
        hm, vars = "feature_class", side = "top", palette = row_palette
    ),
    type = "space"
)
pMain <- plotHeatmapMain(hm)

design <- "
##AG
##BG
CDEG
"
patchwork::wrap_plots(
    A = pColDendro, B = pColData, C = pRowDendro, D = pRowData, E = pMain,
    G = patchwork::guide_area(),
    design = design,
    heights = c(1, 1, 8),
    widths = c(1, 1, 8, 2),
    guides = "collect"
)
```

# Subsetting and reclustering

Subsetting follows SummarizedExperiment semantics: `i` selects rows and `j`
selects columns. Whether a dimension's clustering survives subsetting depends
only on whether *that* dimension's membership or order changed:

- Subsetting rows clears the row dendrogram and resets `rowOrder()` to the
  natural order of the subset. Column clustering is untouched as long as the
  columns themselves are unchanged.
- Subsetting columns clears the column dendrogram and resets `colOrder()`,
  while row clustering is untouched as long as the rows are unchanged.
- Subsetting both dimensions at once clears clustering for both, since both
  have changed.

Subsetting never silently reclusters. Call `clusterRows()`, `clusterCols()`,
or `clusterHeatmap()` afterward to compute new clustering.

Subsetting rows only, keeping all columns, clears the row dendrogram but
leaves the column dendrogram and order exactly as they were in `hm`:

```{r subset-rows}
hm_rows <- hm[1:5, ]
is.null(rowDendro(hm_rows))
rowOrder(hm_rows)
identical(colDendro(hm_rows), colDendro(hm))
identical(colOrder(hm_rows), colOrder(hm))
```

Subsetting columns only, keeping all rows, has the mirror-image effect: the
column dendrogram is cleared, and row clustering is preserved:

```{r subset-cols}
hm_cols <- hm[, c("sample1", "sample3", "sample5")]
is.null(colDendro(hm_cols))
colOrder(hm_cols)
identical(rowDendro(hm_cols), rowDendro(hm))
identical(rowOrder(hm_cols), rowOrder(hm))
```

Subsetting both dimensions at once clears clustering for both, since both row
and column membership changed:

```{r subset-both}
hm_both <- hm[1:5, c("sample1", "sample3", "sample5")]
is.null(rowDendro(hm_both))
is.null(colDendro(hm_both))
```

Recluster explicitly when new clustering is desired. `clusterRows()` and
`clusterCols()` recompute a single dimension; `clusterHeatmap()` recomputes
both:

```{r recluster}
hm_both <- clusterHeatmap(hm_both)
validObject(hm_both)
class(rowDendro(hm_both))
class(colDendro(hm_both))
```

# Replacing assay values

Replacing the assay (`assay(hm) <- ...` or `assays(hm) <- ...`) applies the
same invalidation rule as subsetting, since a stored dendrogram and its
implied order are only valid for the assay values they were computed from.
Whenever a dendrogram is currently stored for an axis, replacing the assay
clears it and resets that axis's order to natural order; axes with no stored
dendrogram (e.g. a manually-supplied order) are left untouched. Assay
replacement never reclusters automatically:

```{r assay-replace}
hm_new_values <- hm
SummarizedExperiment::assay(hm_new_values) <-
    SummarizedExperiment::assay(hm_new_values) * -1
is.null(rowDendro(hm_new_values))
is.null(colDendro(hm_new_values))
rowOrder(hm_new_values)
colOrder(hm_new_values)
```

Call `clusterRows()`, `clusterCols()`, or `clusterHeatmap()` afterward to
recompute clustering against the new values.

# Session information

```{r session-info}
sessionInfo()
```
