ggplot2: Facets, Themes & Scales
Reviewed & published by Brayan K
This lesson takes ggplot2 beyond the basic plot: facets split a chart into small multiples, themes restyle every non-data element, and scales control your axes, labels, and colours — the tools that turn a rough plot into a polished, publication-ready graphic.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll use facet_wrap()/facet_grid(), apply themes like theme_minimal() and fine-tune with theme(), format axes with scale_*_continuous(), label everything with labs(), and export with ggsave().
What You'll Learn in This Lesson
- How facet_wrap() and facet_grid() build small multiples
- When to free panel scales with scales = "free"
- How complete themes restyle a plot in one line
- How theme() fine-tunes individual elements
- How scales control axis ranges, breaks, labels, and colours
- How labs() labels everything and ggsave() exports to a file
1️⃣ Faceting: Small Multiples
Faceting breaks a single plot into a grid of panels, one per group, all sharing the same axes so comparisons are honest. facet_wrap(~ var) wraps panels for one variable; facet_grid(rows ~ cols) crosses two variables into a matrix.
# Faceting splits ONE plot into a grid of small multiples, one panel
# per group — so you compare subgroups on identical axes.
library(ggplot2)
# facet_wrap(): wrap panels by a single variable into a tidy grid.
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
facet_wrap(~ class) # one panel per car class
# facet_grid(): lay panels out by TWO variables (rows ~ columns).
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # drive type down rows, cylinders across
# Free the axes per panel when ranges differ a lot:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, scales = "free")What renders: the first plot shows seven scatter panels (one per class); the second a grid with drive type down the rows and cylinder count across the columns; the third frees each panel's axes so groups with different ranges aren't squashed.
2️⃣ Themes: Restyle Everything
A theme governs all the non-data ink. A complete theme swaps the entire look in one call; then theme() lets you adjust individual elements like legend position or title weight.
# A THEME controls all the non-data ink: background, gridlines, fonts,
# legend position. Swap the whole look with one complete theme...
library(ggplot2)
p <- ggplot(mpg, aes(displ, hwy, colour = class)) + geom_point()
p + theme_minimal() # clean, no grey panel
p + theme_classic() # axis lines, no gridlines
p + theme_bw() # black-and-white, light grid
# ...then fine-tune individual elements with theme():
p +
theme_minimal() +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold", size = 16),
panel.grid.minor = element_blank()
)What renders: theme_minimal() removes the grey panel for a clean white background; theme_classic() draws bare axis lines; the final block keeps the minimal base but pushes the legend to the bottom, bolds the title, and hides the minor gridlines.
3️⃣ Scales, Labels & Saving
Scales map data to what you see. scale_x_continuous() and friends set axis ranges, breaks, and tick labels; colour scales pick the palette. The scales package supplies label helpers like comma and percent.
# SCALES translate data values into what you SEE — axis ranges, tick
# labels, and the colours of an aesthetic.
library(ggplot2)
library(scales) # for label helpers like comma() and percent()
ggplot(mpg, aes(displ, hwy, colour = class)) +
geom_point() +
scale_x_continuous(
name = "Engine displacement (L)",
limits = c(0, 8),
breaks = seq(0, 8, by = 2)
) +
scale_y_continuous(labels = scales::comma) + # 1,000-style labels
scale_colour_brewer(palette = "Set2") # a colourblind-safe palette
# percent labels are just as easy when y is a proportion:
# scale_y_continuous(labels = scales::percent)Finally, labs() centralises every text label and ggsave() exports the figure at a precise size and resolution.
# labs() sets every text label in one place; ggsave() writes the last
# (or a named) plot to a file at a chosen size and resolution.
library(ggplot2)
p <- ggplot(mpg, aes(displ, hwy, colour = drv)) +
geom_point() +
labs(
title = "Fuel economy vs engine size",
subtitle = "Larger engines tend to be less efficient",
x = "Displacement (litres)",
y = "Highway MPG",
colour = "Drive",
caption = "Source: ggplot2::mpg"
) +
theme_minimal()
p # draws to the screen / plot pane
# Save it: width/height are in inches by default; dpi sets resolution.
ggsave("economy.png", plot = p, width = 8, height = 5, dpi = 300)Your turn. Fill in the ___ blank, run it in RStudio, and check the rendered plot against the description.
# 🎯 YOUR TURN — replace each ___ then run it.
library(ggplot2)
# Goal: a faceted, titled, minimal-theme scatter of the built-in
# 'mpg' data, with one panel per drive type.
ggplot(mpg, aes(x = displ, y = hwy, colour = class)) +
geom_point() +
facet_wrap(~ drv) + # one panel per drive type
labs(
title = ___, # 👉 replace ___ with "MPG by drive type"
x = "Displacement (L)",
y = "Highway MPG"
) +
scale_y_continuous(labels = scales::comma) +
theme_minimal()
# Expect: three panels (4, f, r), points coloured by car class, a bold
# title, clean white background, comma-formatted y-axis labels.Mini-Challenge: Publication-Ready Chart
Bring facets, a theme, labels, and export together into one finished figure. Build it from the outline, then run it in RStudio and inspect the saved PNG.
# 🎯 MINI-CHALLENGE: a publication-ready faceted bar chart.
#
# Using the built-in 'mpg' dataset, build a single ggplot that:
# 1. Shows a BAR chart of the count of cars per 'class'
# (geom_bar() counts automatically).
# 2. FACETS by 'drv' so each drive type gets its own panel
# (try facet_wrap(~ drv, scales = "free_x")).
# 3. Applies theme_minimal() and moves the legend to the bottom.
# 4. Adds a title, subtitle, axis labels, and a caption via labs().
# 5. Saves the result to "cars.png" at 8x5 inches, 300 dpi.
#
# Plots can't print here — run it in RStudio and inspect the result.
# your code hereCommon Errors (and the fix)
- ❌ facet_wrap(class) errors — ✅ faceting needs a formula: write facet_wrap(~ class) with the tilde.
- ❌ Constant colour creates a useless legend — ✅ a fixed colour goes OUTSIDE aes(): geom_point(colour = "blue"), not inside it.
- ❌ colour has no effect on bars — ✅ bars use fill, not colour (which only outlines them).
- ❌ ggsave() saved the wrong plot — ✅ pass plot = p explicitly; without it, ggsave saves the last plot drawn.
Pro Tips
- 💡 Set a default theme once: theme_set(theme_minimal()) applies it to every plot in the session.
- 💡 Colourblind-safe palettes: scale_colour_viridis_d() and scale_colour_brewer() are perceptually friendly defaults.
- 💡 Combine plots: the patchwork package lets you arrange separate plots with p1 + p2.
📋 Quick Reference — Facets, Themes & Scales
| Task | R Syntax |
|---|---|
| Panels by one var | + facet_wrap(~ class) |
| Panels by two vars | + facet_grid(drv ~ cyl) |
| Clean theme | + theme_minimal() |
| Move the legend | + theme(legend.position = "bottom") |
| Format axis labels | + scale_y_continuous(labels = comma) |
| Save to file | ggsave("p.png", width = 8, height = 5) |
🎉 Lesson Complete!
- ✅ facet_wrap()/facet_grid() build small multiples
- ✅ Complete themes restyle a plot; theme() tweaks single elements
- ✅ scale_*_continuous() controls axis range, breaks, and labels
- ✅ Colour vs fill, and mapping (inside aes()) vs setting (outside)
- ✅ labs() labels everything; ggsave() exports at a chosen size/dpi
Practice quiz
What does faceting do in ggplot2?
- Splits one plot into a grid of panels by group
- Changes the colour palette
- Saves the plot to a file
- Adds a trend line
Answer: Splits one plot into a grid of panels by group. Facets create small multiples: one panel per group on identical axes.
Which function wraps panels by a SINGLE variable?
- facet_grid()
- facet_wrap()
- facet_split()
- facet_one()
Answer: facet_wrap(). facet_wrap(~ var) wraps panels for one variable into a tidy grid.
Which function lays panels out by TWO variables as rows ~ columns?
- facet_pair()
- facet_wrap()
- facet_grid()
- facet_matrix()
Answer: facet_grid(). facet_grid(rows ~ cols) crosses two variables into a matrix of panels.
What does facet_wrap() REQUIRE as its argument?
- A plain column name
- A character string
- A list
- A formula with a tilde, e.g. ~ class
Answer: A formula with a tilde, e.g. ~ class. Faceting needs a formula: facet_wrap(~ class), not facet_wrap(class).
Which is a COMPLETE theme that restyles the whole plot in one call?
- theme_minimal()
- theme(legend.position = "bottom")
- element_text()
- labs()
Answer: theme_minimal(). Complete themes like theme_minimal() reset every element at once.
Which call moves the legend to the bottom?
- labs(legend = "bottom")
- theme(legend.position = "bottom")
- scale_legend("bottom")
- guides("bottom")
Answer: theme(legend.position = "bottom"). theme() tweaks individual elements such as legend.position.
Which function sets axis range, breaks, and tick labels on a continuous x-axis?
- coord_x()
- axis_x()
- scale_x_continuous()
- xlim_set()
Answer: scale_x_continuous(). scale_x_continuous() controls limits, breaks, and labels for a continuous x-axis.
To colour points BY a variable, where must the colour mapping go?
- Outside aes(), in the geom
- In ggsave()
- In a separate theme() call
- Inside aes(), e.g. aes(colour = class)
Answer: Inside aes(), e.g. aes(colour = class). Data-driven aesthetics go inside aes(); a fixed colour goes outside aes().
Which function sets the title, axis labels, and caption in one place?
- labs()
- theme()
- annotate()
- ggtitle_all()
Answer: labs(). labs() centralises title, subtitle, axis labels, legend titles, and caption.
Which function exports the plot to a file at a chosen size and dpi?
- export()
- ggsave()
- write_plot()
- save.image()
Answer: ggsave(). ggsave() writes a plot to a file; width/height are inches and dpi sets resolution.
Continue this course
- Previous: Dates with lubridate
- Next: Probability Distributions — The d/p/q/r functions, set.seed reproducibility, and sampling
- Quick reference: R cheat sheet
Frequently asked questions
When should I use facet_wrap vs facet_grid?
Use facet_wrap(~ var) when you're splitting by a single variable and just want the panels arranged compactly into rows and columns — it 'wraps' them like text. Use facet_grid(rows ~ cols) when you have two variables and want a true matrix where every row is one variable's level and every column another's, so empty combinations still get a cell. facet_grid is best for crossing two factors; facet_wrap is best for many levels of one factor.
What's the difference between a complete theme and theme()?
A complete theme like theme_minimal(), theme_bw(), or theme_classic() resets every element to a coordinated look in one call — think of it as a stylesheet. The theme() function, by contrast, tweaks individual elements (legend.position, axis.text, plot.title, panel.grid) on top of whatever complete theme you applied. The usual pattern is to add a complete theme first for the overall style, then a theme() call to adjust specific details.
Why isn't my colour aesthetic doing anything?
Two common causes. First, colour vs fill: points and lines use colour, while bars, areas, and boxplots use fill — set the one that matches your geom. Second, mapping vs setting: to colour BY a variable it must go inside aes() (aes(colour = class)); to paint everything one fixed colour it goes OUTSIDE aes() (geom_point(colour = "blue")). Putting a constant inside aes() makes ggplot create a one-level legend instead of colouring the points.
How do I control the size and quality of a saved plot?
ggsave() infers the file format from the extension (.png, .pdf, .svg) and lets you set width and height (inches by default, or use units = "cm"). For raster formats like PNG, dpi controls resolution — 300 is a good print quality, 96 is fine for screens. PDF and SVG are vector formats, so they stay crisp at any size and ignore dpi. If you omit the plot argument, ggsave() saves the last plot you drew.