6

I have arranged two plots: a line chart on top and a heatmap below.

I want the heatmap legend to have the same height as the plot area of the heatmap, i.e. the same length as the y-axis. I know that I can change the height and size of the legend using theme(legend.key.height = unit(...)), but this would take many trial and errors before I find an adequate setting.

Is there a way to specify the height of the legend so that it is exactly the same height of the plot area of the heatmap and would retain that ratio when plotting to a pdf?

A reproducible example with code I have tried:

#Create some test data
pp <- function (n, r = 4) {
  x <- seq(1:100)
  df <- expand.grid(x = x, y = 1:10)
  df$z <- df$x*df$y
  df
}
testD <- pp(20)
#Define groups
colbreaks <- seq(min(testD[ , 3]), max(testD[ , 3] + 1), length = 5)
library(Hmisc)
testD$group <- cut2(testD[ , 3], cuts = c(colbreaks))
detach(package:Hmisc, unload = TRUE)

#Create data for the top plot
testD_agg <- aggregate(.~ x, data=testD[ , c(1, 3)], FUN = sum)

#Bottom plot (heatmap)
library(ggplot2)
library(gtable)

p <- ggplot(testD, aes(x = x, y = y)) +
  geom_tile(aes(fill = group)) +
  scale_fill_manual(values = c("red", "orange", "yellow", "lightgreen")) +
  coord_cartesian(xlim = c(0, 100), ylim = c(0.5, 10.5)) +

  theme_bw() +
  theme(legend.position = "right",
        legend.key = element_blank(),
        legend.text = element_text(colour = "black", size = 12),
        legend.title = element_blank(),
        axis.text.x = element_text(size = 12, angle = 45, vjust = +0.5),
        axis.text.y = element_text(size = 12),
        axis.title = element_text(size = 14),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        plot.margin = unit(c(0, 0, 0, 0), "line"))

#Top plot (line)
p2 <- ggplot(testD_agg, aes(x = x, y = z)) +
  geom_line() +
  xlab(NULL) +
  coord_cartesian(xlim = c(0, 100), ylim = c(0, max(testD_agg$z))) +

  theme_bw() +
  theme(legend.position = "none",
        legend.key = element_blank(),
        legend.text = element_text(colour = "black", size = 12),
        legend.title = element_text(size = 12, face = "plain"),
        axis.text.x = element_blank(),
        axis.text.y = element_text(size = 12),
        axis.title = element_text(size = 14),
        axis.ticks.x = element_blank(),
        panel.grid.major = element_blank(),
        panel.grid.minor = element_blank(),
        plot.margin = unit(c(0.5, 0.5, 0, 0), "line"))

#Create gtables
gp <- ggplotGrob(p) 
gp2 <- ggplotGrob(p2)

#Add space to the right of the top plot with width equal to the legend of the bottomplot 
legend.width <- gp$widths[7:8] #obtain the width of the legend in pff2
gp2 <- gtable_add_cols(gp2, legend.width, 4) #add a colum to pff with with legend.with
#combine the plots
cg <- rbind(gp2, gp, size = "last")
#set the ratio of the plots
panels <- cg$layout$t[grep("panel", cg$layout$name)]
cg$heights[panels] <- unit(c(2,3), "null")
#remove white spacing between plots
cg <- gtable_add_rows(cg, unit(0, "npc"), pos = nrow(gp))

pdf("test.pdf", width = 8, height = 7)
print(grid.draw(cg))
dev.off()

#The following did not help solve my problem but I think I got close
old.height <- cg$grobs[[16]]$heights[2]
#It seems the height of the legend is given in "mm", change to "npc"?
gp$grobs[[8]]$grobs[[1]]$heights <- c(rep(unit(0, "npc"), 3), rep(unit(1/4, "npc"), 4), rep(unit(0, "mm"),1))
#this does allow for adjustment of the heights but not the exact control I need.

My actual data has some more categories, but the gist is the same. Here is an image produced with the code above and annotated with what I would like to do.

Thanks in advance! Maarten

Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122

1 Answers1

5

It seems there are two sets of heights that need adjustment: the heights of the legend keys, and the overall height of the legend. Picking up from your cg grob, I extract the legend, make the adjustments to the heights, then insert the legend grob back into the layout.

leg = gtable_filter(cg, "guide-box")

library(grid)

# Legend keys
leg[[1]][[1]][[1]][[1]]$heights = unit.c(rep(unit(0, "mm"), 3),
                                         rep(unit(1/4, "npc"), 4),
                                         unit(0, "mm"))

# Legend
leg[[1]][[1]]$heights[[3]] = sum(rep(unit(0, "mm"), 3),
                                 rep(unit(1/4, "npc"), 4),
                                 unit(0, "mm"))

# grid.draw(leg)  # Check that heights are correct

cg.new = gtable_add_grob(cg, leg, t = 17, l = 8)

grid.newpage()
grid.draw(cg.new)

enter image description here

Sandy Muspratt
  • 31,719
  • 12
  • 116
  • 122
  • This works great! Exactly what I was looking for. I did add a section to create a new heatmap without a legend and adjust it's widths as in my example code before adding the legend grob. – SomeScientist Apr 21 '15 at 09:44
  • @Sandy Muspratt Unfortunately I can't reproduce your result. I get two legends on the bottom plot, none of which have the size of the plot. Are there breaking updates to any of the packages maybe? – Henrik Feb 27 '16 at 20:04
  • 1
    @Henrik The problem lies with different versions of ggplot2. I've edited the code so that it runs in ver 2. – Sandy Muspratt Feb 27 '16 at 20:55
  • @SandyMuspratt Thank you for your rapid reply. It works for me as well. Do you mind to elaborate to an ignorant like me how you pick the `t` and `l` values? Similartly, I would appreciate if you could point to a relevant text for the wizardry in the two preceeding code chunks, where you set the heights of the legend. Cheers. – Henrik Feb 27 '16 at 21:05
  • 1
    @Henrik `gtable_shouw_layout(cg)` gives a diagram of the layout; or `cg$layout` presents the layout as a table. t, l, b, r refer to top, left, bottom, right extents of cells in the layout. The location of the legend is t = b = 9; l = r = 5. When t = l (and l = r) there's no need to set b (or r). – Sandy Muspratt Feb 27 '16 at 21:11
  • There is not much other than SO answers to guide you. `str(leg)`, `str(leg[[1]]`, etc is tedious but helpful, but they generate a lot of output. And knowing a little of the structure of ggplots. – Sandy Muspratt Feb 27 '16 at 21:22
  • That should have been: `gtable_show_layout()`, and "when t = b". – Sandy Muspratt Feb 27 '16 at 21:24
  • And of course, `leg[[1]][[1]][[1]][[1]]$heights` and `leg[[1]][[1]]$heights` give the two sets of heights. – Sandy Muspratt Feb 27 '16 at 21:27
  • FYI, have a look at [this answer](http://stackoverflow.com/questions/19214914/how-can-i-make-the-legend-in-ggplot2-the-same-height-as-my-plot/29783981#29783981). Similar question, but the structure of the legend is different. – Sandy Muspratt Feb 27 '16 at 21:33
  • 1
    @SandyMuspratt Thanks a lot for your kind help. I will walk through your nice answers step by step. – Henrik Feb 27 '16 at 21:48