#install.packages(c("patchwork"))Visualizing Data with ggplot2
1.Setup
library(tidyverse) ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.2.1 ✔ readr 2.2.0
✔ forcats 1.0.1 ✔ stringr 1.6.0
✔ ggplot2 4.0.3 ✔ tibble 3.3.1
✔ lubridate 1.9.5 ✔ tidyr 1.3.2
✔ purrr 1.2.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(agridat)
library(patchwork)
data(australia.soybean)
soy <- as_tibble(australia.soybean) |>
mutate(
protein_level = case_when(
protein < 40 ~ "Low",
protein < 43 ~ "Medium",
TRUE ~ "High"
),
protein_level = fct_relevel(protein_level, "Low", "Medium", "High")
)
glimpse(soy)Rows: 464
Columns: 11
$ env <fct> L70, L70, L70, L70, L70, L70, L70, L70, L70, L70, L70, L…
$ loc <fct> Lawes, Lawes, Lawes, Lawes, Lawes, Lawes, Lawes, Lawes, …
$ year <int> 1970, 1970, 1970, 1970, 1970, 1970, 1970, 1970, 1970, 19…
$ gen <fct> G01, G02, G03, G04, G05, G06, G07, G08, G09, G10, G11, G…
$ yield <dbl> 2.387, 2.282, 2.567, 2.877, 2.392, 2.408, 2.699, 2.457, …
$ height <dbl> 1.445, 1.450, 1.460, 1.260, 1.335, 1.360, 1.300, 0.955, …
$ lodging <dbl> 4.25, 4.25, 3.75, 3.50, 3.50, 4.00, 3.00, 3.25, 3.00, 3.…
$ size <dbl> 8.45, 9.95, 10.85, 10.05, 11.00, 11.75, 11.75, 10.00, 11…
$ protein <dbl> 36.70, 37.55, 37.80, 38.45, 37.50, 38.25, 37.35, 35.20, …
$ oil <dbl> 20.895, 20.740, 21.295, 21.990, 22.130, 21.160, 21.700, …
$ protein_level <fct> Low, Low, Low, Low, Low, Low, Low, Low, Low, Low, Medium…
2.From One-Shot Plots to a Grammar of Graphics
dat <- soy # same alias used for this data in the R/RStudio/Quarto lecture
plot(yield ~ height, data = dat, xlab = "Height (m)", ylab = "Yield (t/ha)")ggplot(data = soy, mapping = aes(x = height, y = yield)) +
geom_point()3.From Data Wrangling to a Publication Figure
soy |>
filter(loc == "Lawes") |>
mutate(
lodging_group = if_else(lodging > median(lodging), "High lodging", "Low lodging")
) |>
ggplot(aes(x = lodging_group, y = yield)) +
geom_boxplot(width = 0.5, fill = "#BDB76B") +
geom_jitter(width = 0.1, alpha = 0.3, size = 2) +
labs(x = NULL, y = "Yield (t/ha)") +
theme_classic()4 Geoms: The Building Blocks 4.1 Mapped vs. Set Aesthetics
p_mapped <- ggplot(soy, aes(x = height, y = yield, color = loc)) +
geom_point() +
labs(title = "Mapped: color = loc") +
theme_minimal()
p_set <- ggplot(soy, aes(x = height, y = yield)) +
geom_point(color = "#8B0000") +
labs(title = 'Set: color = "steelblue"') +
theme_minimal()
p_mapped + p_set4.2 Points — geom_point()
ggplot(soy, aes(x = protein, y = oil, color = factor(year))) +
geom_point(alpha = 0.6) +
labs(x = "Protein (%)", y = "Oil (%)", color = "Year") +
theme_minimal()4.3 Adding a Trend — geom_smooth()
ggplot(soy, aes(x = protein, y = oil)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", se = TRUE, color = "firebrick") +
labs(x = "Protein (%)", y = "Oil (%)") +
theme_minimal()`geom_smooth()` using formula = 'y ~ x'
4.4 Distributions Across a Group — geom_boxplot(), geom_violin(), geom_jitter()
p_box <- ggplot(soy, aes(x = loc, y = yield)) +
geom_boxplot(fill = "#a1d99b") +
labs(title = "geom_boxplot()", x = NULL, y = "Yield (t/ha)") +
theme_minimal()
p_violin <- ggplot(soy, aes(x = loc, y = yield)) +
geom_violin(fill = "#008B8B") +
labs(title = "geom_violin()", x = NULL, y = NULL) +
theme_minimal()
p_box_jitter <- ggplot(soy, aes(x = loc, y = yield)) +
geom_boxplot(outlier.shape = NA) +
geom_jitter(width = 0.15, alpha = 0.4, size = 1) +
labs(title = "boxplot + jitter", x = NULL, y = NULL) +
theme_minimal()
p_box + p_violin + p_box_jitter4.5 One Continuous Variable — geom_histogram(), geom_density()
p_hist <- ggplot(soy, aes(x = yield)) +
geom_histogram(binwidth = 0.25, fill = "#2F4F4F", color = "white") +
labs(title = "geom_histogram()", x = "Yield (t/ha)", y = "Count") +
theme_minimal()
p_dens <- ggplot(soy, aes(x = yield, fill = loc)) +
geom_density(alpha = 0.4) +
labs(title = "geom_density()", x = "Yield (t/ha)", y = "Density", fill = NULL) +
theme_minimal()
p_hist + p_dens4.6 Bar Charts — geom_col()
soy |>
count(protein_level) |>
ggplot(aes(x = protein_level, y = n)) +
geom_col(fill = "#2c7fb8") +
labs(x = NULL, y = "Count") +
theme_minimal()4.7 Bars with Error Bars and Individual Points — geom_errorbar()
soy_summary <- soy |>
group_by(loc) |>
summarise(
mean_yield = mean(yield),
se_yield = sd(yield) / sqrt(n()),
.groups = "drop"
)
ggplot(soy_summary, aes(x = loc, y = mean_yield)) +
geom_col(fill = "grey80", width = 0.6) +
geom_jitter(data = soy, aes(x = loc, y = yield), width = 0.12, alpha = 0.3, size = 1) +
geom_errorbar(aes(ymin = mean_yield - se_yield, ymax = mean_yield + se_yield), width = 0.15) +
labs(x = NULL, y = "Yield (t/ha)") +
theme_classic()4.8 Change Over an Ordered Variable — geom_line()
soy |>
group_by(loc, year) |>
summarise(mean_yield = mean(yield), .groups = "drop") |>
ggplot(aes(x = factor(year), y = mean_yield, color = loc, group = loc)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
labs(x = "Year", y = "Mean yield (t/ha)", color = "Location") +
theme_minimal()- Controlling Scales 5.1 Color and Fill
p_default <- ggplot(soy, aes(x = loc, y = factor(year), fill = yield)) +
geom_tile() +
labs(title = "Default gradient", x = NULL, y = NULL, fill = "Yield") +
theme_minimal()
p_viridis <- ggplot(soy, aes(x = loc, y = factor(year), fill = yield)) +
geom_tile() +
scale_fill_viridis_c() +
labs(title = "scale_fill_viridis_c()", x = NULL, y = NULL, fill = "Yield") +
theme_minimal()
p_default + p_viridisggplot(soy, aes(x = loc, y = yield, fill = loc)) +
geom_boxplot(show.legend = FALSE) +
scale_fill_manual(values = c(
Brookstead = "#1b9e77", Lawes = "#d95f02",
Nambour = "#7570b3", RedlandBay = "#e7298a"
)) +
labs(x = NULL, y = "Yield (t/ha)") +
theme_minimal()5.2 Axis Transformations and Labels
ggplot(soy, aes(x = height, y = yield)) +
geom_point(alpha = 0.5) +
scale_y_log10(labels = scales::label_number(accuracy = 0.1)) +
labs(x = "Height (m)", y = "Yield (t/ha), log scale") +
theme_minimal()6.Coordinate Systems
p_zoom <- ggplot(soy, aes(x = height, y = yield)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", se = FALSE) +
coord_cartesian(ylim = c(0, 3)) +
labs(title = "coord_cartesian(ylim = c(0, 3))") +
theme_minimal()
p_filter <- ggplot(soy, aes(x = height, y = yield)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", se = FALSE) +
scale_y_continuous(limits = c(0, 3)) +
labs(title = "scale_y_continuous(limits = c(0, 3))") +
theme_minimal()
p_zoom + p_filter`geom_smooth()` using formula = 'y ~ x'
`geom_smooth()` using formula = 'y ~ x'
Warning: Removed 47 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 47 rows containing missing values or values outside the scale range
(`geom_point()`).
7.Faceting: Small Multiples
ggplot(soy, aes(x = loc, y = yield)) +
geom_boxplot(fill = "#a1d99b") +
facet_wrap(~ year) +
labs(x = NULL, y = "Yield (t/ha)") +
theme_minimal()8.Themes: From Default to Publication-Ready
base_plot <- ggplot(soy, aes(x = loc, y = yield)) +
geom_boxplot(fill = "#FF4040") +
labs(x = NULL, y = "Yield (t/ha)")
themes_list <- list(
"theme_gray() (default)" = theme_gray(),
"theme_bw()" = theme_bw(),
"theme_linedraw()" = theme_linedraw(),
"theme_light()" = theme_light(),
"theme_dark()" = theme_dark(),
"theme_minimal()" = theme_minimal(),
"theme_classic()" = theme_classic(),
"theme_void()" = theme_void()
)
themes_list |>
imap(~ base_plot + .x + labs(title = .y) + theme(plot.title = element_text(size = 10))) |>
wrap_plots(ncol = 4)theme_soy <- function(base_size = 12) {
theme_classic(base_size = base_size) +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold")
)
}
ggplot(soy, aes(x = loc, y = yield, fill = loc)) +
geom_boxplot(show.legend = FALSE) +
facet_wrap(~ year) +
labs(title = "Yield by location and year", x = NULL, y = "Yield (t/ha)") +
theme_soy()9.Combining Multiple Plots with patchwork
p1 <- ggplot(soy, aes(x = protein, y = oil)) +
geom_point(alpha = 0.2) +
labs(title = "Protein vs. oil") +
theme_classic()
p2 <- ggplot(soy, aes(x = loc, y = yield)) +
geom_boxplot(fill = "#C1FFC1") +
labs(title = "Yield by location", x = NULL) +
theme_classic()
p3 <- ggplot(soy, aes(x = yield)) +
geom_histogram(binwidth = 0.20, fill = "#756bb1", color = "white") +
labs(title = "Yield distribution") +
theme_classic()
(p1 + p2) / p3 +
plot_layout(heights = c(1, 1.2)) +
plot_annotation(tag_levels = "A", title = "australia.soybean: three views")- Heatmaps with geom_tile()
soy |>
group_by(loc, year) |>
summarise(mean_yield = mean(yield), .groups = "drop") |>
ggplot(aes(x = factor(year), y = fct_rev(loc), fill = mean_yield)) +
geom_tile(color = "white", linewidth = 0.6) +
geom_text(aes(label = round(mean_yield, 3)), color = "white") +
scale_fill_viridis_c() +
labs(x = "Year", y = NULL, fill = "Mean\nyield") +
theme_classic()- Exporting a Publication-Ready Figure
final_plot <- ggplot(soy, aes(x = loc, y = yield, fill = loc)) +
geom_boxplot(show.legend = FALSE) +
scale_fill_viridis_d() +
labs(x = NULL, y = "Yield (t/ha)") +
theme_soy()
ggsave(
here::here("figures", "yield_by_location.pdf"),
final_plot,
width = 89, height = 70, units = "mm"
)- Putting It Together: A Publication Figure from Raw Data
p_dist <- ggplot(soy, aes(x = yield)) +
geom_histogram(binwidth = 0.25, fill = "#2c7fb8", color = "white") +
labs(title = "Distribution", x = "Yield (t/ha)", y = "Count") +
theme_soy()
p_rel <- ggplot(soy, aes(x = protein, y = oil, color = loc)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE, color = "black", linewidth = 0.5) +
scale_color_brewer(palette = "Dark2") +
labs(title = "Protein vs. oil", x = "Protein (%)", y = "Oil (%)", color = NULL) +
theme_soy()
p_loc <- soy |>
group_by(loc, year) |>
summarise(mean_yield = mean(yield), .groups = "drop") |>
ggplot(aes(x = factor(year), y = fct_rev(loc), fill = mean_yield)) +
geom_tile(color = "white") +
scale_fill_viridis_c() +
labs(title = "Mean yield heatmap", x = "Year", y = NULL, fill = "t/ha") +
theme_soy()
final_figure <- (p_dist + p_rel) / p_loc +
plot_layout(heights = c(1, 1.1)) +
plot_annotation(
tag_levels = "A",
title = "australia.soybean: an end-to-end visual summary",
caption = "Data: Basford & Tukey (1999), via agridat::australia.soybean"
)
final_figure`geom_smooth()` using formula = 'y ~ x'
ggsave(here::here("figures", "soybean_summary.pdf"), final_figure,
width = 183, height = 120, units = "mm")`geom_smooth()` using formula = 'y ~ x'
- In-Class Exercises Exercise 1 (geoms + theme)
ggplot(soy, aes(x = loc, y = height)) +
geom_boxplot(outlier.shape = NA, fill = "#BBFFFF") +
geom_jitter(width = 0.10, alpha = 0.2, size = 1) +
labs(x = NULL, y = "Height (m)") +
theme_soy()Exercise 2 (scales)
soy |>
group_by(loc, year) |>
summarise(mean_protein = mean(protein), .groups = "drop") |>
ggplot(aes(x = factor(year), y = fct_rev(loc), fill = mean_protein)) +
geom_tile(color = "white") +
scale_fill_viridis_c() +
labs(x = "Year", y = NULL, fill = "Mean\nprotein (%)") +
theme_classic()Exercise 3 (patchwork)
p_a <- ggplot(soy, aes(x = oil)) +
geom_histogram(binwidth = 0.5, fill = "#FFC1C1", color = "white") +
theme_classic()
p_b <- ggplot(soy, aes(x = loc, y = oil)) +
geom_boxplot(fill = "#00CED1") +
labs(x = NULL) +
theme_classic()
p_a / p_b + plot_annotation(tag_levels = "A")Using soy, build one figure of your own.
p1 <- soy |>
ggplot(aes(x = lodging, y = yield, color = protein)) +
geom_point(alpha = 0.6, size = 2) +
geom_smooth(method = "lm", color = "black", se = TRUE, linewidth = 0.8) +
scale_color_viridis_c(option = "plasma") +
labs(
title = "Yield vs Lodging",
x = "Lodging score",
y = "Yield (t/ha)",
color = "Protein (%)"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
p2 <- soy |>
ggplot(aes(x = env, y = yield, fill = env)) +
geom_boxplot(outlier.shape = NA, alpha = 0.7) +
geom_jitter(width = 0.15, alpha = 0.3, size = 0.8) +
scale_fill_brewer(palette = "Set2") +
facet_wrap(~ year) +
labs(
title = "Yield by Environment across Years",
x = "Environment",
y = "Yield (t/ha)"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "none",
axis.text.x = element_text(angle = 45, hjust = 1))
p1 / p2 +
plot_annotation(
title = "Relationship between Lodging, Yield, and Environment",
tag_levels = "A"
)`geom_smooth()` using formula = 'y ~ x'