Exporting publication-quality ggplot figures in R

R
data visualisation
ggplot2
workflow
ecology tutorial
Save ggplot2 figures that meet journal rules: setting physical size and resolution, choosing PNG, TIFF, PDF or SVG, and embedding fonts so your text survives.
Author

Tidy Ecology

Published

2026-05-04

Step 44 of the Tidy Ecology course, Part eight: make the result hold up.

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

A scatter of simulated species richness against elevation with a loess smoother through the points, drawn on a plain white background.

The example figure: simulated species richness along an elevation gradient, with a loess smoother. It sits on white here, not on the page colour the other figures on this site use, because white is what the journal will print.

One deliberate difference from the rest of this site: the figure above is drawn on white. Every other figure here sits on the page colour so that it reads as part of the page, but a submitted figure is not part of a page you control, and a tinted canvas will either be printed as a grey rectangle or asked about in production. Leave the background white in anything you are going to send, and let the journal supply the surround.

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, just under 1004 pixels across, which the device writes out as 1003. 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.

# cairo_pdf embeds and subsets the fonts; fall back if this R was built without cairo
pdf_device <- if (isTRUE(capabilities("cairo"))) cairo_pdf else pdf
ggsave(file.path(out, "fig.pdf"), p, width = 85, height = 85, units = "mm",
       device = pdf_device)
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  20.5
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 (10.1007/978-3-319-24277-4)

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

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.