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.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 7 figures

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

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.

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.

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.

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.

From Overplotted Scatter to Faceted Small MultiplesAll Groups Combinedxyfacet_wrap()Group AGroup BGroup CGroup D
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.

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

panels=k(k=numberofuniquelevelsofthefacetingvariable)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=rowvariablelevels,c=columnvariablelevels)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.

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() vs. facet_grid() Layout Comparisonfacet_wrap(~class)1 variable → ribbon of panelscompactmidsizesuvpickupminivan2seatersubcompactncol = 3 (wraps to next row)facet_grid(drv ~ cyl)2 variables → strict row × col matrix4 cyl6 cyl8 cyl4wdfwdrwdEvery row–col intersection exists, even if empty
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.
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 ~ .

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. 1

    Step 1 — Load libraries and inspect data

    Begin 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. 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. 3

    Step 3 — Add facet_wrap to split by class

    Append 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. 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. 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. 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 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)

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.

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

Strengths vs. limitations of faceting in ggplot2

✦

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.

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 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")

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

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"?
Show answer
Fixed scales ensure that the same physical position on one panel corresponds to the same data value on every other panel, enabling the viewer to compare magnitudes, spreads, and trends across panels by simply moving their eyes. This is the foundational property that makes small multiples effective. You would switch to 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

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.
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

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.
Show answer
Either function works here since we are faceting on a single variable. Using 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

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.
Show answer
The code would be: 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

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.
Show answer
The claim that small multiples are 'always better' ignores cognitive limits. With 120 panels, each panel becomes tiny on screen or paper, making it nearly impossible to discern trends within any single panel or to scan across 120 panels for comparison. A better approach would involve hierarchical faceting or filtering. One option: facet by region only (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.

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