I built a function for quickly plotting a table with a lot of help from this answer from #baptiste.
plotTable<-function(data, title=NULL, footnote=NULL, fontsize=9, plotIt=TRUE, show.rownames=TRUE){
# Generic method to plot tabular data
# Built the base table with/without row names
if(show.rownames){
table <- tableGrob(data, theme=ttheme_default(
core=list(fg_params=list(fontsize=fontsize)),
colhead=list(fg_params=list(fontsize=fontsize)),
rowhead=list(fg_params=list(fontsize=fontsize))))
} else{
table <- tableGrob(data, theme=ttheme_default(
core=list(fg_params=list(fontsize=fontsize)),
colhead=list(fg_params=list(fontsize=fontsize)),
rowhead=list(fg_params=list(fontsize=fontsize))), rows=NULL)
}
# Set the padding
padding <- unit(0.5,"line")
# Add the title if it's not NULL
if(!is.null(title)){
title.grob <- textGrob(title, gp=gpar(fontsize=fontsize+3))
table <- gtable_add_rows(table, heights = grobHeight(title.grob) + padding, pos = 0)
table <- gtable_add_grob(table, list(title.grob), t=1, l=1, r=ncol(table))
}
# Add the footnote if it's not NULL
if(!is.null(footnote)){
footnote.grob <- textGrob(footnote, x=0, hjust=0, gp=gpar(fontsize=fontsize, fontface="italic"))
table <- gtable_add_rows(table, heights = grobHeight(footnote.grob)+ padding)
table <- gtable_add_grob(table, list(footnote.grob), t=nrow(table), l=1, r=ncol(table))
}
# Either plot it or return the grob
if(plotIt) grid.arrange(table) else return(table)
}
But sometimes my title is longer than the actual table and it's getting cut off.
libs <- c("data.table", "grid", "gridExtra", "gtable")
lapply(libs, library, character.only = TRUE)
mytable <- data.table(x=c(1,2,3), y=c(3,2,1))
plotTable(mytable, title="Hello World")
How do I fix this?
You can set the table widths manually, but you'll need to decide which column(s) should be expanded and by how much. I guess a reasonable way to redistribute the widths is to add the same margin to each column, so that the total space gained accounts for the extra room needed for the title.
library(gridExtra)
d <- head(iris[,1:2])
table <- tableGrob(d, rows=NULL)
library(grid)
library(gtable)
title <- textGrob("my long title goes here",gp=gpar(fontsize=30))
padding <- unit(1,"line")
table <- gtable_add_rows(table,
heights = grobHeight(title) + padding,
pos = 0)
table <- gtable_add_grob(table, title,
t=1, l=1,
r=ncol(table))
# check whether the table width is smaller than the title width
missed <- convertWidth(sum(table$widths), "in", valueOnly = TRUE) -
convertWidth(grobWidth(title), "in", valueOnly = TRUE)
if(missed < 0 ) # need to do something about it
table$widths <- table$widths + unit(abs(missed)/ncol(table), "in")
grid.newpage()
grid.draw(table)
another option, if you don't want the cells to be resized, is to turn clipping off,
table$layout$clip <- "off"
Related
I'm a bit stuck on this issue. I have this data obtained from a likert survey (so I make everything a factor):
df1<-data.frame(A=c(1,2,2,3,4,5,1,1,2,3),
B=c(4,4,2,3,4,2,1,5,2,2),
C=c(3,3,3,3,4,2,5,1,2,3),
D=c(1,2,5,5,5,4,5,5,2,3),
E=c(1,4,2,3,4,2,5,1,2,3),
dummy1=c("yes","yes","no","no","no","no","yes","no","yes","yes"),
dummy2=c("high","low","low","low","high","high","high","low","low","high"))
df1[colnames(df1)] <- lapply(df1[colnames(df1)], factor)
I then create a list of dataframes to be used in each plot:
vals <- colnames(df1)[1:5]
dummies <- colnames(df1)[-(1:5)]
step1 <- lapply(dummies, function(x) df1[, c(vals, x)])
step2 <- lapply(step1, function(x) split(x, x[, 6]))
names(step2) <- dummies
tbls <- unlist(step2, recursive=FALSE)
tbls<-lapply(tbls, function(x) x[(names(x) %in% names(df1[c(1:5)]))])
This is the plotting function I made (I used the likert package)
plot_likert <- function(x){
y<-deparse(substitute(x))
y<-sub("\\$", " - ",y)
p<-plot(likert(x),
type ="bar",center=3,
group.order=names(x))+
labs(x = "Theme", subtitle=paste("Number of observations:",nrow(x)))+
guides(fill=guide_legend("Rank"))+
ggtitle(paste("How do they rank? -",gsub("\\.",": ",y)))
png(filename=paste("Ranking -",y,".png"), width = 3000, height = 2000, res=300)
print(p)
dev.off()
}
So that now I can make the plot by writing:
plot_likert(tbls$dummy1.no)
Finally, I apply the function over the whole table by using
lapply(tbls,function(x) {
y<-deparse(substitute(x))
y<-sub("\\$", " - ",y)
plot(likert(x),
type ="bar",center=3,
group.order=names(x))+
labs(x = "Theme", subtitle=paste("Number of observations:",nrow(x)))+
guides(fill=guide_legend("Rank"))+
ggtitle(paste("How do these themes rank? -",gsub("\\.",": ",y)))
}) -> list_plots
But now I don't know how to save each graph in the list as a separate .png file! I managed to put everything in a pdf like this, but it's not what I actually want:
ggsave(
filename = "plots.pdf",
plot = marrangeGrob(list_plots, nrow=1, ncol=1),
width = 15, height = 9
)
Do you have any suggestions on how to fix this? Also, if you have anything to add about my function/procedure overall, everything is welcome! I'm still quite new to R.
Thanks in advance
we can use:
sapply(1:length(list_plots), function(i) ggsave(
filename = paste0("plots ",i,".pdf"),
plot = list_plots[[i]],
width = 15, height = 9
))
For names: see https://stackoverflow.com/a/73370416/5224236
mynames <- sapply(names(tbls), function(x) {
paste("How do they rank? -",gsub("\\.",": ",x))
})
myfilenames <- names(tbls)
plot_likert <- function(x, myname, myfilename){
p <- plot(likert(x),
type ="bar",center=3,
group.order=names(x))+
labs(x = "Theme", subtitle=paste("Number of observations:",nrow(x)))+
guides(fill=guide_legend("Rank"))+
ggtitle(myname)
p
}
list_plots <- lapply(1:length(tbls),function(i) {
plot_likert(tbls[[i]], mynames[i], myfilenames[i])
})
I was wondering if anyone knows of a package that allows partial row labeling of heatmaps. I am currently using pheatmap() to construct my heatmaps, but I can use any package that has this functionality.
I have plots with many rows of differentially expressed genes and I would like to label a subset of them. There are two main things to consider (that I can think of):
The placement of the text annotation depends on the height of the row. If the rows are too narrow, then the text label will be ambiguous without some sort of pointer.
If multiple adjacent rows are significant (i.e. will be labelled), then these will need to be offset, and again, a pointer will be needed.
Below is an example of a partial solution that really only gets maybe halfway there, but I hope illustrates what I'd like to be able to do.
set.seed(1)
require(pheatmap)
require(RColorBrewer)
require(grid)
### Data to plot
data_mat <- matrix(sample(1:10000, 300), nrow = 50, ncol = 6)
rownames(data_mat) <- paste0("Gene", 1:50)
colnames(data_mat) <- c(paste0("A", 1:3), paste0("B", 1:3))
### Set how many genes to annotate
### TRUE - make enough labels that some overlap
### FALSE - no overlap
tooMany <- T
### Select a few genes to annotate
if (tooMany) {
sigGenes_v <- paste0("Gene", c(5,20,26,42,47,16,28))
newMain_v <- "Too Many Labels"
} else {
sigGenes_v <- paste0("Gene", c(5,20,26,42))
newMain_v <- "OK Labels"
}
### Make color list
colors_v <- brewer.pal(8, "Dark2")
colors_v <- colors_v[c(1:length(sigGenes_v), 8)]
names(colors_v) <- c(sigGenes_v, "No")
annColors_lsv <- list("Sig" = colors_v)
### Column Metadata
colMeta_df <- data.frame(Treatment = c(rep("A", 3), rep("B", 3)),
Replicate = c(rep(1:3, 2)),
stringsAsFactors = F,
row.names = colnames(data_mat))
### Row metadata
rowMeta_df <- data.frame(Sig = rep("No", 50),
stringsAsFactors = F,
row.names = rownames(data_mat))
for (gene_v in sigGenes_v) rowMeta_df[rownames(rowMeta_df) == gene_v, "Sig"] <- gene_v
### Heatmap
heat <- pheatmap(data_mat,
annotation_row = rowMeta_df,
annotation_col = colMeta_df,
annotation_colors = annColors_lsv,
cellwidth = 10,
main = "Original Heat")
### Get order of genes after clustering
genesInHeatOrder_v <- heat$tree_row$labels[heat$tree_row$order]
whichSigInHeatOrder_v <- which(genesInHeatOrder_v %in% sigGenes_v)
whichSigInHeatOrderLabels_v <- genesInHeatOrder_v[whichSigInHeatOrder_v]
sigY <- 1 - (0.02 * whichSigInHeatOrder_v)
### Change title
whichMainGrob_v <- which(heat$gtable$layout$name == "main")
heat$gtable$grobs[[whichMainGrob_v]] <- textGrob(label = newMain_v,
gp = gpar(fontsize = 16))
### Remove rows
whichRowGrob_v <- which(heat$gtable$layout$name == "row_names")
heat$gtable$grobs[[whichRowGrob_v]] <- textGrob(label = whichSigInHeatOrderLabels_v,
y = sigY,
vjust = 1)
grid.newpage()
grid.draw(heat)
Here are a few outputs:
original heatmap:
ok labels:
ok labels, with flags:
too many labels
too many labels, with flags
The "with flags" outputs are the desired final results.
I just saved these as images from the Rstudio plot viewer. I recognize that I could save them as pdfs and provide a larger file size to get rid of the label overlap, but then the individual cells would be larger than I want.
Based on your code, you seem fairly comfortable with gtables & grobs. A (relatively) straightforward way to achieve the look you want is to zoom in on the row label grob, & make some changes there:
replace unwanted labels with "";
evenly spread out labels within the available space;
add line segments joining the old and new label positions.
I wrote a wrapper function for this, which works as follows:
# heat refers to the original heatmap produced from the pheatmap() function
# kept.labels should be a vector of labels you wish to show
# repel.degree is a number in the range [0, 1], controlling how much the
# labels are spread out from one another
add.flag(heat,
kept.labels = sigGenes_v,
repel.degree = 0)
add.flag(heat,
kept.labels = sigGenes_v,
repel.degree = 0.5)
add.flag(heat,
kept.labels = sigGenes_v,
repel.degree = 1)
Function (explanations in annotations):
add.flag <- function(pheatmap,
kept.labels,
repel.degree) {
# repel.degree = number within [0, 1], which controls how much
# space to allocate for repelling labels.
## repel.degree = 0: spread out labels over existing range of kept labels
## repel.degree = 1: spread out labels over the full y-axis
heatmap <- pheatmap$gtable
new.label <- heatmap$grobs[[which(heatmap$layout$name == "row_names")]]
# keep only labels in kept.labels, replace the rest with ""
new.label$label <- ifelse(new.label$label %in% kept.labels,
new.label$label, "")
# calculate evenly spaced out y-axis positions
repelled.y <- function(d, d.select, k = repel.degree){
# d = vector of distances for labels
# d.select = vector of T/F for which labels are significant
# recursive function to get current label positions
# (note the unit is "npc" for all components of each distance)
strip.npc <- function(dd){
if(!"unit.arithmetic" %in% class(dd)) {
return(as.numeric(dd))
}
d1 <- strip.npc(dd$arg1)
d2 <- strip.npc(dd$arg2)
fn <- dd$fname
return(lazyeval::lazy_eval(paste(d1, fn, d2)))
}
full.range <- sapply(seq_along(d), function(i) strip.npc(d[i]))
selected.range <- sapply(seq_along(d[d.select]), function(i) strip.npc(d[d.select][i]))
return(unit(seq(from = max(selected.range) + k*(max(full.range) - max(selected.range)),
to = min(selected.range) - k*(min(selected.range) - min(full.range)),
length.out = sum(d.select)),
"npc"))
}
new.y.positions <- repelled.y(new.label$y,
d.select = new.label$label != "")
new.flag <- segmentsGrob(x0 = new.label$x,
x1 = new.label$x + unit(0.15, "npc"),
y0 = new.label$y[new.label$label != ""],
y1 = new.y.positions)
# shift position for selected labels
new.label$x <- new.label$x + unit(0.2, "npc")
new.label$y[new.label$label != ""] <- new.y.positions
# add flag to heatmap
heatmap <- gtable::gtable_add_grob(x = heatmap,
grobs = new.flag,
t = 4,
l = 4
)
# replace label positions in heatmap
heatmap$grobs[[which(heatmap$layout$name == "row_names")]] <- new.label
# plot result
grid.newpage()
grid.draw(heatmap)
# return a copy of the heatmap invisibly
invisible(heatmap)
}
I have recently started using tableGrob and gridextra to combine multiple plots and tables. i want mt tableGrob to have a footnote and title.
The following link answers that nicely:
Adding text to a grid.table plot
But in the above code the footnote gets truncated it its too long. Can someone please suggest an alternative so that the footnote automatically wraps to the next line once it had reached the end of the table? If it can wrap in the middle of the word that is fine as well.
test <- data.frame(boo = c(20,1), do = c(2,10), no = c(3,5),co = c('ed','jeff'))
t1 <- tableGrob(test)
tw <- convertWidth(unit(grobWidth(t1),'npc'),
"in", valueOnly = T)
title <- textGrob("Title is long too or is it??",gp=gpar(fontsize=15))
footnote <- textGrob("footnote is pretty longgg but not unusually longgggggggggkjwd jwkldn", x=0, hjust=0,
gp=gpar( fontface="italic"))
padding <- unit(0.5,"line")
t1 <- gtable_add_rows(t1,
heights = grobHeight(title) + padding,
pos = 0)
t1 <- gtable_add_rows(t1,
heights = grobHeight(footnote)+ padding)
t1 <- gtable_add_grob(t1, list(title, footnote),
t=c(1, nrow(t1)), l=c(1,1),
r=ncol(t1))
grid.arrange(t1)
I want this to work when I have a plot and a table in grid arrange as well. Please help.
I tried using strwrap and setting the width to grobWidth but it did not work for me.
the RGraphics book/package offers a possible solution,
splitString <- function (text, width) {
strings <- strsplit(text, " ")[[1]]
newstring <- strings[1]
linewidth <- stringWidth(newstring)
gapwidth <- stringWidth(" ")
availwidth <- convertWidth(width, "in", valueOnly = TRUE)
for (i in 2:length(strings)) {
width <- stringWidth(strings[i])
if (convertWidth(linewidth + gapwidth + width, "in",
valueOnly = TRUE) < availwidth) {
sep <- " "
linewidth <- linewidth + gapwidth + width
}
else {
sep <- "\n"
linewidth <- width
}
newstring <- paste(newstring, strings[i], sep = sep)
}
newstring
}
tit <- "Title is long too or is it??"
foot <- "footnote is pretty longgg but not unusually longgggggggggkjwd jwkldn"
footnote <- textGrob(splitString(foot, sum(t1$widths)))
title <- textGrob(splitString(tit, sum(t1$widths)))
t1 <- gtable_add_rows(t1, heights = grobHeight(footnote))
t1 <- gtable_add_rows(t1, heights = grobHeight(title), 0)
t1 <- gtable_add_grob(t1, list(title, footnote),
t=c(1, nrow(t1)), l=1, r=ncol(t1))
grid.draw(t1)
Having some trouble aligning a grid graphics object -- have read all the docs I can find, including the Murrell book, but no success. I think what I'm trying to do is pretty straightforward, so hopefully I'm missing simple.
Here's a reproducible example that will make a PDF of all the air carriers by destination in Hadley's hflights package (mirrors what I am trying to do on a different data set).
require(hflights)
require(gridExtra)
require(Cairo)
make_table <- function(df) {
p <- tableGrob(
df
,padding.h=unit(.25, "mm")
,show.rownames=FALSE
,gpar.coretext = gpar(fontsize=8, lineheight=0)
#this doesn't seem to justify the table
,just = c("bottom")
,show.box = T
)
return(p)
}
dests <- unique(hflights$Dest)
#list to hold the plots
plot_list <- list()
#loop over destinations and make a simple report
for (i in dests) {
#just this destination
this_dest <- hflights[hflights$Dest == i, ]
#the title
title <- textGrob(label = i, gp = gpar(fontsize=72, fontface = 'bold'))
#a table of carriers
carriers <- unique(this_dest$UniqueCarrier)
carriers <- data.frame(
carrier=carriers
)
carrier_table <- make_table(carriers)
#put them together
p <- arrangeGrob(
title, carrier_table
,nrow=2
)
plot_list[[i]] <- p
}
#print the report
Cairo(
width = 11, height = 8.5
,file = paste('destinations.pdf', sep = ''), type="pdf"
,units = "in"
)
print(plot_list)
dev.off()
I want the entire table produced by tableGrob (in the make_table function) to justify to the top of the grob. Right now it is centered vertically and horizontally inside the grob. Do I need to do that in the call to tableGrob, or is it in the arrangeGrob call? To ask it another way, in case the above is not clear, how can I make the whole table (not the text inside of it) justify to the top/bottom/left/right of its container?
Thanks!
try this,
library(gridExtra)
justify <- function(x, hjust="center", vjust="center", draw=TRUE){
w <- sum(x$widths)
h <- sum(x$heights)
xj <- switch(hjust,
center = 0.5,
left = 0.5*w,
right=unit(1,"npc") - 0.5*w)
yj <- switch(vjust,
center = 0.5,
bottom = 0.5*h,
top=unit(1,"npc") - 0.5*h)
x$vp <- viewport(x=xj, y=yj)
if(draw) grid.draw(x)
return(x)
}
g <- tableGrob(iris[1:3,1:2])
grid.newpage()
justify(g,"right", "top")
I'm trying to output a dataframe of about 40 rows and 5 columns to a .pdf file using grid.table in gridExtra package of R.
However, 40 rows is too long for a page so the .pdf file only shows part of the dataframe. I want to know if I can print two columns on one page so all of the rows show up on one page. Alternatively, I need to know how to print the dataframe over multiple pages. Thanks, John
Try this for drawing table on a pdf file that span multiple pages using gridExtra package:
Adjust pdf device aspect ratio
pdf(file = myfile.pdf, height = 12, width = 26)
Split the large data frame into chunks and call grid.newpage before drawing a table.
require(gridExtra)
pdf(file = myfile.pdf, height = 12, width = 26)
grid.newpage()
grid.table(sga_hits[1:38, ], show.rownames = FALSE)
grid.newpage()
grid.table(sga_hits[39:75, ], show.rownames = FALSE)
dev.off()
Automate the above as follows:
require(gridExtra)
pdf(file = myfile.pdf, height = 12, width = 26)
total_rows_per_page = 38
start_row = 1
if(total_rows_per_page > nrow(sga_hits)){
end_row = nrow(sga_hits)
}else {
end_row = total_rows_per_page
}
for(i in 1:ceiling(nrow(sga_hits)/total_rows_per_page)){
grid.newpage()
grid.table(sga_hits[start_row:end_row, ], show.rownames = FALSE)
start_row = end_row + 1
if((total_rows_per_page + end_row) < nrow(sga_hits)){
end_row = total_rows_per_page + end_row
}else {
end_row = nrow(sga_hits)
}
}
dev.off()
I'd suggest the following strategy: create the tableGrob, query its heights, split the rows to fit each page,
library(gridExtra)
library(grid)
d <- iris[sample(nrow(iris), 187, TRUE),]
tg <- tableGrob(d, rows = seq_len(nrow(d)))
fullheight <- convertHeight(sum(tg$heights), "cm", valueOnly = TRUE)
margin <- unit(0.51,"in")
margin_cm <- convertHeight(margin, "cm", valueOnly = TRUE)
a4height <- 29.7 - margin_cm
nrows <- nrow(tg)
npages <- ceiling(fullheight / a4height)
heights <- convertHeight(tg$heights, "cm", valueOnly = TRUE)
rows <- cut(cumsum(heights), include.lowest = FALSE,
breaks = c(0, cumsum(rep(a4height, npages))))
groups <- split(seq_len(nrows), rows)
gl <- lapply(groups, function(id) tg[id,])
pdf("multipage.pdf", paper = "a4", width = 0, height = 0)
for(page in seq_len(npages)){
grid.newpage()
grid.rect(width=unit(21,"cm") - margin,
height=unit(29.7,"cm")- margin)
grid.draw(gl[[page]])
}
## alternative to explicit loop:
## print(marrangeGrob(grobs=gl, ncol=1, nrow=1, top=NULL))
dev.off()
One way is to shrink the font the font size and the horizontal/vertical padding.
grid.table(mtcars, gpar.coretext = gpar(fontsize=6), gpar.coltext = gpar(fontsize=6), padding.h=unit(2, "mm"), padding.v=unit(2, "mm"), show.rownames = TRUE)
Implementing viewports from the grid is one potential solution.
A viewport defines a region in the graphics device. It is sometimes useful to define a viewport, then push it and draw inside it. A different viewport may then be pushed and drawn inside of; this method amounts to a simple way to arrange objects on a page.
First, define page and margin sizes.
# Assume total page size is 8.5in x 11in
vp.page <- viewport(x = 0.5, y = 0.5,
width = unit(x = 8.5, units = "inches"),
height = unit(x = 11, units = "inches"))
# Assume 0.5in margins (i.e., 0.5 left, right, bottom, top)
# This totals 1in for each dimension
vp.marg <- viewport(x = 0.5, y = 0.5,
width = (7.5 / 8.5), height = (10 / 11))
Next, Define viewports for each column.
To arrange columns horizontally within a viewport, their x positions will be equally spaced in the interval (0,1).
In the 2 column case, x1 = 0.25 and x2 = 0.75:
# Define the viewport for column 1
vp.col1 <- viewport(x = 0.25, y = 0.5, width = 0.5, height = 1)
# Define the viewport for column 2
vp.col2 <- viewport(x = 0.75, y = 0.5, width = 0.5, height = 1)
Now, actual data is defined.
This data will also need to be "grob'd" to be drawn into viewports.
# Assume data is stored as `dat` and has 40 rows
# Grob the data for column 1
col1 <- tableGrob(dat[1:20,], rows = NULL)
# Grob the data for column 2
col2 <- tableGrob(dat[21:40,], rows = NULL)
Now, draw the pdf:
# Initiate the pdf
pdf("results.pdf", height = 11, width = 8.5)
# Push the viewports for page and margin
pushViewport(vp.page); pushViewport(vp.marg)
# Push column 1
pushViewport(vp.col1)
# Draw column 1
grid.draw(col1)
# Return to the previous viewport
upViewport()
# Push the viewport for column 2
pushViewport(vp.col2)
# Draw column 2
grid.draw(col2)
# End the pdf and save it
dev.off()
pdf() has a width and a height argument.
Your best bet is to enlarge the dimensions and then if you're printing to paper, whichever program you're using would most likely be better suited.
Alternatively, if you want to print two columns on one page, just iterate over the columns:
# assuming `myDF` is your data.frame
pdf("filename.pdf")
for (cl in seq(from=1, to=ncol(myDF)-1, by=2)) {
plot.new()
grid.table(myDF[, cl+(0:1)])
}
dev.off()
I just used a hack. I printed the table to html using R2HTML and then I converted the html to pdf using wkhtmltopdf.
in R:
library(R2HTML)
HTML(table, file="table.html")
in the shell
wkhtmltopdf table.html table.pdf