Activity: Graphing Effectively
Boxplots, axis limits, a reusable themes file, multi-panel figures, and saving publication-ready output
Worksheet: Graphing Effectively
📥 Download the data (.csv) 📥 Download the data (.xlsx) 📥 Download the themes file
pine_data.csv / pine_data.xlsx
Both files hold the same six columns, with exactly these names. Type them exactly as written — R is case-sensitive, and Needle_Length or needle_length will not work:
| Column | What it is |
|---|---|
source_file |
the raw team file the row came from |
team |
field team — one team = one tree |
replicate |
needle number within a tree side (1-8) |
side |
sun exposure — "shady" or "sunny" |
needle_length_mm |
needle length, millimetres |
needle_width_mm |
needle width, millimetres |
After you read the file in, run names(pine_df) and check the names on your screen against this table before you write any other code.
Part 1 · Load the data
Follows Lecture Part 1 — the ggplot grammar.
▶ Run this in your Script:
# ── Part 1.1 · Load the data ────────────────────────────────────────────
library(tidyverse)
pine_df <- read_csv("data/pine_data.csv")Part 2 · Boxplot, from scratch
Follows Lecture Part 2 — boxplot anatomy.
📖 Read more: Whitlock & Schluter Ch. 2 (Displaying Data) on Canvas, and R4DS §9 — Layers.
▶ Run this:
# ── Part 2.1 · Boxplot, from scratch ────────────────────────────────────
pine_box_plot <- ggplot(pine_df, aes(x = side, y = needle_length_mm, fill = side)) +
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
Follows Lecture Part 2 — boxplot + raw points.
📖 Read more: Whitlock & Schluter Ch. 2, “How to draw a good graph” — show the data, not only the summary. Also R4DS §9.6 — Position adjustments for
position_jitter().
▶ Run this:
# ── Part 3.1 · Add the raw points ───────────────────────────────────────
pine_jitter_plot <- pine_box_plot +
geom_point(
position = position_jitter(width = 0.15),
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?
# ── Part 3.2 · Add the raw points ───────────────────────────────────────
# Write your code in the R script:
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()
Follows Lecture Part 2b — labs().
📖 Read more: R4DS §11.1 — Labels.
▶ Run this:
# ── Part 3b.1 · Axis labels and titles with `labs()` ────────────────────
pine_labeled_plot <- pine_box_plot +
labs(
title = "Needle Length by Side of Tree",
subtitle = "Four field teams, four trees",
x = "Side of tree",
y = "Needle length (mm)",
fill = "Side",
caption = "Data: pine_data.csv"
)
pine_labeled_plot✏️ Your turn: Change the title, x, and y text to your own wording. Re-run and confirm the plot updates.
# ── Part 3b.2 · Axis labels and titles with `labs()` ────────────────────
# Write your code in the R script:✏️ Your turn: pine_box_plot maps fill = side. What happens if you write labs(color = "Side") instead of labs(fill = "Side")? Try it and explain why.
_____________________________________________________________________________________
_____________________________________________________________________________________
Part 3c · Axis limits with coord_cartesian()
Follows Lecture Part 2c — coord_cartesian().
📖 Read more: R4DS §11.4 — Scales.
ggplot picks the axis range from your data. That is fine while you explore, and wrong the moment a reader has to judge how big a difference is. Set the limits on purpose.
▶ Run this in your Script:
# ── Part 3c.1 · Axis limits with coord_cartesian() ──────────────────────
# Start the y-axis at zero so the difference is shown at its real size
ggplot(pine_df, aes(x = side, y = needle_length_mm, fill = side)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
labs(
title = "Needle Length by Side of Tree",
x = "Side of tree",
y = "Needle length (mm)"
) +
coord_cartesian(ylim = c(0, 25)) +
theme_minimal() +
theme(legend.position = "none")▶ Run this — both axes at once, on two continuous variables:
# ── Part 3c.2 · Axis limits with coord_cartesian() ──────────────────────
pine_scatter_plot <- ggplot(pine_df, aes(x = needle_length_mm, y = needle_width_mm,
color = side)) +
geom_point(alpha = 0.6, size = 2) +
labs(
title = "Needle Width vs. Length",
x = "Needle length (mm)",
y = "Needle width (mm)",
color = "Side"
) +
coord_cartesian(xlim = c(5, 25), ylim = c(0.4, 1.8)) +
theme_minimal()
pine_scatter_plot⚠️ The trap we just saw on screen — use coord_cartesian(), not ylim()
ylim(14, 16) and scale_y_continuous(limits = c(14, 16)) filter your data before the geom draws it. Every row outside the range is dropped, and the median, quartiles, and whiskers are recomputed from what survives — behind a “removed rows” warning that is very easy to read past.
coord_cartesian() computes the statistics on all the data and then zooms the view. The boxes do not move.
The rule: coord_cartesian() to zoom, filter() when you actually mean to remove data — on purpose, in the data step, where you can see it.
✏️ Your turn: Write a boxplot of needle_length_mm by side with y-axis limits you chose on purpose, and say in one line why you chose them.
# ── Part 3c.3 · Axis limits with coord_cartesian() ──────────────────────
# Write your code in the R script:_____________________________________________________________________________
Part 4 · Map a third variable with color
Follows Lecture Part 3 — mapping color by group.
📖 Read more: R4DS §9.1 — Aesthetic mappings.
▶ Run this:
# ── Part 4.1 · Map a third variable with color ──────────────────────────
pine_team_plot <- ggplot(pine_df, aes(x = side, y = needle_length_mm, color = team)) +
geom_point(
position = position_jitter(width = 0.15),
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 = team inside aes() versus writing color = "darkblue" outside aes()? Try both and describe what changes.
# ── Part 4.2 · Map a third variable with color ──────────────────────────
# Write your code in the R script:Part 4b · Histograms and density curves
Follows Lecture Part 3b — histograms and density.
⏱ If we run short in class, finish this part on your own — nothing later in the worksheet depends on it.
📖 Read more: Whitlock & Schluter Ch. 2 on frequency distributions and bin width; R4DS §10.3 — Variation.
▶ Run this:
# ── Part 4b.1 · Histograms and density curves ───────────────────────────
ggplot(pine_df, aes(x = needle_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?
# ── Part 4b.2 · Histograms and density curves ───────────────────────────
# Write your code in the R script:▶ Run this:
# ── Part 4b.3 · Histograms and density curves ───────────────────────────
ggplot(pine_df, aes(x = needle_length_mm, fill = side)) +
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(~side, ncol = 1) — and compare it to the overlaid density plot above. Which is easier to read?
Part 5 · Mean ± SE, plotted directly
Follows Lecture Part 4 — mean ± SE with stat_summary().
📖 Read more: Whitlock & Schluter Ch. 4 (Estimating with Uncertainty) — what an error bar actually claims; Gotelli & Ellison Ch. 6 on why independence decides which rows you may average.
▶ Run this:
# ── Part 5.1 · Mean ± SE, plotted directly ──────────────────────────────
pine_mean_se_plot <- ggplot(pine_df, aes(x = side, y = needle_length_mm, color = side)) +
geom_point(
position = position_jitter(width = 0.15),
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
Follows Lecture Part 5 — faceting.
⏱ If we run short in class, finish this part on your own — nothing later in the worksheet depends on it.
📖 Read more: R4DS §9.5 — Facets.
▶ Run this:
# ── Part 6.1 · Facet by field team ──────────────────────────────────────
pine_facet_plot <- pine_box_plot +
facet_wrap(~team)
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 · The built-in themes
Follows Lecture Part 6 — themes.
📖 Read more: R4DS §11.5 — Themes.
▶ Run this — try each one:
# ── Part 7.1 · The built-in themes ──────────────────────────────────────
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?
______________________________________________________________________________
None of them is quite publication-ready — which is what Part 7b fixes, once, for the rest of the term.
Part 7b · A themes file you write once and source() forever
Follows Lecture Part 6 — themes.
This file defines three theme functions, each built for a different final printed size:
| Function | Built for | Base font |
|---|---|---|
theme_small() |
~3 × 3 in — a journal panel | 9 pt |
theme_regular() |
~7 × 7 in — a report figure | 16 pt |
theme_large() |
~16 × 16 in — a poster | 36 pt |
All three give the same publication look: white background, black panel border, black axis lines and ticks, bold axis titles, no gridlines.
- Click the download button above and save
r_themes_for_3_sizes.Rinto your project folder — the same folder your.Rprojfile is in, not insidescripts/. - Open it in RStudio and run the whole file:
Ctrl/Cmd + Shift + Enter(or click Source). - Look at your Environment pane.
theme_small,theme_regular, andtheme_largeare now listed under Functions. That is all running it does — it defines them.
▶ Run this — from now on, this one line replaces all of it:
# ── Part 7b.1 · Source the themes file ──────────────────────────────────
# Put this at the TOP of your script, with your library() lines
source("r_themes_for_3_sizes.R")▶ Run this — use a theme like any other layer:
# ── Part 7b.2 · Use the themes ──────────────────────────────────────────
pine_theme_plot <- ggplot(pine_df, aes(x = side, y = needle_length_mm, fill = side)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
labs(
title = "Needle Length by Side of Tree",
x = "Side of tree",
y = "Needle length (mm)"
) +
coord_cartesian(ylim = c(0, 25)) +
theme_small() +
theme(legend.position = "none")
pine_theme_plot✏️ Your turn: Swap theme_small() for theme_regular() and re-run. What changed?
______________________________________________________________________________
✅ Why source() instead of copy-paste
source() runs that file top to bottom, exactly as if you had typed it. Change a margin in the themes file once and every figure in every script picks the change up the next time you run it. Copy-pasted theme code gives you fifteen slightly different themes and no way to fix them all.
⚠️ Watch out!
source() needs the path to be right. With the file in your project root and your .Rproj open, "r_themes_for_3_sizes.R" is enough. If you put it in scripts/, the line becomes source("scripts/r_themes_for_3_sizes.R").
Part 7c · Many plots on one page — patchwork
Follows Lecture Part 7 — patchwork.
▶ Run this — once per computer:
# ── Part 7c.1 · Install patchwork ───────────────────────────────────────
install.packages("patchwork")▶ Run this — every session, with your other library() lines:
# ── Part 7c.2 · Load patchwork ──────────────────────────────────────────
library(patchwork)patchwork reuses operators you already know: + puts plots side by side, / puts one over the other.
▶ Run this:
# ── Part 7c.3 · Combine two plots ───────────────────────────────────────
pine_jitter_plot + pine_mean_se_plot # side by side
pine_jitter_plot / pine_mean_se_plot # stacked▶ Run this — a real three-panel figure:
# ── Part 7c.4 · A three-panel figure ────────────────────────────────────
combined_plot <- (pine_jitter_plot + pine_mean_se_plot) / pine_scatter_plot +
plot_annotation(tag_levels = "A")
combined_plotParentheses group plots exactly like arithmetic: (a + b) / c puts two panels on top and one across the bottom. plot_annotation(tag_levels = "A") tags them A, B, C — the labels a figure caption refers to.
✏️ Your turn: Build your own two-panel figure from any two plots you have made today, stacked with / instead of side by side.
# ── Part 7c.5 · Your own combined figure ────────────────────────────────
# Write your code in the R script:Part 8 · Save your figure
Follows Lecture Part 8 — ggsave().
📖 Read more: R4DS §11 — Communication, and Whitlock & Schluter Ch. 2 on what a figure must carry to stand on its own.
▶ Run this:
# ── Part 8.1 · Save your figure ─────────────────────────────────────────
# One panel, built with theme_small() — save it at the size that theme was written for
ggsave(
"figures/pine_needle_boxplot.png",
plot = pine_theme_plot,
width = 3,
height = 3,
units = "in",
dpi = 300
)▶ Run this — the whole patchwork figure in one call:
# ── Part 8.2 · Save the combined figure ─────────────────────────────────
ggsave(
"figures/pine_needle_combined.png",
plot = combined_plot,
width = 7,
height = 6,
units = "in",
dpi = 300
)A three-panel figure needs roughly three panels’ worth of canvas — size ggsave() for the combined plot, not for one panel.
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()
Follows Lecture Part 9 — scale_color_manual().
📖 Read more: R4DS §11.4 — Scales, including the ColorBrewer palettes.
🚀 This part is optional bonus material. Everything through Part 8 covers the required skills for this module.
▶ Run this:
# ── Part 8b.1 · Advanced / extra work ───────────────────────────────────
ggplot(pine_df, aes(x = side, y = needle_length_mm, color = side)) +
geom_point(position = position_jitter(width = 0.15), size = 2.5) +
stat_summary(fun = mean, geom = "point", size = 4, color = "black") +
scale_color_manual(
name = "Side of tree",
labels = c(shady = "Shady side", sunny = "Sunny side"),
values = c(shady = "#2c7fb8", sunny = "#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.
# ── Part 8b.2 · Advanced / extra work ───────────────────────────────────
# Write your code in the R script:✏️ Your turn: pine_df$side 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:
# ── Part 8b.3 · Advanced / extra work ───────────────────────────────────
ggplot(pine_df, aes(x = side, y = needle_length_mm, fill = side)) +
geom_boxplot(alpha = 0.7, outlier.shape = NA) +
scale_fill_manual(
name = "Side of tree",
labels = c(shady = "Shady side", sunny = "Sunny side"),
values = c(shady = "#2c7fb8", sunny = "#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
Readings
Before next class — chapter PDFs are posted on Canvas:
- Whitlock & Schluter, The Analysis of Biological Data — Ch. 2, Displaying Data. Read “How to draw a good graph” closely: it is the standard every figure you hand in this term is held to
- Gotelli & Ellison, A Primer of Ecological Statistics — Ch. 3, Summary Statistics, for the distributions behind the boxplots and histograms
If you want more:
- Whitlock & Schluter, Ch. 4 — Estimating with Uncertainty, on what an error bar actually claims (Part 5)
W&S tells you which graph is honest; R4DS tells you how to draw it. You need both.
R for Data Science (2nd ed.) — free online at r4ds.hadley.nz:
| Chapter | Covers | Worksheet part |
|---|---|---|
| §1 — Data visualization | the three pieces of every ggplot |
Parts 1-2 |
| §9 — Layers | geoms, aes(), jitter, facets |
Parts 2-4, 6 |
| §10 — Exploratory data analysis | histograms, density, distribution shape | Part 4b |
| §11 — Communication | labs(), scales, themes, ggsave() |
Parts 3b, 7, 8, 8b |