Annotate & Compose
In Lessons 1 and 2, Maya the baker took her three branches, Downtown, Riverside and the big Airport shop, split the crowded chart into small multiples, and restyled it with a clean theme and colourblind-safe colours. The chart is readable and on-brand. Now she has to present it: on Friday she pitches two investors, and a chart that needs her standing beside it to explain it is a chart that fails the moment she sits down.
This lesson is about making a plot speak for itself, then stitching several plots into one figure. Three moves: mark the values that matter with reference lines and notes, label the points so each one is named, and compose several charts into a single slide. The scatter below is Maya's raw canvas, foot traffic against revenue for all three branches. By the end you will turn it, and its siblings, into a briefing.
By the end of this lesson you will be able to:
- Add reference lines and free-floating notes with
geom_hline()andannotate() - Label crowded points without them overlapping, using
ggrepel - Stitch several plots into one captioned figure with
patchwork
Prerequisites: you can build a basic ggplot and map a column inside aes(), ggplot(data, aes(...)) + geom_*() (from Data Visualization with ggplot2, and Lessons 1 and 2 of this course, Facets and Scales and Themes, Colour and Accessibility). Every new function is defined as it appears.
Two kinds of layer: encode, and explain
Every ggplot you have built so far is made of geoms, layers that turn rows of data into ink: geom_point() puts a dot at each row, geom_line() connects them. An annotation is a different kind of layer. It does not read your data row by row; it draws a fixed mark at a position you name: a horizontal target line, a note, a circle around one point. Geoms answer "what does the data say"; annotations answer "what should the reader notice".
Each lesson starts in a fresh R session, so let us put Maya's week back on the page. Run this block once and the rest of the lesson builds on it:
That is Downtown's daily revenue, a clean line. But it says nothing about what matters: what counts as a good day, where the break-even line sits, which day was best. Those are exactly the things an annotation layer adds.
Reference lines: draw a line where it matters
The simplest annotation is a straight line at a value you care about. Say Maya needs each branch to clear $400 of revenue a day just to cover rent and staff: that is her break-even. A reference line drawn straight across at 400 lets a reader see, instantly, which days cleared the bar.
ggplot gives you three reference-line layers:
geom_hline(yintercept = v)draws a horizontal line at heightvgeom_vline(xintercept = v)draws a vertical line at positionvgeom_abline(slope = m, intercept = b)draws a sloped line, for a trend or a y = x reference
Notice the argument is a single number, not a column: yintercept = 400, not aes(yintercept = something). That is the tell of an annotation, you hand it the literal position. Add the break-even line to Downtown's week:
Now the chart has a meaning it did not before: every point above the dashed red line is a day that paid for itself. Downtown crosses it on Thursday and stays above through the weekend.
yintercept = 400), not a data column. It draws the same line no matter how many rows your data has, and it never appears in the legend.Draw the break-even line
Here is Downtown's week again, without the target line. Add the layer that draws a horizontal reference line at Maya's break-even of 400.
Show answer
ggplot(downtown, aes(day, revenue, group = 1)) +
geom_line(colour = "#1f7a55", linewidth = 1) +
geom_point(size = 2) +
geom_hline(yintercept = 400, linetype = "dashed", colour = "red")annotate(): a note exactly where you want it
A line marks a value; sometimes you want words. annotate() drops text, a point, a segment or a box at coordinates you specify. Its first argument is the kind of mark, then the position, then the content:
annotate("text", x = , y = , label = )writes text at the point(x, y)annotate("point", x = , y = )adds a single markerannotate("rect", xmin = , xmax = , ymin = , ymax = )shades a region
Because the x-axis here is the day (a category), its positions are numbered left to right: Mon is 1, Tue is 2, on up to Sun at 7. So x = 6 lands on Saturday. Let us label the break-even line and circle the best day:
The crucial thing: none of those marks came from a row of sales. You typed the positions yourself. That is what separates an annotation from a geom, and it has consequences.
aes() draws one mark per row and earns a legend. Use annotate() for "say this, here"; use a geom for "show every row".One line, two ways
Maya wants a single horizontal target line at $400 on her chart. A colleague suggests: "add a new column to sales that is 400 in every row, then map it with aes(yintercept = target)." What is the better approach, and why?
geom_hline(yintercept = 400) needs nothing added to the data; that is the whole point of an annotation layer.ggrepel: labels that do not pile up
Maya now wants to label points directly, so a reader does not have to trace each dot back to the legend. The obvious tool is geom_text(aes(label = day)), which writes each point's label right at the point. It works beautifully when points are spread out, and falls apart when they are not. Map the day onto all 21 points and the busy lower-left corner, where Downtown and Riverside crowd together, turns into an unreadable pile:
The ggrepel package fixes exactly this. Swap geom_text() for geom_text_repel() (or geom_label_repel() for boxed labels) and it nudges every label away from its neighbours, drawing a thin connector segment back to the point it belongs to:
Every label is now legible and still tied to its point. The seed = 1 argument fixes the random nudging so the layout comes out the same every time you run it.
ggrepel places labels with a small random search, so without seed = the positions shift a little on every redraw. Set a seed for a reproducible figure. And repel has limits: a few dozen labels are fine, but try to label hundreds of dense points and even repel runs out of room. Then, label only the few points that matter.Un-pile the labels
Here is the crowded scatter with plain geom_text(), where the labels collide. Replace the missing layer with the ggrepel version that spreads them out with connector lines.
Show answer
ggplot(sales, aes(foot_traffic, revenue, colour = branch, label = day)) +
geom_point(size = 2) +
geom_text_repel(size = 3, seed = 1)patchwork: many plots, one figure
Maya's slide needs two charts together: revenue by day, and foot traffic by day, side by side, so the investors can see that more visitors track with more money. Saving two images and pasting them into the slide by hand is fiddly and breaks the moment the data changes. The patchwork package lets you combine ggplots with simple arithmetic, as if the plots were numbers.
First build the two plots and store each in a variable:
Three operators do the layout, and the flow on the right summarises them:
p1 + p2(orp1 | p2) places plots beside each other in a rowp1 / p2stacks one above the other- wrap a group in
()to nest, for example(p1 + p2) / p3
Then two finishing functions polish the whole figure at once. plot_layout(guides = "collect") gathers the repeated legends into a single shared legend, and plot_annotation() adds an overall title and automatic panel tags (A, B, C):
That single object is now one captioned figure: two stacked panels, one shared legend, an overall title, and an A and a B tag on the panels, ready to drop straight onto a slide.
Stack them, do not line them up
For her slide, Maya wants the revenue chart directly above the foot-traffic chart, one on top of the other, so the days line up vertically. She has p_rev and p_traffic. Which patchwork expression does that?
/ operator stacks plots vertically, p_rev on top and p_traffic below, so the day axes line up underneath each other.* compose operator. To stack, use /; to go side by side, use + or |.References
A few authoritative places to take this further:
- patchwork: the official site - every way to assemble, nest and annotate a multi-plot figure, by the package author.
- ggrepel documentation and examples - the full gallery of repelled-label options, including the seed and label-box controls.
- ggplot2 reference: annotate() - the exact arguments for text, point, segment and rect annotations.
- ggplot2 reference: geom_abline, geom_hline and geom_vline - the three reference-line layers in one place.
- R Graphics Cookbook: Annotations chapter (free online) - a recipe-style tour of annotating ggplots, with worked examples.
Lesson 3 complete, and the course
You took a plain chart and made it brief its reader, four moves and no new data: geom_hline() to mark a threshold, annotate() to drop a note or marker at coordinates you choose, ggrepel to label crowded points without the pile-up, and patchwork to compose several plots into one captioned figure with a shared legend.
That closes Advanced ggplot2. Across the three lessons you learned to break a crowded chart into small multiples and control its scales (Lesson 1), restyle it with deliberate, accessible colour and reusable themes (Lesson 2), and now make it explain itself and stand on a slide. Those are the moves that turn a correct chart into a convincing one. Take them back to your own data, and revisit the course landing any time for a refresher.