Search code examples
rimageggplot2facetfacet-grid

autocrop faceted plots made by ggplot


When making faceted plots in ggplot and changing the aspect ratio, usually there is a lot of white space either left and right or above and below the graph. E.g:

library(ggplot2)
df <- data.frame(x=rep(1,3), y=rep(1,3), z=factor(letters[1:3]))
p <- ggplot(df, aes(x, y)) + geom_point() + coord_fixed(ratio=1) + facet_grid(z ~ .)
ggsave("plot.jpg", p, scale=1, device="jpeg")

Is there a way to autocrop the graph?


Solution

  • This is what I came up with. I tested it on your sample & it seems to work okay there, but apologies in advance if it breaks somewhere else. I had to dig into the grob version for the width/height information, and depending on whether the plot is faceted or not, the attribute information for "unit" is located at different places.

    Hopefully someone more well-versed with the grid package's unit object can chip in, but here's what I've got:

    # Note that you need to set an upper limit to the maximum height/width
    # that the plot can occupy, in the max.dimension parameter (defaults to
    # 10 inches in this function)
    
    ggsave_autosize <- function(filename, plot = last_plot(), device = NULL, path = NULL, scale = 1, 
                                max.dimension = 10, units = c("in", "cm", "mm"), 
                                dpi=300, limitsize = TRUE){
    
      sumUnitNull <- function(x){
        res <- 0
        for(i in 1:length(x)){ 
          check.unit <- ifelse(!is.null(attr(x[i], "unit")), attr(x[i], "unit"), 
                               ifelse(!is.null(attr(x[i][[1]], "unit")), attr(x[i][[1]], "unit"), NA))
          if(!is.na(check.unit) && check.unit == "null") res <- res + as.numeric(x[i])
        }
        return(res)
      }
    
      # get width/height information from the plot object (likely in a mixture of different units)
      w <- ggplotGrob(plot)$widths
      h <- ggplotGrob(plot)$heights
    
      # define maximum dimensions
      w.max <- grid::unit(max.dimension, units) %>% grid::convertUnit("in") %>% as.numeric()
      h.max <- grid::unit(max.dimension, units) %>% grid::convertUnit("in") %>% as.numeric()
    
      # sum the inflexible size components of the plot object's width/height 
      # these components have unit = "in", "mm", "pt", "grobheight", etc
      w.in <- w %>% grid::convertUnit("in") %>% as.numeric() %>% sum()
      h.in <- h %>% grid::convertUnit("in") %>% as.numeric() %>% sum()
    
      # obtain the amount of space available for the flexible size components
      w.avail <- w.max - w.in
      h.avail <- h.max - h.in
    
      # sum the flexible sized components of the plot object's width/height 
      # these components have unit = "null"
      w.f <- sumUnitNull(w)
      h.f <- sumUnitNull(h)
    
      # shrink the amount of avilable space based on what the flexible components would actually take up
      if(w.f/h.f > w.avail/h.avail) h.avail <- w.avail/w.f*h.f else w.avail <- h.avail/h.f*w.f
    
      w <- w.in + w.avail
      h <- h.in + h.avail
    
      ggsave(filename, plot = plot, device = device, path = path, scale = scale,
             width = w, height = h, units = units, dpi = dpi, limitsize = limitsize)
    }
    
    
    p <- ggplot(mpg, aes(displ, cty)) + geom_point() + coord_fixed(ratio=1)
    p <- p + facet_grid(. ~ cyl)
    
    ggsave("pOriginal.png", p + ggtitle("original"))
    ggsave_autosize("pAutoSize.png", p + ggtitle("auto-resize"))
    ggsave_autosize("pAutoSize8.png", p + ggtitle("auto-resize, max dim = 8in x 8in"), max.dimension = 8, units = "in")
    

    Original version w/o cropping. There's black space on the left / right:

    original

    Automatically cropped version. Height = 10 inches:

    autosize

    Automatically cropped version. Height = 8 inches (so the font looks slightly larger):

    autosize2