ggplot2 legend width and legend strings size - r

I have 2 plots of the exact same thing, excepts the colors filling the bars are different on each plot. Since the legend on the different plots have different widths because of the size of the names on it, the ratio between graph and legend becomes different on each plot. I need to make both look the same.
This is an example:
library(ggplot2)
x = c(rep("a",20),rep("b",10))
y = c(x = c(rep("BIGTEXT",20),rep("EVENBIGGERTEXT",10)))
df = data.frame(x,y)
p1 = ggplot(df,aes(x,fill=x)) + geom_bar()
p2 = ggplot(df,aes(y,fill=y)) + geom_bar()
p1
p2

You can set the gtable widths to a common value,
library(gtable)
library(grid)
gl <- lapply(list(p1, p2), ggplotGrob)
gwidth <- do.call(unit.pmax, lapply(gl, "[[", "widths"))
gl <- lapply(gl, "[[<-", "widths", value = gwidth)
gridExtra::grid.arrange(grobs=gl)
Alternatively, you can set the panel size to a fixed value.

Following up #baptiste's comment: Fiddly but yes, it can be done. But no guarantees that this will work in future versions. The solution was taken from here, but that solution too needed updating.
library(ggplot2) # v2.2.1
library(gtable) # v0.2.0
library(grid)
library(gridExtra) # v2.2.1
x = c(rep("a",20),rep("b",10))
y = c(x = c(rep("BIGTEXT",20),rep("EVENBIGGERTEXT",10)))
df = data.frame(x,y)
p1 = ggplot(df,aes(x,fill=x)) + geom_bar()
p2 = ggplot(df,aes(y,fill=y)) + geom_bar()
# Get the grobs
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# Get the widths of the legends
index = which(gA$layout$name == "guide-box")
leg1 <- convertX(sum(with(gA$grobs[[index]], grobs[[1]]$widths)), "mm")
leg2 <- convertX(sum(with(gB$grobs[[index]], grobs[[1]]$widths)), "mm")
# Add an empty column of width "abs(diff(c(leg1, leg2))) mm" to the right of
# legend box for gA (the smaller legend box)
gA$grobs[[index]] <- gtable_add_cols(gA$grobs[[index]], unit(abs(diff(c(leg1, leg2))), "mm"))
# Set widths to maximums of corresponding widths
gl <- list(gA, gB)
gwidth <- do.call(unit.pmax, lapply(gl, "[[", "widths"))
gl <- lapply(gl, "[[<-", "widths", value = gwidth)
# Draw the plots
grid.newpage()
grid.draw(gl[[1]])
grid.newpage()
grid.draw(gl[[2]])

Related

ggplot - Align ticks with axis

I am using the ndodge function explained by #jan-glx here;
https://stackoverflow.com/a/60650595/13399047
However I could not figure out how to align the axis ticks aligned as for example;
I should probably use theme(axis.ticks.length=) but I am not sure how to do it in an even/odd way.
Please help!
As far as I am aware there is no build in way to do this in ggplot, though that might change when they rewrite the guide system.
It is neither pretty nor easy, but here is an example how you could do it by messing around in the gtable / grid.
library(ggplot2)
library(grid)
data(diamonds)
diamonds$cut <- paste("Super Dee-Duper",as.character(diamonds$cut))
g <- ggplot(diamonds, aes(cut, carat)) +
geom_boxplot() +
scale_x_discrete(guide = guide_axis(n.dodge = 2))
# Convert to gtable
gt <- ggplotGrob(g)
# Grab bottom axis
is_axis <- grep("axis-b", gt$layout$name)
axisgrob <- gt$grobs[is_axis][[1]]
axis <- axisgrob$children$axis
# Grab tickmarks
is_ticks <- which(vapply(axis$grobs, inherits, logical(1), "polyline"))
ticks <- axis$grobs[[is_ticks]]
# Modify tickmarks
labelheight <- axis$heights[[2]] # First row of labels
modify <- which(seq_along(ticks$y) %% 4 == 0) - 1 # Change every the 3rd item in every quadruplet
ticks$y[modify] <- ticks$y[modify] - labelheight
# Insert ticks back into axis back into table
axis$grobs[[is_ticks]] <- ticks
axisgrob$children$axis <- axis
gt$grobs[[is_axis]] <- axisgrob
# Plot
grid.newpage()
grid.draw(gt)
Created on 2020-05-18 by the reprex package (v0.3.0)
Here is a solution using just ggplot2 stuff and not modifying any grobs. It requires ggplot2 3.0.0 and is based off https://stackoverflow.com/a/51312611/6615512
library(ggplot2)
data(diamonds)
diamonds$cut <- paste("Super Dee-Duper",as.character(diamonds$cut))
tick_min_pos_odd = -0.6
tick_min_pos_even = -0.4
custom_ticks = data.frame(cut = sort(unique(diamonds$cut)))
n_discrete_x_values = nrow(custom_ticks)
# Alternate tick lengths
custom_ticks$tick_min_pos = ifelse(1:n_discrete_x_values %% 2 == 0, tick_min_pos_odd, tick_min_pos_even)
ggplot(diamonds, aes(cut, carat)) +
geom_boxplot() +
scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
geom_linerange(data = custom_ticks, # The custom tickmarks
aes(x=cut, ymax=-0.25, ymin=tick_min_pos),
size=0.5, color='black',
inherit.aes = F) +
coord_cartesian(clip='off', ylim=c(0,NA)) + # Clip off makes it so the geoms can be drawn outside the plot
# ylim sets the y-axis from 0 to the max.
theme(plot.margin = margin(0,0,20,0), # Add some whitespace to the bottom of the plot
axis.title.x = element_text(vjust=-1.5), # nudge the x-axis title and text down a tad
axis.text.x = element_text(vjust=-1.5))

R, how to set multiple bar_chart plots to have the same width through grid.arrange? [duplicate]

I've got a few different categories that I want to plot. These are different categories, each with their own set of labels, but which makes sense to group together in the document. The following gives some simple stacked bar chart examples:
df <- data.frame(x=c("a", "b", "c"),
y=c("happy", "sad", "ambivalent about life"))
ggplot(df, aes(x=factor(0), fill=x)) + geom_bar()
ggplot(df, aes(x=factor(0), fill=y)) + geom_bar()
The problem is that with different labels, the legends have different widths, which means the plots have different widths, leading to things looking a bit goofy if I make a table or \subfigure elements. How can I fix this?
Is there a way to explicitly set the width (absolute or relative) of either the plot or the legend?
Edit: Very easy with egg package
# install.packages("egg")
library(egg)
p1 <- ggplot(data.frame(x=c("a","b","c"),
y=c("happy","sad","ambivalent about life")),
aes(x=factor(0),fill=x)) +
geom_bar()
p2 <- ggplot(data.frame(x=c("a","b","c"),
y=c("happy","sad","ambivalent about life")),
aes(x=factor(0),fill=y)) +
geom_bar()
ggarrange(p1,p2, ncol = 1)
Original Udated to ggplot2 2.2.1
Here's a solution that uses functions from the gtable package, and focuses on the widths of the legend boxes. (A more general solution can be found here.)
library(ggplot2)
library(gtable)
library(grid)
library(gridExtra)
# Your plots
p1 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=x)) + geom_bar()
p2 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=y)) + geom_bar()
# Get the gtables
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# Set the widths
gA$widths <- gB$widths
# Arrange the two charts.
# The legend boxes are centered
grid.newpage()
grid.arrange(gA, gB, nrow = 2)
If in addition, the legend boxes need to be left justified, and borrowing some code from here written by #Julius
p1 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=x)) + geom_bar()
p2 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=y)) + geom_bar()
# Get the widths
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# The parts that differs in width
leg1 <- convertX(sum(with(gA$grobs[[15]], grobs[[1]]$widths)), "mm")
leg2 <- convertX(sum(with(gB$grobs[[15]], grobs[[1]]$widths)), "mm")
# Set the widths
gA$widths <- gB$widths
# Add an empty column of "abs(diff(widths)) mm" width on the right of
# legend box for gA (the smaller legend box)
gA$grobs[[15]] <- gtable_add_cols(gA$grobs[[15]], unit(abs(diff(c(leg1, leg2))), "mm"))
# Arrange the two charts
grid.newpage()
grid.arrange(gA, gB, nrow = 2)
Alternative solutions There are rbind and cbind functions in the gtable package for combining grobs into one grob. For the charts here, the widths should be set using size = "max", but the CRAN version of gtable throws an error.
One option: It should be obvious that the legend in the second plot is wider. Therefore, use the size = "last" option.
# Get the grobs
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# Combine the plots
g = rbind(gA, gB, size = "last")
# Draw it
grid.newpage()
grid.draw(g)
Left-aligned legends:
# Get the grobs
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# The parts that differs in width
leg1 <- convertX(sum(with(gA$grobs[[15]], grobs[[1]]$widths)), "mm")
leg2 <- convertX(sum(with(gB$grobs[[15]], grobs[[1]]$widths)), "mm")
# Add an empty column of "abs(diff(widths)) mm" width on the right of
# legend box for gA (the smaller legend box)
gA$grobs[[15]] <- gtable_add_cols(gA$grobs[[15]], unit(abs(diff(c(leg1, leg2))), "mm"))
# Combine the plots
g = rbind(gA, gB, size = "last")
# Draw it
grid.newpage()
grid.draw(g)
A second option is to use rbind from Baptiste's gridExtra package
# Get the grobs
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# Combine the plots
g = gridExtra::rbind.gtable(gA, gB, size = "max")
# Draw it
grid.newpage()
grid.draw(g)
Left-aligned legends:
# Get the grobs
gA <- ggplotGrob(p1)
gB <- ggplotGrob(p2)
# The parts that differs in width
leg1 <- convertX(sum(with(gA$grobs[[15]], grobs[[1]]$widths)), "mm")
leg2 <- convertX(sum(with(gB$grobs[[15]], grobs[[1]]$widths)), "mm")
# Add an empty column of "abs(diff(widths)) mm" width on the right of
# legend box for gA (the smaller legend box)
gA$grobs[[15]] <- gtable_add_cols(gA$grobs[[15]], unit(abs(diff(c(leg1, leg2))), "mm"))
# Combine the plots
g = gridExtra::rbind.gtable(gA, gB, size = "max")
# Draw it
grid.newpage()
grid.draw(g)
The cowplot package also has the align_plots function for this purpose (output not shown),
both2 <- align_plots(p1, p2, align="hv", axis="tblr")
p1x <- ggdraw(both2[[1]])
p2x <- ggdraw(both2[[2]])
save_plot("cow1.png", p1x)
save_plot("cow2.png", p2x)
and also plot_grid which saves the plots to the same file.
library(cowplot)
both <- plot_grid(p1, p2, ncol=1, labels = c("A", "B"), align = "v")
save_plot("cow.png", both)
As #hadley suggests, rbind.gtable should be able to handle this,
grid.draw(rbind(ggplotGrob(p1), ggplotGrob(p2), size="last"))
however, the layout widths should ideally be size="max", which doesn't cope well with some types of grid units.
Just by chance, I noticed that Arun's solution he had suggested in his comments hasn't been picked up. I feel his simple and efficient approach is really worth to be illustrated.
Arun suggested to move the legend to the top or bottom:
ggplot(df, aes(x=factor(0), fill=x)) + geom_bar() + theme(legend.position = "bottom")
ggplot(df, aes(x=factor(0), fill=y)) + geom_bar() + theme(legend.position = "bottom")
Now, the plots have the same width as requested. In addition, the plot area is equally sized in both cases.
If there are more factors or even longer labels, it might become necessary to play around with the legend, e.g., to display the legend in two ore more rows. theme() and guide_legend() have several parameters to control the position and appearance of legends in ggplot2.
I created a little function based on the answer of #Sandy.
same.size.ggplot <- function(vector.string.graph, # a vector of strings which correspond to Robject ggplot graphs
reference.string.graph, # a string of a Robject ggplot graphs where height and/or height will be taken for reference
width = T, # if you wanna adapat only the width
height = F # if you wanna adapat only the height
) {
# example: same.size.ggplot(p0rep(c("a", "b"), thre), "a30")
which(vector.string.graph %in% reference.string.graph)
newref <- ggplotGrob(get(reference.string.graph))
ref.width <- newref$widths
ref.height <- newref$heights
assign(reference.string.graph, newref, env = parent.frame(1))
for(i in seq_along(vector.string.graph)) {
if(vector.string.graph[i] != reference.string.graph) {
new <- ggplotGrob(get(vector.string.graph[i]))
if( width ) {
new$widths <- ref.width
}
if( height ) {
new$heights <- ref.height
}
assign(vector.string.graph[i], new, env = parent.frame(1))
}
}
}
p1 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=x)) + geom_bar()
p2 <- ggplot(data.frame(x=c("a","b","c"),y=c("happy","sad","ambivalent about life")),aes(x=factor(0),fill=y)) + geom_bar()
p3 <- ggplot(data.frame(x=c("a","b","c"),y=c("Crazy happy","sad","Just follow the flow")),aes(x=factor(0),fill=y)) + geom_bar()
grid.arrange(p1, p2, p3, ncol = 1)
same.size.ggplot(c("p1", "p2", "p3"), "p2") # same as same.size.ggplot(c("p2", "p3"), "p1")
grid.arrange(p1, p2, p3, ncol = 1)
Before
After
You could also use the patchwork-package for that:
require(ggplot2)
require(patchwork)
# data
df = data.frame(x = c("a", "b", "c"),
y = c("happy", "sad", "ambivalent about life"))
p1 = ggplot(df, aes(x=factor(0), fill=x)) + geom_bar()
p2 = ggplot(df, aes(x=factor(0), fill=y)) + geom_bar()
# Patchwork 1: Does it automatically
p1 / p2
# Patchwork 2: Create a list
l = patchwork::align_patches(p1, p2)

ggplot: clipping lines between facets

Say I have a plot like this:
# Load libraries
library(ggplot2)
library(grid)
# Load data
data(mtcars)
# Plot results
p <- ggplot(data = mtcars)
p <- p + geom_bar(aes(cyl))
p <- p + coord_flip()
p <- p + facet_wrap(~am)
print(p)
Now, I want to plot lines all the way across both facets where the bars are. I add this:
p <- p + geom_vline(aes(xintercept = cyl))
which adds the lines, but they don't cross both facets. So, I try to turn off clipping using this solution:
# Turn off clipping
gt <- ggplot_gtable(ggplot_build(p))
gt$layout$clip[gt$layout$name == "panel"] <- "off"
# Plot results
grid.draw(gt)
but that doesn't solve the problem: the lines are still clipped. So, I wondered if this is specific to geom_vline and tried approaches with geom_abline and geom_line (the latter with values across ±Inf), but the results are the same. In other posts, the clipping solution seems to work for text and points, but presumably in this case the lines are only defined within the limits of the figure. (I even tried gt$layout$clip <- "off" to switch off all possible clipping, but that didn't solve the problem.) Is there a workaround?
library(grid)
library(gtable)
# Starting from your plot `p`
gb <- ggplot_build(p)
g <- ggplot_gtable(gb)
# Get position of y-axis tick marks
ys <- gb$layout$panel_ranges[[1]][["y.major"]]
# Add segments at these positions
# subset `ys` if you only want to add a few
# have a look at g$layout for relevant `l` and `r` positions
g <- gtable_add_grob(g, segmentsGrob(y0=ys, y1=ys,
gp=gpar(col="red", lty="dashed")),
t = 7, l = 4, r=8)
grid.newpage()
grid.draw(g)
see ggplot, drawing multiple lines across facets for how to rescale values for more general plotting. ie
data2npc <- function(x, panel = 1L, axis = "x") {
range <- pb$layout$panel_ranges[[panel]][[paste0(axis,".range")]]
scales::rescale(c(range, x), c(0,1))[-c(1,2)]
}
start <- sapply(c(4,6,8), data2npc, panel=1, axis="y")
g <- gtable_add_grob(g, segmentsGrob(y0=start, y1=start),
t=7, r=4, l=8)

Annotate specific area of ggplot2 facet backgrounds with images of a specified size

My question is similar to this one; I wish to annotate each facet with a different image in the bottom-left corner.
Using gtable_add_grob I am able to replace the facets with images like so:
library(ggplot2)
library(gtable)
library(RCurl)
library(png)
d <- expand.grid(x=1:2,y=1:2, f=letters[1:2])
p <- qplot(x,y,data=d) + facet_wrap(~f)
g <- ggplot_gtable(ggplot_build(p))
shark <- readPNG(getURLContent("http://i.imgur.com/EOc2V.png"))
tiger <- readPNG(getURLContent("http://i.imgur.com/zjIh5.png"))
facets <- grep("panel", g$layout$name)
new_grobs <- list(rasterGrob(shark, width=1, height=1),
rasterGrob(tiger, width=1, height=1))
g2 <- with(g$layout[facets,],
gtable_add_grob(g, new_grobs,
t=t, l=l, b=b, r=r, name="pic_predator") )
grid.draw(g2)
However, what I'm really wanting is something like this, but I can't figure out the appropriate gtable command to shrink and place the image on each facet:
I'm happy for the solution to not use gtable, if that is necessary.
Controlling the width and height arguments in rasterGrob shrinks the image, and setting the x and y positions (or using hjust or vjust I suppose) controls the placement of the image.
library(ggplot2)
library(gtable)
library(RCurl)
library(png)
d <- expand.grid(x=1:2,y=1:2, f=letters[1:2])
p <- qplot(x,y,data=d) + facet_wrap(~f)
g <- ggplot_gtable(ggplot_build(p))
shark <- readPNG(getURLContent("http://i.imgur.com/EOc2V.png"))
tiger <- readPNG(getURLContent("http://i.imgur.com/zjIh5.png"))
facets <- grep("panel", g$layout$name)
new_grobs <- list(rasterGrob(shark, width=.2, height=.05, x = .2, y = .05),
rasterGrob(tiger, width=.2, height=.05, x = .2, y = .05))
g2 <- with(g$layout[facets,],
gtable_add_grob(g, new_grobs,
t=t, l=l, b=b, r=r, name="pic_predator") )
grid.draw(g2)

Adjust space between gridded, same-sized ggplot2 figures

I am attempting to arrange multiple ggplot2 plots into one output/grid. I'd like the plots (without considering the labels) to be the same size. I have found a way to do this, but now I'd like to adjust the space between the plots.
For example:
In this plot, I'd like to reduce the amount of space between the two plots. I've tried adjusting margins, removing ticks, etc. This has removed some of the space.
Is there a way to have more control of the spacing adjustment between plots in situations such as these?
library(MASS)
data(iris)
library(ggplot2)
library(grid)
library(gridExtra)
p1 <- ggplot(iris,aes(Species,Sepal.Width))+geom_violin(fill="light gray")+geom_boxplot(width=.1) +coord_flip() +
theme(axis.title.y = element_blank()) + ylab("Sepal Width")
p2 <- ggplot(iris,aes(Species,Petal.Width))+geom_violin(fill="light gray")+geom_boxplot(width=.1) + coord_flip() +
theme(axis.title.y = element_blank(), axis.text.y=element_blank()) + ylab("Petal Width")
p11 <- p1 + theme(plot.margin = unit(c(-0.5,-0.5,-0.5,-0.5),"mm"))
p22 <- p2 + theme(plot.margin = unit(c(-0.5,-0.5,-0.5,-0.5),"mm"), axis.ticks.y=element_blank())
# https://stackoverflow.com/questions/24709307/keep-all-plot-components-same-size-in-ggplot2-between-two-plots
# make plots the same size, even with different labels
gl <- lapply(list(p11,p22), ggplotGrob)
widths <- do.call(unit.pmax, lapply(gl, "[[", "widths"))
heights <- do.call(unit.pmax, lapply(gl, "[[", "heights"))
lg <- lapply(gl, function(g) {g$widths <- widths; g$heights <- heights; g})
# https://stackoverflow.com/questions/1249548/side-by-side-plots-with-ggplot2-in-r?lq=1
grid.arrange(lg[[1]],lg[[2]], ncol=2) #in gridExtra
You have set the 'widths' to be the maximums of the two plots. This means that the widths for the y-axis of the 'Petal Widths' plot will be the same as the widths of the y-axis of the 'Sepal Widths' plot.
One way to adjust the spacing is, first, to combine the two grobs into one layout, deleting the y-axis and left margin of the second plot:
# Your code, but using p1 and p2, not the plots with adjusted margins
gl <- lapply(list(p1, p2), ggplotGrob)
widths <- do.call(unit.pmax, lapply(gl, "[[", "widths"))
heights <- do.call(unit.pmax, lapply(gl, "[[", "heights"))
lg <- lapply(gl, function(g) {g$widths <- widths; g$heights <- heights; g})
# New code
library(gtable)
gt = cbind(lg[[1]], lg[[2]][, -(1:3)], size = "first")
Then the width of the remaining space between the two plots can be adjusted:
gt$widths[5] = unit(2, "lines")
# Draw the plot
grid.newpage()
grid.draw(gt)

Resources