SECTION 1
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.
1983 • Tufte Defines Small Multiples
1999 • Wilkinson's Grammar of Graphics
2005 • ggplot (v0.x) Prototype
2007 • ggplot2 1.0 with facet_wrap and facet_grid
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.2020+ • Extensions and Modern Use
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.
SECTION 2
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.KEY TAKEAWAY
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.SECTION 3
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.
SECTION 4
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).
PANEL COUNT — FACET_WRAP
When using 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.
PANEL COUNT — FACET_GRID
With 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.
Design Decision
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.SECTION 5
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 ~ . |
Feature comparison: facet_wrap vs. facet_grid
SECTION 6
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.
Faceted Scatter Plot of mpg Data
- 1
Step 1 — Load libraries and inspect data
Begin by loading ggplot2 and previewing the dataset. Thempgdata frame contains 234 rows and 11 columns. Theclasscolumn 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 levels - 2
Step 2 — Create the base scatter plot (unfaceted)
Mapdisplto the x-axis andhwyto the y-axis, usinggeom_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) - 3
Step 3 — Add facet_wrap to split by class
Appendfacet_wrap(~class)to the plot. By default, ggplot2 chooses a roughly square arrangement. We can override this withncol = 3to 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) - 4
Step 4 — Allow free y-axis for detail
The pickup and SUV panels are dominated by low-hwy points, compressing the vertical spread of 2seater and compact panels. Addingscales = "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") - 5
Step 5 — Switch to facet_grid for two-variable faceting
To explore how drivetrain (drv: f, r, 4) interacts with number of cylinders (cyl: 4, 5, 6, 8), replace the facet layer withfacet_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)
SECTION 7
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 |
Strengths vs. limitations of faceting in ggplot2
KEY TAKEAWAY
SECTION 8
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") |
Intro vs. advanced faceting capabilities
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
scales = "free"?Show answer
scales = "free" when one or more groups span a dramatically different range from the others—for example, if one group's y-values range from 0 to 10 and another's range from 0 to 10,000. In that scenario, fixed scales would compress the smaller group into an unreadable flat line, so freeing the scales reveals within-panel structure at the cost of cross-panel positional comparison.Question 2: BASIC CALCULATION
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.Show answer
facet_grid(cut ~ color) produces 5 × 7 = 35 panels arranged in a 5-row by 7-column matrix. facet_wrap(~cut) produces 5 panels, wrapped into a layout that ggplot2 auto-selects (likely 2 rows × 3 columns, with one empty cell). The code is: ggplot(diamonds, aes(carat, price)) + geom_point(alpha=0.05) + facet_grid(cut ~ color) for the grid version and the same base plot with + facet_wrap(~cut) for the wrap version.Question 3: INTERMEDIATE
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.Show answer
facet_wrap: ggplot(mpg, aes(x = hwy)) + geom_histogram(binwidth = 2) + facet_wrap(~drv, ncol = 1, scales = "free_y"). The equivalent using facet_grid would be facet_grid(drv ~ ., scales = "free_y") where the dot on the right side of the formula means 'no column variable.' Either approach is valid; facet_grid(drv ~ .) is more idiomatic when you want a strict single-column layout, while facet_wrap(~drv, ncol = 1) is more explicit about the column count.Question 4: APPLIED
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.Show answer
ggplot(logs, aes(x = timestamp, y = response_ms)) + geom_line(alpha = 0.7) + facet_wrap(~endpoint, ncol = 4, scales = "free_y") + labs(x = "Time", y = "Response (ms)"). Using facet_wrap is appropriate here because 12 endpoints would create a ribbon layout (3 rows × 4 columns). Regarding scales: if some endpoints average 10 ms and others 2000 ms, fixed y-axes would squash the fast endpoints into flat lines. In that scenario, scales = "free_y" is justified to reveal per-endpoint trends. Keep x fixed so temporal alignment is preserved across panels, enabling cross-endpoint event correlation.Question 5: CRITICAL THINKING
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.Show answer
facet_wrap(~region, ncol = 4), 8 panels) and use color to encode product_line, with an interactive filter or a separate set of plots for selected product lines. Another option: aggregate product lines into 3–5 categories, reducing the grid to 8 × 4 = 32 panels. A third option is to use ggforce::facet_wrap_paginate() to split the 120 panels across multiple pages, each page showing one region's 15 product-line panels. The key principle is that faceting is a tool, and like all tools its effectiveness depends on appropriate application. Panel counts above ~20–30 generally exceed the viewer's ability to scan and compare.SUMMARY
Lesson Summary
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.