Plotting in R with ggplot2

Will Gearty

2026-06-12

Introduction

# load the ggplot2 package
library(ggplot2)

Introduction

# load the ggplot2 package
library(ggplot2)
data(penguins)
dplyr::glimpse(penguins)
Rows: 344
Columns: 8
$ species     <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Ad…
$ island      <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgersen, Tor…
$ bill_len    <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, 42.0, …
$ bill_dep    <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, 20.2, …
$ flipper_len <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186, 180,…
$ body_mass   <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, 4250, …
$ sex         <fct> male, female, female, NA, female, male, female, male, NA, …
$ year        <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…

The ggplot2 basics

ggplot(penguins)

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len)

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len) +
  geom_point()

Missing data

You may have noticed a warning that some rows of the dataset contain missing values. There are two penguins without any measurements in the dataset. How might we remove them using dplyr so we don’t get this warning over and over?

Solution
penguins <- penguins |>
  dplyr::filter(!is.na(body_mass))

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len, color = island) +
  geom_point()

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len, color = island) +
  geom_point() +
  scale_color_viridis_d(end = 0.7) # avoid yellow at the end of the palette

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len, color = island) +
  geom_point() +
  scale_color_brewer(palette = "Set1")

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1")

More missing data

Urgh, more missing data! It appears that nine of the penguins are missing sex identification data. We can ignore the warning message, but that extra “NA” category in the legend is quite annoying. We can remove this category from the legend using na.translate = FALSE within scale_shape_discrete() (you could also use scale_shape_manual() if you wanted to supply your own shapes):

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE)

The ggplot2 basics

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)")

Theming

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)") +
  theme_classic()

Theming

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)") +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

More complex features

Other layers

Histograms

ggplot(penguins) +
  aes(x = body_mass, fill = species) +
  geom_histogram() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Histograms

Histograms don’t require a y-axis aesthetic by default. The counts are tabulated for you. If you specify a “fill” aesthetic, the default is to stack the bars which can sometimes be a bit misleading. You can also dodge them to fix this:

ggplot(penguins) +
  aes(x = body_mass, fill = species) +
  geom_histogram(position = "dodge") +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Boxplots and Violin Plotss

ggplot(penguins) +
  aes(x = island, y = bill_len) +
  geom_boxplot() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Boxplots and Violin Plotss

ggplot(penguins) +
  aes(x = island, y = bill_len) +
  geom_violin(scale = "width", draw_quantiles = c(0.25, 0.5, 0.75)) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Geom options

Note that many of these “geom”s have lots of options. For example, here we’ve decided to scale all of the violin plots to the same width and to draw the quartiles on them (mimicking the boxplots above). You can see all of the options for a geom by checking out it’s help page (?geom_violin) or on the ggplot website.

2D Contours

ggplot(penguins) +
  aes(x = bill_len, y = bill_dep) +
  geom_density_2d(linewidth = 0.25, colour = "black") +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

2D Contours

ggplot(penguins) +
  aes(x = bill_len, y = bill_dep) +
  geom_density_2d(linewidth = 0.25, colour = "black") +
  scale_y_continuous(limits = c(12.5, NA)) + # NA means "don't change"
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Time Series

ggplot(economics, aes(x = date, y = unemploy)) +
  geom_line() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Time Series

ggplot(economics, aes(x = unemploy / pop, y = psavert)) +
  geom_path(aes(colour = as.numeric(date))) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Multiple columns for individual aesthetics

Note how we’ve used multiple columns of the data to define the x-axis here. You can use any sort of mathematical operators to combine multiple columns into a single aesthetic, as long as you are doing row-wise math.

Combining layers

ggplot(penguins) +
  aes(x = bill_len, y = bill_dep) +
  geom_point() +
  geom_density_2d_filled(alpha = 0.5) +
  geom_density_2d(linewidth = 0.25, colour = "black") +
  scale_x_continuous(limits = c(30, 60)) +
  scale_y_continuous(limits = c(12, 23)) +
  coord_cartesian(expand = FALSE) + # don't expand the axes so the background is all filled
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Layer order

When combining layers, the layers are added to the plot in order, so in this case the points are the bottom layer and the contour lines are the top layer. We changed the alpha of the middle layer to prevent the points from being blocked. I’ve also used the coord_cartesian() function to remove the default axis expansion. This way the background color reaches both axes and doesn’t have a white gap.

Facetting

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)") +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Facetting

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)") +
  facet_wrap(vars(species), ncol = 1) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Facetting

ggplot(penguins) +
  aes(x = body_mass, y = flipper_len,
      color = island, shape = sex) +
  geom_point() +
  scale_color_brewer(palette = "Set1") +
  scale_shape_discrete(na.translate = FALSE) +
  scale_x_continuous(name = "Body Mass (grams)") +
  scale_y_continuous(name = "Flipper Length (millimeters)") +
  facet_grid(rows = vars(species), cols = vars(year)) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Combining plots

library(patchwork)

Combining plots

library(patchwork)
g1 <- ggplot(penguins) +
  aes(x = body_mass, y = flipper_len) +
  geom_point() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

g2 <- ggplot(penguins) +
  aes(x = bill_len, y = bill_dep) +
  geom_point() +
  geom_density_2d_filled(alpha = 0.5) +
  geom_density_2d(linewidth = 0.25, colour = "black") +
  scale_x_continuous(limits = c(30, 60)) +
  scale_y_continuous(limits = c(12, 23)) +
  coord_cartesian(expand = FALSE) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

Combining plots

library(patchwork)
g1 <- ggplot(penguins) +
  aes(x = body_mass, y = flipper_len) +
  geom_point() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

g2 <- ggplot(penguins) +
  aes(x = bill_len, y = bill_dep) +
  geom_point() +
  geom_density_2d_filled(alpha = 0.5) +
  geom_density_2d(linewidth = 0.25, colour = "black") +
  scale_x_continuous(limits = c(30, 60)) +
  scale_y_continuous(limits = c(12, 23)) +
  coord_cartesian(expand = FALSE) +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))
g1 + g2

Combining plots

g3 <- ggplot(penguins) +
  aes(x = island, y = bill_len) +
  geom_boxplot() +
  theme_classic() +
  theme(axis.text = element_text(color = "black"))

(g1 | g2) / g3

Saving plots

gg <- (g1 | g2) / g3
ggsave("figures/penguins_1.pdf", gg, height = 10, width = 10)
ggsave("figures/penguins_1.jpg", gg, height = 10, width = 10)