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
ggplot2-extensions— Extend ggplot2 with patchwork, ggrepel, and gganimatebase-r-plotting— Base R graphics and plotting alternativesinteractive-plots-plotly— Interactive visualizations with plotly