Chatomics Field GuideWhat They Don't Teach You

Glossary · Visualization

Heatmap

A heatmap is not a picture, it's a clustering algorithm plus a color choice, and both steps can quietly reverse your biology.

By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 3 min read

Also: clustered heatmap

Definition

A heatmap is a matrix visualization in which every cell's value is encoded as a color, with rows and columns typically reordered by hierarchical clustering so that similar rows or columns sit next to each other. In practice a heatmap bundles two independent operations into one image: a clustering step that decides row/column order (based on a distance metric over the data), and a color-mapping step that decides how numeric values are rendered (based on a colormap and optional scaling). The two steps can use different transformations of the data, for example, clustering on raw values while displaying row-scaled colors, which is the single most common source of misreading a heatmap.

You'll meet a heatmap the first time you cluster a gene expression matrix or a single-cell dataset and want to show which genes or samples group together. It looks like the simplest plot in your toolkit: rows, columns, colored cells. It is not simple. Every heatmap you generate is really two separate decisions bundled into one image, how the rows and columns get ordered (clustering), and how values get mapped to color (scaling and colormap), and most default function calls make both decisions for you without telling you what they picked.

That matters because the ordering is the biology claim. If two genes end up adjacent in the heatmap, a reader assumes they're co-regulated. If clustering used raw expression magnitude instead of expression pattern, that adjacency can be an artifact of the default distance metric, not a finding.

Why it matters

Get the clustering distance wrong and you get biology that isn't there. Take four genes across eight timepoints where two pairs share the same up-down trend but sit at different absolute expression levels. Default Euclidean distance clusters the two high-magnitude genes together and the two low-magnitude genes together, because it measures absolute difference, not shape. The genes that actually move together biologically end up in different branches of the dendrogram, and a reader scanning the heatmap will report the wrong co-expression modules.

The fix is a scaling decision, and it has to happen before clustering, not just before coloring. Row-wise Z-scoring (t(scale(t(mat)))) converts each gene into "how far above or below its own mean," which puts pattern before magnitude. This is also exactly why heatmap() in base R (default scale = "row") and heatmap.2() in gplots (default scale = "none") can produce different dendrograms from the same input matrix and the same function call syntax, the scale argument in most of these functions only changes what color a cell shows, not what distance the clustering algorithm used, unless you explicitly scale the matrix yourself before passing it in.

Where people get it wrong

People assume the scale argument in heatmap() or pheatmap() changes the clustering result, when in most implementations it only changes the color mapping displayed after clustering has already run on the (often unscaled) input matrix. That means two analysts can run visually similar code, get different dendrograms, and neither of them notices because the plot "looks fine", it's colored consistently even when the row order is driven by magnitude instead of pattern. The second version of this mistake shows up in Python: seaborn.heatmap() never clusters anything, it just draws the matrix in the row/column order you feed it, so people who expect clustering from heatmap() and get none conclude their data has "no structure" when they simply called the wrong function, they wanted seaborn.clustermap().

A concrete example

Four genes, two real expression patterns, different magnitudes. Default distance groups by magnitude; row-scaling groups by pattern, which is what you almost always want for expression heatmaps.

r
h1 <- c(10,20,10,20,10,20,10,20)
h2 <- c(20,10,20,10,20,10,20,10)
l1 <- c(1,3,1,3,1,3,1,3)
l2 <- c(3,1,3,1,3,1,3,1)
mat <- rbind(h1,h2,l1,l2)

# Euclidean distance clusters by magnitude: h1~h2, l1~l2 (wrong -- ignores pattern)
dist(mat)

# Fix: z-score each row before clustering, then disable further column reordering
mat.scaled <- t(scale(t(mat)))
heatmap(mat.scaled, Colv = NA, scale = "none")

# Alternative: cluster on pattern directly using correlation distance
hc <- hclust(as.dist(1 - cor(t(mat))))
heatmap(mat, Rowv = as.dendrogram(hc), Colv = NA, scale = "row")

Related terms

Questions people ask

What is a heatmap used for in bioinformatics?

It visualizes a matrix, most often genes-by-samples expression or a correlation matrix, as colored cells so you can spot patterns across many rows and columns at once. Rows and columns are usually reordered by hierarchical clustering so similar genes or samples sit next to each other.

How do I read a heatmap correctly?

Check three things before you trust the picture: what distance metric ordered the rows and columns, whether scaling happened before or after clustering, and whether the colormap is sequential (one-directional data) or diverging (data with a meaningful zero or midpoint). If you can't answer those three, you're reading a picture, not a result.

Why does my heatmap gene expression clustering look wrong?

Almost always because clustering ran on absolute expression levels using Euclidean distance, which groups genes by magnitude rather than by shape of the trend across samples. Z-score each row first (or cluster on 1 minus correlation) if you want genes with the same pattern to cluster regardless of their expression level.

What's the best R package for heatmaps?

For quick exploratory plots, base R's heatmap() or pheatmap are fine as long as you know their scaling defaults. For anything you're publishing, or single-cell data with column annotations and gene sets outside the top variable features, use ComplexHeatmap, it gives you explicit control over row/column clustering, distance metrics, and slicing by cell type or condition.

Should I use seaborn.heatmap or seaborn.clustermap in Python?

seaborn.heatmap() draws the matrix in whatever row/column order you give it and does not cluster. seaborn.clustermap() runs hierarchical clustering on both axes and draws dendrograms. If you want the rows or columns reordered by similarity, clustermap is the one you want, not heatmap with data pre-sorted by eye.

Related pages

Related reading on the blog

Sources

  1. Complex heatmap visualization — Definition and history of heatmaps in bioinformatics
  2. A tale of two heatmap functions — heatmap() vs heatmap.2() default scale arguments and clustering behavior
  3. seaborn.heatmap documentation — heatmap vs clustermap, cmap and center parameters
  4. ComplexHeatmap Reference — clustering_distance_rows and other clustering controls
  5. Enhancement of scRNAseq heatmap using complexheatmap — Seurat DoHeatmap limitations vs ComplexHeatmap