tableGrob: set the height and width of a grid.table - r

I'm trying to make a function that will give me a plot ready for indesign, illustrator or inkscape. In trying to do so, I have 2 problems I cannot solve.
1) set the width and height of my plot (or just the grobTable):
The output I get is very small and when upscaling it in illustrator the font follows and become way to big. Therefore I want to make plots with manually defined widths and heights.
2) Sometimes the title, note and rownames gets "misplaced" (see the difference between plot 1 and 2 for details). It happens when the rownames are short.
library(gridExtra)
library(ggplot2)
data(diamonds)
## plot function
kryds.row <- function(x,y, p=100, decor="%", digits=3,
titel="", note="", red=219, green=55, blue= 153){
c <- table(x, y)
s <- as.character(sum(c))
s <- paste("Antal svarpersoner=", s, sep=" ")
j <- prop.table(c,1)
r <- c(rownames(j),"Total")
k <- c(colnames(j), "Total")
j <- addmargins(j, margin =2, FUN = sum)
j <- round(j, digits)
j[]<-paste(j*p, decor, sep=" ")
farve <- rgb(red,green,blue, maxColorValue =255)
table <- tableGrob(j,
cols = k,
gpar.coretext = gpar(fontsize = 12),
gpar.coltext = gpar(fontsize = 12,col="white"),
gpar.rowtext = gpar(fontsize = 12, fontface="bold"),
gpar.corefill = gpar(fill = rgb(255,255,255, maxColorValue =255), alpha = 1, col = NA),
gpar.rowfill = gpar(fill = rgb(255,255,255, maxColorValue =255), alpha = 1, col = NA),
gpar.colfill = gpar(fill = 0, alpha = 1 ,col= "white"),
equal.width = TRUE,
show.rownames = TRUE,
show.rsep = TRUE,
show.hlines = TRUE,
show.csep = FALSE,
show.vlines = FALSE,
show.box = FALSE,
padding.h = unit(15, "mm"),
padding.v = unit(8, "mm"),
core.just = "center",
row.just = "left",
separator = farve)
hh <- grobHeight(table)
ww <- grobWidth(table)
border <- roundrectGrob(x=0.5, y=0.5, width=ww, height=hh,
default.units="npc",
r=unit(0.1, "snpc"),
just="centre",
name=NULL, gp=gpar(col="white", fill=farve, vp=NULL))
border2 <- roundrectGrob(x=0.5, y=0.5, width=ww, height=hh,
default.units="npc",
r=unit(0.1, "snpc"),
just="centre",
name=NULL, gp=gpar(fill=NA, col=farve, vp=NULL))
title <- textGrob(titel,
x=unit(0.5,"npc") -0.5*ww + unit(5, "mm"),
y=unit(0.5,"npc") +0.5*hh + unit(2, "mm"),
vjust=0,hjust=0, gp=gpar(fontsize=12, fontface="bold"))
footnote <- textGrob(note,
x=unit(0.5,"npc") - 0.5*ww + unit(5,"mm"),
y=unit(0.5,"npc") - 0.5*hh,
vjust=1, hjust=0,gp=gpar( fontsize=10))
svarpersoner <- textGrob(s,
x=unit(0.5,"npc") + 0.5*ww -unit(5, "mm"),
y=unit(0.5,"npc") + 0.5*hh + unit(2, "mm"),
vjust=0, hjust=1,gp=gpar( fontsize=10))
grid.newpage()
gt <- gTree(children=gList(border,table,border2, title, footnote, svarpersoner))
grid.draw(gt)
}
# Plot it
kryds.row(diamonds$color, diamonds$cut, titel="title", note="note") # plot 1
kryds.row(diamonds$cut, diamonds$color, titel="title", note="note") # plot 2
# Problems
#1: The title, note and the j in the row.text is very badly placed in plot 1 but not plot 2
#2 I cannot set the width and height of my table
I have not cleaned up in my code yet, so please bare with it!

The current version of gridExtra::tableGrob doesn't let you set the widths/heights. You can however try a different (experimental) version of tableGrob built from scratch using gtable.
#library(devtools)
#install_github("tablegrob", "baptiste")
require(tablegrob)
d <- iris[sample(seq.int(nrow(iris)), 6),]
grid.newpage()
pushViewport(viewport(height=0.8,width=0.9))
g2 <- tableGrob(d, rows=NULL,
widths=unit(1,"null"), heights=unit(1/(nrow(d)),"npc"))
grid.draw(g2)
grid.roundrect(y=unit(0,"line"), height=unit(1,"npc") +unit(1,"lines"),
just="bottom", r=unit(0.05, "snpc"))
Edit (08/2015): you can now edit the widths/heights, since grid.table is now based on gtable.

The solution I went with was to manipulate the length of the levels like this:
longest <- 0
longestnumber <- 0
for (i in 1:length(levels(x))){
if (longest < nchar(levels(x))[i]){
longest <- nchar(levels(x))[i]
longestnumber <- i
}
}
for (i in 1:(100-longest)){
levels(x)[longestnumber] <- paste(levels(x)[longestnumber], " ", sep="")
}
That way I can control the width of the table

Related

grid arrange textGrob() and ggplots. Title and subtitle gridExtra

Basically, I want to add a title and subtitle to a grid.arrange() plot.
I have plot_list which is a list of 15 ggplots and
tg <- textGrob('Title', gp = gpar(fontsize = 13, fontface = 'bold'))
sg <- textGrob('Subtitle', gp = gpar(fontsize = 10))
But this doesn't work. I don't get any errors but tg and sg don't show up in the plot.
grid.arrange(tg, sg, grobs = plot_list, ncol = 3)
To be honest, I'm no expert in gridExtra and grid so any advice will be appreciated
Following changing multiple line title in multiplot ggplot2 using grid.arrange I could do what you asked for by creating two grids, first with only the plots and second with title, subtitle and the first grid. Using a synthetic plot_list:
df <- data.frame(v1 = rnorm(1000))
plot_list <- list()
for (i in 1:15) {
df[,ncol(df)+1] <- rnorm(1000)
names(df)[ncol(df)] <- paste0("V_",as.character(i))
local({
i <- i
plot_list[[i]] <<- ggplot(df) + geom_point(aes_string(x = "v1", y = paste0("V_",as.character(i))))
})
}
tg <- textGrob('Title', gp = gpar(fontsize = 13, fontface = 'bold'))
sg <- textGrob('Subtitle', gp = gpar(fontsize = 10))
margin <- unit(0.5, "line")
grided <- gridExtra::grid.arrange(grobs = plot_list, ncol = 3)
gridExtra::grid.arrange(tg, sg, grided,
heights = unit.c(grobHeight(tg) + 1.2*margin,
grobHeight(sg) + margin,
unit(1,"null")))
Hope this helps!
You need to combine tg, sg and your plots into a list. I would specify a layout matrix, which gives you a bit more control, and plot using grid.arrange:
First, we have tg,sg and I make a plot_list of 3 with mtcars.
library(ggplot2)
library(gridExtra)
library(grid)
tg <- textGrob('Title', gp = gpar(fontsize = 13, fontface = 'bold'))
sg <- textGrob('Subtitle', gp = gpar(fontsize = 10))
plot_list <- lapply(c("drat","wt","qsec"),function(i){
ggplot(mtcars,aes_string("mpg",i))+geom_point()
})
We combine your plots in a list:
g = c(list(tg),list(sg),plot_list)
So now tg is 1st element, sg is 2nd element and your plots are 3-5. We specify the layout:
N = length(plot_list)
laym = rbind(rep(1,N),rep(2,N),(3:(N+2)))
laym
[,1] [,2] [,3]
[1,] 1 1 1
[2,] 2 2 2
[3,] 3 4 5
This matrix will have the first one (tg), 1 takes up first row, sg 2nd row and your plots third row. If you have other kinds of arrangements or list, you can change this accordingly.
Now we plot, and specify the relative heights using heights=...
grid.arrange(grobs=g,layout_matrix=laym,heights=c(1,1,10))
the top argument can take any grob but it needs to know its height to be given the right space,
library(grid)
library(gridExtra)
lg <- replicate(12, ggplot2::ggplot(), simplify = FALSE)
tg <- textGrob('Title', gp = gpar(fontsize = 50, fontface = 'bold'))
sg <- textGrob('Subtitle', gp = gpar(fontsize = 10))
lt <- list(tg, sg)
heights <- do.call(unit.c, lapply(lt, function(.g) 1.5*grobHeight(.g)))
titles <- gtable::gtable_matrix('title',
grobs = matrix(lt, ncol=1),
widths = unit(1,'npc'),
heights = heights)
grobHeight.gtable <- function(g) sum(g$heights)
grid.arrange(grobs = lg, top = titles)

Difficulty positioning heatmap.2 components

I have been really struggling to position the components of my heatmap.2 output.
I found this old answer explaining how the element positioning worked from #IanSudbery which seemed really clear and I thought it had given me the understanding I need, but I'm still not grasping something.
I understand that the elements are all essentially put in a lattice of windows but they aren't behaving in a way I understand.
Here is my code and the current output (at the very bottom is the bit of interest which orders the figure elements):
for(i in 1:length(ConditionsAbbr)) {
# creates its own colour palette
my_palette <- colorRampPalette(c("snow", "yellow", "darkorange", "red"))(n = 399)
# (optional) defines the colour breaks manually for a "skewed" colour transition
col_breaks = c(seq(0,0.09,length=100), #white 'snow'
seq(0.1,0.19,length=100), # for yellow
seq(0.2,0.29,length=100), # for orange 'darkorange'
seq(0.3,1,length=100)) # for red
# creates a 5 x 5 inch image
png(paste(SourceDir, "Heatmap_", ConditionsAbbr[i], "XYZ.png"), # create PNG for the heat map
width = 5*600, # 5 x 600 pixels
height = 5*600,
res = 300, # 300 pixels per inch
pointsize = 8) # smaller font size
heatmap.2(ConditionsMtx[[ConditionsAbbr[i]]],
cellnote = ConditionsMtx[[ConditionsAbbr[i]]], # same data set for cell labels
main = paste(ConditionsAbbr[i], "XYZ"), # heat map title
notecol="black", # change font color of cell labels to black
density.info="none", # turns off density plot inside color legend
trace="none", # turns off trace lines inside the heat map
margins =c(12,9), # widens margins around plot
col=my_palette, # use on color palette defined earlier
breaks=col_breaks, # enable color transition at specified limits
dendrogram="none", # No dendogram
srtCol = 0 , #correct angle of label numbers
asp = 1 , #this overrides layout methinks and for some reason makes it square
adjCol = c(NA, -35) ,
adjRow = c(53, NA) ,
keysize = 1.2 ,
Colv = FALSE , #turn off column clustering
Rowv = FALSE , # turn off row clustering
key.xlab = paste("Correlation") ,
lmat = rbind( c(0, 3), c(2,1), c(0,4) ),
lhei = c(0.9, 4, 0.5) )
dev.off() # close the PNG device
}
This gives:
As you can see, the key is right of the matrix, there are huge amounts of white space between the matrix, the title above and key below, and it's not even as if the title and matrix are centred in the PNG?
I think to myself "well I'll just create a 3x3 that is easy to understand and edit" e.g.
| |
| | (3)
| |
--------------------------
| (1) |
(2) | Matrix |
| |
--------------------------
| (4) |
| Key |
| |
And then I can get rid of the white space so it's more like this.
| |(3)
------------------
| (1) |
(2)| Matrix |
| |
------------------
|(4) Key |
I do this using:
lmat = rbind( c(0, 0, 3), c(2, 1, 0), c(0, 4, 0) ),
lhei = c(0.9, 4, 0.5) ,
lwid = c(1, 4, 1))
This is what it looks like:
As great as it is to see my matrix in the centre, my key is still aligned to the right of my matrix and my title is taking the Silk Road East? Not to mention all the excess white space?
How do I get these to align and to all move together so the figure components fit snugly together?
EDIT: reducing my margins helped to reduce the whitespace but it's still excessive.
Here are the final changes I made to get my results, however, I would recommend using the advice of Maurits Evers if you aren't too invested in heatmap.2. Don't overlook the changes I made to the image dimensions.
# creates my own colour palette
my_palette <- colorRampPalette(c("snow", "yellow", "darkorange", "red"))(n = 399)
# (optional) defines the colour breaks manually for a "skewed" colour transition
col_breaks = c(seq(0,0.09,length=100), #white 'snow'
seq(0.1,0.19,length=100), # for yellow
seq(0.2,0.29,length=100), # for orange 'darkorange'
seq(0.3,1,length=100)) # for red
# creates an image
png(paste(SourceDir, "Heatmap_XYZ.png" )
# create PNG for the heat map
width = 5*580, # 5 x 580 pixels
height = 5*420, # 5 x 420 pixels
res = 300, # 300 pixels per inch
pointsize =11) # smaller font size
heatmap.2(ConditionsMtx[[ConditionsAbbr[i]]],
cellnote = ConditionsMtx[[ConditionsAbbr[i]]], # same data set for cell labels
main = "XYZ", # heat map title
notecol="black", # change font color of cell labels to black
density.info="none", # turns off density plot inside color legend
trace="none", # turns off trace lines inside the heat map
margins=c(0,0), # widens margins around plot
col=my_palette, # use on color palette defined earlier
breaks=col_breaks, # enable color transition at specified limits
dendrogram="none", # only draw a row dendrogram
srtCol = 0 , #correct angle of label numbers
asp = 1 , #this overrides layout methinks and for some reason makes it square
adjCol = c(NA, -38.3) , #shift column labels
adjRow = c(77.5, NA) , #shift row labels
keysize = 2 , #alter key size
Colv = FALSE , #turn off column clustering
Rowv = FALSE , # turn off row clustering
key.xlab = paste("Correlation") , #add label to key
cexRow = (1.8) , # alter row label font size
cexCol = (1.8) , # alter column label font size
notecex = (1.5) , # Alter cell font size
lmat = rbind( c(0, 3, 0), c(2, 1, 0), c(0, 4, 0) ) ,
lhei = c(0.43, 2.6, 0.6) , # Alter dimensions of display array cell heighs
lwid = c(0.6, 4, 0.6) , # Alter dimensions of display array cell widths
key.par=list(mar=c(4.5,0, 1.8,0) ) ) #tweak specific key paramters
dev.off()
Here is the output, which I will continue to refine until all spacing and font sizes suit my aesthetic preference. I would tell you exactly what I've done but I'm not 100% sure, frankly it all feels like it's held together with old gum and bailer twine, but don't kick a gift horse in the code, as they say.
I don't know if you're open to non-heatmap.2-based solutions. In my opinion ggplot offers greater flexibility and with a bit of tweaking you can reproduce a heatmap similar to the one you're showing quite comfortably while maximising plotting "real-estate" and avoiding excessive whitespace.
I'm happy to remove this post if you're only looking for heatmap.2 solutions.
That aside, a ggplot2 solution may look like this:
First off, let's generate some sample data
set.seed(2018)
df <- as_tibble(matrix(runif(7*10), ncol = 10), .name_repair = ~seq(1:10))
Prior to plotting we need to reshape df from wide to long
library(tidyverse)
df <- df %>%
rowid_to_column("row") %>%
gather(col, Correlation, -row) %>%
mutate(col = as.integer(col))
Then to plot
ggplot(df, aes(row, col, fill = Correlation)) +
geom_tile() +
scale_fill_gradientn(colours = my_palette) + # Use your custom colour palette
theme_void() + # Minimal theme
labs(title = "Main title") +
geom_text(aes(label = sprintf("%2.1f", Correlation)), size = 2) +
theme(
plot.title = element_text(hjust = 1), # Right-aligned text
legend.position="bottom") + # Legend at the bottom
guides(fill = guide_colourbar(
title.position = "bottom", # Legend title below bar
barwidth = 25, # Extend bar length
title.hjust = 0.5))
An example with multiple heatmaps in a grid layout via facet_wrap
First off, let's generate more complex data.
set.seed(2018)
df <- replicate(
4,
as_tibble(matrix(runif(7*10), ncol = 10), .name_repair = ~seq(1:10)), simplify = F) %>%
setNames(., paste("data", 1:4, sep = "")) %>%
map(~ .x %>% rowid_to_column("row") %>%
gather(col, Correlation, -row) %>%
mutate(col = as.integer(col))) %>%
bind_rows(.id = "data")
Then the plotting is identical to what we did before plus an additional facet_wrap(~data, ncol = 2) statement
ggplot(df, aes(row, col, fill = Correlation)) +
geom_tile() +
scale_fill_gradientn(colours = my_palette) + # Use your custom colour palette
theme_void() + # Minimal theme
labs(title = "Main title") +
geom_text(aes(label = sprintf("%2.1f", Correlation)), size = 2) +
facet_wrap(~ data, ncol = 2) +
theme(
plot.title = element_text(hjust = 1), # Right-aligned text
legend.position="bottom") + # Legend at the bottom
guides(fill = guide_colourbar(
title.position = "bottom", # Legend title below bar
barwidth = 25, # Extend bar length
title.hjust = 0.5))
One final update
I thought it'd be fun/interesting to see how far we can get towards a complex heatmap similar to the one you link to from the paper.
The sample data is included at the end, as this takes up a bit of space.
We first construct three different ggplot2 plot objects that show the main heatmap (gg3), an additional smaller heatmap with missing values (gg2), and a bar denoting group labels for every row (gg1).
gg3 <- ggplot(df.cor, aes(col, row, fill = Correlation)) +
geom_tile() +
scale_fill_distiller(palette = "RdYlBu") +
theme_void() +
labs(title = "Main title") +
geom_text(aes(label = sprintf("%2.1f", Correlation)), size = 2) +
scale_y_discrete(position = "right") +
theme(
plot.title = element_text(hjust = 1),
legend.position="bottom",
axis.text.y = element_text(color = "black", size = 10)) +
guides(fill = guide_colourbar(
title.position = "bottom",
barwidth = 10,
title.hjust = 0.5))
gg2 <- ggplot(df.flag, aes(col, row, fill = Correlation)) +
geom_tile(colour = "grey") +
scale_fill_distiller(palette = "RdYlBu", guide = F, na.value = "white") +
theme_void() +
scale_x_discrete(position = "top") +
theme(
axis.text.x = element_text(color = "black", size = 10, angle = 90, hjust = 1, vjust = 0.5))
gg1 <- ggplot(df.bar, aes(1, row, fill = grp)) +
geom_tile() +
scale_fill_manual(values = c("grp1" = "orange", "grp2" = "green")) +
theme_void() +
theme(legend.position = "left")
We can now use egg::ggarrange to position all three plots such that the y axis ranges are aligned.
library(egg)
ggarrange(gg1, gg2, gg3, ncol = 3, widths = c(0.1, 1, 3))
Sample data
library(tidyverse)
set.seed(2018)
nrow <- 7
ncol <- 20
df.cor <- matrix(runif(nrow * ncol, min = -1, max = 1), nrow = nrow) %>%
as_tibble(.name_repair = ~seq(1:ncol)) %>%
rowid_to_column("row") %>%
gather(col, Correlation, -row) %>%
mutate(
row = factor(
paste("row", row, sep = ""),
levels = paste("row", 1:nrow, sep = "")),
col = factor(
paste("col", col, sep = ""),
levels = paste("col", 1:ncol, sep = "")))
nrow <- 7
ncol <- 10
df.flag <- matrix(runif(nrow * ncol, min = -1, max = 1), nrow = nrow) %>%
as_tibble(.name_repair = ~seq(1:ncol)) %>%
rowid_to_column("row") %>%
gather(col, Correlation, -row) %>%
mutate(
row = factor(
paste("row", row, sep = ""),
levels = paste("row", 1:nrow, sep = "")),
col = factor(
paste("col", col, sep = ""),
levels = paste("col", 1:ncol, sep = ""))) %>%
mutate(Correlation = ifelse(abs(Correlation) < 0.5, NA, Correlation))
df.bar <- data.frame(
row = 1:nrow,
grp = paste("grp", c(rep(1, nrow - 3), rep(2, 3)), sep = "")) %>%
mutate(
row = factor(
paste("row", row, sep = ""),
levels = paste("row", 1:nrow, sep = "")))

Center-align legend title and legend keys in ggplot2 for long legend titles

I am having a hard time making the title of a legend center-aligned relative to the legend keys when the legend title is long. There is a question from a year ago that works for short titles, but it doesn't seem to work for long ones.
Example, first with a short legend title:
library(ggplot2)
ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width, color=Petal.Width)) + geom_point(size = 3) +
scale_color_distiller(palette = "YlGn", type = "seq", direction = -1,
name = "A") +
theme(legend.title.align = 0.5)
Everything is as expected, the legend title is centered above the legend key.
Now the same with a long legend title:
ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width, color=Petal.Width)) + geom_point(size = 3) +
scale_color_distiller(palette = "YlGn", type = "seq", direction = -1,
name = "Long legend heading\nShould be centered") +
theme(legend.title.align = 0.5)
We can see that the text is center aligned to itself but not relative to the legend key. I have tried modifying other theme options, such as legend.justification = "center", but none seem to move the key from its left-most position in the legend box.
A couple of comments:
I'm running the development version of ggplot2, v2.2.1.9000 from a few days ago.
I specifically need a solution for a continuous colorscale palette.
I hacked the source code similar to the way described by baptiste in one of the above comments: put the colour bar / label / ticks grobs into a child gtable, & position it to have the same row span / column span (depending on the legend's direction) as the title.
It's still a hack, but I'd like to think of it as a 'hack once for the whole session' approach, without having to repeat the steps manually for every plot.
Demonstration with different title widths / title positions / legend directions:
plot.demo <- function(title.width = 20,
title.position = "top",
legend.direction = "vertical"){
ggplot(iris,
aes(x=Sepal.Length, y=Sepal.Width, color=Petal.Width)) +
geom_point(size = 3) +
scale_color_distiller(palette = "YlGn",
name = stringr::str_wrap("Long legend heading should be centered",
width = title.width),
guide = guide_colourbar(title.position = title.position),
direction = -1) +
theme(legend.title.align = 0.5,
legend.direction = legend.direction)
}
cowplot::plot_grid(plot.demo(),
plot.demo(title.position = "left"),
plot.demo(title.position = "bottom"),
plot.demo(title.width = 10, title.position = "right"),
plot.demo(title.width = 50, legend.direction = "horizontal"),
plot.demo(title.width = 10, legend.direction = "horizontal"),
ncol = 2)
This works with multiple colourbar legends as well:
ggplot(iris,
aes(x=Sepal.Length, y=Sepal.Width,
color=Petal.Width, fill = Petal.Width)) +
geom_point(size = 3, shape = 21) +
scale_color_distiller(palette = "YlGn",
name = stringr::str_wrap("Long legend heading should be centered",
width = 20),
guide = guide_colourbar(title.position = "top"),
direction = -1) +
scale_fill_distiller(palette = "RdYlBu",
name = stringr::str_wrap("A different heading of different length",
width = 40),
direction = 1) +
theme(legend.title.align = 0.5,
legend.direction = "vertical",
legend.box.just = "center")
(Side note: legend.box.just = "center" is required to align the two legends properly. I was worried for a while since only "top", "bottom", "left", and "right" are currently listed as acceptable parameter values, but it turns out both "center" / "centre" are accepted as well, by the underlying grid::valid.just. I'm not sure why this isn't mentioned explicitly in the ?theme help file; nonetheless, it does work.)
To change the source code, run:
trace(ggplot2:::guide_gengrob.colorbar, edit = TRUE)
And change the last section of code from this:
gt <- gtable(widths = unit(widths, "cm"), heights = unit(heights,
"cm"))
... # omitted
gt
}
To this:
# create legend gtable & add background / legend title grobs as before (this part is unchanged)
gt <- gtable(widths = unit(widths, "cm"), heights = unit(heights, "cm"))
gt <- gtable_add_grob(gt, grob.background, name = "background",
clip = "off", t = 1, r = -1, b = -1, l = 1)
gt <- gtable_add_grob(gt, justify_grobs(grob.title, hjust = title.hjust,
vjust = title.vjust, int_angle = title.theme$angle,
debug = title.theme$debug), name = "title", clip = "off",
t = 1 + min(vps$title.row), r = 1 + max(vps$title.col),
b = 1 + max(vps$title.row), l = 1 + min(vps$title.col))
# create child gtable, using the same widths / heights as the original legend gtable
gt2 <- gtable(widths = unit(widths[1 + seq.int(min(range(vps$bar.col, vps$label.col)),
max(range(vps$bar.col, vps$label.col)))], "cm"),
heights = unit(heights[1 + seq.int(min(range(vps$bar.row, vps$label.row)),
max(range(vps$bar.row, vps$label.row)))], "cm"))
# shift cell positions to start from 1
vps2 <- vps[c("bar.row", "bar.col", "label.row", "label.col")]
vps2[c("bar.row", "label.row")] <- lapply(vps2[c("bar.row", "label.row")],
function(x) x - min(unlist(vps2[c("bar.row", "label.row")])) + 1)
vps2[c("bar.col", "label.col")] <- lapply(vps2[c("bar.col", "label.col")],
function(x) x - min(unlist(vps2[c("bar.col", "label.col")])) + 1)
# add bar / ticks / labels grobs to child gtable
gt2 <- gtable_add_grob(gt2, grob.bar, name = "bar", clip = "off",
t = min(vps2$bar.row), r = max(vps2$bar.col),
b = max(vps2$bar.row), l = min(vps2$bar.col))
gt2 <- gtable_add_grob(gt2, grob.ticks, name = "ticks", clip = "off",
t = min(vps2$bar.row), r = max(vps2$bar.col),
b = max(vps2$bar.row), l = min(vps2$bar.col))
gt2 <- gtable_add_grob(gt2, grob.label, name = "label", clip = "off",
t = min(vps2$label.row), r = max(vps2$label.col),
b = max(vps2$label.row), l = min(vps2$label.col))
# add child gtable back to original legend gtable, taking tlrb reference from the
# rowspan / colspan of the title grob if title grob spans multiple rows / columns.
gt <- gtable_add_grob(gt, justify_grobs(gt2, hjust = title.hjust,
vjust = title.vjust),
name = "bar.ticks.label", clip = "off",
t = 1 + ifelse(length(vps$title.row) == 1,
min(vps$bar.row, vps$label.row),
min(vps$title.row)),
b = 1 + ifelse(length(vps$title.row) == 1,
max(vps$bar.row, vps$label.row),
max(vps$title.row)),
r = 1 + ifelse(length(vps$title.col) == 1,
min(vps$bar.col, vps$label.col),
max(vps$title.col)),
l = 1 + ifelse(length(vps$title.col) == 1,
max(vps$bar.col, vps$label.col),
min(vps$title.col)))
gt
}
To reverse the change, run:
untrace(ggplot2:::guide_gengrob.colorbar)
Package version used: ggplot2 3.2.1.
Update Oct. 4, 2019:
A while back I wrote a fairly general function based on the original idea I posted here almost two years ago. The function is on github here but it's not part of any officially published package. It is defined as follows:
align_legend <- function(p, hjust = 0.5)
{
# extract legend
g <- cowplot::plot_to_gtable(p)
grobs <- g$grobs
legend_index <- which(sapply(grobs, function(x) x$name) == "guide-box")
legend <- grobs[[legend_index]]
# extract guides table
guides_index <- which(sapply(legend$grobs, function(x) x$name) == "layout")
# there can be multiple guides within one legend box
for (gi in guides_index) {
guides <- legend$grobs[[gi]]
# add extra column for spacing
# guides$width[5] is the extra spacing from the end of the legend text
# to the end of the legend title. If we instead distribute it by `hjust:(1-hjust)` on
# both sides, we get an aligned legend
spacing <- guides$width[5]
guides <- gtable::gtable_add_cols(guides, hjust*spacing, 1)
guides$widths[6] <- (1-hjust)*spacing
title_index <- guides$layout$name == "title"
guides$layout$l[title_index] <- 2
# reconstruct guides and write back
legend$grobs[[gi]] <- guides
}
# reconstruct legend and write back
g$grobs[[legend_index]] <- legend
g
}
The function is quite flexible and general. Here are a few examples of how it can be used:
library(ggplot2)
library(cowplot)
#>
#> ********************************************************
#> Note: As of version 1.0.0, cowplot does not change the
#> default ggplot2 theme anymore. To recover the previous
#> behavior, execute:
#> theme_set(theme_cowplot())
#> ********************************************************
library(colorspace)
# single legend
p <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Petal.Width)) + geom_point()
ggdraw(align_legend(p)) # centered
ggdraw(align_legend(p, hjust = 1)) # right aligned
# multiple legends
p2 <- ggplot(mtcars, aes(disp, mpg, fill = hp, shape = factor(cyl), size = wt)) +
geom_point(color = "white") +
scale_shape_manual(values = c(23, 24, 21), name = "cylinders") +
scale_fill_continuous_sequential(palette = "Emrld", name = "power (hp)", breaks = c(100, 200, 300)) +
xlab("displacement (cu. in.)") +
ylab("fuel efficiency (mpg)") +
guides(
shape = guide_legend(override.aes = list(size = 4, fill = "#329D84")),
size = guide_legend(
override.aes = list(shape = 21, fill = "#329D84"),
title = "weight (1000 lbs)")
) +
theme_half_open() + background_grid()
# works but maybe not the expected result
ggdraw(align_legend(p2))
# more sensible layout
ggdraw(align_legend(p2 + theme(legend.position = "top", legend.direction = "vertical")))
Created on 2019-10-04 by the reprex package (v0.3.0)
Original answer:
I found a solution. It requires some digging into the grob tree, and it may not work if there are multiple legends, but otherwise this seems a reasonable solution until something better comes along.
library(ggplot2)
library(gtable)
library(grid)
p <- ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width, color=Petal.Width)) +
geom_point(size = 3) +
scale_color_distiller(palette = "YlGn", type = "seq", direction = -1,
name = "Long legend heading\nShould be centered") +
theme(legend.title.align = 0.5)
# extract legend
g <- ggplotGrob(p)
grobs <- g$grobs
legend_index <- which(sapply(grobs, function(x) x$name) == "guide-box")
legend <- grobs[[legend_index]]
# extract guides table
guides_index <- which(sapply(legend$grobs, function(x) x$name) == "layout")
guides <- legend$grobs[[guides_index]]
# add extra column for spacing
# guides$width[5] is the extra spacing from the end of the legend text
# to the end of the legend title. If we instead distribute it 50:50 on
# both sides, we get a centered legend
guides <- gtable_add_cols(guides, 0.5*guides$width[5], 1)
guides$widths[6] <- guides$widths[2]
title_index <- guides$layout$name == "title"
guides$layout$l[title_index] <- 2
# reconstruct legend and write back
legend$grobs[[guides_index]] <- guides
g$grobs[[legend_index]] <- legend
grid.newpage()
grid.draw(g)
you'd have to change the source code. Currently it computes the widths for the title grob and the bar+labels, and left-justifies the bar+labels in the viewport (gtable). This is hard-coded.

A shared legend for z-scores and corresponding p-values in a heatmap

I have a z-scores matrix:
set.seed(1)
z.score.mat <- matrix(rnorm(1000),nrow=100,ncol=10)
which are the result of some biological experimental data, and a corresponding p-value matrix:
p.val.mat <- pnorm(abs(z.score.mat),lower.tail = F)
Both have identical dimnames:
rownames(z.score.mat) <- paste("p",1:100,sep="")
colnames(z.score.mat) <- paste("c",1:10,sep="")
rownames(p.val.mat) <- paste("p",1:100,sep="")
colnames(p.val.mat) <- paste("c",1:10,sep="")
I'm plotting a hierarchically clustered heatmap of the z-scores like this:
hc.col <- hclust(dist(z.score.mat))
dd.col <- as.dendrogram(hc.col)
col.ord <- order.dendrogram(dd.col)
hc.row <- hclust(dist(t(z.score.mat)))
dd.row <- as.dendrogram(hc.row)
row.ord <- order.dendrogram(dd.row)
clustered.mat <- z.score.mat[col.ord,row.ord]
clustered.mat.names <- attr(clustered.mat,"dimnames")
clustered.mat.df <- as.data.frame(clustered.mat)
colnames(clustered.mat.df) <- clustered.mat.names[[2]]
clustered.mat.df[,"process"] <- clustered.mat.names[[1]]
clustered.mat.df[,"process"] <- with(clustered.mat.df,factor(clustered.mat.df[,"process"],levels=clustered.mat.df[,"process"],ordered=TRUE))
require(reshape2)
clustered.mat.df <- reshape2::melt(clustered.mat.df,id.vars="process")
colnames(clustered.mat.df)[2:3] <- c("condition","z.score")
clustered.mat.df$p.value <- sapply(1:nrow(clustered.mat.df),function(x) p.val.mat[which(rownames(p.val.mat) == clustered.mat.df$process[x]),which(colnames(p.val.mat) == clustered.mat.df$condition[x])])
lab.legend <- colnames(clustered.mat.df)[3]
lab.row <- colnames(clustered.mat.df)[1]
lab.col <- colnames(clustered.mat.df)[2]
require(ggplot2)
ggplot(clustered.mat.df,aes(x=condition,y=process))+
geom_tile(aes(fill=z.score))+
scale_fill_gradient2(lab.legend,high="darkred",low="darkblue")+
theme_bw()+
theme(legend.key=element_blank(),
legend.position="right",
panel.border=element_blank(),
strip.background=element_blank(),
axis.text.x=element_text(angle=45,vjust=0.5)
)
My question is if it is possible, and how, to have on one side of the legend bar the z-score range (which is currently on the right hand) and on the other side the corresponding p-value range?
This is quite fiddly when the plot dimensions change, but you do get the required result:
br <- seq(-3, 3, 1)
lab <- round(pnorm(abs(br),lower.tail = F), 3)
p <- ggplot(clustered.mat.df,aes(x=condition,y=process))+
geom_tile(aes(fill=z.score), show.legend = FALSE)+
scale_fill_gradient2(lab.legend, high="darkred", low="darkblue", breaks = br)
p1 <- ggplot(clustered.mat.df,aes(x=condition,y=process))+
geom_tile(aes(fill=z.score))+
scale_fill_gradient2(lab.legend, high="darkred", low="darkblue", breaks = br) +
guides(fill = guide_colorbar(title = '', label.position = 'right', barheight = 10))
p2 <- ggplot(clustered.mat.df,aes(x=condition,y=process))+
geom_tile(aes(fill=z.score))+
scale_fill_gradient2(lab.legend, high="darkred", low="darkblue", breaks = br, labels = lab) +
guides(fill = guide_colorbar('', label.position = 'left', barheight = 10))
library(cowplot)
l1 <- get_legend(p1)
l2 <- get_legend(p2)
ggdraw() +
draw_plot(p, width = 0.85) +
draw_grob(l1, 0.89, 0, 0.1, 1) +
draw_grob(l2, 0.85, 0, 0.1, 1) +
draw_label('p z', 0.88, 0.675, hjust = 0)
This approach uses gtable and grid functions. It takes the legend from your plot, edits the legend so that the p values appear on the left side, then puts the edited legend back into the plot.
# Your data
set.seed(1)
z.score.mat <- matrix(rnorm(1000),nrow=100,ncol=10)
# which are the result of some biological experimental data, and a corresponding p-value matrix:
p.val.mat <- pnorm(abs(z.score.mat),lower.tail = F)
rownames(z.score.mat) <- paste("p",1:100,sep="")
colnames(z.score.mat) <- paste("c",1:10,sep="")
rownames(p.val.mat) <- paste("p",1:100,sep="")
colnames(p.val.mat) <- paste("c",1:10,sep="")
hc.col <- hclust(dist(z.score.mat))
dd.col <- as.dendrogram(hc.col)
col.ord <- order.dendrogram(dd.col)
hc.row <- hclust(dist(t(z.score.mat)))
dd.row <- as.dendrogram(hc.row)
row.ord <- order.dendrogram(dd.row)
clustered.mat <- z.score.mat[col.ord,row.ord]
clustered.mat.names <- attr(clustered.mat,"dimnames")
clustered.mat.df <- as.data.frame(clustered.mat)
colnames(clustered.mat.df) <- clustered.mat.names[[2]]
clustered.mat.df[,"process"] <- clustered.mat.names[[1]]
clustered.mat.df[,"process"] <- with(clustered.mat.df,factor(clustered.mat.df[,"process"],levels=clustered.mat.df[,"process"],ordered=TRUE))
require(reshape2)
clustered.mat.df <- reshape2::melt(clustered.mat.df,id.vars="process")
colnames(clustered.mat.df)[2:3] <- c("condition","z.score")
clustered.mat.df$p.value <- sapply(1:nrow(clustered.mat.df),function(x) p.val.mat[which(rownames(p.val.mat) == clustered.mat.df$process[x]),which(colnames(p.val.mat) == clustered.mat.df$condition[x])])
lab.legend <- colnames(clustered.mat.df)[3]
lab.row <- colnames(clustered.mat.df)[1]
lab.col <- colnames(clustered.mat.df)[2]
# Your plot
require(ggplot2)
p = ggplot(clustered.mat.df,aes(x=condition,y=process))+
geom_tile(aes(fill=z.score))+
scale_fill_gradient2(lab.legend,high="darkred",low="darkblue") +
theme_bw()+
theme(legend.key=element_blank(),
legend.position="right",
panel.border=element_blank(),
strip.background=element_blank(),
axis.text.x=element_text(angle=45,vjust=0.5))
library(gtable)
library(grid)
# Get the ggplot grob
g = ggplotGrob(p)
# Get the legend
index = which(g$layout$name == "guide-box")
leg = g$grobs[[index]]
# Get the legend labels
# and calculate corresponding p values
z.breaks = as.numeric(leg$grobs[[1]]$grobs[[3]]$label)
p.breaks = as.character(round(pnorm(abs(z.breaks), lower.tail = F), 3))
# Get the width of the longest p.break string, taking account of font and font size
w = lapply(na.omit(p.breaks), function(x) grobWidth(textGrob(x,
gp = gpar(fontsize = leg$grobs[[1]]$grobs[[3]]$gp$fontsize,
fontfamily = leg$grobs[[1]]$grobs[[3]]$gp$fontfamily))))
w = do.call(unit.pmax, w)
w = convertX(w, "mm")
# Add columns to the legend gtable to take p.breaks,
# setting the width of relevant column to w
leg$grobs[[1]] = gtable_add_cols(leg$grobs[[1]], leg$grobs[[1]]$widths[3], 1)
leg$grobs[[1]] = gtable_add_cols(leg$grobs[[1]], w, 1)
# Construct grob containing p.breaks
# Begin with the z.score grob, then make relevant changes
p.values = leg$grobs[[1]]$grobs[[3]]
p.values[c("label", "x", "hjust")] = list(p.breaks, unit(1, "npc"), 1)
# Put the p.values grob into the legend gtable
leg$grobs[[1]] = gtable_add_grob(leg$grobs[[1]], p.values, t=4, l=2,
name = "p.values", clip = "off")
# Put 'p' and 'z' labels into the legend gtable
leg$grobs[[1]] = gtable_add_grob(leg$grobs[[1]], list(textGrob("p"), textGrob("z")),
t=2, l=c(2,6), clip = "off")
# Drop the current legend title
leg$grobs[[1]]$grobs[[4]] = nullGrob()
# Put the legend back into the plot,
# and make sure the relevant column is wide enough to take the new legend
g$grobs[[index]] = leg
g$widths[8] = g$widths[8] + sum(leg$grobs[[1]]$widths[2:3])
# Draw the plot
grid.newpage()
grid.draw(g)
Not precisely what you described, but you could put both p values and z values into the same labels on one side of the legend:
z.breaks = c(-2,0,2)
p.breaks = pnorm(abs(z.breaks),lower.tail = F)
ggplot(clustered.mat.df,aes(x=condition,y=process)) +
geom_tile(aes(fill = z.score)) +
scale_fill_gradient2("z score (p value)", high="darkred",low="darkblue",
breaks = z.breaks,
labels = paste0(z.breaks, ' (p = ', round(p.breaks,2), ')') ) +
theme_bw() +
theme(legend.key = element_blank(),
legend.position = 'right',
panel.border = element_blank(),
strip.background = element_blank(),
axis.text.x=element_text(angle=45,vjust=0.5))

Axis labels for facet_grid of ggplot [duplicate]

Here is some minimal code to generate a graph with two sets of facets.
library("ggplot2", quietly = TRUE, warn.conflicts = FALSE)
library("RColorBrewer", quietly = TRUE, warn.conflicts = FALSE)
val.a <- rnorm(10)
val.b <- rnorm(10)
val.c <- c("A","B","A","A","B","B","B","B","A","B")
val.d <- c("D","D","E","D","E","E","E","D","D","E")
val.e <- rnorm(10)
maya <- data.frame(val.a,val.b,val.c,val.d,val.e)
ggplot(maya, aes(x=val.a, y=val.b)) +
geom_point(shape=20,size=3, aes(colour=val.e)) +
facet_grid(val.c~val.d) +
xlab("Leonardo") + ylab("Michaelangelo") +
scale_colour_gradientn(colours=brewer.pal(9,"YlGnBu"), name="Splinter")
I can't figure out how to add an overall facet label so that the names Donatello and Raphael are on the top and right hand side axis.
I saw some similar solutions on SO, but I can't make heads or tails of the code. Please would you suggest an answer to my conundrum?
Similar question here, but it fails for me if I have more than two facets. The labels show up somewhere inside the graph. Is there a way to make this work for the general case?
So I tried rawr's solution at the link above, and it ended up at the same place for multiple columns. Here's the code updated to rawr's solution, but it's producing the labels in unexpected (for me because I don't understand the solution) places.
library("ggplot2", quietly = TRUE, warn.conflicts = FALSE)
library("RColorBrewer", quietly = TRUE, warn.conflicts = FALSE)
val.a <- rnorm(20)
val.b <- rnorm(20)
val.c <- c("A","B","C","D","E","F","G","H","I","J")
val.d <- c("A","B","C","D","E","F","G","H","I","J")
val.e <- rnorm(20)
maya <- data.frame(val.a,val.b,val.c,val.d,val.e)
p <- ggplot(maya, aes(x=val.a, y=val.b)) + geom_point(shape=20,size=3, aes(colour=val.e)) + facet_grid(val.c~val.d) + xlab("Leonardo") + ylab("Michaelangelo") + scale_colour_gradientn(colours=brewer.pal(9,"YlGnBu"), name="Splinter")
z <- ggplotGrob(p)
library(grid)
library(gtable)
# add label for right strip
z <- gtable_add_cols(z, unit(z$width[[7]], 'cm'), 7)
z <- gtable_add_grob(z,
list(rectGrob(gp = gpar(col = NA, fill = gray(0.5))),
textGrob("Variable 1", rot = -90, gp = gpar(col = gray(1)))),
4, 8, 6, name = paste(runif(2)))
# add label for top strip
z <- gtable_add_rows(z, unit(z$heights[[3]], 'cm'), 2)
z <- gtable_add_grob(z,
list(rectGrob(gp = gpar(col = NA, fill = gray(0.5))),
textGrob("Variable 2", gp = gpar(col = gray(1)))),
3, 4, 3, 6, name = paste(runif(2)))
# add margins
z <- gtable_add_cols(z, unit(1/8, "line"), 7)
z <- gtable_add_rows(z, unit(1/8, "line"), 3)
# draw it
grid.newpage()
grid.draw(z)
Please would someone kindly point out to me the part of the code that's telling it how wide the general facet label should be?
This is fairly general. The current locations of the top and right strips are given in the layout data frame. This solution uses those locations to position the new strips. The new strips are constructed so that heights, widths, background colour, and font size and colour are the same as in current strips. There are some explanations below.
# Packages
library(ggplot2)
library(RColorBrewer)
library(grid)
library(gtable)
# Data
val.a <- rnorm(20)
val.b <- rnorm(20)
val.c <- c("A","B","C","D","E","F","G","H","I","J")
val.d <- c("A","B","C","D","E","F","G","H","I","J")
val.e <- rnorm(20)
maya <- data.frame(val.a,val.b,val.c,val.d,val.e)
# Base plot
p <- ggplot(maya, aes(x = val.a, y = val.b)) +
geom_point(shape = 20,size = 3, aes(colour = val.e)) +
facet_grid(val.c ~ val.d) +
xlab("Leonardo") + ylab("Michaelangelo") +
scale_colour_gradientn(colours = brewer.pal(9,"YlGnBu"), name = "Splinter")
# Labels
labelR = "Variable 1"
labelT = "Varibale 2"
# Get the ggplot grob
z <- ggplotGrob(p)
# Get the positions of the strips in the gtable: t = top, l = left, ...
posR <- subset(z$layout, grepl("strip-r", name), select = t:r)
posT <- subset(z$layout, grepl("strip-t", name), select = t:r)
# Add a new column to the right of current right strips,
# and a new row on top of current top strips
width <- z$widths[max(posR$r)] # width of current right strips
height <- z$heights[min(posT$t)] # height of current top strips
z <- gtable_add_cols(z, width, max(posR$r))
z <- gtable_add_rows(z, height, min(posT$t)-1)
# Construct the new strip grobs
stripR <- gTree(name = "Strip_right", children = gList(
rectGrob(gp = gpar(col = NA, fill = "grey85")),
textGrob(labelR, rot = -90, gp = gpar(fontsize = 8.8, col = "grey10"))))
stripT <- gTree(name = "Strip_top", children = gList(
rectGrob(gp = gpar(col = NA, fill = "grey85")),
textGrob(labelT, gp = gpar(fontsize = 8.8, col = "grey10"))))
# Position the grobs in the gtable
z <- gtable_add_grob(z, stripR, t = min(posR$t)+1, l = max(posR$r) + 1, b = max(posR$b)+1, name = "strip-right")
z <- gtable_add_grob(z, stripT, t = min(posT$t), l = min(posT$l), r = max(posT$r), name = "strip-top")
# Add small gaps between strips
z <- gtable_add_cols(z, unit(1/5, "line"), max(posR$r))
z <- gtable_add_rows(z, unit(1/5, "line"), min(posT$t))
# Draw it
grid.newpage()
grid.draw(z)

Resources