Faceting in ggplot2
Introduction
When you have a categorical variable buried inside a scatter plot, spotting group-level differences becomes hard. Overplotting smears points from different categories into the same space, and even with color coding you end up with a cluttered chart.
Faceting solves this by splitting one plot into multiple panels, each showing a subset of your data. Each panel gets its own axes, and the groups sit side by side instead of piled on top of each other. The result is a figure that reveals patterns ggplot2’s built-in group aesthetic simply cannot.
ggplot2 has two facet functions: facet_wrap() and facet_grid(). They look similar but behave differently under the hood, and choosing the right one depends on how many conditioning variables you have and how you want the layout structured.
facet_wrap(): One Variable, Many Panels
Use facet_wrap() when you have a single conditioning variable (or a combination of variables expressed as a formula) and you want to arrange the resulting panels into a rectangle. It takes a one-dimensional ribbon of panels and wraps it across rows and columns.
The core syntax is a formula:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class)
This builds a scatter plot of engine displacement versus highway fuel economy, then creates one panel per vehicle class. The tilde (~) in ~class tells ggplot2 to condition on the class column.
By default, facet_wrap() arranges panels in a grid that adapts to the number of levels. You can control the layout directly with ncol or nrow:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, ncol = 3)
Setting ncol = 3 forces exactly three columns. Use nrow instead if you prefer a fixed row count.
The dir argument controls the filling direction. dir = "h" (the default) fills rows first, left to right. dir = "v" fills columns first, top to bottom, which is useful when you have many categories and want a taller layout.
You can move the strip labels with strip.position. By default, labels appear at the top of each panel. Setting strip.position = "bottom" moves them below the panels, which helps when laying out plots horizontally and you want to keep labels visible without crowding the figure.
facet_grid(): Two Variables, One Grid
Use facet_grid() when you want to cross two conditioning variables in a full two-dimensional grid. Every combination of the row and column variables gets its own panel.
The formula syntax uses a tilde on both sides: rows ~ cols.
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl)
This creates a grid where each row corresponds to a drive type (drv) and each column to a cylinder count (cyl). If you have three drive types and four cylinder counts, you get twelve panels arranged in a 3-by-4 grid.
You can also use a single-sided formula for one-dimensional layouts. Use .~ var to put the variable across columns in a single row:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(. ~ class)
Or var ~ . to put it down a single column:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(class ~ .)
One thing trips up newcomers: facet_grid() does not use strips. Row variable labels appear in the left margin and column variable labels appear in the top margin. strip.position only works with facet_wrap().
Controlling Scales with scales
By default, every panel in a faceted plot shares the same x and y scale. That makes direct comparison straightforward, but sometimes a subset’s range is so different that a shared scale obscures its pattern. That’s where the scales argument comes in.
| Value | Behaviour |
|---|---|
"fixed" | All panels share x and y scales (default) |
"free" | Each panel gets its own x and y scale |
"free_x" | x scale varies; y scale is shared |
"free_y" | y scale varies; x scale is shared |
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, scales = "free_y")
With scales = "free_y", each panel gets its own y-axis range while x-axes stay aligned across the row. This is particularly useful when group sizes differ dramatically — a class with only a handful of observations might look flat on a shared y-axis even if it has a clear within-group trend.
With facet_grid(), the grid structure imposes a constraint: all panels in a column share an x scale and all panels in a row share a y scale. This is because columns imply a shared x dimension and rows imply a shared y dimension. scales = "free" still gives each panel independent scales, but the column/row linking means you cannot independently free x across all panels.
Cleaning Up Labels with labeller
Facet labels show the variable name and value by default. For example, a panel conditioned on class = compact shows the strip label "class: compact". The labeller argument controls this display.
label_both is the default and shows both parts:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, labeller = label_both)
label_parsed treats the label text as a mathematical expression, useful when your variable names contain symbols:
# Suppose a variable is named "class (type)"
facet_wrap(~class, labeller = label_parsed)
You can also pass a custom function that receives a data frame of facet variables and returns a character vector of labels. This is handy when you want to rename levels for display without permanently changing your factor levels:
my_labels <- function(labels) {
lapply(labels, function(x) paste0("Class: ", x))
}
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, labeller = my_labels)
Dropping Unused Factor Levels
ggplot2 preserves all factor levels by default, even if some combinations have no data. This can leave you with empty panels, which is sometimes useful for context but usually just wastes space.
# Shows empty panels for combinations with no data
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl)
# Only shows combinations that exist in the data
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl, drop = TRUE)
Setting drop = TRUE removes any panel whose combination does not appear in the subset, which keeps your figure tight and focused.
Adjusting Panel Spacing
The space between panels is controlled through theme(), not through a facet argument. Use panel.spacing for uniform spacing across all panels:
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, ncol = 3) +
theme(panel.spacing = unit(1, "lines"))
For independent control over horizontal and vertical spacing, use panel.spacing.x and panel.spacing.y:
theme(
panel.spacing.x = unit(2, "cm"),
panel.spacing.y = unit(0.5, "cm")
)
The built-in approach handles most layout needs. If you need more complex relative sizing of panels and strips, the ggh4x package extends ggplot2 with functions like rel_sizes(), but the core theme() approach will carry you far.
A Practical Before-and-After
Compare the same data with and without faceting:
# Before: all points in one panel
ggplot(mpg, aes(displ, hwy)) +
geom_point()
# After: each class gets its own panel
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~class, ncol = 3)
The faceted version reveals that 2seater vehicles cluster at high displacement with moderate highway MPG, while compact and midsize cars bunch at lower displacements. SUVs form their own low-MPG cluster. These patterns are barely visible in the unfaceted plot.
Common Mistakes
Confusing facet_wrap() with facet_grid(). facet_wrap() handles one conditioning variable (or multiple expressed as an interaction). facet_grid() creates a full two-dimensional grid from two conditioning variables. They are not interchangeable.
Using strip.position with facet_grid(). It does nothing. facet_grid() labels live in the margins, not in strips.
Forgetting that scales = "free" with facet_grid() still constrains columns and rows. You cannot independently free x across every panel when the grid has columns — panels within a column share an x scale.
Leaving drop = FALSE when unused levels produce empty panels. If your factor has 12 levels but only 6 appear in the subset, you will get 6 empty panels unless you set drop = TRUE.
See Also
- Introduction to ggplot2 — the foundation: geoms, aesthetics, and layers that faceting builds on.
- Customizing ggplot2 Themes — how
theme()controls spacing, strips, and panel appearance. - ggplot2 Advanced Geoms — combining more complex geom layers with faceting for richer visualisations.