R PROGRAMMING • R-SPECIFIC TOPICS (DATA WORKFLOWS)

Faceting — Use facets for small multiples (facet_wrap/facet_grid) (intro)

Split complex datasets into coordinated panel arrays that reveal patterns hidden in aggregate views.

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
Edward Tufte publishes The Visual Display of Quantitative Information, introducing the term 'small multiples' to describe repeated, consistently-scaled chart panels that invite comparison.
1999
Wilkinson's Grammar of Graphics
Leland Wilkinson publishes The Grammar of Graphics, providing the theoretical framework that decomposes every statistical graphic into layers, scales, coordinates, and facets—the foundation for ggplot2's architecture.
2005
ggplot (v0.x) Prototype
Hadley Wickham releases the first ggplot package for R, translating Wilkinson's grammar into a usable API. Early faceting primitives appear, though the syntax evolves significantly before ggplot2.
2007
ggplot2 1.0 with facet_wrap and facet_grid
ggplot2 ships with two dedicated faceting functions—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
Extension packages such as 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.

1

Small Multiples Principle

Every panel reproduces the same plot type with identical scales, aesthetics, and coordinate system. Only the data subset changes. This constancy enables rapid visual comparison.
2

Faceting Variable(s)

One or more discrete (categorical) variables partition the dataset. Each unique level, or combination of levels, generates its own panel. Continuous variables must be binned first (e.g., with cut()).
3

facet_wrap — Ribbon Layout

Takes a single faceting specification and wraps panels into a 2-D grid from left to right, top to bottom—like text on a page. You control columns with ncol or rows with nrow.
4

facet_grid — Matrix Layout

Maps one variable to rows and another to columns, producing a strict matrix where every row/column intersection is a panel. The syntax is rows ~ cols.
5

Scales Parameter

The 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
Think of faceting as a SQL GROUP BY for plots. Just as 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.

Left: all four groups plotted in a single panel create overplotting. Right: 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).

PANEL COUNT — FACET_WRAP
panels = k (k = number of unique levels of the faceting variable)
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
panels = r × c (r = row variable levels, c = column variable levels)
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
Default to 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.

Left: 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 comparison: facet_wrap vs. facet_grid
Featurefacet_wrap()facet_grid()
Typical # of faceting variables1 (can accept more via interaction())2 (one for rows, one for columns)
LayoutRibbon — wraps panels into a user-specified number of rows or columnsMatrix — rows and columns are locked to variable levels
Empty panelsOmitted (no wasted space)Shown as blank (preserves grid structure)
Layout controlncol, nrow, dirDetermined by row/col variable levels
Best forOne categorical variable with many levelsExploring the interaction between exactly two categorical variables
Marginal panels (. ~)N/ACan 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.

Faceted Scatter Plot of mpg Data
1
Step 1 — Load libraries and inspect dataBegin by loading ggplot2 and previewing the dataset. The 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 levels
2
Step 2 — Create the base scatter plot (unfaceted)Map displ 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)
3
Step 3 — Add facet_wrap to split by classAppend 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)
4
Step 4 — Allow free y-axis for detailThe pickup and SUV panels are dominated by low-hwy points, compressing the vertical spread of 2seater and compact panels. Adding 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")
5
Step 5 — Switch to facet_grid for two-variable facetingTo explore how drivetrain (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 vs. limitations of faceting in ggplot2
StrengthsLimitations
Eliminates overplotting by partitioning data into separate panelsPanel count grows linearly (wrap) or multiplicatively (grid) with factor levels—too many panels overwhelm the reader
Shared axes make cross-panel comparison direct and honestWhen 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 managementOnly 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 ambiguityLong label text can overflow the strip; custom labeller functions may be needed
KEY TAKEAWAY
Faceting is like a microservice architecture for visualization: each panel is a self-contained, identically-structured unit that does one thing well (show one group). The power emerges from the collection, not any individual panel. But just as spinning up hundreds of microservices creates orchestration overhead, creating too many facet panels overwhelms the viewer's working memory. A practical guideline: keep the panel count under ~20 for print and under ~30 for interactive dashboards.

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.

Intro vs. advanced faceting capabilities
This Lesson (Intro)Advanced Techniques
facet_wrap(~var) with one variablefacet_wrap(~var1 + var2) to wrap combinations of two+ variables
scales = "free" for uniform free scalesggh4x::facet_grid2() for independent scales on specific panels (not uniform free)
Default strip labels from factor levelsCustom labeller() functions, nested strips via ggh4x::facet_nested()
All panels on one pageggforce::facet_wrap_paginate() to paginate large faceted plots across multiple pages
Facets show subsets onlyOverlay 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

PROBLEM 1CONCEPTUAL
Explain in your own words why shared (fixed) scales are the default in ggplot2's faceting functions. Under what specific circumstance would you switch to scales = "free"?
PROBLEM 2BASIC CALCULATION
The 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.
PROBLEM 3INTERMEDIATE
Using the 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.
PROBLEM 4APPLIED
You are analyzing server response-time logs. Your data frame 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.
PROBLEM 5CRITICAL THINKING
A colleague produces a 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.

Varsity Tutors • R Programming • Faceting — Use facets for small multiples (facet_wrap/facet_grid) (intro)