Activity 03 - GGPlot I

Grouped summaries, facets, and the pipe into ggplot

ggplot
tidyverse

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

Author

Bill Perry

Published

September 10, 2026

Grouped Summaries and Facets

Recap from Activity 02

  • Set up a project folder with data/, scripts/, and figures/ subfolders
  • Loaded the leaf data from Excel with read_excel() and clean_names()
  • Used the pipe %>% to chain steps
  • Made a first geom_point() plot

Today’s Objectives

  1. Start every plot by piping the data frame into ggplot()
  2. Assign exact colors to exact groups with scale_fill_manual() / scale_color_manual()
  3. Split a plot into panels with facet_wrap() and facet_grid()
  4. Build a mean ± SE plot with stat_summary()
  5. 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 into leaf_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(), and stat_summary() by hand — instead of pasting — is what makes the syntax stick.


Part 1 · Load libraries and data

Tip📂 Get the 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_df in every activity from here on. The shade column is "sunny" or "shady"; the leaf measurements are mass_g, petiole_mm, and thickness_mm.

📌 jitter_pos keeps overlapping points from stacking (the seed makes the scatter reproducible). dodge_pos does nothing while there is only one group per x, but you reuse the same object on geom_point(), stat_summary(), and geom_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, sunny and shady. How many colors do we need to list inside scale_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_se returns 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)

  1. Pick your own two colors for sunny and shady — not "goldenrod"/"forestgreen", and not the same pair as your neighbour.
  2. Build a mean ± SE plot (two stat_summary() layers) of petiole_mm by shade, using your two colors with scale_color_manual(), raw jittered points behind.
  3. 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)

  1. 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?
  2. If you had written "Shady" instead of "shady" inside scale_color_manual(values = c(...)), what would the plot have shown, and why does R treat those as different?
  3. In plain language, what does facet_wrap() do to your data that scale_color_manual() does not — and why does that distinction matter?

Getting unstuck

  1. Read the error message out loud. R usually names the line and the problem.
  2. %>% or +? Data steps use %>%. Plot layers use +. Mixing them is the #1 ggplot error.
  3. 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.
  4. Cheat sheetshttps://posit.co/resources/cheatsheets/
  5. 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.