rguides

Publication-Ready Charts with ggplot2

Publication-ready charts require more than default ggplot2 aesthetics. Journals, conferences, and reports each have specific requirements for font sizes, dimensions, and color schemes. This guide walks you through creating professional charts that meet these standards.

Understanding publication requirements

Different output contexts demand different specifications:

  • Journal articles: 300 DPI minimum, TIFF or PDF format, typically 85mm or 174mm width (one or two columns)
  • Presentations: Vector formats preferred, larger fonts, high contrast
  • Web/HTML: PNG or SVG, responsive dimensions, web-safe fonts

The key is controlling every visual element deliberately.

Setting up your environment

# Install and load required packages
install.packages(c("ggplot2", "scales", "showtext", "ggthemes"))
library(ggplot2)
library(scales)
library(showtext)
library(ggthemes)

Enable higher-resolution rendering:

# Set DPI for raster outputs
options(device = "png")

Controlling dimensions and aspect ratio

Publication charts typically use specific width standards:

# Standard journal widths in mm (one column / two column)
width_one_col <- 85
width_two_col <- 174
height_mm <- width_one_col / 1.618  # Golden ratio

# Convert to inches for ggplot
width_inch <- width_one_col / 25.4
height_inch <- height_mm / 25.4

Save with precise dimensions:

ggsave(
  "figure1.tiff",
  width = width_inch,
  height = height_inch,
  dpi = 300,
  units = "in",
  compression = "lzw"
)

Customizing theme elements

The theme system gives you complete control:

# Create a publication-ready theme
pub_theme <- theme_bw() +
  theme(
    # Text sizes (in points, matching journal requirements)
    plot.title = element_text(size = 12, face = "bold", hjust = 0),
    plot.subtitle = element_text(size = 10, hjust = 0),
    axis.title = element_text(size = 10, face = "bold"),
    axis.text = element_text(size = 9),
    legend.title = element_text(size = 9, face = "bold"),
    legend.text = element_text(size = 8),
    legend.position = "bottom",
    
    # Grid lines - minimal for publications
    panel.grid.minor = element_blank(),
    panel.grid.major = element_line(color = "gray90"),
    
    # Remove background clutter
    panel.border = element_rect(color = "black", fill = NA, linewidth = 0.5),
    strip.background = element_rect(fill = "gray95", color = "black")
  )

# Apply the theme
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 2) +
  labs(title = "Car Weight vs. Miles Per Gallon",
       subtitle = "Data from motor trend magazine",
       x = "Weight (1000 lbs)",
       y = "Miles per Gallon",
       color = "Cylinders") +
  pub_theme

Professional color palettes

Avoid default rainbow scales. Use perceptually uniform palettes:

# Color-blind friendly palette
cb_palette <- c("#000000", "#E69F00", "#56B4E9", "#009E73", 
                "#F0E442", "#0072B2", "#D55E00", "#CC79A7")

# Viridis scale (perceptually uniform)
scale_color_viridis_d(option = "D")

# Use with your plot
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
  geom_point(size = 2) +
  scale_color_manual(values = cb_palette) +
  pub_theme

For sequential data:

# Gradient from light to dark
scale_fill_gradient(low = "#F7FBFF", high = "#08306B")

# Or use the viridis continuous scale
scale_fill_viridis_c(option = "C")

Typography and font handling

Proper fonts improve readability and professionalism:

# Load Google Fonts (requires showtext)
font_add_google("Roboto", "roboto")
font_add_google("Roboto Slab", "roboto-slab")

# Set default font
theme_set(theme_bw(base_family = "roboto"))

# In your theme definition
pub_theme <- theme_bw(base_family = "roboto", base_size = 10) +
  theme(...)

Exporting for specific formats

Different outputs require different settings:

# For journals (TIFF, 300 DPI)
ggsave(
  "figure1.tiff",
  plot = last_plot(),
  width = 3.35,
  height = 2.5,
  dpi = 300,
  units = "in",
  compression = "lzw"
)

# For presentations (PDF, vector)
ggsave(
  "figure1.pdf",
  plot = last_plot(),
  width = 6,
  height = 4,
  device = cairo_pdf
)

# For web (PNG, optimized)
ggsave(
  "figure1.png",
  plot = last_plot(),
  width = 800,
  height = 600,
  units = "px",
  dpi = 96
)

Building a reusable chart function

Create a function for consistent styling across figures:

pub_plot <- function(data, x, y, fill = NULL, 
                     x_label = NULL, y_label = NULL,
                     title = NULL, subtitle = NULL,
                     color_palette = NULL) {
  
  p <- ggplot(data, aes(x = {{x}}, y = {{y}})) 
  
  if (!is.null(fill)) {
    p <- p + aes(fill = {{fill}}) + geom_col()
  } else {
    p <- p + geom_point()
  }
  
  p <- p + labs(title = title, subtitle = subtitle,
                x = x_label, y = y_label) +
    pub_theme
  
  if (!is.null(color_palette)) {
    p <- p + scale_fill_manual(values = color_palette)
  }
  
  return(p)
}

# Use the function
pub_plot(mtcars, wt, mpg, fill = factor(cyl),
         x_label = "Weight (1000 lbs)",
         y_label = "Miles per Gallon",
         title = "Weight vs. Efficiency",
         color_palette = cb_palette)

Complete example: multi-Panel figure

Combine multiple plots into a publication figure:

library(patchwork)

# Create individual plots
p1 <- ggplot(mtcars, aes(x = wt, y = mpg)) + 
  geom_point() + geom_smooth(method = "lm") +
  labs(title = "A") + pub_theme

p2 <- ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
  geom_boxplot() +
  labs(title = "B") + pub_theme

p3 <- ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 10) +
  labs(title = "C") + pub_theme

# Combine with patchwork
combined <- (p1 | p2) / p3

# Save combined figure
ggsave(
  "figure2.tiff",
  plot = combined,
  width = width_two_col / 25.4,
  height = 4,
  dpi = 300,
  units = "in",
  compression = "lzw"
)

Common pitfalls to avoid

  • Tiny text: Always check font sizes at final dimensions
  • Low resolution: Use 300+ DPI for print
  • Rainbow colors: Use color-blind friendly palettes
  • Cluttered grids: Minimal gridlines for publications
  • Inconsistent styling: Create and reuse a theme function
  • Wrong aspect ratios: Match journal specifications exactly

Color and contrast

Publication journals typically require figures that are readable in both color and grayscale. Use scale_color_grey() and scale_fill_grey() for grayscale variants. For color, ColorBrewer palettes (scale_color_brewer(palette = "Set2")) are designed for clarity and are colorblind-safe. The colorspace package provides WCAG-compliant color palettes with contrast ratio guarantees. Test your color choices with colorspace::demoplot() to preview how they appear under various color vision deficiencies.

Export settings

Target plot dimensions to the journal’s column width specification, typically 3.5 inches for a single column or 7 inches for full page. ggsave("fig.pdf", width = 3.5, height = 3, units = "in", dpi = 300) at 300 DPI meets most submission requirements. Vector formats (PDF, SVG, EPS) are preferred for publication because they scale without pixelation. Check the journal’s style guide for the required format before finalizing.

Annotations and labels

Publication charts often require annotations to highlight specific data points or add context. ggplot2::annotate() adds text, rectangles, arrows, or other geoms at specified coordinates. ggrepel::geom_text_repel() places text labels that avoid overlapping each other and the data points. For significance brackets in statistical comparisons, ggsignif::geom_signif() draws brackets with p-value annotations between groups.

Aspect ratio and plot size

Match the plot’s aspect ratio to its intended use. For figures spanning two journal columns (7 inches wide), a 7:4 aspect ratio (width:height) is common for landscape data. For insets or narrow columns, 1:1 or portrait ratios may suit the data better. coord_fixed(ratio = 1) locks the x and y scales to equal units, essential for geographic data and scatter plots where perceived slope should reflect actual slope.

Typography and fonts

Publication charts require careful typography. Journals often specify the font family (Computer Modern for LaTeX, Helvetica or Arial for many journals). extrafont::loadfonts() makes system fonts available to ggplot2 PDF output. ragg::agg_png() renders text through the AGG library, supporting custom fonts without extra configuration.

Font sizes follow a hierarchy. Main title: 14-16pt. Axis titles: 11-12pt. Axis tick labels: 9-10pt. Annotation text: 8-9pt. Legend text: 9-10pt. A common mistake is using the default ggplot2 font size (11pt) for everything, the title and tick labels should not be the same size.

theme(text = element_text(family = "Arial", size = 10)) sets a base size and family for all text elements. Individual element_text() calls in theme() then modify specific elements relative to this base.

Color for publications

Many journals publish in black and white, or authors need charts that remain readable when printed without color. Design for grayscale first: shapes, patterns, or line types carry the categorical distinction without color. scale_shape_manual() and scale_linetype_manual() provide non-color categorical encodings.

For color figures, colorblind-accessible palettes are essential in scientific publications. scale_color_viridis_d() (discrete) and scale_color_viridis_c() (continuous) are perceptually uniform and colorblind-safe. ggthemes::scale_color_colorblind() provides an 8-color palette designed for colorblind readers. Test your palette with colorBlindness::cvdPlot(plot).

Avoid the default ggplot2 red-green palette for categorical data with two groups, red-green color blindness (deuteranopia) is the most common form.

Figure dimensions and resolution

Journal submission guidelines specify figure dimensions and resolution. Typical targets: 3.5 inches wide (single column), 7 inches wide (double column); 300 DPI for color figures, 600 DPI for line art. ggsave("fig1.tiff", plot, width = 3.5, height = 2.8, units = "in", dpi = 300) hits these targets.

cairo_pdf("fig1.pdf") writes vector PDF, which scales to any resolution. PDF is preferred for line art and text. For raster figures with gradients or photographic content, TIFF at 300 DPI is standard.

Point sizes in exported figures correspond to print dimensions: at 3.5 inches and 300 DPI, the figure is 1050 pixels wide. A 10pt font at 72 points/inch is 139 pixels at 300 DPI, large enough to read. Preview at actual print size rather than in the RStudio viewer, which shows device pixels.

Layout and multi-Panel figures

patchwork arranges multiple ggplot2 plots into a single figure: p1 + p2 places panels side by side; p1 / p2 stacks them vertically. (p1 | p2) / p3 combines operators for complex layouts. plot_annotation(tag_levels = "A") adds panel labels (A, B, C).

Align axes across panels with patchwork::align_patches() or set identical coord_cartesian(xlim, ylim) on each panel. Shared legends reduce chart area: plot_layout(guides = "collect") moves all legends to a single position.

For very complex multi-panel figures with precise control over panel sizes, gridExtra::grid.arrange() or cowplot::plot_grid() offer pixel-level alignment control.

Removing chart junk

Publication figures prioritize data density over decoration. Remove elements that do not carry information: theme(panel.grid.minor = element_blank()) removes minor gridlines; panel.grid.major = element_blank() removes major gridlines for charts where gridlines add noise rather than reference. panel.border = element_blank() removes the box around the plot area. axis.line = element_line() adds just the axis lines if needed.

Legends should be positioned where they waste least space. If a chart has only two series and the annotation is obvious, label the lines directly with geom_label() and remove the legend with theme(legend.position = "none"). Direct labels reduce the eye travel between chart and legend key.

Publication standards

Charts in academic publications, reports for external audiences, and journalism have higher standards than charts for internal analysis. Publication charts must be self-contained, they must communicate their message without the surrounding text that explains context. They must be visually clean, no unnecessary chart junk, consistent fonts, appropriate figure dimensions. They must be accessible, sufficient contrast, patterns not only colors for categories, readable at smaller sizes.

The difference between an analysis chart and a publication chart is mostly decisions about defaults. ggplot2’s defaults are chosen for exploratory work, not publication. Changing the theme, controlling the font, adjusting axis labels, and removing unnecessary elements converts an exploratory chart to a publication-quality one without changing the underlying data mapping.

Removing chart junk and visual noise

Edward Tufte’s data-ink ratio principle holds that every ink mark on a chart should earn its place by conveying information. Background grid lines that are not needed for reading values, tick marks without labels, axis titles that duplicate obvious information, these are candidates for removal. Each removal simplifies the chart and focuses attention on the data.

In ggplot2, theme adjustments control every visual element. The minimal themes (theme_minimal, theme_classic) remove the background and reduce the grid. From a minimal theme, selectively adding back elements, a light horizontal grid for bar charts where reading values matters, axis ticks for continuous scales — produces a clean chart that retains the useful navigational elements.

See also