21

Introduction by @backlin

Multiple simple plots can combined as panels in a single figure by using layout or par(mfrow=...). However, more complex plots tend to setup their own panel layout internally disabling them from being used as panels. Is there a way to create a nested layout and encapsulating a complex plot into a single panel?

I have a feeling the grid package can accomplish this, e.g. by ploting the panels in separate viewports, but haven't been able to figure out how. Here is a toy example to demonstrate the problem:

my.plot <- function(){
    a <- matrix(rnorm(100), 10, 10)
    plot.new()
    par(mfrow=c(2,2))
    plot(1:10, runif(10))
    plot(hclust(dist(a)))
    barplot(apply(a, 2, mean))
    image(a)
}
layout(matrix(1:4, 2, 2))
for(i in 1:4) my.plot()
# How to avoid reseting the outer layout when calling `my.plot`?

Original question by @alittleboy

I use the heatmap.2 function in the gplots package to generate heatmaps. Here is a sample code for a single heatmap:

library(gplots)
row.scaled.expr <- matrix(sample(1:10000),nrow=1000,ncol=10)
heatmap.2(row.scaled.expr, dendrogram ='row',
          Colv=FALSE, col=greenred(800), 
          key=FALSE, keysize=1.0, symkey=FALSE, density.info='none',
          trace='none', colsep=1:10,
          sepcolor='white', sepwidth=0.05,
          scale="none",cexRow=0.2,cexCol=2,
          labCol = colnames(row.scaled.expr),                 
          hclustfun=function(c){hclust(c, method='mcquitty')},
          lmat=rbind( c(0, 3), c(2,1), c(0,4) ), lhei=c(0.25, 4, 0.25 ),                 
)

However, since I want to compare multiple heatmaps in a single plot, I use par(mfrow=c(2,2)) and then call heatmap.2 four times, i.e.

row.scaled.expr <- matrix(sample(1:10000),nrow=1000,ncol=10)
arr <- array(data=row.scaled.expr, dim=c(dim(row.scaled.expr),4))
par(mfrow=c(2,2))
for (i in 1:4)
heatmap.2(arr[ , ,i], dendrogram ='row',
          Colv=FALSE, col=greenred(800), 
          key=FALSE, keysize=1.0, symkey=FALSE, density.info='none',
          trace='none', colsep=1:10,
          sepcolor='white', sepwidth=0.05,
          scale="none",cexRow=0.2,cexCol=2,
          labCol = colnames(arr[ , ,i]),                 
          hclustfun=function(c){hclust(c, method='mcquitty')},
          lmat=rbind( c(0, 3), c(2,1), c(0,4) ), lhei=c(0.25, 4, 0.25 ),                 
)

However, the result is NOT four heatmaps in a single plot, but four separate heatmaps. In other words, if I use pdf() to output the result, the file is four pages instead of one. Do I need to change any parameters somewhere? Thank you so much!

Backlin
  • 14,612
  • 2
  • 49
  • 81
alittleboy
  • 10,616
  • 23
  • 67
  • 107
  • If you look at the code of `heatmap.2`, e.g. with `page(heatmap.2)`, you'll notice that it calls `plot.new()` which overrides your call to `par(mfrow=c(2,2))`. I tried to use the `grid` engine to confine each `heatmap.2` plot in a subsection of the plot area, but didn't figure out how to do it. – Backlin Oct 26 '12 at 12:25
  • 1
    This problem has occurred to me before with other functions and I have also struggled with it. Would you mind if I rephrase your question and add a more general (but short) introduction to it? – Backlin Oct 26 '12 at 12:27
  • 1
    I've done this with the regular heatmap() function by commenting a section of the function out and then using layout(), but it's kind of ugly. – mmarchin Oct 26 '12 at 15:54
  • @Backlin: thank you so much for the comments! Sure, I appreciate if you could rephrase my question and add introduction to the topic :) – alittleboy Oct 26 '12 at 18:31
  • 1
    I was away during the weekend, but let's hope someone picks up on it now. – Backlin Oct 29 '12 at 08:43
  • @Backlin: thanks again! hopefully someone can help on this topic (using the grid or ggplot2 packages) – alittleboy Oct 29 '12 at 17:40
  • How about `gridExtra:::grid.arrange`? See http://stackoverflow.com/questions/5226807/multiple-graphs-in-one-canvas-using-ggplot2 – Roman Luštrik Dec 13 '12 at 09:59

4 Answers4

20

Okay. I suppose this question has been sitting unanswered for enough time that the long answer should be written up.

The answer to most difficult graphics issues is (as @backlin suggests) the raw use of the 'grid' package. Many prebuilt graphics packages override all current viewports and plot device settings, so if you want something done a very specific way, you have to build it yourself.

I recommend picking up Paul Murrell's book "R Graphics" and going over the chapter on the 'grid' package. It's a crazy useful book, and a copy sits on my desk all the time.

For your heatmap, I've written up a quick primer that will get you started quickly.

Functions to know

  • grid.newpage() This initializes the plotting device. Use it without parameters.
  • grid.rect() This draws a rectangle. Your heatmap is basically just a giant set of colored rectangles, so this will be bulk of your graphic. It works like so: grid.rect(x=x_Position, y=y_Position, width=width_Value, height=height_Value, gp=gpar(col=section_Color, fill=section_Color), just=c("left", "bottom"), default.units="native") The 'just' argument specifies which point of the rectangle will sit on your specified (x, y) coordinates.
  • grid.text() This draws text. It works like so: grid.text("Label Text", x_Value, y_Value, gp=gpar(col=color_Value, cex=font_Size), just=c("right","center"), rot=rot_Degrees, default.units="native")
  • grid.lines() This draws a line. It works like so: grid.lines(c(x_Start,x_End), c(y_Start, y_End), gp=gpar(col=color_Value), default.units="native")
  • dataViewport() This defines the attributes of a plotting window, which 'grid' refers to as a "viewport." Use it like so: pushViewport(dataViewport(xData=x_Data, yData=y_Data, xscale=c(x_Min, x_Max), yscale=c(y_Min, y_Max), x=x_Value, y=y_Value, width=width_Value, height=height_Value, just=c("left","center"))) There is some stuff to keep in mind here... see the more detailed explanation of viewports.
  • pushViewport() This is used to initialize a veiwport. You wrap this around a viewport definition to actually execute the viewport, like so: pushViewport(dataViewport([stuff in here]))
  • popViewport() This finalizes a viewport and moves you up one level in the hierarchy of viewports. See the more detailed explanation of viewports.

Viewports in a nutshell

Viewports are temporary drawing spaces that define where and how 'grid' objects will be drawn. Everything inside the viewport is drawn relative to the viewport. If the viewport is rotated, everything inside will be rotated. Viewports can be nested, can overlap, and are almost infinitely flexible, with one exception: they are always a rectangle.

Something that messes a lot of people up initially is the coordinate system. Every viewport, including the initial 'grid.newpage()' viewport, goes from 0 to 1 on both the x and y axes. The origin (0,0) is the far lower left corner, and the max (1,1) is the far upper right corner. This is the "npc" unit system, and everything that doesn't have a set of units specified will likely end up being drawn according to this system. This means two things for you:

  1. Use the "npc" system when specifying viewport sizes and locations. Just assume that your viewports have to use the "npc" coordinates, and you'll save yourself a LOT of hassle. This means if I want to draw two plots next to each other, the definitions for the two viewports would look something like:
    • viewport(x=0, y=0, width=0.5, height=1, just=c("left","lower")) and
    • viewport(x=0.5, y=0, width=0.5, height=1, just=c("left","lower"))
  2. If your viewport has a different coordinate system (for example a viewport for plotting a graph), then you will need to specify the 'default.units' argument for every 'grid' object you draw. For instance, if you tried to plot a point at (2,4) you would never see the point, because it would be far off-screen. Specifying default.units="native" would tell that point to use the viewport's own coordinate system and would draw the point correctly.

Viewports can be navigated and written to directly, but unless you're doing something very automated, it is easier to specify a viewport, draw inside it, and then "pop" (finalize) the viewport. This returns you to the parent viewport, and you can start on the next viewport. Popping each viewport is a clutter-free approach and will suit most purposes (and make it easier to debug!).

The 'dataViewport' function is all important when plotting a graph. This is a special type of viewport that handles all of the coordinates and scales for you, as long as you tell it what data you are using. This is the one I use for any plotting area. When I first started using the 'grid' package, I adjusted all of my values to fit the "npc" coordinate system, but that was a mistake! The 'dataViewport' function makes is all easy as long as you remember to use the "native" units for each drawing item.

Disclaimer

Data visualization is my forte, and I don't mind spending half a day scripting up a good visual. The 'grid' package allows me to create quite sophisticated visuals faster than anything else I found. I script up my visuals as functions, so I can load various data quickly. I couldn't be happier.

However, if you don't like to script things, 'grid' will be your enemy. Also, if you consider half a day to be too much time for a visual, then 'grid' won't help you too much. The (in)famous 'ggplot2' package is what most people settle on, and I heartily recommend it, even though I don't personally find it useful.

If someone wants help learning 'grid' graphics, I'm more than willing to help teach. It has completely revolutionized my ability to create fast, intelligent, and good-looking data visuals.

Dinre
  • 4,196
  • 17
  • 26
  • @Dinre: Why is it that you don't find 'ggplot2' useful? So far, (about 1 year in) I haven't found it limiting in my work (economics and information systems). Would be glad to read your thoughts! – Peter Lustig Dec 29 '13 at 12:34
  • @PeterLustig The 'ggplot2' package is strictly bound to the shape of the data you feed it, and it has some particulars about how the layers can be rendered. I have a tendency to combine data displays in ways that 'ggplot2' doesn't allow (see http://stackoverflow.com/questions/15043172/arrangement-of-large-number-of-plots-and-connect-with-lines-in-r/15170021#15170021), so I need the lower-level access that 'grid' provides. In fact, 'ggplot2' is just a set of wrapper functions for the 'grid' functions. – Dinre Aug 04 '14 at 19:57
  • @Dinre Thanks for the link to your other write-up on the grid package. I will have a look at it. – Peter Lustig Aug 05 '14 at 07:00
11

The gridGraphics package might help,

enter image description here

library(gridGraphics)
library(grid)

grab_grob <- function(){
  grid.echo()
  grid.grab()
}

arr <- replicate(4, matrix(sample(1:100),nrow=10,ncol=10), simplify = FALSE)

library(gplots)
gl <- lapply(1:4, function(i){
  heatmap.2(arr[[i]], dendrogram ='row',
            Colv=FALSE, col=greenred(800), 
            key=FALSE, keysize=1.0, symkey=FALSE, density.info='none',
            trace='none', colsep=1:10,
            sepcolor='white', sepwidth=0.05,
            scale="none",cexRow=0.2,cexCol=2,
            labCol = colnames(arr[[i]]),                 
            hclustfun=function(c){hclust(c, method='mcquitty')},
            lmat=rbind( c(0, 3), c(2,1), c(0,4) ), lhei=c(0.25, 4, 0.25 ),                 
  )
  grab_grob()
})

grid.newpage()
library(gridExtra)
grid.arrange(grobs=gl, ncol=2, clip=TRUE)
baptiste
  • 75,767
  • 19
  • 198
  • 294
  • 3
    This is an elegant solution that allows one to continue to use packages such as `pheatmap` as is. BTW, to use pheatmap, one does not need the call to `grid.echo()` since `pheatmap` already uses the grid package. – Lance Jun 29 '16 at 14:42
  • how can you add padding between grobs with this solution? can't do it with normal way with ggplots – Brian Wiley Sep 30 '20 at 12:56
2

I struggled with a similar problem and came up with a solution that is very easy but requires imagemagick installed. The idea is to plot the heatmaps to separate files and then combine them with the montage command:

library(gplots)
row.scaled.expr <- matrix(sample(1:10000),nrow=1000,ncol=10)
arr <- array(data=row.scaled.expr, dim=c(dim(row.scaled.expr),4))
par(mfrow=c(2,2))
for (i in 1:4) {
    ifile <- paste0(i,'_heatmap.pdf')
    pdf(ifile)
    heatmap.2(arr[ , ,i], dendrogram ='row',
                        Colv=FALSE, col=greenred(800), 
                        key=FALSE, keysize=1.0, symkey=FALSE, density.info='none',
                        trace='none', colsep=1:10,
                        sepcolor='white', sepwidth=0.05,
                        scale="none",cexRow=0.2,cexCol=2,
                        labCol = colnames(arr[ , ,i]),                 
                        hclustfun=function(c){hclust(c, method='mcquitty')},
                        lmat=rbind( c(0, 3), c(2,1), c(0,4) ), lhei=c(0.25, 4, 0.25 ),                 
    )
    dev.off()
}
system('montage -geometry 100% -tile 2x2 ./*_heatmap.pdf outfile.pdf')
chakalakka
  • 464
  • 2
  • 9
2

Just as Dinre said, the "grid" pacakge can handle all complex plots. For the original question by @alittleboy, I think the package "ComplexHeatmap" (which is also base on grid) from Bionconductor can be a nice solution (http://www.bioconductor.org/packages/release/bioc/vignettes/ComplexHeatmap/inst/doc/ComplexHeatmap.html)

japrin
  • 21
  • 2