Activity: Graphing Effectively
Boxplots, group comparisons, and saving a publication-ready figure
Worksheet: Graphing Effectively
How to use this worksheet
Work through each part in order, at your own pace. Type every line of code yourself into a plain R script — do not copy-paste. Blocks marked ▶ Run this are code you should type and execute. Blocks marked ✏️ Your turn ask you to write, modify, or answer something. Boxes marked 🚀 If you finish early are optional bonus material.
Part 1 · Load the data
▶ Run this in your Script:
library(tidyverse)
pine_df <- read_csv("data/pine_needles.csv")Part 2 · Boxplot, from scratch
▶ Run this:
pine_box_plot <- ggplot(pine_df, aes(x = n_s, y = length_mm, fill = n_s)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
labs(
title = "Needle Length by Side of Tree",
x = "Side of tree", y = "Needle length (mm)", fill = "Side"
) +
theme_minimal() +
theme(legend.position = "none")
pine_box_plot✏️ Your turn: In the plot above, what does the line in the middle of each box represent? What do the box edges represent?
Middle line: ________________________
Box edges: ________________________
Part 3 · Add the raw points
▶ Run this:
pine_jitter_plot <- pine_box_plot +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
alpha = 0.6,
size = 2
)
pine_jitter_plot✏️ Your turn: Change width = 0.15 to width = 0.4 and rerun. What happens to the points? Which version do you prefer, and why?
# Write your code here:
Which is better and why: __________________________________________________________________
___________________________________________________________________________________________
🚀 If you finish early: Try geom_violin() instead of geom_boxplot() underneath your jittered points.
Part 3b · Axis labels and titles with labs()
▶ Run this:
pine_labeled_plot <- pine_box_plot +
labs(
title = "Needle Length by Side of Tree",
subtitle = "Four field teams, four trees",
x = "Side of tree (shady = n, sunny = s)",
y = "Needle length (mm)",
fill = "Side",
caption = "Data: pine_needles.csv"
)
pine_labeled_plot✏️ Your turn: Change the title, x, and y text to your own wording. Re-run and confirm the plot updates.
# Write your code here:
✏️ Your turn: pine_box_plot maps fill = n_s. What happens if you write labs(color = "Side") instead of labs(fill = "Side")? Try it and explain why.
_____________________________________________________________________________________
_____________________________________________________________________________________
Part 4 · Map a third variable with color
▶ Run this:
pine_team_plot <- ggplot(pine_df, aes(x = n_s, y = length_mm, color = group)) +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
size = 2.5, alpha = 0.8
) +
labs(x = "Side of tree", y = "Needle length (mm)", color = "Field team") +
theme_minimal()
pine_team_plot✏️ Your turn: What’s the difference between writing color = group inside aes() versus writing color = "darkblue" outside aes()? Try both and describe what changes.
# Write your code here:
Part 4b · Histograms and density curves
▶ Run this:
ggplot(pine_df, aes(x = length_mm)) +
geom_histogram(binwidth = 2, fill = "darkblue", color = "white") +
labs(title = "Distribution of Needle Length", x = "Needle length (mm)", y = "Count") +
theme_minimal()✏️ Your turn: Try binwidth = 0.5 and binwidth = 5. Which value tells the real story about this data, and which one hides or exaggerates it?
# Write your code here:
▶ Run this:
ggplot(pine_df, aes(x = length_mm, fill = n_s)) +
geom_density(alpha = 0.5) +
labs(x = "Needle length (mm)", y = "Density", fill = "Side") +
theme_minimal()✏️ Your turn: Do the shady and sunny density curves look like they overlap a lot, or are they clearly separated?
________________________
🚀 If you finish early: Build a faceted histogram — geom_histogram() + facet_wrap(~n_s, ncol = 1) — and compare it to the overlaid density plot above. Which is easier to read?
Part 5 · Mean ± SE, plotted directly
▶ Run this:
pine_mean_se_plot <- ggplot(pine_df, aes(x = n_s, y = length_mm, color = n_s)) +
geom_point(
position = position_jitter(width = 0.15, seed = 42),
alpha = 0.3, size = 2
) +
stat_summary(fun = mean, geom = "point", size = 4) +
stat_summary(fun.data = mean_se, geom = "errorbar", width = 0.15) +
labs(x = "Side of tree", y = "Needle length (mm)") +
theme_minimal() +
theme(legend.position = "none")
pine_mean_se_plot✏️ Your turn: Which two stat_summary() calls compute the mean point and the SE error bars? Copy them below and label which is which.
Mean point: ________________________
SE bars: ________________________
Part 6 · Facet by field team
▶ Run this:
pine_facet_plot <- pine_box_plot +
facet_wrap(~group)
pine_facet_plot✏️ Your turn: Looking at the four panels, does every field team show the same shady-vs-sunny pattern, or does one team look different?
________________________________________________________________________________________
Part 7 · Pick a theme
▶ Run this — try each one:
pine_jitter_plot + theme_bw()
pine_jitter_plot + theme_classic()
pine_jitter_plot + theme_minimal()✏️ Your turn: Which theme do you like best for this kind of biological comparison? Why?
______________________________________________________________________________
Part 8 · Save your figure
▶ Run this:
ggsave(
"figures/pine_needle_facet_boxplot.png",
plot = pine_facet_plot,
width = 3,
height = 3,
units = "in",
dpi = 300
)Check your figures/ folder — the PNG should be there.
✏️ Your turn: What are the four arguments (besides the filename and plot =) you should always set explicitly in ggsave()?
_____________________________________________________________________________
Part 8b · Advanced / extra work — scale_color_manual()
🚀 This part is optional bonus material. Everything through Part 8 covers the required skills for this module.
▶ Run this:
ggplot(pine_df, aes(x = n_s, y = length_mm, color = n_s)) +
geom_point(position = position_jitter(width = 0.15, seed = 42), size = 2.5) +
stat_summary(fun = mean, geom = "point", size = 4, color = "black") +
scale_color_manual(
name = "Side of tree",
labels = c(n = "Shady (sheltered)", s = "Sunny"),
values = c(n = "#2c7fb8", s = "#d95f0e")
) +
labs(x = "Side of tree", y = "Needle length (mm)") +
theme_minimal()✏️ Your turn: Pick two different hex colors of your own choosing and substitute them into values =. Re-run and confirm the legend and points update.
# Write your code here:
✏️ Your turn: pine_df$n_s only has two levels (n, s). What do you think happens if values = is missing an entry for one of them? Try removing the s = "#d95f0e" entry and see what R does.
________________________________________________________________________________
________________________________________________________________________________
▶ Run this — the fill equivalent, on a boxplot:
ggplot(pine_df, aes(x = n_s, y = length_mm, fill = n_s)) +
geom_boxplot(alpha = 0.7, outlier.shape = NA) +
scale_fill_manual(
name = "Side of tree",
labels = c(n = "Shady (sheltered)", s = "Sunny"),
values = c(n = "#2c7fb8", s = "#d95f0e")
) +
labs(x = "Side of tree", y = "Needle length (mm)") +
theme_minimal()✏️ Your turn: In one sentence, when do you reach for scale_color_manual() versus scale_fill_manual()?
________________________________________________________________________________________
Part 9 · Review and checkpoint
At this point you can:
✏️ Your turn — before you move on: Run your whole script top to bottom. Ran cleanly? Y / N — if not, the error was:
_________________________________________________________________________________
📤 What to turn in before next class
Upload both of these to the course management system:
- Your code — the
scripts/folder (or just04_graphing_effectively.R) and the saved figure infigures/ - This worksheet, with your written answers
Getting unstuck
When code breaks — and it will, that is normal:
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects: did you run
library(tidyverse)? Iscolor/fillinsideaes()when it should be? ?function_nameopens the built-in help page.- Bring the exact error (copy-paste it) to class or office hours.
💡 Key idea: Every working scientist googles error messages daily. Getting stuck is not failing — it is the job.