9  Data visualization with ggplot2

Plotting our data is one of the best ways to quickly explore it and the various relationships between variables.

While there exists a plotting system in base R, most people use the ggplot2 package from the tidyverse suite of packages.

ggplot2 is built on the grammar of graphics, the idea that any plot can be expressed from the same set of components: a data set, a coordinate system, and a set of geoms – the visual representation of data points.

The key to understanding ggplot2 is thinking about a figure in layers. This idea may be familiar to you if you have used image editing programs like Photoshop, Illustrator, or Inkscape.

Let’s start with an example:

library(tidyverse)
ggplot(data = gapminder, mapping = aes(x = gdpPercap, y = lifeExp)) +
  geom_point()

So the first thing we do is call the ggplot() function. This function lets R know that we’re creating a new plot, and any of the arguments we give the ggplot function are the global options for the plot: they apply to all layers on the plot.

We’ve passed in two arguments to ggplot.

  1. First, we tell ggplot what data we want to show on our figure, in this example the gapminder data we read in earlier.

  2. For the second argument, we passed in the aes function, which tells ggplot how variables in the data map to aesthetic properties of the figure, in this case, the x and y locations. Here we told ggplot we want to plot the “gdpPercap” column of the gapminder data frame on the x-axis, and the “lifeExp” column on the y-axis.

Notice that we didn’t need to explicitly pass aes these columns (e.g. x = gapminder[, "gdpPercap"]), this is because ggplot is smart enough to know to look in the data for that column!

However, by itself, the call to ggplot isn’t enough to draw a figure:

ggplot(data = gapminder, mapping = aes(x = gdpPercap, y = lifeExp))

Plotting area with axes for a scatter plot of life expectancy vs GDP with no data points visible.

We need to tell ggplot how we want to visually represent the data, which we do by adding a new geom layer. In our example, we used geom_point, which tells ggplot we want to visually represent the relationship between x and y as a scatterplot of points:

ggplot(data = gapminder, mapping = aes(x = gdpPercap, y = lifeExp)) +
  geom_point()

Scatter plot of life expectancy vs GDP per capita, showing a positive correlation between the two variables with data points added.

Challenge 1

Create a scatterplot of gdpPercap (x) versus life expectancy (y):

Recreate your figure using just the gapminder data from 2007. Hint: use the pipe to pipe the output of a filter() function into the ggplot() function

Here is one possible solution:

ggplot(data = gapminder, mapping = aes(x = gdpPercap, y = lifeExp)) + 
  geom_point()

To use just the gapminder data from 2007, we can use filter() to filter to just 2007 and then pipe the results of this filtering into the first argument of ggplot()

gapminder |>
  filter(year == 2007) |>
  ggplot(mapping = aes(x = gdpPercap, y = lifeExp)) + 
  geom_point()

Challenge 2

In the previous examples and challenge, we’ve used the aes() function to tell the scatterplot geom about the x and y locations of each point. Another aesthetic property we can modify is the point color.

Modify the code from the previous challenge to color the points by the “continent” column. What trends do you see in the data? Are they what you expected?

The solution presented below adds color=continent to the call of the aes function. The general trend seems to indicate an increased life expectancy over the years. For continents with stronger economies, we find a longer life expectancy.

gapminder |>
  filter(year == 2007) |>
  ggplot(mapping = aes(x = gdpPercap, y = lifeExp, color = continent)) + 
  geom_point()

Captions

You can add a caption to a figure in a quarto document by supplying a label and fig-cap quarto chunk option:

```{r}
#| label: fig-gdp-lifeexp
#| fig-cap: "GDP per capita vs life expectancy"

gapminder |>
filter(year == 2007) |>
ggplot(mapping = aes(x = gdpPercap, y = lifeExp, color = continent)) + 
geom_point()
```

This will produce the figure with a caption underneath:

Figure 9.1: GDP per capita vs life expectancy

9.1 Line plots

Using a scatterplot probably isn’t the best for visualizing change over time. Instead, let’s tell ggplot to visualize the data as a line plot (dropping the argument names for brevity):

ggplot(gapminder, aes(x = year, y = lifeExp, color = continent)) +
  geom_line()

Instead of adding a geom_point layer, we’ve added a geom_line layer.

However, the result doesn’t look quite as we might have expected: it seems to be jumping around a lot in each continent. This is because we haven’t told ggplot2 to plot a separate line for each country. We can do that by adding a group argument inside the aes() function:

ggplot(gapminder, aes(x = year, y = lifeExp, color = continent, group = country)) +
  geom_line()

The group aesthetic tells ggplot to draw a line for each country.

But what if we want to visualize both lines and points in the plot? We can add another layer to the plot:

ggplot(gapminder, aes(x = year, y = lifeExp, group = country, color = continent)) +
  geom_line() + 
  geom_point()

It’s important to note that each layer is drawn on top of the previous layer. In this example, the points have been drawn on top of the lines. Here’s a demonstration:

ggplot(gapminder, aes(x = year, y = lifeExp, group = country)) +
  geom_line(aes(color = continent)) + geom_point()

In this example, the aesthetic mapping of color has been moved from the global plot options in ggplot to the geom_line layer so it no longer applies to the points layer. Now we can clearly see that the points are drawn on top of the lines.

Tip: Setting an aesthetic to a value instead of a mapping

So far, we’ve seen how to use an aesthetic (such as color) as a mapping to a variable in the data. For example, when we use geom_line(aes(color = continent)), ggplot will give a different color to each continent.

But what if we want to change the color of all lines to blue?

You may think that aes(color="blue") should work, but it doesn’t.

ggplot(gapminder) +
  geom_line(aes(x = year, y = lifeExp, group = country, color = "blue")) 

Since we don’t want to create a mapping to a specific variable from our data frame, we need to move the color specification outside of the aes() function, like this: geom_line(color="blue").

ggplot(gapminder) +
  geom_line(aes(x = year, y = lifeExp, group = country), color = "blue") 

Challenge 3

Switch the order of the point and line layers from the previous example. What happens?

The lines now get drawn over the points!

ggplot(gapminder, aes(x = year, y = lifeExp, group = country)) +
  geom_point() + 
  geom_line(mapping = aes(color = continent))

Scatter plot of life expectancy vs GDP per capita with a trend line summarising the relationship between variables. The plot illustrates the possibilities for styling visualizations in ggplot2 with data points enlarged, colored orange, and displayed without transparency.

9.2 Transformations

ggplot2 also makes it easy to overlay statistical models over the data. To demonstrate we’ll go back to our earlier example:

ggplot(data = gapminder, mapping = aes(x = gdpPercap, y = lifeExp)) +
  geom_point()

Currently, it’s hard to see the relationship between the points due to some strong outliers in GDP per capita. We can change the scale of units on the x axis using the scale functions. These control the mapping between the data values and visual values of an aesthetic.

We can also modify the transparency of the points, using the alpha function, which is especially helpful when you have a large amount of data thatxs is very clustered.

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.5) + 
  scale_x_log10()

Scatterplot of GDP vs life expectancy showing logarithmic x-axis data spread

The scale_x_log10() function applied a transformation to the coordinate system of the plot so that each multiple of 10 is evenly spaced from left to right. For example, a GDP per capita of 1,000 is the same horizontal distance away from a value of 10,000 as the 10,000 value is from 100,000. This helps to visualize the spread of the data along the x-axis.

Tip Reminder: Setting an aesthetic to a value instead of a mapping

Notice that we used geom_point(alpha = 0.5). As the previous tip mentioned, using a setting outside of the aes() function will cause this value to be used for all points, which is what we want in this case. But just like any other aesthetic setting, alpha can also be mapped to a variable in the data. For example, we can give a different transparency to each continent with geom_point(mapping = aes(alpha = continent)).

We can also fit a simple relationship to the data by adding another layer, geom_smooth():

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "lm") +
  scale_x_log10() 
`geom_smooth()` using formula = 'y ~ x'

Scatter plot of life expectancy vs GDP per capita with a blue trend line summarising the relationship between variables, and gray shaded area indicating 95% confidence intervals for that trend line.

We can make the line thicker by setting the linewidth aesthetic in the geom_smooth layer:

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(alpha = 0.5) + 
  geom_smooth(method = "lm", linewidth = 1.5) +
  scale_x_log10() 
`geom_smooth()` using formula = 'y ~ x'

Scatter plot of life expectancy vs GDP per capita with a trend line summarising the relationship between variables. The blue trend line is slightly thicker than in the previous figure.

Challenge 4a

In the previous example, uniformly set the color of all points to “orange” and the size of all points to 3.

Hint: do this outside the aes() function.

Here is a possible solution:

Notice that the color and size arguments are supplied outside of the aes() function.

This means that it applies to all data points on the graph and is not related to a specific variable.

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  # set size and color
  geom_point(size = 3, color = "orange") + 
  geom_smooth(method = "lm", size = 1.5) +
  scale_x_log10() 
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.
`geom_smooth()` using formula = 'y ~ x'

Scatter plot of life expectancy vs GDP per capita with a trend line summarising the relationship between variables. The plot illustrates the possibilities for styling visualizations in ggplot2 with data points enlarged, colored orange, and displayed without transparency.

Challenge 4b

Modify your solution to Challenge 4a so that the color of the points is determined by the continent variable and the size is determined by the pop variable.

Hint: The color and size arguments must now be used inside the aesthetic.

Here is a possible solution:

Notice that supplying the color and size arguments inside the aes() functions enables you to connect it to a certain variable.

ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  # set size and color
  geom_point(aes(size = pop, color = continent)) + 
  geom_smooth(method = "lm", size = 1.5) +
  scale_x_log10() 
`geom_smooth()` using formula = 'y ~ x'

Supplying the size and color arguments inside the ggplot() function will also apply them to the geom_smooth() layer.

9.3 Multi-panel figures

Earlier we visualized the change in life expectancy over time across all countries in one plot like this:

ggplot(gapminder, aes(x = year, y = lifeExp, color = continent, group = country)) +
  geom_line()

Another way to view this data is to split this out over multiple panels by adding a layer of facet panels.

Since there are a lot of countries, we will first filter just to the “Americas”:

gapminder |> 
  filter(continent == "Americas") |>
  ggplot() +
  geom_line(aes(x = year, y = lifeExp)) +
  # make a separate plot for each country in the facet
  facet_wrap(~country) +
  # set the x-axis angle to 45 degrees
  theme(axis.text.x = element_text(angle = 90))

The facet_wrap layer took a “formula” as its argument, denoted by the tilde (~). This tells R to draw a panel for each unique value in the country column of the gapminder dataset.

9.4 Modifying labels

To clean this figure up for a publication we need to change some of the text elements. The x-axis is too cluttered, and the y-axis should read “Life expectancy”, rather than the column name in the data frame.

We can do this by adding a couple of different layers. The theme layer controls the axis text and overall text size. Labels for the axes, plot title, and any legend can be set using the labs function.

gapminder |> 
  filter(continent == "Americas") |>
  ggplot() +
  geom_line(aes(x = year, y = lifeExp)) +
  # make a separate plot for each country in the facet
  facet_wrap(~country) +
  labs(x = "Year",              
       y = "Life expectancy",   
       title = "Life expectancy by year in the Americas") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

Challenge 5

Using geom_boxplot(), generate boxplots to compare life expectancy between the different continents, faceted by year. Color each boxplot by continent and rename each label so that it is nicely formatted and human-readable.

Here is a possible solution:

gapminder |> 
  ggplot(aes(x = continent, y = lifeExp, fill = continent)) +
  geom_boxplot() + 
  facet_wrap(~year) +
  labs(x = "Continent",
       y = "Life Expectancy",
       fill = "Continent") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5))

9.5 Built-in themes

There are several themes for making your plots even prettier. For example,

  • theme_classic():
ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(aes(size = pop, color = continent)) +
  scale_x_log10()  +
  theme_classic()

  • theme_minimal():
ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(aes(size = pop, color = continent)) +
  scale_x_log10()  +
  theme_minimal()

  • theme_bw():
ggplot(gapminder, aes(x = gdpPercap, y = lifeExp)) +
  geom_point(aes(size = pop, color = continent)) +
  scale_x_log10()  +
  theme_bw()

9.6 Exporting the plot

The ggsave() function allows you to export a plot created with ggplot. You can specify the dimension and resolution of your plot by adjusting the appropriate arguments (width, height, and dpi) to create high-quality graphics for publication. In order to save the plot from above, we first assign it to a variable lifeExp_plot, then tell ggsave to save that plot in png format to a directory called results. (Make sure you have a results/ folder in your working directory.)

facet_plot <- gapminder |> 
  filter(continent == "Americas") |>
  ggplot() +
  geom_line(aes(x = year, y = lifeExp)) +
  # make a separate plot for each country in the facet
  facet_wrap(~country) +
  labs(x = "Year",              
       y = "Life expectancy",   
       title = "Life expectancy by year in the Americas") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

ggsave(filename = "results/lifeExp.png", plot = facet_plot, width = 12, height = 10, dpi = 300, units = "cm")

There are two nice things about ggsave. First, it defaults to the last plot, so if you omit the plot argument it will automatically save the last plot you created with ggplot. Secondly, it tries to determine the format you want to save your plot in from the file extension you provide for the filename (for example .png or .pdf). If you need to, you can specify the format explicitly in the device argument.

This is a taste of what you can do with ggplot2.

RStudio provides a really useful cheat sheet of the different layers available, and more extensive documentation is available on the ggplot2 website. Finally, if you have no idea how to change something, a quick Google search will usually send you to a relevant question and answer on Stack Overflow with reusable code to modify!