Save plot with a given aspect ratio
Asked Answered
G

5

27

I'm working with the really awesome library ggplot2. I figured out how to set the aspect ratio of a plot by using coord_fixed. Now, I'd like to save the plot to a PDF with a specified width (e.g 10 cm) and let required height get calculated. I did not figure out how to achieve this. Is this even possible?

Greasy answered 7/5, 2013 at 15:24 Comment(0)
H
15

You can use grid functions to calculate the full size of the ggplot grob, but there are (edit: at least) two caveats:

  • an extra device window will open, to do the unit conversion

  • the plot panel size will be 0 by default, as it is meant to be calculated on-the-fly according to the device (viewport) it lives in, not the opposite.

That being said, the following function attempts to open a device that fits the ggplot exactly,

library(ggplot2)
library(grid)

sizeit <- function(p, panel.size = 2, default.ar=1){

  gb <- ggplot_build(p)
  # first check if theme sets an aspect ratio
  ar <- gb$plot$coordinates$ratio

  # second possibility: aspect ratio is set by the coordinates, which results in 
  # the use of 'null' units for the gtable layout. let's find out
  g <- ggplot_gtable(gb)
  nullw <- sapply(g$widths, attr, "unit")
  nullh <- sapply(g$heights, attr, "unit")

  # ugly hack to extract the aspect ratio from these weird units
  if(any(nullw == "null"))
    ar <- unlist(g$widths[nullw == "null"]) / unlist(g$heights[nullh == "null"])

  if(is.null(ar)) # if the aspect ratio wasn't specified by the plot
       ar <- default.ar

  # ensure that panel.size is always the larger dimension
  if(ar <= 1 ) panel.size <- panel.size / ar

  g$fullwidth <- convertWidth(sum(g$widths), "in", valueOnly=TRUE) + 
    panel.size
  g$fullheight <- convertHeight(sum(g$heights), "in", valueOnly=TRUE) + 
    panel.size / ar

  class(g) <- c("sizedgrob", class(g))
  g
}


print.sizedgrob <- function(x){
  # note: dev.new doesn't seem to respect those parameters
  # when called from Rstudio; in this case it 
  # may be replaced by x11 or quartz or ...
  dev.new(width=x$fullwidth, height=x$fullheight)
  grid.draw(x)
}


p1 <- ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point() + coord_fixed() +
  theme(plot.background = element_rect(colour = "red"))

p2 <- p1 + aes(x = mpg, y = wt)

# need for an explicit dummy device open, otherwise it's a bit off
# for no apparent reason that I can understand
dev.new() 

sizeit(p1, 0.1)

enter image description here

sizeit(p2, 2)

enter image description here

Humph answered 8/5, 2013 at 13:29 Comment(3)
Thanks for your answer, baptiste. It seems there's no pretty solution.Greasy
Accepted your answer as I think there's no better solution showing up. Thanks everybody.Greasy
This doesn't work for me. When I ggsave (to pdf) your sizeit(p1, 0.1) plot, the output has bizarrely transposed the axes. I think it would be preferable to use a function that returns an aspect ratio that be used to determine the ggsave width and height parameters.Dowdell
G
5

Based on baptiste's answer I stripped his code down to return the aspect ratio as suggested by geotheory. This was much more convenient for me, because I either wanted a fixed width or height and also passed everything through an existing wrapper function that also adds fonts to my pdf.

Oh, and if you used facets you need to take them into account manually. Divide by rows and multiply by columns. Not sure whether there is a better way.....

ggGetAr <- function(p, default.ar=-1){

    gb <- ggplot_build(p)
    # first check if theme sets an aspect ratio
    ar <- gb$plot$coordinates$ratio

    # second possibility: aspect ratio is set by the coordinates, which results in 
    # the use of 'null' units for the gtable layout. let's find out
    g <- ggplot_gtable(gb)
    nullw <- sapply(g$widths, attr, "unit")
    nullh <- sapply(g$heights, attr, "unit")

    # ugly hack to extract the aspect ratio from these weird units
    if(any(nullw == "null"))
        ar <- unlist(g$widths[nullw == "null"]) / unlist(g$heights[nullh == "null"])

    if(is.null(ar)) # if the aspect ratio wasn't specified by the plot
        ar <- default.ar

    ar[1]
}
Gradate answered 17/7, 2015 at 14:19 Comment(0)
B
4

If you use ggsave you can simply specify the width and height of the graphics device. If you specify the aspect ratio of the plot itself, it is also good to have this aspect ratio (roughly) in your graphics device. The unit of height and width when saving pdf is inches:

ggplot(...) # make a plot here
ggsave("plot.pdf", width = 10)

Now you only have to transform the 10 cm into inches. In addition, height is not forced to a certain aspect ratio if you do not specify it. If you want a 16:9 aspect ratio, you can easily calculate the height based on the width:

ggplot(...) # make plot
width = 10
height = (9/16) * width
ggsave("plot.pdf", width = width, height = height)

You could wrap this in a function if you really want to.


edit: The crux is to synchronize the aspect ratio of the plot (through coord_fixed()) and the aspect ratio of the graphics device. For example

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point() + coord_fixed()
ggsave("plt.png", width = 7, height = 7)

enter image description here leads to a lot of white space. While the following ggsave call, which has a much better fit in aspect ratio, does not have this amount of white space (sorry for the large picture, could not set the maximum size :)):

ggsave("plt.png", width = 2, height = 7)

enter image description here

Bennybenoit answered 7/5, 2013 at 15:36 Comment(2)
I thought that Paul, but that is the size of the device, not the plot. Once you add in the title, axis labels, and legend etc, the plotting region itself may not be square and with coord_fixed() the plot region itself then doesn't fill the available space. I don't know a good fix other than to resize the plotting window until ggsave() without specifying the width gets you a 10cm wide image.Gettings
Thank you Paul for your answer. Gavin, it's exactly like you pointed out in your comment. I think this is quite an issue, having to crop each plot manually. Let's wait to see if someone comes up with a solution.Greasy
M
2

Not sure, but is something like this what you're after?

ggplot(data.frame(x = seq(10), y = seq(10)), aes(x = x, y = y)) +
    geom_point() +
    coord_equal() +
    theme(aspect.ratio = 1)

This looks fine to me:

ggsave("test.pdf", width = 4, height = 4)

Too much white space, but the graphic itself has aspect ratio 1:

ggsave("test2.pdf", width = 4)

Message: Saving 4 x 6.93 in image

Malia answered 8/5, 2013 at 12:28 Comment(1)
Thank you for your answer Dennis. Unfortunately it's not appropriate as ggsave just takes the size of the current plotting window by default.Greasy
W
1

A more simplistic solution would be to save the plot with default margins and to trim the resulting png with ImageMagick.

require(ggplot2)
require(dplyr)

ggplot(iris, aes(Sepal.Length, Sepal.Width)) + geom_point() + coord_fixed(0.3)
ggsave("untrimmed.png")


system("convert untrimmed.png -trim -bordercolor white -border 20  reframed.png")

For sure the trimming will differ depending on the used output device. E.g. in case of pdf you could use pdfcrop as described here.

Writ answered 7/10, 2015 at 9:39 Comment(1)
This worked perfectly for me -- was afraid that the system call required installing additional software but it didn't (for me).Congregationalism

© 2022 - 2024 — McMap. All rights reserved.