31

Creating heatmaps in R has been a topic of many posts, discussions and iterations. My main problem is that it's tricky to combine visual flexibility of solutions available in lattice levelplot() or basic graphics image(), with effortless clustering of basic's heatmap(), pheatmap's pheatmap() or gplots' heatmap.2(). It's a tiny detail I want to change - diagonal orientation of labels on x-axis. Let me show you my point in the code.

#example data
d <- matrix(rnorm(25), 5, 5)
colnames(d) = paste("bip", 1:5, sep = "")
rownames(d) = paste("blob", 1:5, sep = "")

You can change orientation to diagonal easily with levelplot():

require(lattice)
levelplot(d, scale=list(x=list(rot=45)))

enter image description here

but applying the clustering seems pain. So does other visual options like adding borders around heatmap cells.

Now, shifting to actual heatmap() related functions, clustering and all basic visuals are super-simple - almost no adjustment required:

heatmap(d)

enter image description here

and so is here:

require(gplots)
heatmap.2(d, key=F)

enter image description here

and finally, my favourite one:

require(pheatmap)
pheatmap(d) 

enter image description here

But all of those have no option to rotate the labels. Manual for pheatmap suggests that I can use grid.text to custom-orient my labels. What a joy it is - especially when clustering and changing the ordering of displayed labels. Unless I'm missing something here...

Finally, there is an old good image(). I can rotate labels, in general it' most customizable solution, but no clustering option.

image(1:nrow(d),1:ncol(d), d, axes=F, ylab="", xlab="")
text(1:ncol(d), 0, srt = 45, labels = rownames(d), xpd = TRUE)
axis(1, label=F)
axis(2, 1:nrow(d), colnames(d), las=1)

enter image description here

So what should I do to get my ideal, quick heatmap, with clustering and orientation and nice visual features hacking? My best bid is changing heatmap() or pheatmap() somehow because those two seem to be most versatile in adjustment. But any solutions welcome.

Geek On Acid
  • 6,330
  • 4
  • 44
  • 64
  • 1
    Base graphics doesn't allow your to control the rotation of tick labels to arbitrary angles --- hence you have to use the `text` "hack" that you show in the last `image` example. I would probably pass `xaxt = FALSE` to my `heatmap` call and then add the axis without labels and then add the labels using `text`, just as you do with `image`. – Gavin Simpson Mar 19 '13 at 16:56
  • @GavinSimpson The problem with this approach is that you have to manually define the order of labels on x-axis when you're clustering. Possible, but a bit painful. Still, thanks for pointing out to me that `heatmap()` is build using base graphics rather then grid (I thought it was grid like `pheatmap()`). – Geek On Acid Mar 19 '13 at 17:09
  • There is a solution to that - I have something working that I'm just writing up as an answer. It was a little more involved than I thought. Solution coming soon... – Gavin Simpson Mar 19 '13 at 17:12
  • 1
    +1 for an entertaining read as well as being a good question. – Simon O'Hanlon Mar 19 '13 at 17:50

6 Answers6

20

To fix pheatmap, all you really want to do is to go into pheatmap:::draw_colnames and tweak a couple of settings in its call to grid.text(). Here's one way to do that, using assignInNamespace(). (It may need additional adjustments, but you get the picture ;):

library(grid)     ## Need to attach (and not just load) grid package
library(pheatmap)

## Your data
d <- matrix(rnorm(25), 5, 5)
colnames(d) = paste("bip", 1:5, sep = "")
rownames(d) = paste("blob", 1:5, sep = "")

## Edit body of pheatmap:::draw_colnames, customizing it to your liking
draw_colnames_45 <- function (coln, ...) {
    m = length(coln)
    x = (1:m)/m - 1/2/m
    grid.text(coln, x = x, y = unit(0.96, "npc"), vjust = .5, 
        hjust = 1, rot = 45, gp = gpar(...)) ## Was 'hjust=0' and 'rot=270'
}

## For pheatmap_1.0.8 and later:
draw_colnames_45 <- function (coln, gaps, ...) {
    coord = pheatmap:::find_coordinates(length(coln), gaps)
    x = coord$coord - 0.5 * coord$size
    res = textGrob(coln, x = x, y = unit(1, "npc") - unit(3,"bigpts"), vjust = 0.5, hjust = 1, rot = 45, gp = gpar(...))
    return(res)}

## 'Overwrite' default draw_colnames with your own version 
assignInNamespace(x="draw_colnames", value="draw_colnames_45",
ns=asNamespace("pheatmap"))

## Try it out
pheatmap(d)

enter image description here

Josh O'Brien
  • 159,210
  • 26
  • 366
  • 455
  • 1
    Well, for you it's a small tweak, for me it's a big step. At the end of the day, you are the Master Of The Grid ;) Thanks Josh! – Geek On Acid Mar 19 '13 at 17:55
  • @GeekOnAcid -- Well, thanks as usual for the interesting question! In truth, this is the first time I've used `assignInNamespace()`, and both it and `pheatmap` are nice finds. I first did `trace(pheatmap:::draw_colnames, edit=TRUE)` to try a couple of things out, but once I found a fix, wanted something less interactive than that. Turns out `assignInNamespace()` is the ticket, and I'll be using it quite a bit going forward. Cheers. – Josh O'Brien Mar 19 '13 at 18:04
  • +1 The same thing could be done for the `heatmap` version too of course, but it is easier in that case to just run the plot call twice and use `add.expr`. – Gavin Simpson Mar 19 '13 at 19:16
  • 1
    Any idea how to increase margin if the labels are long and go off left side? – Colin D Feb 17 '17 at 23:33
  • 2
    This workaround is no longer necessary. The `angle_col` argument is supported in version 1.0.12 of pheatmap (see answer below for details). – Tom Kelly Feb 28 '19 at 06:33
14

The latest version of pheatmap (1.0.12) released on 2019-01-04 supports this with the angle_col argument.

#example data
d <- matrix(rnorm(25), 5, 5)
colnames(d) = paste("bip", 1:5, sep = "")
rownames(d) = paste("blob", 1:5, sep = "")

#update to latest version on CRAN
install.packages("pheatmap")
library("pheatmap")
pheatmap(d, angle_col = 45)

pretty heatmap with angled column labels

I've created a package on GitHub with an improved version of the heatmap.2 function. This supports adjusting the axis labels, including the srtCol argument which is passed to the axis function. It can be installed from: https://github.com/TomKellyGenetics/heatmap.2x

library("devtools")
install_github("TomKellyGenetics/heatmap.2x")
library("heatmap.2x")

heatmap.2x(d, scale = "none", trace = "none", col = heat.colors, srtCol = 45)

enhanced heatmap.2x with angled column labels

As of version 2.12.1 of gplots, the heatmap.2 function also supports the srtCol argument.

library("gplots")
heatmap.2(d, scale = "none", trace = "none", srtCol = 45)

heatmap.2 with angled column labels

Tom Kelly
  • 1,458
  • 17
  • 25
10

It is a little more complex than my comment presumed, because heatmap breaks up the plotting region in order to draw the dendrograms and the last plot region is not the image plot to which you want to attach the labels.

There is a solution though as heatmap provides the add.expr argument which takes an expression to be evaluated when the image is drawn. One also needs to know the reordering of the labels that takes place due to the dendrogram ordering. The last bit involves a bit of an inelegant hack as I will draw the heatmap first to get the reordering information and then use that to draw the heatmap properly with the angled labels.

First an example from ?heatmap

 x  <- as.matrix(mtcars)
 rc <- rainbow(nrow(x), start = 0, end = .3)
 cc <- rainbow(ncol(x), start = 0, end = .3)
 hv <- heatmap(x, col = cm.colors(256), scale = "column",
               RowSideColors = rc, ColSideColors = cc, margins = c(5,10),
               xlab = "specification variables", ylab =  "Car Models",
               main = "heatmap(<Mtcars data>, ..., scale = \"column\")")

At this stage, the labels aren't how we want them, but hv contains the information we need to reorder the colnames of mtcars in its component $colInd:

> hv$colInd
 [1]  2  9  8 11  6  5 10  7  1  4  3

You use this like you would the output from order e.g.:

> colnames(mtcars)[hv$colInd]
 [1] "cyl"  "am"   "vs"   "carb" "wt"   "drat" "gear" "qsec" "mpg"  "hp"  
[11] "disp"

Now use that to generate the labels we want in the correct order:

 labs <- colnames(mtcars)[hv$colInd]

Then we re-call heatmap but this time we specify labCol = "" to suppress the labelling of the column variables (using zero length strings). We also use a call to text to draw the labels at the desired angle. The call to text is:

text(x = seq_along(labs), y = -0.2, srt = 45, labels = labs, xpd = TRUE)

which is essentially what you have in your question. Play with the value of y as you need to adjust this to the length of the strings so as to have the labels not overlap with the image plot. We specify labels = labs to pass in the labels we want draw in the order required. The entire text call is passed to add.expr unquoted. Here is the entire call:

 hv <- heatmap(x, col = cm.colors(256), scale = "column",
               RowSideColors = rc, ColSideColors = cc, margins = c(5,10),
               xlab = "specification variables", ylab =  "Car Models",
               labCol = "",
               main = "heatmap(<Mtcars data>, ..., scale = \"column\")",
               add.expr = text(x = seq_along(labs), y = -0.2, srt = 45,
                               labels = labs, xpd = TRUE))

Which results in:

enter image description here

Gavin Simpson
  • 170,508
  • 25
  • 396
  • 453
  • Nice one. Thanks. Getting the labels position is crucial, so thanks for this solution, however 'crude' it is:) – Geek On Acid Mar 19 '13 at 17:27
  • Yeah, very nice. It was just last month I learned from you about `plot(..., panel.last)`, and now here is `heatmap(..., add.expr)`. Good reminders that I should keep a better eye out for handy arguments like those (or, maybe better, go scan some of your back posts for similar gems). – Josh O'Brien Mar 20 '13 at 03:01
8

I am also looking for method to rotate label text with heatmap. Eventually I have managed to find this solution:

library(gplots)

library(RColorBrewer)

heatmap.2(x,col=rev(brewer.pal(11,"Spectral")),cexRow=1,cexCol=1,margins=c(12,8),trace="none",srtCol=45)

The key argument is srtCol(or srtRow for row labels), which is used to rotate column labels in gplots.

jAC
  • 5,195
  • 6
  • 40
  • 55
  • Nope, when I use my example data with your solution it doesn't work. It gives me an error that `"srtCol" is not a graphical parameter`. – Geek On Acid Nov 18 '13 at 13:12
5

A solution using lattice::levelplot and latticeExtra::dendrogramGrob:

library(lattice)
library(latticeExtra)

The example data:

d <- matrix(rnorm(25), 5, 5)
colnames(d) = paste("bip", 1:5, sep = "")
rownames(d) = paste("blob", 1:5, sep = "")

You must define the dendrograms for rows and columns (computed internally in heatmap):

dd.row <- as.dendrogram(hclust(dist(d)))
row.ord <- order.dendrogram(dd.row)

dd.col <- as.dendrogram(hclust(dist(t(d))))
col.ord <- order.dendrogram(dd.col)

and pass them to the dendrogramGrob function in the legend argument of levelplot.

I have defined a new theme with colors from RColorBrewer, and modified the width and color of the cells borders with border and border.lwd:

myTheme <- custom.theme(region=brewer.pal(n=11, 'RdBu'))

levelplot(d[row.ord, col.ord],
          aspect = "fill", xlab='', ylab='',
          scales = list(x = list(rot = 45)),
          colorkey = list(space = "bottom"),
          par.settings=myTheme,
          border='black', border.lwd=.6,
          legend =
          list(right =
               list(fun = dendrogramGrob,
                    args =
                    list(x = dd.col, ord = col.ord,
                         side = "right",
                         size = 10)),
               top =
               list(fun = dendrogramGrob,
                    args =
                    list(x = dd.row,
                         side = "top"))))

levelplot with dendrogram

You can even use the shrink argument to scale the cells size proportional to their value.

levelplot(d[row.ord, col.ord],
          aspect = "fill", xlab='', ylab='',
          scales = list(x = list(rot = 45)),
          colorkey = list(space = "bottom"),
          par.settings=myTheme,
          border='black', border.lwd=.6,
          shrink=c(.75, .95),
          legend =
          list(right =
               list(fun = dendrogramGrob,
                    args =
                    list(x = dd.col, ord = col.ord,
                         side = "right",
                         size = 10)),
               top =
               list(fun = dendrogramGrob,
                    args =
                    list(x = dd.row,
                         side = "top"))))

levelplot with dendrogram and scaled cell sizes

Oscar Perpiñán
  • 4,491
  • 17
  • 28
3

I was able to take Gavin Simpson's answer and trimmed it down a bit to work for me for simple prototyping purposes, where data1 is the read.csv() object, and data1_matrix of course the matrix produced from that

heatmap(data_matrix, Rowv=NA, Colv=NA, col=heat.colors(64), scale='column', margins=c(5,10),
   labCol="", add.expr = text(x = seq_along(colnames(data1)), y=-0.2, srt=45, 
   labels=colnames(data1), xpd=TRUE))

Boom! Thanks Gavin.

A key bit for this to work is the part before the add.expr bit where he set the labCol to "", which is necessary to prevent the former (straight-down) labels from overlapping with the new 45 degree ones

boulder_ruby
  • 38,457
  • 9
  • 79
  • 100