4

I've currently got a script that generates multiple figures of the same type when run (plotting the effect of different treatments on the same categories in my data set) which are all dodged bar graphs with the same values in the x-axis). An example would be:

d <- data.frame(x = runif(1000), y = runif(1000)^2, very_long_label_name = runif(1000)^3)
d <- round(d, 1)
d <- melt(d)
qplot(data = d[d$variable != "very_long_label_name",], factor(value), position = "dodge", 
      geom = "histogram", fill = variable)
ggsave("test1.png", height = 3.5, width = 4.5)
qplot(data = d[d$variable != "y",], factor(value), position = "dodge",
      geom = "histogram", fill = variable)
ggsave("test2.png", height = 3.5, width = 4.5)

Since my figures are dodged bar charts, I've got a legend on the side for bar colors. But since each figure compares different treatments, the labels on the legends are of different lengths, so the figures end up coming out with differing aspect ratios and text sizes. Is there a way for me to control the size of text and the width of the x-axis across multiple figures?

I've tried looking at How to control ggplot's plotting area proportions instead of fitting them to devices in R? but coord_fixed() seems to get ignored by ggsave(). How can I make consistent-width plots in ggplot (with legends)? is a very similar problem, but the answers all seem to assume that I'll be putting the figures together rather than spreading them out across a paper. Using theme_set() would seem promising for the font size issue at least, except that the ultimate size of the text is then affected by the size specified in ggsave().

It seems like the feature I'm actually looking for would be the ability to determine an image's output width by setting the width of the plotting region, rather than the width of the entire drawing area as ggsave("test.png", width = 3) does. Does such a feature exist in ggplot2?

Community
  • 1
  • 1
Empiromancer
  • 3,778
  • 1
  • 22
  • 53

2 Answers2

2

One (unfortunately not so lazy) approach would be to extract the legends as separate grobs (graphical objects), then lay out the plots and legends separately. This gives you more control over how much space is allocated to each object. Fortunately, a couple of helper functions make the process relatively painless. Here's an example:

library(gridExtra) 

p1 = qplot(data = d[d$variable != "very_long_label_name",], factor(value), 
           position = "dodge", geom = "histogram", fill = variable)

p2 = qplot(data = d[d$variable != "y",], factor(value), position = "dodge",
      geom = "histogram", fill = variable)

We need a couple of helper functions, one to extract the legend and one to justify the legend grobs so they line up:

# Function to extract legend as a separate grob
# Source: http://stackoverflow.com/a/12539820/496488
get_leg = function(a.gplot){
  tmp <- ggplot_gtable(ggplot_build(a.gplot))
  leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
  legend <- tmp$grobs[[leg]]
  legend
}

# Function to left justify the legends so they line up
# Source: http://stackoverflow.com/a/25456672/496488
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)
}

Now extract the legends and lay out the plots:

# Extract each legend
leg1 = get_leg(p1)
leg2 = get_leg(p2)

# Allocate proportions of layout width to plot and legend
w = c(0.6,0.4)

# Lay out plot and legend separately
png("test1.png", height = 3, width = 6, units="in", res=100)
grid.arrange(p1 + theme(legend.position="none"), 
             justify(leg1,"left","center"),
             widths=w, ncol=2)
dev.off()

png("test2.png", height = 3, width = 6, units="in", res=100)
grid.arrange(p2 + theme(legend.position="none"), 
             justify(leg2,"left","center"),
             widths=w, ncol=2)
dev.off()

enter image description here

enter image description here

Just one more wrinkle in case this comes up with your actual data: If the y-values of your plots vary, you can end up with the plot areas not being vertically aligned, due to more width being allocated to the y-axis labels (and less to the plot region) when the y values have more characters. Then you also need to equalize the widths of the plot regions so that that plot regions line up vertically as well. Here's an example:

# New plot with a different data set
p3 = ggplot(iris, aes(Sepal.Length, Sepal.Width*1e6, colour=Species)) +
        geom_point()

leg3 = get_leg(p3)

# Justify widths
# Source: http://stackoverflow.com/a/13295880/496488
gA <- ggplotGrob(p1 + theme(legend.position="none"))
gB <- ggplotGrob(p3 + theme(legend.position="none"))
maxWidth = grid::unit.pmax(gA$widths[2:5], gB$widths[2:5])
gA$widths[2:5] <- as.list(maxWidth)
gB$widths[2:5] <- as.list(maxWidth)

png("test1a.png", height = 3, width = 6, units="in", res=100)
grid.arrange(gA, justify(leg1,"left","center"),
             widths=w, ncol=2)
dev.off()

png("test3.png", height = 3, width = 6, units="in", res=100)
grid.arrange(gB, justify(leg3,"left","center"),
widths=w, ncol=2)
dev.off()

Now here's the original p1, followed by the width-adjusted versions of p1 and p3:

enter image description here

enter image description here

enter image description here

eipi10
  • 91,525
  • 24
  • 209
  • 285
0

Lazy solution, but might just work for your purposes: put the legend on the bottom

qplot(data = d[d$variable != "very_long_label_name",], factor(value), 
      position = "dodge", geom = "histogram", fill = variable) +
  theme(legend.position = 'bottom')

qplot(data = d[d$variable != "y",], factor(value),
      position = "dodge", geom = "histogram", fill = variable)  +
  theme(legend.position = 'bottom')

enter image description here

enter image description here

arvi1000
  • 9,393
  • 2
  • 42
  • 52