Activity: Graphing Effectively

Boxplots, group comparisons, and saving a publication-ready figure

Hands-on worksheet: boxplots with raw points, writing axis labels with labs(), mapping color by group, histograms and density curves, mean ± SE plots, faceting, themes, custom colors with scale_color_manual(), and ggsave().
Author

Bill Perry

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

___________________________________________________________________________________________
Tip

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

________________________

Tip

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

Tip

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

_________________________________________________________________________________

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

Getting unstuck

When code breaks — and it will, that is normal:

  1. Read the error message out loud. R usually names the line and the problem.
  2. Check the usual suspects: did you run library(tidyverse)? Is color/fill inside aes() when it should be?
  3. ?function_name opens the built-in help page.
  4. 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.