Activity: Graphing Effectively

Boxplots, axis limits, a reusable themes file, multi-panel figures, and saving publication-ready output

Hands-on worksheet: boxplots with raw points, axis labels with labs(), mapping color by group, setting axis limits with coord_cartesian(), mean ± SE plots, sourcing a reusable themes file for publication-ready figures, combining panels with patchwork, and ggsave().
Author

Bill Perry

Worksheet: Graphing Effectively

📥 Download the data (.csv) 📥 Download the data (.xlsx) 📥 Download the themes file

📥 Script skeleton (.R)

ImportantColumn names in 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: __________________________________________________________________

___________________________________________________________________________________________
Tip

🚀 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
Warning

⚠️ 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.

Tip

⏱ 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?

________________________

Tip

🚀 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.

Tip

⏱ 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.

📥 Download the themes file

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.

ImportantDo this once, now
  1. Click the download button above and save r_themes_for_3_sizes.R into your project folder — the same folder your .Rproj file is in, not inside scripts/.
  2. Open it in RStudio and run the whole file: Ctrl/Cmd + Shift + Enter (or click Source).
  3. Look at your Environment pane. theme_small, theme_regular, and theme_large are 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?

______________________________________________________________________________

Note

✅ 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.

Warning

⚠️ 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_plot

Parentheses 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.

Tip

🚀 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:

_________________________________________________________________________________

Note

📤 What to turn in before next class

Upload both of these to the course management system:

  1. Your code — the scripts/ folder (or just 04_graphing_effectively.R) and the saved figure in figures/
  2. 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