# Load all packages at the top of every script ----------
library(readxl) # reading Excel files
library(tidyverse) # data wrangling + ggplot2
library(janitor) # cleans up messy column namesLecture 04 — GGPlot II
More geoms, zooming safely, and picking a theme
Mapping shape alongside color, adding a trend line with geom_smooth(), coord_cartesian() vs. xlim()/ylim(), and choosing a built-in theme.
Where we left off (Lecture 03 — GGPlot I)
- Piping straight into a plot:
leaf_df %>% ggplot(...) scale_color_manual()/scale_fill_manual()— colors you choosefacet_wrap()andfacet_grid()— one panel per groupstat_summary()— mean and mean ± SE, drawn from raw data
✅ Key idea from Lecture 03
You can already build a grouped, colored, faceted figure. Today: one more geom, a safer way to zoom, and picking a theme that looks intentional instead of default.
Goals for Today
- Map shape alongside color, and add a trend line with
geom_smooth() - Zoom into a plot without silently dropping data —
coord_cartesian() - Pick a built-in theme and adjust it with
theme() - Save a figure in the right format for the job
🖐 Try it yourself
By the end you will recognize the single most common ggplot mistake — zooming with xlim()/ylim() — before you ever make it yourself.
References:
- 📖 Whitlock & Schluter, Ch. 2 — Displaying Data
- 📖 R4DS Ch 1 — Data Visualization
Both PDFs are in the course readings/ folder.
How to Use These Slides — Predict · Type · Run
This lecture runs in three short chunks. After each chunk you switch to the activity and type the code yourself into your R script.
✅ Why bother?
xlim() and coord_cartesian() produce plots that look identical for simple scatter plots — the difference only shows up once you compute something (a smooth line, a boxplot). Typing both yourself and comparing outputs is the only way this sticks.
Load Libraries and Data
# Read the leaf data from the data folder ---------------
leaf_df <- read_excel("data/2026_09_03_data_sci_leaf_area.xlsx") %>%
clean_names()🧩 Chunk 1 of 3 · Shape, Color, and a Trend Line
We will cover: mapping shape alongside color, and geom_smooth().
Mapping Shape Alongside Color
🔮 Predict first: We map both color = shade and shape = shade to the same variable. Will ggplot draw one legend or two?
# Map color AND shape to the same variable -------------
leaf_df %>%
ggplot(aes(x = petiole_mm, y = mass_g, color = shade, shape = shade)) +
geom_point(size = 2.5, alpha = 0.8) +
scale_color_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(
title = "Leaf Mass vs. Petiole Length",
x = "Petiole Length (mm)", y = "Leaf Mass (g)",
color = "Shade", shape = "Shade"
) +
theme_minimal()
- Mapping the same variable to both
colorandshapemerges them into one legend - This makes the plot easier to read in black-and-white or for colorblind readers
- If
colorandshapelegend titles differ inlabs(), you get two legends instead — usually not what you want
Adding a Trend Line — geom_smooth()
# geom_smooth() fits a line straight from the data -----
leaf_df %>%
ggplot(aes(x = petiole_mm, y = mass_g)) +
geom_point(alpha = 0.6, color = "grey40") +
geom_smooth(method = "lm", color = "tomato", fill = "tomato", alpha = 0.15) +
labs(
title = "Leaf Mass vs. Petiole Length, with Trend Line",
x = "Petiole Length (mm)", y = "Leaf Mass (g)"
) +
theme_minimal()
method = "lm"fits a straight line (linear model)- The shaded ribbon is the 95% confidence band around the line
- We’ll fit this same relationship formally with
lm()in the Regression unit later this semester
geom_smooth() is exploration, not a hypothesis test — it shows you whether a line is worth fitting formally before you commit to one.
→ ACTIVITY 4 Parts 1–2 now
🧩 Chunk 2 of 3 · Zooming Without Losing Data
We will cover: coord_cartesian() vs. xlim()/ylim() — they look the same until they don’t.
coord_cartesian() — Zoom Without Removing Data
# coord_cartesian() zooms the VIEW, keeps all the data -
leaf_df %>%
ggplot(aes(x = petiole_mm, y = mass_g)) +
geom_point(alpha = 0.6, color = "grey40") +
geom_smooth(method = "lm", color = "tomato", fill = "tomato", alpha = 0.15) +
coord_cartesian(xlim = c(50, 70), ylim = c(0.4, 0.6)) +
labs(
title = "Zoomed with coord_cartesian()",
x = "Petiole Length (mm)", y = "Leaf Mass (g)"
) +
theme_minimal()
- Zooms the view — every point still contributes to the trend line
- The trend line is fit on all 53 leaves, then the plot window is cropped afterward
- Safe to use anytime you just want a closer look
xlim() / ylim() — Removes Data First
🔮 Predict first: If we drop the leaves outside our zoom window before fitting the trend line, will the line look the same as before, or different?
# xlim()/ylim() DELETE rows outside the range first ----
leaf_df %>%
ggplot(aes(x = petiole_mm, y = mass_g)) +
geom_point(alpha = 0.6, color = "grey40") +
geom_smooth(method = "lm", color = "tomato", fill = "tomato", alpha = 0.15) +
xlim(50, 70) +
ylim(0.4, 0.6) +
labs(
title = "Zoomed with xlim() / ylim() — different line!",
x = "Petiole Length (mm)", y = "Leaf Mass (g)"
) +
theme_minimal()
⚠️ Watch out!
xlim()/ylim() silently delete rows outside the range before anything is computed. The trend line above is fit only on the leaves that remain — a different line than coord_cartesian() produced, with no warning that data went missing.
Rule of thumb: use coord_cartesian() to zoom. Only use xlim()/ylim() when you actually want those rows excluded from the analysis, not just out of view.
→ ACTIVITY 4 Part 3 now
🧩 Chunk 3 of 3 · Themes and Saving
We will cover: picking a built-in theme, theme() tweaks, and choosing a file format with ggsave().
Picking a Theme
# theme_classic() — clean, close to journal defaults ---
leaf_df %>%
ggplot(aes(x = shade, y = mass_g, fill = shade)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
scale_fill_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(x = "Shade", y = "Leaf Mass (g)", title = "Leaf Mass by Shade") +
theme_classic() +
theme(legend.position = "none")
Five built-in themes to try on the same plot:
| Theme | Look |
|---|---|
theme_grey() |
default — grey background |
theme_bw() |
white background, grey gridlines |
theme_minimal() |
no background, subtle gridlines |
theme_classic() |
white background, axis lines only |
theme_light() |
light grey lines and border |
theme_classic() is a safe default for scientific figures — clean, uncluttered, close to what most journals expect.
Adjusting a Theme with theme()
# theme() overrides individual elements afterward -----
leaf_df %>%
ggplot(aes(x = shade, y = mass_g, fill = shade)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
scale_fill_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(x = "Shade", y = "Leaf Mass (g)", title = "Leaf Mass by Shade") +
theme_classic() +
theme(
legend.position = "none",
axis.text = element_text(size = 12),
axis.title = element_text(size = 13)
)
- Pick a built-in theme first (
theme_classic()), then addtheme()afterward to tweak individual pieces legend.position = "none"— drop a redundant legendaxis.text/axis.title— control tick labels and axis titles separately
✅ Key idea
You rarely need to build a theme from scratch. Start from a built-in theme and override only what looks wrong.
Saving in the Right Format
leaf_plot <- leaf_df %>%
ggplot(aes(x = shade, y = mass_g, fill = shade)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
scale_fill_manual(values = c("sunny" = "goldenrod", "shady" = "forestgreen")) +
labs(x = "Shade", y = "Leaf Mass (g)") +
theme_classic() +
theme(legend.position = "none")
# PNG — good for Word docs, slides, web
ggsave("figures/leaf_mass_theme.png",
plot = leaf_plot, width = 5, height = 5,
units = "in", dpi = 300)
# PDF — vector, scales to any size, good for print
ggsave("figures/leaf_mass_theme.pdf",
plot = leaf_plot, width = 5, height = 5, units = "in")| Format | Best for | Scales? |
|---|---|---|
.png |
Word docs, slides, web | ❌ fixed pixels |
.pdf |
Print, journals | ✅ infinite |
.tiff |
Journal submission | ❌ fixed pixels |
⚠️ Watch out! ggsave() takes the filename first, then plot =. Always name the plot object explicitly — do not rely on ggsave grabbing “the last plot shown.”
→ ACTIVITY 4 Parts 4–5 now
What We Learned Today
- Map
shapealongsidecolorto merge legends geom_smooth(method = "lm")— a quick trend line, not a hypothesis testcoord_cartesian()zooms safely;xlim()/ylim()silently deletes rows first- Built-in themes (
theme_classic(), etc.) +theme()for small overrides .pngfor documents/slides,.pdffor print/journals
References:
- 📖 Whitlock & Schluter, Ch. 2 — Displaying Data
- 📖 R4DS Ch 1 — Data Visualization
Up next — Lecture 05, Wrangling:
filter(),select(),mutate(),arrange()- Chaining verbs into one pipeline