ggplot2 Guide & Cookbook (R): Geoms, Scales, Labels, Themes, Facets

ggplot2 Guide & Cookbook (R): Geoms, Scales, Labels, Themes, Facets
ggplot2 Guide & Cookbook (R): Geoms, Scales, Labels, Themes, Facets

Updated 2025-11-24 04:58

ggplot2 is the most widely used R package for data visualization, built on the grammar of graphics and available as part of tidyverse suite of packages.

It lets you create elegant, layered charts with consistent syntax, but newcomers (and even seasoned R users) often struggle with details like formatting labels, adjusting legends, or customizing facets.

This hub brings together 35 step-by-step ggplot2 tutorials that solve the most common visualization challenges.

Whether you want to make titles bold, rotate axis labels, customize legends, or annotate plots with p-values and arrows, you’ll find practical examples here.

Instead of searching scattered blog posts, this page acts as a ggplot2 cookbook, organized by topic—labels, legends, axes, facets, plot types, and annotations.

Each tutorial includes code samples and visuals so you can quickly adapt the solutions to your own datasets.

By working through these resources, you’ll learn how to:

  • Format text elements (titles, subtitles, axis ticks) for readability.
  • Master legend positioning and styling.
  • Improve clarity with facet layouts and strip labels.
  • Add annotations, p-values, and highlights for storytelling.
  • Extend ggplot2 with boxplots, violins, density curves, and scree plots.

If you’re building reproducible data visualizations in R, bookmark this hub. It’s designed to grow with new tutorials, ensuring you always have an up-to-date reference for the most frequently asked ggplot2 questions.

Table of Contents

Titles & Labels

Clear, well-formatted text is the fastest way to improve chart readability. Titles guide the eye, axis labels provide context, and tick labels help the audience interpret numeric values. In ggplot2, text is controlled through labs() and theme(), and numbers can be formatted with the scales package. This section covers bolding, centering, resizing, wrapping, and coloring text elements so your plots communicate cleanly.

Most production charts need a few tweaks: make the title larger and bold, add a descriptive subtitle, and format axis ticks (e.g., currency). You’ll also run into practical layout issues like long category names; wrapping or rotating labels often makes the difference between cluttered and polished.

Quick example: titles and axis label formatting

library(ggplot2)
library(scales)

df <- data.frame(group = c("Alpha", "Beta", "Gamma"),
                 value = c(12345, 67890, 43210))

ggplot(df, aes(group, value)) +
  geom_col(fill = "#2C7FB8") +
  scale_y_continuous(labels = label_dollar()) +
  labs(title = "Revenue by Group",
       subtitle = "FY2025 YTD",
       x = "Group", y = "Revenue ($)") +
  theme(
    plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
    axis.title = element_text(face = "bold"),
    axis.text.x = element_text(angle = 0)
  )

Related Tutorials

Legends

Legends decode color, shape, and size encodings — but defaults can become crowded. In ggplot2 you can change legend keys, spacing, order, position, and even move the legend inside the plotting region. Getting the legend right helps the viewer scan comparisons quickly without hunting for meaning.

Typical fixes include enlarging key sizes for readability on slides, reversing item order to match bar order, or folding long legends into multiple rows. For compact layouts, placing the legend inside the plot with a light background can save space while keeping focus on the data.

Quick example: inside-plot legend with custom key sizes

library(ggplot2)
theme_set(theme_bw())
p <- mpg |>
  ggplot(aes(displ, hwy, color = class)) +
  geom_point() +
  labs(title = "Fuel Economy vs Displacement")
 
p +
  theme(legend.position = c(0.85, 0.6)) +
  guides(color = guide_legend(override.aes = list(size = 3))) +
  theme(legend.background = element_rect(fill = alpha("white",
                                                      0.7), 
                                         color = NA))
ggsave("inside-plot-custom-legend-ggplot2.png")
Inside-plot legend with with custom legend key size

Related Tutorials

Axes & Scales

Axes do more than display numbers: they set the stage for interpretation. Rotating labels, choosing an appropriate scale (linear/log), formatting currency or percentages, and selectively removing ticks all improve clarity. Poorly configured axes cause clutter and misreadings; well‑tuned axes let the data speak.

In ggplot2, scales are managed with scale_xxx()/scale_yyy() functions. The scales package helps format labels. For categorical axes, thoughtful ordering and shorter labels often beat default alphabetical sorting.

Quick example: currency formatting and rotated x‑labels

library(ggplot2)
library(dplyr)
library(scales)

theme_set(theme_bw(12))
df <- data.frame(month = month.abb[1:6],
                 sales = c(12000, 18000, 15000,
                           22000, 17000, 24000)) |>
  mutate(month = factor(month, levels = month.abb))

df |>
  ggplot(aes(month, sales)) +
  geom_col(fill = "#1A9850") +
  scale_y_continuous(labels = label_dollar()) +
  labs(x = "Month",
       y = "Sales ($)", 
       title = "Rotating x-axis labels") +
  theme(axis.text.x = element_text(angle = 45,
                                   hjust = 1))
ggsave("rotate_x_axis_labels_45degree_ggplot2.png")
add dollar symbol and rotate x-axis labels 45 degree with ggplot2

Related Tutorials

Facets

Faceting creates small multiples that reveal patterns across categories, time periods, or segments. It’s one of ggplot2’s superpowers, but the defaults often need refinement: strip labels can be long, panel borders may distract, and ordering can imply the wrong story. Clean faceting improves scannability and comparison.

Common upgrades include wrapping long strip labels, removing heavy borders, customizing facet label text, or enforcing a business‑friendly order (e.g., Jan–Dec instead of alphabetical). Pair facets with consistent scales to keep comparisons fair.

Quick example: tidier facet labels and light theme


library(ggplot2)
 
p <- ggplot(mpg, aes(displ, hwy)) +
  geom_point(alpha = 0.6) +
  facet_wrap(~ class, ncol=2)
 
p + labs(title = "Small multiples with faceting") +
  theme(strip.text = element_text(face = "bold"))
ggsave("small_multiples_with_facetting_ggplot2.png",
       width=6, height=8)
faceting example in ggplot2

Related Tutorials

Plot Types & Geoms

Beyond scatter and bars, ggplot2 supports a wide range of geoms to show distributions, uncertainty, and relationships. Boxplots and violins summarize spread; scree plots explain PCA variance; connecting paired points emphasizes before/after relationships. Choosing the right geom clarifies the message and reduces cognitive load.

In practice, you’ll often combine geoms (e.g., half‑violin + boxplot + jittered points) to show both summary and detail. Keep aesthetics consistent across layers and use subtle transparency to manage overplotting.

Quick example: grouped boxplot with reordered categories

library(ggplot2)
library(dplyr)

df <- mtcars |>
  transform(cyl = factor(cyl)) |>
  dplyr::select(cyl, mpg)

ggplot(df, aes(reorder(cyl, mpg, FUN = median), mpg)) +
  geom_boxplot(fill = "#3182BD") +
  labs(x = "Cylinders (ordered by median mpg)", y = "MPG")

Related Tutorials

Annotations & Labels

Annotations turn charts into explanations. Percentage labels on stacked bars, p‑values on facets, arrows and circles to highlight key points — these small additions guide attention and communicate insights directly on the visual. The goal is to help your audience understand why the pattern matters.

Use annotation layers sparingly and keep text compact. Align labels with the data, and consider color contrast and overlap. For complex layouts, packages like ggrepel help avoid collisions.

Quick example: percentage labels on stacked bars

library(ggplot2)
library(dplyr)

df <- as.data.frame(Titanic) |>
  group_by(Sex, Survived) |>
  summarise(n = sum(Freq), .groups = "drop") |>
  group_by(Sex) |>
  mutate(pct = n / sum(n))

ggplot(df, aes(Sex, pct, fill = Survived)) +
  geom_col(position = "fill") +
  geom_text(aes(label = scales::percent(pct, accuracy = 1)),
            position = position_fill(vjust = 0.5), color = "white") +
  scale_y_continuous(labels = scales::percent) +
  labs(y = "Percent", title = "Survival by Sex (Titanic)")

Related Tutorials


See also: Matplotlib Guide · Seaborn Guide


Frequently Asked Questions

How do I rotate axis labels in ggplot2 without overlapping?

Overlapping x‑axis labels are common when categories have long names or you have many of them. Rotating labels improves legibility, but angle alone isn’t enough—you should also adjust justification and consider alternatives like horizontal bars or faceting.

library(ggplot2)

# 1) Angle with justification
ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
  geom_boxplot() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# 2) Horizontal labels via coord_flip()
ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
  geom_col() + coord_flip()

# 3) Newer guide for rotation (ggplot2 3.3.0+)
ggplot(mtcars, aes(factor(cyl), mpg)) +
  geom_point() +
  scale_x_discrete(guide = guide_axis(angle = 45))

Pitfalls: vertical (90°) labels are harder to read; avoid shrinking fonts until they’re illegible; if labels are truly long, wrap or abbreviate them with stringr::str_wrap(). Best practice: start with 45° + hjust = 1, consider coord_flip() for bars, and facet when categories are too dense for a single panel.

How do I add percentage labels on bars in ggplot2?

There are two common cases: (1) you’re plotting counts, or (2) you already computed percentages. For counts, let ggplot2 compute frequencies and place labels using proportional positioning; for pre‑computed percentages, map labels directly. Keep the y‑axis formatted as percent for consistency and avoid clipping by expanding limits slightly.

library(ggplot2); library(scales); library(dplyr)

# 1) Counts - percentages on stacked bars
ggplot(as.data.frame(Titanic), aes(Sex, Freq, fill = Survived)) +
  geom_col(position = "fill") +
  geom_text(aes(label = percent(..prop.., accuracy = 1)),
            stat = "prop", position = position_fill(vjust = 0.5)) +
  scale_y_continuous(labels = percent)

# 2) Precomputed percentages
df <- tibble::tibble(group = c("A","B","C"), pct = c(0.25, 0.5, 0.25))
ggplot(df, aes(group, pct)) +
  geom_col() +
  geom_text(aes(label = percent(pct)), vjust = -0.2) +
  scale_y_continuous(labels = percent, limits = c(0, 1.1))

Gotchas: don’t compute percentages per bar but then label stacked segments with those — it misleads. Use position_fill() for stacked proportions. For grouped bars, compute group‑level percentages first. Always format the axis and labels with scales::percent for clarity.

How can I reorder categories in a ggplot2 boxplot or bar chart?

Ordering categories by a meaningful statistic makes comparisons clearer. You can reorder with base reorder() inside aes(), or use forcats helpers (fct_reorder, fct_reorder2) for more control. For boxplots, ordering by median is common; for bars, order by the value itself. Always ensure the ordering matches the story you want to tell.

library(ggplot2); 
library(forcats); 
library(dplyr)

# Boxplots ordered by median using base reorder()
ggplot(mtcars, aes(x = reorder(factor(cyl), mpg, FUN = median), y = mpg)) +
  geom_boxplot() + labs(x = "Cylinders (ordered by median MPG)")

# Bars ordered by value using forcats
df <- tibble::tibble(group = c("A","B","C"), value = c(30, 10, 20))
ggplot(df, aes(x = fct_reorder(group, value), y = value)) +
  geom_col()

Pitfalls: reordering on a filtered subset can yield different orders when you later plot a different subset; if you need consistency across views, reorder on the full dataset first. For line charts, fct_reorder2() orders by the last y value in time, which better reflects end‑state ranking.

How do I add regression lines and confidence intervals by group in ggplot2?

ggplot2 makes trend lines straightforward with geom_smooth(). To draw per‑group regressions, map a grouping aesthetic (e.g., color) and let ggplot2 fit a model per group. Use method = “lm” for linear regression; set se = TRUE/FALSE to show or hide confidence intervals. For non‑linear relationships, use loess or specify a polynomial formula.

library(ggplot2)

# Per-group linear fits with CIs
ggplot(mpg, aes(displ, hwy, color = class)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = TRUE)

# LOESS smoother without CI
ggplot(mpg, aes(displ, hwy, color = drv)) +
  geom_point() +
  geom_smooth(method = "loess", se = FALSE, span = 0.8)

Best practices: don’t extrapolate beyond observed x; LOESS is O(n²) and slows on large data; overlapping CIs don’t guarantee non‑significance — rely on formal tests if needed. If lines clutter the legend, hide point legend entries or adjust guides for clarity.

Why doesn’t ggplot2 support dual y‑axes, and what are the alternatives?

Dual y‑axes are discouraged because they invite misleading comparisons; differing scales make unrelated series appear correlated. ggplot2 allows a secondary axis only as a transformation of the primary scale via sec_axis(), meaning both axes must be linearly related (e.g., meters↔feet).


library(ggplot2)
 
# Allowed: secondary axis as a transformation of primary
economics |>
  ggplot(aes(date, psavert)) +
  geom_line() +
  scale_y_continuous(
    name = "Personal savings rate (%)",
    sec.axis = sec_axis(~ . * 1000, 
                        name = "Scaled (×1000)")
  )+
  labs(title = "Dual y-axis example with sec.axis")
ggsave("ggplot2_dual_y_axis_example.png")

# Safer alternative: facets
# ggplot(df_long, aes(date, value)) + geom_line() + facet_wrap(~ series, scales = "free_y")
Dual y-axis with ggplot2

Guidance: only use sec_axis() when the two axes share a meaningful linear transform. Otherwise, prefer facets, normalize series (index to 100), or plot differences/ratios. These designs keep the story honest and easier to read.

Want to use Seaborn in Python?

We also maintain a complete Seaborn guide with 35+ tutorials and recipes.

Explore the Seaborn Guide →


Exit mobile version