Last active
October 27, 2022 04:49
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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