Activity 03 - GGPlot I
Grouped summaries, facets, and the pipe into ggplot
Hands-on companion to the GGPlot I lecture. Pipe into ggplot(), choose colors on purpose, split plots with facets, and build a mean ± SE plot with stat_summary().
Grouped Summaries and Facets
Recap from Activity 02
- Set up a project folder with
data/,scripts/, andfigures/subfolders - Loaded the leaf data from Excel with
read_excel()andclean_names() - Used the pipe
%>%to chain steps - Made a first
geom_point()plot
Today’s Objectives
- Start every plot by piping the data frame into
ggplot() - Assign exact colors to exact groups with
scale_fill_manual()/scale_color_manual() - Split a plot into panels with
facet_wrap()andfacet_grid() - Build a mean ± SE plot with
stat_summary() - Save the finished plot to
figures/
How this activity works
You are building one R script this whole class, and you turn it in. Create it now:
scripts/03_ggplot_1.R.
- Every line you run goes in that script — not in the Console, not typed into this page. Type it, don’t paste it.
- Start every code chunk in your script with a short
#comment that says what it does. The comments are part of the grade.- The top of your script, in this order: a title comment, then your
library()calls, then the line that loads the data intoleaf_df.- Code marked ▶ Run this is typed into your script exactly as shown. Code marked ✏️ Your turn is a change you make in that same script and run.
🔮 Predict before you run
Before you run any ▶ Run this block, cover the output and predict what R will draw. Typing
scale_fill_manual(),facet_wrap(), andstat_summary()by hand — instead of pasting — is what makes the syntax stick.
Part 1 · Load libraries and data
You have used this file since Activity 02 — it should already be in your project’s data/ folder. If not, download it again: 2026_09_03_data_sci_leaf_area.xlsx and put it in data/.
▶ Type this at the very top of scripts/03_ggplot_1.R:
# ---- Activity 03: GGPlot I --------------------------------
# your name, today's date
# ---- Libraries -------------------------------------------
library(readxl) # read Excel files
library(tidyverse) # dplyr for wrangling + ggplot2 for plots
library(janitor) # clean_names() tidies messy column names# ---- Load data ------------------------------------------
# leaf data lives in the data/ folder of your project
leaf_df <- read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
clean_names()
glimpse(leaf_df) # always look at a data frame right after loading# ---- Positions: define once, reuse on every layer -------
jitter_pos <- position_jitter(width = 0.15, seed = 42) # raw points: spread them out
dodge_pos <- position_dodge(width = 0.2) # grouped means / lines: shift side by side📌 We call the data frame
leaf_dfin every activity from here on. Theshadecolumn is"sunny"or"shady"; the leaf measurements aremass_g,petiole_mm, andthickness_mm.📌
jitter_poskeeps overlapping points from stacking (theseedmakes the scatter reproducible).dodge_posdoes nothing while there is only one group per x, but you reuse the same object ongeom_point(),stat_summary(), andgeom_line()so grouped summaries stay aligned.
Part 2 · Piping into ggplot, and choosing colors
Pipe the data frame straight into ggplot()
▶ Run this:
# boxplot of leaf mass by shade, raw points on top
leaf_df %>%
ggplot(aes(x = shade, y = mass_g, fill = shade)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
geom_point(position = jitter_pos, alpha = 0.6) +
labs(
title = "Leaf Mass by Shade",
x = "Shade",
y = "Leaf Mass (g)",
fill = "Shade"
) +
theme_minimal()⚠️ Watch out!
%>%connects data steps.+connects plot layers. Do not mix them up.
✏️ Your turn — in your script: Copy the block above and change y = mass_g to y = petiole_mm (and fix the y-axis label). Run it.
Assign colors on purpose
🔮 Predict first: We have two groups,
sunnyandshady. How many colors do we need to list insidescale_fill_manual()?
▶ Run this:
# same boxplot, but we pick the color for each group by name
leaf_df %>%
ggplot(aes(x = shade, y = mass_g, fill = shade)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
geom_point(position = jitter_pos, alpha = 0.6) +
scale_fill_manual(values = c(
"sunny" = "goldenrod",
"shady" = "forestgreen"
)) +
labs(
title = "Leaf Mass by Shade",
x = "Shade",
y = "Leaf Mass (g)",
fill = "Shade"
) +
theme_minimal() +
theme(legend.position = "none")✏️ Your turn — in your script: Change the two colors to "orange" and "darkgreen". Then try misspelling "shady" as "Shady" in values = c(...) and run it.
What happened when you misspelled the name:
Part 3 · Facets — facet_wrap() and facet_grid()
facet_wrap() — one panel per group
▶ Run this:
# one histogram panel per shade level
leaf_df %>%
ggplot(aes(x = mass_g, fill = shade)) +
geom_histogram(binwidth = 0.05, color = "white") +
facet_wrap(~shade) +
scale_fill_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(x = "Leaf Mass (g)", y = "Count", title = "Mass Distribution by Shade") +
theme_minimal() +
theme(legend.position = "none")✏️ Your turn — in your script: Make the same faceted histogram for thickness_mm instead of mass_g (pick a sensible binwidth).
facet_grid() — a strict grid
▶ Run this:
# facet_grid stacks the panels in a fixed row/column layout
leaf_df %>%
ggplot(aes(x = mass_g, fill = shade)) +
geom_histogram(binwidth = 0.05, color = "white") +
facet_grid(shade ~ .) +
scale_fill_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(x = "Leaf Mass (g)", y = "Count") +
theme_minimal() +
theme(legend.position = "none")✏️ Your turn — in your script: Change facet_grid(shade ~ .) to facet_grid(. ~ shade) and run it.
What changed (rows or columns):
✏️ Your turn: In one sentence, when would you reach for facet_grid() instead of facet_wrap()?
Your answer:
Part 4 · Mean ± SE with stat_summary()
The mean as a layer
▶ Run this:
# raw points, with the group mean drawn on top as a black dot
leaf_df %>%
ggplot(aes(x = shade, y = mass_g, color = shade)) +
geom_point(position = jitter_pos, alpha = 0.4, size = 2) +
stat_summary(fun = mean, geom = "point", size = 4, color = "black",
position = dodge_pos) +
scale_color_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(
title = "Leaf Mass with Group Means",
x = "Shade", y = "Leaf Mass (g)"
) +
theme_minimal() +
theme(legend.position = "none")Mean ± SE
🔮 Predict first:
fun.data = mean_sereturns three numbers per group. Two of them are the top and bottom of the error bar. What’s the third?
▶ Run this:
# store the plot in an object so we can save it later
leaf_mean_se_plot <- leaf_df %>%
ggplot(aes(x = shade, y = mass_g, color = shade)) +
geom_point(position = jitter_pos, alpha = 0.35, size = 2) +
stat_summary(fun = mean, geom = "point", size = 4,
position = dodge_pos) +
stat_summary(
fun.data = mean_se,
geom = "errorbar",
width = 0.15,
linewidth = 0.9,
position = dodge_pos
) +
scale_color_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(
title = "Mean ± SE Leaf Mass by Shade",
x = "Shade", y = "Leaf Mass (g)"
) +
theme_minimal() +
theme(legend.position = "none")
leaf_mean_se_plot✏️ Your turn — in your script: Change fun = mean in the first stat_summary() to fun = median and run it.
What changed:
✏️ Your turn: Do the error bars for sunny and shady overlap? What might that suggest about whether the two groups actually differ?
Do the error bars overlap? Y / N
What that might suggest:
Part 5 · Save your plot
▶ Run this:
# write the finished figure to the figures/ folder at print resolution
ggsave("figures/leaf_mass_mean_se.png",
plot = leaf_mean_se_plot,
width = 5,
height = 5,
units = "in",
dpi = 300)Part 6 · Review and checkpoint
At this point you should be able to:
✏️ Your turn — before you move on: Run your entire script top to bottom with Ctrl/Cmd + Shift + Enter (Source). Does it complete without errors?
Ran cleanly? Y / N
If not, what error appeared:
What your project folder should contain
tree_project/
├── data/
│ └── 2026_09_03_data_sci_leaf_area.xlsx <- never edit this
├── figures/
│ └── leaf_mass_mean_se.png <- Part 5
└── scripts/
└── 03_ggplot_1.R <- your script (turn this in)
Extension — out of class (~30–40 min)
Add this to the bottom of scripts/03_ggplot_1.R and turn it in with the rest. Put your written answers in # comments right under the code they go with. In class you used one trick at a time (colors, facets, stat_summary). Now you combine two, with choices that are yours alone.
E1 · Your own combo plot (4 pts)
- Pick your own two colors for
sunnyandshady— not"goldenrod"/"forestgreen", and not the same pair as your neighbour. - Build a mean ± SE plot (two
stat_summary()layers) ofpetiole_mmbyshade, using your two colors withscale_color_manual(), raw jittered points behind. - Add
facet_wrap(~ shade)to that same plot — faceting by the variable you already coloured by is a bit redundant; do it anyway and watch what happens to the x-axis.
Save it to figures/ at dpi = 300. Record the exact R colour names you chose in a comment.
E2 · Predict, then check (3 pts)
Before running E1: in a comment, sketch in words the faceted mean ± SE plot — two panels, roughly where each mean and its error bars sit, which colour goes where. Run the code, then write 2–3 sentences: were you right about which side has more spread? Did faceting by shade look as redundant as you expected?
E3 · Explain it, using YOUR plot (3 pts)
- Do your sunny and shady error bars overlap? Using their direction and rough size, what does that informally suggest about whether the sides differ in
petiole_mm? - If you had written
"Shady"instead of"shady"insidescale_color_manual(values = c(...)), what would the plot have shown, and why does R treat those as different? - In plain language, what does
facet_wrap()do to your data thatscale_color_manual()does not — and why does that distinction matter?
Getting unstuck
- Read the error message out loud. R usually names the line and the problem.
%>%or+? Data steps use%>%. Plot layers use+. Mixing them is the #1 ggplot error.- Color not showing up? Check that the names inside
scale_*_manual(values = c(...))exactly match the values in your data — spelling and case both count. - Cheat sheets — https://posit.co/resources/cheatsheets/
- Bring the exact error (copy-paste it) to class, Canvas, or office hours.
End of the GGplot I activity. Next: GGplot II — more geoms, coord_cartesian(), and themes.