Exporting publication-quality ggplot figures: resolution, vector formats, and fonts

R
Data visualization
ggplot2
Workflow
How to save ggplot2 figures that meet journal requirements: setting physical size and resolution, choosing PNG, TIFF, PDF or SVG, and embedding fonts so your text survives, with an ecology example.
Author

Tidy Ecology

Published

2026-06-23

You have a figure you are happy with. Then the submission portal asks for 300 dpi, a width of exactly 85 mm, fonts embedded, and a vector file if possible. The good news is that ggsave does all of this from a few arguments, and you never have to screenshot a plot again. This post walks through the settings that matter, using one ecology figure as the running example.

library(ggplot2)

set.seed(7)
elev <- runif(120, 200, 2600)
rich <- rpois(120, exp(1.4 + 1.7 * exp(-((elev - 1300) / 600)^2)))
df <- data.frame(elev, rich)

p <- ggplot(df, aes(elev, rich)) +
  geom_point(color = "#2f6d54", alpha = 0.75, size = 2) +
  geom_smooth(method = "loess", formula = y ~ x, se = TRUE,
              color = "#1d5b4e", fill = "#cfe0d4", linewidth = 0.9) +
  labs(x = "Elevation (m)", y = "Species richness",
       title = "Species richness peaks at mid elevation") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        panel.grid.major = element_line(color = "#dad9ca", linewidth = 0.3),
        plot.title = element_text(face = "bold", color = "#16241d"))
p

The example figure: simulated species richness along an elevation gradient, with a loess smoother.

Size and resolution

The two arguments that decide everything are the physical size (width, height, with units) and, for raster output, the resolution (dpi). Set the size to the space the figure will occupy in print, not to whatever looks right on screen. Most journals use a single-column width near 85 mm and a double-column width near 170 mm. For resolution, 300 dpi is the usual minimum for plots with shading or colour, and 600 dpi or more is asked for line art where thin strokes must stay crisp.

The pixel dimensions of a raster file are just the physical size times the resolution. A figure 85 mm wide at 300 dpi is 85 / 25.4 * 300, almost exactly 1004 pixels across. We can write the file and read the dimensions back to confirm it.

out <- tempdir()
ggsave(file.path(out, "fig_300.png"), p, width = 85, height = 85, units = "mm", dpi = 300)
ggsave(file.path(out, "fig_600.png"), p, width = 85, height = 85, units = "mm", dpi = 600)

png_dim <- function(f) {                       # read width and height from the PNG header
  x <- readBin(f, "integer", n = 6, size = 4, endian = "big")
  c(width = x[5], height = x[6])
}
rbind(`300 dpi` = png_dim(file.path(out, "fig_300.png")),
      `600 dpi` = png_dim(file.path(out, "fig_600.png")))
        width height
300 dpi  1003   1003
600 dpi  2007   2007

The 300 dpi file is about 1003 pixels square and the 600 dpi file is twice that on each side. Doubling the resolution quadruples the pixel count and the file size, so do not ask for 600 dpi unless the figure needs it.

Prefer vector when you can

A raster file (PNG, TIFF) is a fixed grid of pixels: zoom in far enough and you see the squares. A vector file (PDF, SVG) stores the lines and text as instructions, so it stays sharp at any size and the text remains real text. For scatter plots, line plots, and anything text-heavy, vector is the better choice when the journal accepts it, and the files are usually smaller too.

ggsave(file.path(out, "fig.pdf"), p, width = 85, height = 85, units = "mm", device = cairo_pdf)
Warning in grSoftVersion(): unable to load shared object '/Library/Frameworks/R.framework/Resources/modules//R_X11.so':
  dlopen(/Library/Frameworks/R.framework/Resources/modules//R_X11.so, 0x0006): Library not loaded: /opt/X11/lib/libSM.6.dylib
  Referenced from: <C09D78D1-7747-3352-8D6A-DBD3D49D82B0> /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/modules/R_X11.so
  Reason: tried: '/opt/X11/lib/libSM.6.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/X11/lib/libSM.6.dylib' (no such file), '/opt/X11/lib/libSM.6.dylib' (no such file), '/Library/Frameworks/R.framework/Resources/lib/libSM.6.dylib' (no such file), '/Library/Java/JavaVirtualMachines/jdk-11.0.18+10/Contents/Home/lib/server/libSM.6.dylib' (no such file)
Warning in cairoVersion(): unable to load shared object '/Library/Frameworks/R.framework/Resources/library/grDevices/libs//cairo.so':
  dlopen(/Library/Frameworks/R.framework/Resources/library/grDevices/libs//cairo.so, 0x0006): Library not loaded: /opt/X11/lib/libXrender.1.dylib
  Referenced from: <02FE3153-979A-31F7-9F1C-7F836882B951> /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library/grDevices/libs/cairo.so
  Reason: tried: '/opt/X11/lib/libXrender.1.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/opt/X11/lib/libXrender.1.dylib' (no such file), '/opt/X11/lib/libXrender.1.dylib' (no such file), '/Library/Frameworks/R.framework/Resources/lib/libXrender.1.dylib' (no such file), '/Library/Java/JavaVirtualMachines/jdk-11.0.18+10/Contents/Home/lib/server/libXrender.1.dylib' (no such file)
Warning in device(filename =
"/var/folders/q_/7sk8rjz12jl6c9x87f199l4c0000gn/T//RtmpVtgiR6/fig.pdf", :
failed to load cairo DLL
ggsave(file.path(out, "fig.svg"), p, width = 85, height = 85, units = "mm")
ggsave(file.path(out, "fig.tiff"), p, width = 85, height = 85, units = "mm",
       dpi = 300, compression = "lzw")

files <- c("fig_300.png", "fig.tiff", "fig.pdf", "fig.svg")
data.frame(file = files,
           kb = round(file.size(file.path(out, files)) / 1024, 1))
         file    kb
1 fig_300.png 149.9
2    fig.tiff 147.1
3     fig.pdf    NA
4     fig.svg  25.9

The PDF and SVG come out a fraction of the size of the raster files and will print sharply at any scale. TIFF is the format some journals still insist on; use LZW compression to keep it from ballooning.

Embed your fonts

Here is the trap that bites people after acceptance. The base PDF device does not embed fonts by default, so a reader (or a typesetter) without your exact font sees a substitute, and labels can shift or break. The cairo_pdf device embeds and subsets the fonts into the file, which is why we used it above. You can confirm it from a terminal with pdffonts fig.pdf: the font shows up with emb set to yes. For SVG, the svglite device writes clean, standards-compliant files, and most submission systems handle them well.

If a publisher cannot accept embedded fonts at all, the fallback is to convert the text to outlines (paths) so it no longer depends on a font being present. Reaching for cairo_pdf first avoids the problem in nearly every case.

A small recipe

For day-to-day work it helps to wrap the common cases in one function, so every figure in a manuscript comes out at the same size and quality.

save_fig <- function(plot, name, width = 85, height = 85, dpi = 300) {
  ggsave(paste0(name, ".pdf"), plot, width = width, height = height,
         units = "mm", device = cairo_pdf)                       # vector master
  ggsave(paste0(name, ".png"), plot, width = width, height = height,
         units = "mm", dpi = dpi)                                 # raster preview
  invisible(name)
}
# save_fig(p, file.path(tempdir(), "richness_elevation"), width = 170)

Keep a vector master and a raster preview of every figure, set the width to the column it will sit in, and let the journal tell you which file to upload. With the size and device fixed up front, a revision is one call, not an afternoon in an image editor.

References

Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis, 2nd edition. Springer, Cham. https://doi.org/10.1007/978-3-319-24277-4

Wickham, H., Henry, L., Pedersen, T. L., Luciani, T. J., Decorde, M. and Lise, V. (2023). svglite: An SVG Graphics Device. R package version 2.1.3. https://CRAN.R-project.org/package=svglite