Historical Context & Motivation
The idea of displaying data as a grid of repeated, identically-scaled charts—each conditioned on a different subset—has roots in the statistical graphics tradition that predates any programming language. The concept is most commonly known as small multiples, a term coined by Edward Tufte in his 1983 book The Visual Display of Quantitative Information. Tufte argued that small multiples are the single most effective technique for presenting multivariate data because the viewer's eye can compare panels effortlessly when the coordinate system, axis limits, and aesthetic mappings remain constant across every panel.
Before ggplot2 existed, producing small multiples in R required manual looping over subsets of data, creating individual plots, and stitching them together with par(mfrow) or layout(). This approach was error-prone: axis ranges drifted between panels, legends were inconsistent, and the code was tightly coupled to a particular dataset's factor levels. Hadley Wickham's Grammar of Graphics implementation in ggplot2 formalized faceting as a first-class operation, where a single declarative function call—facet_wrap() or facet_grid()—handles subsetting, layout, shared scales, and labeling automatically.
facet_wrap() for wrapping a single variable's levels into a 2-D grid, and facet_grid() for mapping two variables to rows and columns.ggforce and ggh4x add advanced faceting (nested facets, independent axes), while ggplot2's vars() helper modernizes the faceting formula syntax.The central problem that faceting solves is the overplotting and cognitive overload that occurs when multiple groups coexist in a single plotting area. How can you simultaneously compare distributions, trends, or relationships across many categorical levels while keeping every panel visually simple? Faceting provides a principled, declarative answer.
Core Principles & Definitions
Faceting in ggplot2 rests on a handful of interlocking ideas. Understanding them lets you choose between facet_wrap() and facet_grid() confidently, reason about scale sharing, and anticipate the visual layout before running code. The following grid distills the core concepts.
Small Multiples Principle
Faceting Variable(s)
cut()).facet_wrap — Ribbon Layout
ncol or rows with nrow.facet_grid — Matrix Layout
rows ~ cols.Scales Parameter
scales argument controls whether axes are shared ("fixed") or allowed to vary per panel ("free", "free_x", "free_y"). Fixed scales preserve comparability; free scales reveal within-panel detail.GROUP BY category splits a query's aggregation into per-group results, facet_wrap(~category) splits a ggplot into per-group panels. The underlying data pipeline is the same—partition, apply, combine—but the output is a visual layout rather than a table.Visual Explanation — How Faceting Transforms a Plot
The diagram below illustrates the transformation from a single, overplotted scatter plot (left) to a faceted small-multiples display (right). Notice how the same data, axes, and point aesthetics are preserved across all four panels—only the data subset changes. The strip labels at the top of each panel identify the faceting variable's level, and the shared x/y axes make cross-panel comparison immediate.
facet_wrap(~group) produces one panel per group level. Strip labels at the top of each panel (e.g., Group A) identify the subset. Axes are shared so cross-panel comparison is immediate.Observe that in the left panel, the overlapping color-coded points make it difficult to isolate any single group's trend. After faceting, the signal-to-noise ratio within each panel improves dramatically because each panel plots only the relevant subset. Importantly, the axes remain identical across all four panels, which means any visual difference in slope, spread, or density directly reflects a real difference in the data rather than an artifact of rescaled axes.
How Faceting Works Under the Hood
Faceting is not merely a cosmetic layout operation; it involves a well-defined data pipeline inside ggplot2. When you add a facet specification to a plot, ggplot2 performs a split–apply–combine operation on the data frame before rendering. Understanding this pipeline clarifies why certain parameters exist and how they affect the final output.
The Faceting Pipeline
First, ggplot2 inspects the faceting variable(s) and computes the unique levels (or cross-product of levels for facet_grid()). For a single variable with k levels, facet_wrap() generates k panels. For facet_grid(row_var ~ col_var) with r and c levels respectively, the grid produces r × c panels—even if some combinations contain no observations (those panels render empty).
facet_wrap(~var), the number of panels equals the number of unique factor levels. The panels wrap into the specified ncol or nrow layout, filling left-to-right, top-to-bottom.facet_grid(row_var ~ col_var), the layout is a strict matrix. Unlike facet_wrap, you cannot freely choose ncol or nrow; they are determined by the data.Scale Sharing Modes
The scales parameter accepts four values: "fixed" (default), "free", "free_x", and "free_y". Choosing "fixed" guarantees that every panel uses the same axis limits, which is essential for fair comparison. Choosing "free" lets each panel zoom to its own data range—useful when different groups span vastly different magnitudes, but it sacrifices direct positional comparison across panels.
scales = "fixed" unless you have a strong reason not to. The power of small multiples comes from shared scales. Switching to "free" should be a conscious choice—for instance, when panel-to-panel magnitude differences obscure within-panel patterns.facet_wrap vs. facet_grid — When to Use Each
Choosing between facet_wrap() and facet_grid() is one of the first decisions you make when faceting. While both produce panels from categorical variables, they differ fundamentally in layout semantics and the number of faceting variables they are designed for. The diagram and table below clarify the distinction.
facet_wrap(~class, ncol = 3) wraps 7 levels of the class variable into rows of 3, filling left-to-right. Right: facet_grid(drv ~ cyl) creates a 3 × 3 matrix where drv maps to rows and cyl maps to columns.| Feature | facet_wrap() | facet_grid() |
|---|---|---|
| Typical # of faceting variables | 1 (can accept more via interaction()) | 2 (one for rows, one for columns) |
| Layout | Ribbon — wraps panels into a user-specified number of rows or columns | Matrix — rows and columns are locked to variable levels |
| Empty panels | Omitted (no wasted space) | Shown as blank (preserves grid structure) |
| Layout control | ncol, nrow, dir | Determined by row/col variable levels |
| Best for | One categorical variable with many levels | Exploring the interaction between exactly two categorical variables |
| Marginal panels (. ~) | N/A | Can facet by one variable on rows or columns using . ~ var or var ~ . |
Worked Example — Building a Faceted Plot Step by Step
We will use the built-in mpg dataset (shipped with ggplot2) to visualize the relationship between engine displacement (displ) and highway fuel economy (hwy), faceted by vehicle class. The goal is to determine whether the negative correlation between displacement and fuel economy varies across vehicle types.
mpg data frame contains 234 rows and 11 columns. The class column has 7 unique levels: 2seater, compact, midsize, minivan, pickup, subcompact, suv.library(ggplot2)
str(mpg) # 234 obs. of 11 variables
levels(factor(mpg$class)) # 7 levelsdispl to the x-axis and hwy to the y-axis, using geom_point(). At this stage all 234 points appear in a single panel. The negative trend is visible but cluttered.p <- ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.6)facet_wrap(~class) to the plot. By default, ggplot2 chooses a roughly square arrangement. We can override this with ncol = 3 to request three columns, producing a 3 × 3 grid (with the last two cells empty because there are only 7 levels).p + facet_wrap(~class, ncol = 3)scales = "free_y" lets each panel's y-axis range adapt to its data. We keep the x-axis fixed to maintain displacement comparability.p + facet_wrap(~class, ncol = 3, scales = "free_y")drv: f, r, 4) interacts with number of cylinders (cyl: 4, 5, 6, 8), replace the facet layer with facet_grid(drv ~ cyl). This produces a 3 × 4 matrix of panels, making it easy to spot that rear-wheel drive (r) exists primarily in the 6- and 8-cylinder columns.ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.6) +
facet_grid(drv ~ cyl)Strengths & Limitations of Faceting
Faceting is an exceptionally powerful visualization technique, but like any tool it has trade-offs. Recognizing when faceting excels and when it falls short prevents you from producing misleading or cluttered graphics.
| Strengths | Limitations |
|---|---|
| Eliminates overplotting by partitioning data into separate panels | Panel count grows linearly (wrap) or multiplicatively (grid) with factor levels—too many panels overwhelm the reader |
| Shared axes make cross-panel comparison direct and honest | When free scales are used, viewers may mistakenly compare magnitudes across panels with different axis limits |
| Declarative syntax—one line of code replaces dozens of lines of manual subplot management | Only supports categorical (discrete) faceting variables; continuous variables require preprocessing with cut() or ntile() |
| Naturally highlights group-level patterns (slopes, distributions, outliers) | Global patterns (overall trend across all groups) may be less visible compared to a single colored plot |
| Strip labels clearly identify each panel's subset, reducing ambiguity | Long label text can overflow the strip; custom labeller functions may be needed |
Connection to Advanced Faceting Techniques
The introductory facet_wrap() and facet_grid() calls covered in this lesson are the starting point for a rich ecosystem of advanced faceting capabilities. As your visualization needs grow more complex—nested groupings, asymmetric scale freeing, paginated output—extension packages and deeper ggplot2 configuration options become essential.
| This Lesson (Intro) | Advanced Techniques |
|---|---|
facet_wrap(~var) with one variable | facet_wrap(~var1 + var2) to wrap combinations of two+ variables |
scales = "free" for uniform free scales | ggh4x::facet_grid2() for independent scales on specific panels (not uniform free) |
| Default strip labels from factor levels | Custom labeller() functions, nested strips via ggh4x::facet_nested() |
| All panels on one page | ggforce::facet_wrap_paginate() to paginate large faceted plots across multiple pages |
| Facets show subsets only | Overlay full dataset in grey behind each panel using geom_point(data = transform(df, var = NULL), color = "grey80") |
A particularly powerful technique worth previewing is the context layer pattern: you add a copy of geom_point() that uses the full dataset (with the faceting variable removed so it appears in every panel) rendered in a muted color. This lets the viewer see each group in context against the global distribution. Mastering this pattern—and the advanced extensions listed above—will be the subject of a follow-up lesson.
Practice Problems
scales = "free"?diamonds dataset in ggplot2 has a cut column with 5 levels and a color column with 7 levels. How many panels would facet_grid(cut ~ color) produce? How many would facet_wrap(~cut) produce? Write both answers and the R code.mpg dataset, write ggplot2 code that creates a histogram of hwy faceted by drv (drivetrain), arranged in a single column (i.e., three panels stacked vertically) with free y-axis scales. Explain why you chose facet_wrap or facet_grid for this task.logs has columns: timestamp (POSIXct), response_ms (numeric), endpoint (character, 12 unique API routes), and http_method (GET, POST, PUT, DELETE). Write code that produces a faceted time-series line plot showing response time over time, faceted by endpoint (wrapped in 4 columns). Discuss whether you would use fixed or free scales.facet_grid(region ~ product_line) plot with 8 regions and 15 product lines, yielding 120 panels. They argue that small multiples are always better than a single complex chart. Critique this claim. Propose an alternative visualization strategy that retains the benefits of faceting without producing an unreadable 120-panel grid.Lesson Summary
Faceting is ggplot2's declarative mechanism for producing small multiples—a grid of identically-structured panels, each showing a subset of the data defined by one or more categorical faceting variables. Use facet_wrap(~var) when you have a single faceting variable whose levels should wrap into a configurable ribbon layout. Use facet_grid(row_var ~ col_var) when two variables should map to the rows and columns of a strict matrix. The scales parameter ("fixed", "free", "free_x", "free_y") controls whether axes are shared across panels, balancing cross-panel comparability against within-panel detail.
Faceting excels at eliminating overplotting and revealing group-specific patterns that would be hidden in a single aggregated chart. Keep panel counts manageable (roughly under 20 panels) and default to fixed scales unless magnitude differences across groups justify freeing them. Advanced extensions like ggh4x and ggforce build on this foundation with nested facets, independent axis scales, and pagination for large panel sets.