Facets & Scales
Maya the neighbourhood baker did well enough to open two more branches. She now runs three: Downtown, Riverside, and the new one inside the Airport. Each branch logs the same two numbers every day, the foot traffic (people who walked in) and the revenue (dollars taken). When she draws all three branches on one chart, it turns into a tangle: the Airport branch is so much bigger that the other two flatten into the floor.
The fix is one of the most useful moves in all of data visualization: stop forcing every group onto one panel, and give each group its own little chart. Below is exactly that, the same data as one chart, then split into small multiples. Flip the toggle.
By the end of this lesson you will be able to:
- Say when small multiples beat cramming every group into one panel, and what
facet_wrap(~ var)does - Split a plot into one panel per group with
facet_wrap(), and one panel per combination withfacet_grid() - Choose fixed vs free panel scales without misleading your reader, and bend the axis, guides and legend to your will
Prerequisites: you can build a basic ggplot and map a column to colour, ggplot(data, aes(...)) + geom_*() (from Data Visualization with ggplot2, especially the grammar of graphics). Every new function is defined as it appears.
When one panel has too many stories
Let us put Maya's three branches into this fresh R session. Each lesson starts clean, so the data lives right here on the page, run this block once and the rest of the lesson builds on it:
The instinct is to map branch to colour and draw all 21 points on one panel. Try it and you hit a wall: Airport's revenue runs into the thousands while Riverside barely clears a few hundred, so the shared y-axis stretches to fit Airport and crushes the other two branches into a flat band at the bottom. Worse, the points pile on top of each other (that is overplotting), so even the colours cannot rescue it. The widget below is that crowded one-panel chart, three branches, one squashed scale.
facet_wrap: one panel per group
Here is the move. Instead of one panel holding every branch, facet the plot: ggplot splits it into a grid of small panels, one per group, each drawn the same way. This is what people mean by small multiples, the same chart repeated for each slice of the data so your eye can scan across them.
You add it as one more layer with +, just like a geom. The argument is a formula: facet_wrap(~ branch). The tilde ~ reads as "by", so ~ branch means "make one panel for each value of branch". The leading tilde with nothing before it is just how facet_wrap wants a single faceting variable written:
Now each branch gets its own little panel, side by side, and the within-branch pattern (more foot traffic, more revenue) is suddenly easy to read in all three. The toggle below makes the before and after concrete: one combined chart, then facet_wrap(~ branch).
facet_wrap(~ var) draws one panel per level of var and wraps the panels into a tidy grid. It is the natural answer to "show me this same chart, broken out by group."The one limit to remember: facets shine for a handful of groups. Facet by a variable with 50 levels and you get 50 thumbnails too small to read. For that many groups, summarise first or facet by a coarser grouping.
Facets or just colour?
Maya already tried colouring the three branches on one panel and it came out as an unreadable squashed tangle. A colleague says "faceting is just colouring by group with extra steps, it will look the same." Is that right?
facet_grid: cross two variables
facet_wrap splits by one variable. Sometimes you want to split by two at once, laid out as a true matrix: one variable down the rows, another across the columns. That is facet_grid(rows ~ cols), with the row variable on the left of the ~ and the column variable on the right.
Suppose Maya wants to compare weekdays against weekends for each branch. First derive a part column that tags each day as either "weekend" or "weekday" (%in% tests whether the day is Sat or Sun, and ifelse picks the label), then facet branch (rows) by part (columns):
That draws a 3-by-2 grid: three branches down, weekday and weekend across, six panels in all. Reading along a row compares one branch's weekdays to its weekends; reading down a column compares the branches within weekdays or within weekends. One formula, a whole table of little charts.
facet_grid(branch ~ part) makes a panel for every combination of the two variables, even empty ones. With 3 branches and 2 parts that is 6 panels; with two big variables the grid can explode, so cross two variables only when you actually want every combination.Split it into small multiples
Maya wants the foot-traffic-vs-revenue scatter broken into one panel per branch. The plot below has the data and the geom, but the faceting layer is missing. Add the layer that draws one panel per branch.
Show answer
ggplot(sales, aes(x = foot_traffic, y = revenue)) +
geom_point(size = 2) +
facet_wrap(~ branch)Free the scales (when it is honest to)
Look closely at your faceted plot and you will notice every panel shares the same x and y axes. That is the default, and it is usually the right one: a shared scale means a point's height means the same thing in every panel, so you can compare branches at a glance. But it is also why Riverside still looks like a tiny smudge, its few hundred dollars are drawn on an axis stretched to fit Airport's thousands.
You can give each panel its own axis range with the scales argument: "free_y" frees the y-axis, "free_x" the x-axis, and "free" both:
Now each panel zooms to its own data, so Riverside's day-to-day rise is finally visible instead of flat-lined. But you paid a price, and it matters.
What did free scales cost?
Maya sets scales = "free_y" and Riverside's panel now fills its own height, with the daily rise clearly visible. Her colleague glances at the three panels and says "great, now I can see Riverside is nearly as big as Airport." What is wrong?
Bend the axis, guides and legend
Faceting laid the panels out. The last piece of control is the scales and guides, the parts of the plot that translate data into position, ticks, labels and legends. There are two ways to keep Airport from squashing the small branches without lying with free scales.
First, you can keep the shared axis but make it readable. A scale controls how a variable maps to the page; scale_y_continuous() is the continuous y-axis, and its labels argument formats the tick text. With the scales package, label_dollar() turns 2100 into $2,100:
Second, and better for data that spans this wide a range, switch the axis to a logarithmic scale with scale_y_log10(). A log axis places a value \(v\) at position \(\log_{10} v\), so each tenfold jump in dollars takes the same amount of space on the page. That pulls Airport's thousands and Riverside's hundreds onto one axis where you can read both at once:
Mapping colour = branch made ggplot add a legend automatically; a legend is one kind of guide, the key that tells the reader what a colour or size means. You bend guides with three small tools:
And when a colour mapping is redundant (say you have also faceted by branch, so each panel is already labelled), drop the legend entirely with guides(colour = "none") so it stops wasting space:
Un-squash the branches with a log axis
Here is the crowded one-panel chart of all three branches. The shared linear axis squashes Riverside and Downtown under Airport. Add the layer that switches the y-axis to a log scale so every branch becomes readable on one panel.
Show answer
ggplot(sales, aes(x = foot_traffic, y = revenue, colour = branch)) +
geom_point(size = 2) +
scale_y_log10()References
A few authoritative places to take this further:
- ggplot2 reference: facet_wrap() - every argument, including
nrow,ncoland thescalesoption you used. - ggplot2 reference: facet_grid() - the two-variable matrix layout, with
rows ~ colsand margins explained. - ggplot2: Elegant Graphics for Data Analysis, Faceting chapter (free online) - the canonical treatment of small multiples by the package author.
- ggplot2: position scales and their transformations -
scale_y_continuous(), log transforms and label formatting in one place. - R for Data Science (2e): Layers, the facets section - a gentle, worked introduction to faceting with exercises.
Lesson 1 complete
You took one crowded, squashed chart and made it readable four ways: facet_wrap(~ var) for one panel per group, facet_grid(rows ~ cols) for one panel per combination, scales = "free_y" to reveal each panel's shape (knowing it costs you cross-panel comparison), and the scale-and-guide controls, scale_y_log10(), label_dollar(), labs(colour=), theme(legend.position=) and guides(colour = "none"), to bend the axis and legend to your reader's needs.
Next, Lesson 2: Themes, Colour and Accessibility. You styled what the chart shows; now you will restyle how it looks without touching the data, with built-in and custom themes, deliberate colour scales, and palettes that stay readable for everyone, including colourblind readers.