Chapter 1 Layers:

Total points: 43

Instructions:

  • This is the complete lessons version with all working examples
  • Run all the code as you go to see how everything works
  • Exercise questions are shown but without answer spaces
  • Use the STUDENT version to practice writing your own code

1.1 Introduction

You’ve already learned the basics of ggplot2: how to create scatterplots and boxplots, how to map aesthetics like color and shape, and how to customize your plots with themes and labels.

In this chapter, we’ll dive deeper into the grammar of graphics - the idea that you can build every graph from the same components: a dataset, a coordinate system, and geoms (geometric objects that represent data points). We’ll explore:

  • A deeper understanding of aesthetic mappings (inside vs outside aes)

  • More geometric objects (geoms) for different plot types

  • Faceting - creating multiple plots split by categories

  • Position adjustments - how geoms are positioned relative to each other

We won’t cover every single function and option, but we’ll walk you through the most important and commonly used functionality in ggplot2.

1.1.1 Prerequisites

Load the necessary packages into your session:

library(tidyverse)                # for ggplot2 and mpg
library(patchwork)                # for combining plots
library(scales)                   # for axis formatting

1.2 Aesthetic mappings

The mpg data frame is bundled with the ggplot2 package and contains 234 observations on 38 car models from fueleconomy.gov.

Let’s start by visualizing the relationship between displ and hwy for various classes of cars. We can do this with a scatterplot where the numerical variables are mapped to the x and y aesthetics and the categorical variable is mapped to an aesthetic like color or shape.

# Mapping class to color works well
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
  geom_point()

Notice how each class gets a different color automatically. Now let’s try using shape instead:

# Mapping class to shape produces warnings
ggplot(mpg, aes(x = displ, y = hwy, shape = class)) +
  geom_point()

When class is mapped to shape, we get two warnings:

1: The shape palette can deal with a maximum of 6 discrete values because more than 6 becomes difficult to discriminate; we have 7. Consider specifying shapes manually if you must have them.

2: Removed 62 rows containing missing values (geom_point()).

Since ggplot2 will only use six shapes at a time, by default, additional groups will go unplotted when you use the shape aesthetic. The second warning is related – there are 62 SUVs in the dataset and they’re not plotted.

We have to remember that ggplot2 does not replace, but layers. For example:

# Create a saved plot
p1 <- ggplot(mpg, aes(x = displ, y = hwy, color = class)) + 
  geom_point(size = 2)

p1  # p1

1.2.1 Step1: make the points bigger

What if I wanted to make all the points bigger? If I add another geom_point(size = 6), it looks like it worked!

p2 = p1 + geom_point(size = 6)

p2

1.2.2 Step 2: Now try to make them smaller

What if I wanted to make all the points smaller?

p3 = p2+ geom_point(size = 2, color = "black") # "Wait... now I see BOTH layers! Small points on top of the original size!"

p3

The grammar of graphics works like painting; each + adds a new layer of paint on top. You can paint over something, but the old layer is still there underneath. When the new layer is bigger, it hides the original. When it’s smaller, you see both!

Layers stack, they don’t replace!

To actually change p1, you need to recreate it from scratch:

p1 <- ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
  geom_point(size = 6)

p1

However, you CAN modify non-data elements - these are like changing the frame around your painting, not the painting itself:

# Change the labels
p1 + labs(title = "Fuel Efficiency by Car Class")

# Change the theme  
p1 + theme_classic()

These don’t add new data layers - they modify how the existing plot is displayed.

The rule: - Data layers (geom_point, geom_line, etc.) → Stack like layers of paint - Display elements (labs, themes) → Modify the frame and presentation

Sometimes it’s helpful to compare plots side-by-side. The patchwork package makes this easy:

# Save plots to variables
p1 <- ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
  geom_point()

p2 <- ggplot(mpg, aes(x = displ, y = hwy, shape = class)) +
  geom_point()

# Combine side-by-side with +
p1 + p2

You can also stack plots vertically using /:

# Stack plots vertically
p1 / p2

This is particularly useful when comparing different visualizations of the same data, as you’ll often need to do in your research when comparing treatment groups or experimental conditions.

What about other aesthetics? Similarly, we can map class to size or alpha aesthetics as well, which control the size and the transparency of the points, respectively.

# class mapped to size
ggplot(mpg, aes(x = displ, y = hwy, size = class)) +
  geom_point() 

# class mapped to alpha
ggplot(mpg, aes(x = displ, y = hwy, alpha = class)) +
  geom_point() 

Both of these produce warnings as well:

Using size for a discrete variable is not advised. Using alpha for a discrete variable is not advised.

Mapping an unordered discrete (categorical) variable (class) to an ordered aesthetic (size or alpha) is generally not a good idea because it implies a ranking that does not in fact exist.

1.2.3 Setting Visual Properties

Once you map an aesthetic, ggplot2 automatically: - Chooses appropriate values (colors, shapes, sizes) - Creates a legend showing what each value means - For x and y aesthetics, creates axis labels instead of legends

You can also set the visual properties of your geom manually as an argument of your geom function (outside of aes()) instead of relying on a variable mapping to determine the appearance. For example, we can make all of the points in our plot blue:

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point(color = "blue") 

1.2.4 Where do aesthetics go in ggplot2?

The key question: Is this value from your data, or is it a constant?

From your data (a column name) → INSIDE aes()

# color varies by class (class is a column in mpg)
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
  geom_point()

A constant (same for all points) → OUTSIDE aes(), in the geom layer

# all points are pink
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(color = "pink", size = 3)

Memory trick: Can you do mpg$class in R? YES → inside aes()
Can you do mpg$pink? NO → outside aes()

Important: Constants must go in geom_*(), NOT in ggplot():

# ❌ This fails!
ggplot(mpg, aes(x = displ, y = hwy), color = "blue") +
  geom_point()

# ✅ This works!
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(color = "blue")

1.2.5 The Universal Rule for ALL Aesthetics

Where can variables and constants go?

Location Variable (from data)
Example: color = class
Constant (fixed value)
Example: color = "blue"
ggplot(aes(...)) ✅ YES
aes(color = class)
❌ NO
geom_*(aes(...)) ✅ YES
aes(color = class)
❌ NO
geom_*(...) outside aes ❌ NO ✅ YES
color = "blue"

This pattern applies to all aesthetics: size, color, shape, alpha, fill, etc.

1.2.6 Setting Constant Values

You’ll need to pick appropriate values for each aesthetic:

  • The name of a color as a character string, e.g., color = "blue"
  • The size of a point in mm, e.g., size = 1
  • The shape of a point as a number, e.g, shape = 1, as shown in Figure 1.1
    Mapping between shapes and their numeric codes.

    Figure 1.1: Mapping between shapes and their numeric codes.

So far we have discussed aesthetics that we can map or set in a scatterplot, when using a point geom. You can learn more about all possible aesthetic mappings in the aesthetic specifications vignette at https://ggplot2.tidyverse.org/articles/ggplot2-specs.html.

The specific aesthetics you can use for a plot depend on the geom you use to represent the data.

In the next section we dive deeper into geoms.

1.2.1 Exercises

  1. Create a scatterplot of hwy vs. displ where the points are pink filled in triangles. +2pts

  2. What does the stroke aesthetic do? What shapes does it work with? (Hint: use ?geom_point). Add stroke to the above plot. +2pts

  3. Why did the following code not result in a plot with blue points? +2pts

ggplot(mpg) + 
  geom_point(aes(x = displ, y = hwy, color = "blue"))
  1. What’s wrong with the following code? How would you fix it? +2pts
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(color = class, size = 3)
  1. What happens if you map an aesthetic to something other than a variable name, like aes(color = displ < 5)? Try it by adding color to the following plot: +2pts
ggplot(mpg) + geom_point(aes(x = displ, y = hwy))
  1. What’s wrong with the following code? Why doesn’t it make blue points? How would you fix it? +2pts
ggplot(mpg, aes(x = displ, y = hwy), color = "blue") + 
  geom_point()

1.3 Geometric objects

You’ve already learned about geom_point() and geom_boxplot() in your previous lessons. A geom is the geometrical object that a plot uses to represent data. People often describe plots by the type of geom that the plot uses. For example, bar charts use bar geoms (geom_bar()), line charts use line geoms (geom_line()), histograms use histograms geoms (geom_histogram()), and density plots use density geoms (geom_density()).

How are these two plots similar?

# Scatterplot
ggplot(mpg, aes(x = displ, y = hwy)) +  
  geom_point() 

# Smooth line
ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_smooth()

Both plots contain the same x variable, the same y variable, and both describe the same data. But the plots are not identical. Each plot uses a different geometric object, geom, to represent the data. The first plot uses the point geom, and the second uses the smooth geom, a smooth line fitted to the data.

To change the geom in your plot, change the geom function that you add to ggplot().

Every geom function in ggplot2 takes a mapping argument, either defined locally in the geom layer or globally in the ggplot() layer.

However, not every aesthetic works with every geom. You could set the shape of a point, but you couldn’t set the “shape” of a line. If you try, ggplot2 will silently ignore that aesthetic mapping. On the other hand, you could set the linetype of a line.

geom_smooth() will draw a different line, with a different linetype, for each unique value of the variable that you map to linetype.

# Shape doesn't work with geom_smooth - no legend appears
ggplot(mpg, aes(x = displ, y = hwy, shape = drv)) + 
  geom_smooth()

# Linetype DOES work with geom_smooth
ggplot(mpg, aes(x = displ, y = hwy, linetype = drv)) + 
  geom_smooth()

Here, geom_smooth() separates the cars into three lines based on their drv value, which describes a car’s drive train. Here, 4 stands for four-wheel drive, f for front-wheel drive, and r for rear-wheel drive.

Notice there is no legend for the first plot because geom_smooth does not have a shape aesthetic.

If this sounds strange, we can make it clearer by overlaying the lines on top of the raw data and then coloring everything according to drv.

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point() + 
  geom_smooth(aes(linetype = drv))

Notice that this plot contains two geoms in the same graph. This is a powerful feature of ggplot2 - you can layer multiple geoms to show different aspects of your data.

You can use the same idea to specify different data for each layer. Here, we use red points to highlight two-seater cars. The local data argument in geom_point() overrides the global data argument in ggplot() for that layer only.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_point(
   data  = mpg |> filter(class == '2seater'),
    color = "red",
    size = 3
  )

geoms are the fundamental building blocks of ggplot2. You can completely transform the look of your plot by changing its geom, and different geoms can reveal different features of your data.

For example, these three plots show the same data (highway mileage) but reveal different patterns:

# Histogram shows distribution
ggplot(mpg, aes(x = hwy)) + 
  geom_histogram(binwidth = 2)

# Density plot shows smooth distribution
ggplot(mpg, aes(x = hwy)) + 
  geom_density()

# Boxplot shows summary statistics and outliers
ggplot(mpg, aes(x = hwy)) + 
  geom_boxplot()

The histogram and density plot reveal that the distribution of highway mileage is bimodal and right skewed, while the boxplot reveals two potential outliers. c To learn more about any single geom, use the help (e.g., ?geom_smooth).

1.3.1 Exercises

  1. What geom would you use to draw a line chart? A boxplot? A histogram? A scatterplot with points spread out to avoid overlap? (‘jitter plot’) +4pts or +5pts (bonus point for line charts using TWO geometries)

  2. Create a plot showing highway fuel economy (hwy) by car class (class) that includes both a boxplot and jittered individual points. Make the points semi-transparent. +3pts

  3. What does show.legend = FALSE do in the code below? What happens if you remove it? +2pts

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_smooth(aes(color = drv), show.legend = FALSE)

  1. What does the se argument to geom_smooth() do? +2pts

  2. Will these two graphs look different? Why/why not? +2pts

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_smooth()

ggplot() +
  geom_point(data = mpg, mapping = aes(x = displ, y = hwy)) +
  geom_smooth(data = mpg, mapping = aes(x = displ, y = hwy))

1.4 Facets

Facets are a powerful way to split your plot into multiple subplots based on the values of one or more categorical variables. This is especially useful when you have too many categories to distinguish by color alone, or when you want to compare patterns across groups side-by-side.

1.4.1 Faceting by One Variable

To facet your plot by a single variable, use facet_wrap(). The first argument of facet_wrap() is a formula (created with ~ followed by a variable name). The variable passed to facet_wrap() should be categorical.

# Instead of trying to use 7 different colors for class...
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_wrap(~ class, nrow = 2)

This makes it much easier to see the relationship between engine size and fuel efficiency within each vehicle class. Notice how 2seaters behave differently from SUVs!

1.4.2 Faceting by Two Variables

To facet your plot with the combination of two variables, use facet_grid(). The first argument of facet_grid() is also a formula, but now it’s a double sided formula: rows ~ cols.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_grid(drv ~ cyl)

The first variable (drv) determines the faceting by row, the second (cyl) by column. This lets you see how both drive type and number of cylinders affect the relationship between engine size and highway mileage.

1.4.3 Adjusting Scales

By default each of the facets share the same scale and range for x and y axes. This is useful when you want to compare data across facets, but it can be limiting when you want to visualize the relationship within each facet better.

Setting the scales argument in a faceting function to "free" will allow for different axis scales across both rows and columns, "free_x" will allow for different scales across columns, and "free_y" will allow for different scales across rows.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_grid(drv ~ cyl, scales = "free_y")

Notice how the y-axis now adjusts for each row, making it easier to see the variation in highway mileage within each drive type.

1.4.1 Exercises

  1. What happens if you facet on a continuous variable? Try faceting the plot below on hwy. +2pts
ggplot(mpg, aes(x = drv, y = cyl)) + 
  geom_point()

  1. Run the following code and facet on drv vs cyl using facet_grid(). What do the empty cells mean? +2pts
ggplot(mpg, aes(x = drv, y = cyl)) + 
  geom_point()

  1. What plots does the following code make? What does . do? +2pts
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_grid(drv ~ .)

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_grid(. ~ cyl)

  1. Take the first faceted plot in this section:
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  facet_wrap(~ class, nrow = 2)

What are the advantages to using faceting instead of the color aesthetic? What are the disadvantages? +4pts

  1. Read ?facet_wrap. What does nrow do? What does ncol do? Why doesn’t facet_grid() have nrow and ncol arguments? +2pts

1.5 Position adjustments

Position adjustments control how geoms are arranged when they might overlap. This is particularly important for bar charts.

You can color bar charts using either the color aesthetic (outline) or the fill aesthetic (inside):

# Fill colors the inside of the bars
ggplot(mpg, aes(x = drv, fill = drv)) + 
  geom_bar()

When you map fill to another variable, the bars are automatically stacked. Each colored rectangle represents a combination of the two variables:

# Stacked bars (default)
ggplot(mpg, aes(x = drv, fill = class)) + 
  geom_bar()

The stacking is controlled by the position argument. You can use different positions to change how the bars are displayed:

1.5.0.1 position = "fill" - Show proportions

Makes each set of stacked bars the same height, making it easier to compare proportions across groups:

# All bars go to 100%
ggplot(mpg, aes(x = drv, fill = class)) +
  geom_bar(position = "fill")

1.5.0.2 position = "dodge" - Side-by-side comparison

Places bars directly beside one another, making it easier to compare individual counts:

# Side-by-side bars
ggplot(mpg, aes(x = drv, fill = class)) +
  geom_bar(position = "dodge")

Position adjustments summary: - position = "stack" (default for bars): Shows total counts with composition breakdown - position = "fill": Shows proportions (all bars reach 100%) - position = "dodge": Shows side-by-side for easy comparison - position = "jitter": Adds random noise to points to avoid overplotting (you learned this with geom_jitter() earlier!)

To learn more about position adjustments, see ?position_dodge, ?position_fill, ?position_jitter, and ?position_stack.

1.6.1 Exercises

  1. What’s the difference between position = "stack" and position = "fill" for bar charts? When would you use each? +2pts

  2. Create a dodged bar chart showing the count of cars by class and drv. +2pts

  3. What’s the default position adjustment for geom_boxplot()? Create a visualization of the mpg dataset that demonstrates it. +2pts

1.6 Summary

In this chapter you learned about the layered grammar of graphics starting with aesthetics and geometries to build a simple plot, facets for splitting the plot into subsets, and position adjustments for controlling how geoms are displayed when they might overlap.

1.6.1 Key concepts learned:

  1. Aesthetics: Map variables from your data to visual properties
    • Inside aes() for variables: aes(color = class)
    • Outside aes() for constants: color = "blue"
    • Memory trick: Can you do mpg$class? YES → inside aes()
  2. Geoms: The geometric objects that represent your data
    • geom_point(), geom_line(), geom_bar(), geom_boxplot(), geom_jitter(), etc.
    • Can layer multiple geoms in one plot
    • Layers stack, they don’t replace!
  3. Facets: Split your plot into subplots
    • facet_wrap(~ variable) for one variable
    • facet_grid(rows ~ cols) for two variables
    • Use . as placeholder: facet_grid(. ~ cols) or facet_grid(rows ~ .)
  4. Position adjustments: Fine-tune positioning for overlapping geoms
    • position = "stack" - default for bars (shows totals with composition)
    • position = "fill" - proportional stacking (all bars reach 100%)
    • position = "dodge" - side-by-side bars
    • position = "jitter" - adds random noise to avoid overplotting

1.6.2 The grammar of graphics works like painting:

Each + adds a new layer of paint on top. You can paint over something, but the old layer is still there underneath. Data layers stack, but display elements (themes, labels, scales) can be modified.

Two very useful resources for getting an overview of the complete ggplot2 functionality are the ggplot2 cheatsheet (which you can find at https://posit.co/resources/cheatsheets) and the ggplot2 package documentation at https://ggplot2.tidyverse.org/reference.