Skip to content

Instantly share code, notes, and snippets.

@infotroph
Last active October 27, 2022 04:49
Show Gist options
  • Save infotroph/a0a7fcf034e70de8ed75 to your computer and use it in GitHub Desktop.
Save infotroph/a0a7fcf034e70de8ed75 to your computer and use it in GitHub Desktop.
Compute dimensions of the largest fixed-aspect ggplot that fits inside the given maximum height and width.
# The problem: Plotting from R to PNG requires that you specify x and y
# dimensions, which therefore also fixes the aspect ratio of
# the whole image. In most of my plots, I want a fixed *panel* aspect ratio,
# but the overall dimensions of the full *plot* still depend on the dimensions
# of other plot elements: axes, legends, titles, etc.
# In a facetted ggplot, this gets even trickier: "OK, three panels, each
# with aspect ratio of 1.5, that adds up to... wait, will every panel
# have its own y-axis, or just the leftmost one?"
# ggplot apparently computes absolute dimensions for everything EXCEPT
# the panels, so the approach here is to build the plot inside a
# throwaway device, subtract off the parts used for non-panel elements,
# then divide the remainder up between panels.
# One dimension will be constrained by the size of the throwaway
# device, and we can then calculate the other dimension from the
# panel aspect ratio.
# Known issues:
# 1. It's inefficient, because we have to built the plot twice.
# Since dimensions aren't calculated until the plot is built,
# I don't know of any way around this. Do you?
# 2. For me at least, it seems to create a new, empty Rplots.pdf on exit.
# Probably related to stackoverflow.com/questions/17348359?
# Required packages: ggplot for all of ggplot, grid for convertHeight and convertWidth.
get_dims = function(ggobj, maxheight, maxwidth=maxheight, units="in", ...){
####
# Computes dimensions of the largest fixed-aspect ggplot that
# fits inside the given maximum height and width.
#
# Arguments:
# ggobj
# A ggplot or gtable object.
# maxheight, maxwidth
# Numeric, giving argest allowable dimensions of the plot.
# The final image will exactly match one of these and not exceed
# the other.
# units
# Character, giving units for the dimensions.
# Must be recognized by BOTH png() and grid::convertUnit,
# probably limited to "in", "cm", "mm".
# Note especially that "px" does NOT work.
# ...
# Other arguments passed to png() when setting up the throwaway plot.
#
# Intended usage:
# a=ggplot(...)
# d=get_dims(a, maxheight=1200, maxwidth=800)
# png("plot_of_a.png", height=d$height, width=d$width)
# plot(a)
# dev.off()
####
# open a plotting device to do the calculations inside
tmpf = tempfile(pattern="dispos-a-plot", fileext= ".png")
png(filename=tmpf, height=maxheight, width=maxwidth, units=units, ...)
on.exit({dev.off(); unlink(tmpf)})
# Doesn't give us the real aspect ratio, but if both are unset
# then we can exit without building -- the plot will fill the whole area.
if ("ggplot" %in% class(ggobj)
&& is.null(ggobj$theme$aspect.ratio)
&& is.null(ggobj$coordinates$ratio)){
return(c(height=maxheight, width=maxwidth))}
if("ggplot" %in% class(ggobj)){
g = ggplot_gtable(ggplot_build(g))
}else if ("gtable" %in% class(ggobj)){
g = ggobj
}else{
stop(paste(
"Don't know how to get sizes for object of class",
class(ggobj)))
}
panel_layout = g$layout[grepl("panel", g$layout$name),]
n_rows = length(unique(panel_layout$t))
n_cols = length(unique(panel_layout$l))
# panels have unit "null", but they carry ratio information anyway.
gw_units <- sapply(g$widths, attr, "unit")
gh_units <- sapply(g$heights, attr, "unit")
asp = (unlist(g$heights[gh_units == "null"])
/ unlist(g$widths[gw_units == "null"]))
if(length(unique(asp)) > 1){
stop("panels have different aspect ratios?!")}
asp = asp[1]
# convertUnit treats null units as 0,
# so this sum gives the dimensions of *non-panel* grobs.
known_hts = sum(grid::convertHeight(g$heights, units, valueOnly=T))
known_wds = sum(grid::convertWidth(g$widths, units, valueOnly=T))
free_ht = maxheight - known_hts
free_wd = maxwidth - known_wds
# Whew. Now we're ready to calculate image dimensions.
height = maxheight
width = ((free_ht / n_rows) / asp * n_cols) + known_wds
if (width > maxwidth){
width = maxwidth
height = ((free_wd / n_cols) * asp * n_rows) + known_hts}
return(list(height=height, width=width))
}
gg_png = function(
ggobj,
filename,
maxheight=400,
maxwidth=400,
res=300,
units="in",
...){
# Plot a ggplot or gtable object to a correctly-shaped PNG.
# This is a thin wrapper around get_dims for the simplest use case.
# For more complex setups, use get_dims to compute the device size
# and then handle the plotting yourself.
dims = get_dims(
ggobj=ggobj,
maxheight=maxheight,
maxwidth=maxwidth,
res=res,
units=units,
...)
png(
filename=filename,
height=dims$height,
width=dims$width,
res=res,
units=units, ...)
plot(ggobj)
dev.off()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment