The grammar of graphics

Lecture 03

Dr. Benjamin Soltoff

Cornell University
INFO 3312/5312 - Fall 2026

September 1, 2026

Announcements

Announcements

  • Homework 01

Number of restaurants by cuisine type

  • What is the story?
  • How does the design of the chart make you think that is the story?

Learning objectives

  • Review the grammar of graphics
  • Utilize the grammar of graphics to conceptually define Minard’s graph of Napoleon’s invasion of Russia
  • Map variables to aesthetics
  • Create small-multiples plots using faceting

The grammar of graphics

The grammar of graphics

  • “The fundamental principles or rules of an art or science”
  • A grammar used to describe and create a wide range of statistical graphics
  • Originated by Leland Wilkinson in 2001, expanded by Hadley Wickham in 2005 with {ggplot2}

A fuzzy monster in a beret and scarf, critiquing their own column graph on a canvas in front of them while other assistant monsters (also in berets) carry over boxes full of elements that can be used to customize a graph (like themes and geometric shapes). In the background is a wall with framed data visualizations. Stylized text reads 'ggplot2: build a data masterpiece.'

{ggplot2} \(\in\) {tidyverse}

  • {ggplot2} is tidyverse’s data visualization package
  • Structure of the code for plots can be summarized as
ggplot(data = [dataset], 
       mapping = aes(x = [x-variable], 
                     y = [y-variable])) +
   geom_[chart-type]() +
   other options

Data: Palmer Penguins

Measurements for penguin species, island in Palmer Archipelago, size (flipper length, body mass, bill dimensions), and sex.

glimpse(penguins)
Rows: 333
Columns: 8
$ species     <fct> Adelie, Adelie, Adelie, Adelie, Adelie…
$ island      <fct> Torgersen, Torgersen, Torgersen, Torge…
$ bill_len    <dbl> 39.1, 39.5, 40.3, 36.7, 39.3, 38.9, 39…
$ bill_dep    <dbl> 18.7, 17.4, 18.0, 19.3, 20.6, 17.8, 19…
$ flipper_len <int> 181, 186, 195, 193, 190, 181, 195, 182…
$ body_mass   <int> 3750, 3800, 3250, 3450, 3650, 3625, 46…
$ sex         <fct> male, female, female, female, male, fe…
$ year        <int> 2007, 2007, 2007, 2007, 2007, 2007, 20…

ggplot(data = penguins, 
       mapping = aes(x = bill_dep, y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)",
       color = "Species")

Coding out loud

Start with the penguins data frame

ggplot(data = penguins)

Start with the penguins data frame, map bill depth to the x-axis

ggplot(data = penguins,
       mapping = aes(x = bill_dep))

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis.

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len))

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len)) + 
  geom_point()

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point.

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point()

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length")

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins")

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”, label the x and y axes as “Bill depth (mm)” and “Bill length (mm)”, respectively

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)")

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”, label the x and y axes as “Bill depth (mm)” and “Bill length (mm)”, respectively, label the legend “Species”

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)",
       color = "Species")

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”, label the x and y axes as “Bill depth (mm)” and “Bill length (mm)”, respectively, label the legend “Species”, and add a caption for the data source.

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)",
       color = "Species",
       caption = "Source: Palmer Station LTER / palmerpenguins package")

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis. Represent each observation with a point and map species to the color of each point. Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”, label the x and y axes as “Bill depth (mm)” and “Bill length (mm)”, respectively, label the legend “Species”, and add a caption for the data source. Finally, use a discrete color scale that is designed to be perceived by viewers with common forms of color blindness.

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)",
       color = "Species",
       caption = "Source: Palmer Station LTER / palmerpenguins package") +
  scale_color_viridis_d()

ggplot(data = penguins,
       mapping = aes(x = bill_dep,
                     y = bill_len,
                     color = species)) +
  geom_point() +
  labs(title = "Bill depth and length",
       subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
       x = "Bill depth (mm)", y = "Bill length (mm)",
       color = "Species",
       caption = "Source: Palmer Station LTER / palmerpenguins package") +
  scale_color_viridis_d()

Start with the penguins data frame, map bill depth to the x-axis and map bill length to the y-axis.

Represent each observation with a point and map species to the color of each point.

Title the plot “Bill depth and length”, add the subtitle “Dimensions for Adelie, Chinstrap, and Gentoo Penguins”, label the x and y axes as “Bill depth (mm)” and “Bill length (mm)”, respectively, label the legend “Species”, and add a caption for the data source.

Finally, use a discrete color scale that is designed to be perceived by viewers with common forms of color blindness.

Alternative implementations

# Map species to viridis colors
species <- factor(penguins$species)
cols <- viridisLite::viridis(nlevels(species))

# Extra bottom margin for the caption, extra top for title + subtitle
par(mar = c(6, 4, 4, 2) + 0.1)

plot(penguins$bill_dep, penguins$bill_len,
     col = cols[species],
     pch = 19,
     xlab = "Bill depth (mm)",
     ylab = "Bill length (mm)",
     main = "")   # title added separately so we can add a subtitle

# Title and subtitle
title(main = "Bill depth and length", adj = 0)
mtext("Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
      side = 3, line = 0.3, adj = 0, cex = 0.9)

# Legend
legend("topright",
       legend = levels(species),
       col = cols,
       pch = 19,
       title = "Species")

# Caption
mtext("Source: Palmer Station LTER / palmerpenguins package",
      side = 1, line = 4.5, adj = 1, cex = 0.7, col = "gray40")

import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
from palmerpenguins import load_penguins

# Load the dataset (returns a pandas DataFrame)
penguins = load_penguins()

# The real column names in the package are the full versions,
# not the abbreviated bill_dep / bill_len from the original R snippet
x_col, y_col = "bill_depth_mm", "bill_length_mm"

# Drop rows with missing bill measurements or species
penguins = penguins.dropna(subset=[x_col, y_col, "species"])

fig, ax = plt.subplots(figsize=(8, 6))

# Assign each species a color from viridis (discrete)
species_list = penguins["species"].unique()
colors = cm.viridis(np.linspace(0, 1, len(species_list)))

for sp, color in zip(species_list, colors):
    subset = penguins[penguins["species"] == sp]
    ax.scatter(subset[x_col], subset[y_col], color=color, label=sp)

# Axis labels and legend
ax.set_xlabel("Bill depth (mm)")
ax.set_ylabel("Bill length (mm)")
ax.legend(title="Species")

# Title + subtitle (matplotlib has no native subtitle)
fig.suptitle(
    "Bill depth and length", fontsize=14, fontweight="bold", x=0.125, ha="left"
)
ax.set_title(
    "Dimensions for Adelie, Chinstrap, and Gentoo Penguins", fontsize=10, loc="left"
)

# Caption
fig.text(
    0.98,
    0.01,
    "Source: Palmer Station LTER / palmerpenguins package",
    ha="right",
    va="bottom",
    fontsize=8,
    color="gray",
)

plt.tight_layout()
plt.show()

import matplotlib.pyplot as plt
import seaborn as sns
from palmerpenguins import load_penguins

# Load the dataset (returns a pandas DataFrame)
penguins = load_penguins()

# The palmerpenguins package uses full column names
x_col, y_col = "bill_depth_mm", "bill_length_mm"

# Drop rows with missing bill measurements or species
penguins = penguins.dropna(subset=[x_col, y_col, "species"])

fig, ax = plt.subplots(figsize=(8, 6))

# seaborn handles the per-species coloring and legend via hue
sns.scatterplot(data=penguins, x=x_col, y=y_col,
                hue="species", palette="viridis", ax=ax)

# Axis labels and legend title
ax.set_xlabel("Bill depth (mm)")
ax.set_ylabel("Bill length (mm)")
ax.legend(title="Species")

# Title + subtitle (matplotlib has no native subtitle)
fig.suptitle("Bill depth and length", fontsize=14, fontweight="bold",
             x=0.125, ha="left")
ax.set_title("Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
             fontsize=10, loc="left")

# Caption
fig.text(0.98, 0.01,
         "Source: Palmer Station LTER / palmerpenguins package",
         ha="right", va="bottom", fontsize=8, color="gray")

plt.tight_layout()
plt.show()

from palmerpenguins import load_penguins
from plotnine import ggplot, aes, geom_point, labs, scale_color_cmap_d

# Load the dataset (returns a pandas DataFrame)
penguins = load_penguins()

# The palmerpenguins package uses full column names
x_col, y_col = "bill_depth_mm", "bill_length_mm"

# Drop rows with missing bill measurements or species
penguins = penguins.dropna(subset=[x_col, y_col, "species"])

plot = (
    ggplot(penguins, aes(x=x_col, y=y_col, color="species"))
    + geom_point()
    + labs(
        title="Bill depth and length",
        subtitle="Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
        x="Bill depth (mm)",
        y="Bill length (mm)",
        color="Species",
        caption="Source: Palmer Station LTER / palmerpenguins package",
    )
    + scale_color_cmap_d(cmap_name="viridis")
)

plot.show()

Aesthetics

Aesthetics options

Commonly used channels for discrete categories that can be mapped to a specific variable in the data are

  • color
  • shape
  • size
  • alpha (transparency)

Color

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species
  )
) +
  geom_point() +
  scale_color_viridis_d()

Shape

Mapped to a different variable than color

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species,
    shape = island
  )
) +
  geom_point() +
  scale_color_viridis_d()

Shape

Mapped to same variable as color

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species,
    shape = species
  )
) +
  geom_point() +
  scale_color_viridis_d()

Size

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species,
    shape = species,
    size = body_mass
  )
) +
  geom_point() +
  scale_color_viridis_d()

Alpha

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species,
    shape = species,
    size = body_mass,
    alpha = flipper_len
  )
) +
  geom_point() +
  scale_color_viridis_d()

Mapping

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    size = body_mass,
    alpha = flipper_len
  )
) +
  geom_point()

Setting

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len
  )
) +
  geom_point(size = 2, alpha = 0.5)

Mapping vs. setting

  • Mapping: Determine the size, alpha, etc. of points based on the values of a variable in the data
    • goes into aes()
  • Setting: Determine the size, alpha, etc. of points not based on the values of a variable in the data
    • goes into geom_*()

Faceting

Faceting

  • Smaller plots that display different subsets of the data
  • Useful for exploring conditional relationships and large data

ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_grid(rows = vars(species), cols = vars(island))

Various ways to facet

In the next few slides describe what each plot displays. Think about how the code relates to the output.

Note: The plots in the next few slides do not have proper titles, axis labels, etc. because we want you to figure out what’s happening in the plots. But you should always label your plots!

ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_grid(rows = vars(species), cols = vars(sex))
ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_grid(rows = vars(sex), cols = vars(species))
ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_wrap(facets = vars(species))
ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_grid(rows = NULL, cols = vars(species))
ggplot(data = penguins, mapping = aes(x = bill_dep, y = bill_len)) + 
  geom_point() +
  facet_wrap(facets = vars(species), ncol = 2)

Faceting summary

  • facet_grid():
    • 2 dimensional grid
    • rows = vars(<VARIABLE>), cols = vars(<VARIABLE>)
    • Alternative: rows ~ cols
  • facet_wrap(): 1 dimensional ribbon wrapped according to number of rows and columns specified or available plotting area

Facet and color

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species
  )
) +
  geom_point() +
  facet_grid(species ~ sex) +
  scale_color_viridis_d()

Facet and color, no legend

ggplot(
  data = penguins,
  mapping = aes(
    x = bill_dep,
    y = bill_len,
    color = species
  )
) +
  geom_point() +
  facet_grid(species ~ sex) +
  scale_color_viridis_d(guide = "none")

“The best statistical graphic ever drawn”

Building Minard’s map in R

troops

# A tibble: 51 × 4
    long   lat survivors direction
   <dbl> <dbl>     <dbl> <chr>    
 1  24    54.9    340000 A        
 2  24.5  55      340000 A        
 3  25.5  54.5    340000 A        
 4  26    54.7    320000 A        
 5  27    54.8    300000 A        
 6  28    54.9    280000 A        
 7  28.5  55      240000 A        
 8  29    55.1    210000 A        
 9  30    55.2    180000 A        
10  30.3  55.3    175000 A        
# ℹ 41 more rows

cities

# A tibble: 20 × 3
    long   lat city          
   <dbl> <dbl> <chr>         
 1  24    55   Kowno         
 2  25.3  54.7 Wilna         
 3  26.4  54.4 Smorgoni      
 4  26.8  54.3 Moiodexno     
 5  27.7  55.2 Gloubokoe     
 6  27.6  53.9 Minsk         
 7  28.5  54.3 Studienska    
 8  28.7  55.5 Polotzk       
 9  29.2  54.4 Bobr          
10  30.2  55.3 Witebsk       
11  30.4  54.5 Orscha        
12  30.4  53.9 Mohilow       
13  32    54.8 Smolensk      
14  33.2  54.9 Dorogobouge   
15  34.3  55.2 Wixma         
16  34.4  55.5 Chjat         
17  36    55.5 Mojaisk       
18  37.6  55.8 Moscou        
19  36.6  55.3 Tarantino     
20  36.5  55   Malo-Jarosewii

Application exercise

Define the conceptual grammar of graphics for Minard’s visualization

Instructions

Data
  • Troops
    • Latitude
    • Longitude
    • Survivors
    • Advance/retreat
  • Cities
    • Latitude
    • Longitude
    • City name

Wrap up

Recap

  • {ggplot2} is based on the grammar of graphics
  • Use the ggplot() function to initialize a plot
  • aes() maps variables to aesthetics
  • Use geom_*() to add geoms to a plot
  • Use facet_*() to facet a plot

Acknowledgements