Data visualisation

Using ggplot2: The grammar of graphics

Arun Mitra

2026-09-09

What are we doing today?

  • Learn the grammar of graphics
  • We take one dataset, tidy it, and build a couple of figures
  • Save the plot as a standalone image for later use
  • Hands-on: you will do the same with your own dataset

The data: Low Birth Weight Dataset : 189 births

Hosmer & Lemeshow’s low birth weight study, as an Excel file in your data/ folder.

births_raw <- import(here("data/data_demo.xlsx"), setclass = "tibble")

glimpse(births_raw)
Rows: 189
Columns: 10
$ low   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ age   <dbl> 19, 33, 20, 21, 18, 21, 22, 17, 29, 26, 19, 19, 22, 30, 18, 18, …
$ lwt   <dbl> 182, 155, 105, 108, 107, 124, 118, 103, 123, 113, 95, 150, 95, 1…
$ race  <dbl> 2, 3, 1, 1, 1, 3, 1, 3, 1, 1, 3, 3, 3, 3, 1, 1, 2, 1, 3, 1, 3, 1…
$ smoke <dbl> 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0…
$ ptl   <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0…
$ ht    <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ ui    <dbl> 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1…
$ ftv   <dbl> 0, 3, 1, 2, 0, 0, 1, 1, 1, 0, 0, 1, 0, 2, 0, 0, 0, 3, 0, 1, 2, 3…
$ bwt   <dbl> 2523, 2551, 2557, 2594, 2600, 2622, 2637, 2637, 2663, 2665, 2722…

First, a little wrangling

births <- import(here("data/data_demo.xlsx"),
                 setclass = "tibble") |>
  clean_names() |>
  mutate(
    smoke = case_when(smoke == 1 ~ "Smoker",
                      smoke == 0 ~ "Non-smoker"),
    race  = case_when(race == 1 ~ "White",
                      race == 2 ~ "Black",
                      race == 3 ~ "Other")
  )
1
import() picks the reader from the file extension — one function for .csv, .xlsx, .dta, .sav. here() builds the path from your project root, so it works on any machine.
2
Give it back as a tibble, so printing does not flood the console.
3
clean_names(): lower case, underscores, no spaces or dots.
4
case_when() turns codes into words. Label before you plot — the figure inherits whatever you leave in the data.

This is where we are going

Every plot needs three things

data + mapping + geometry

ggplot(data = births, aes(x = smoke, y = bwt)) +
  geom_boxplot()
  • data — which table
  • aes() — which column goes on which axis
  • geom_ — what marks appear

+ joins layers. |> moves data.

+ stacks a layer onto a plot

ggplot(...) +
  geom_point()

|> sends a table into a function

births |>
  filter(smoke == "Smoker")

Swap them and R gives a long, unhelpful error. Everyone does this in the first week.

One variable: how heavy are these babies?

ggplot(births, aes(x = bwt)) +
  geom_histogram(binwidth = 250,
                 fill = "#0f5257",
                 colour = "white")

Two variables: does smoking matter?

ggplot(births,
       aes(x = smoke,
           y = bwt,
           fill = smoke)) +
  geom_boxplot(alpha = .75,
               outlier.shape = NA) +
  geom_jitter(width = .12,
              alpha = .35, size = 1.2) +
  theme(legend.position = "none")

Inside aes() or outside?

You write It means
aes(fill = smoke) Colour is mapped to a column
geom_point(colour = "navy") Colour is fixed decoration

A legend with one meaningless entry means you put a constant inside aes().

Which geometry?

You have Use Not
One continuous histogram, density pie
One categorical bar of counts pie
Continuous by group box + points bar + error bar
Two continuous point, + smooth line
Long labels coord_flip() rotated text

One question per plot. If you cannot say the question, you cannot choose the geom.

Why not a bar chart?

Colour is a decision

  • About 1 in 12 men cannot separate red from green
  • Someone will print your figure in greyscale
  • scale_fill_viridis_d() is safe by default

If the figure still works in black and white, colour was a bonus, not a crutch.

Does it hold in every group?

ggplot(births, aes(bwt, fill = smoke)) +
  geom_density(alpha = .6) +
  facet_wrap(~ race) +
  scale_fill_manual(values = smoke_cols) +
  theme(legend.position = "bottom")

Making it stand alone

labs(
  title    = "Birth weight by maternal smoking",
  subtitle = "189 births; dashed line marks low birth weight (2500 g)",
  y = "Birth weight (g)", x = NULL,
  caption  = "Data: Hosmer & Lemeshow low birth weight study (n = 189)."
)

ggsave("output/fig1_birthweight.png",
       width = 7, height = 4.5, dpi = 300, bg = "white")

Never screenshot a plot.

Hands-on — 60 min

Four exercises on the website.

  1. Wrangle, then draw your first histogram
  2. Add a group
  3. The figure, and why this geometry
  4. (stretch) Split it by race

Green sticky when Exercise 1 draws something.