Facets, coordinates, annotations, themes, and professional export in ggplot2
Today we move beyond geoms/aesthetics to compose, clarify, and polish plots.
coord_cartesian() (zoom w/o dropping), coord_flip() (long labels). Beware truncated axes.geom_text(), geom_label(), ggrepel, arrows/callouts.gray, bw, minimal, classic, void) + ggthemes (e.g., economist, wsj); quick tweaks.ggarrange() • gganimate/ggiraph • ggsave (formats, aspect, DPI).Today’s focus:
Facets split the data into multiple small plots (panels).
When to use facets?
When to use color/fill aesthetics?
Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
This is our dataset:
library(ggplot2)
set.seed(123)
# Toy dataset with US and UK
eg5 <- data.frame(
  year = rep(c(2000, 2004, 2008, 2012, 2016, 2020), times = 2),
  turnout = c(
    # US presidential elections
    54, 60, 62, 58, 56, 65,
    # UK general elections (closest years aligned to US election years for teaching)
    59, 61, 65, 66, 68, 67
  ),
  country = rep(c("United States", "United Kingdom"), each = 6)
)Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
This is what it looks like:
| year | turnout | country | 
|---|---|---|
| 2000 | 54 | United States | 
| 2004 | 60 | United States | 
| 2008 | 62 | United States | 
| 2012 | 58 | United States | 
| 2016 | 56 | United States | 
| 2020 | 65 | United States | 
| 2000 | 59 | United Kingdom | 
Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
This is what we had previously:
Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
This is the alternative using facets:
Suppose we surveyed people about their trust in government on a 1–10 scale (1 = no trust, 10 = complete trust). We want to compare typical values and how spread out the answers are for men and women.
Suppose we surveyed people about their trust in government on a 1–10 scale (1 = no trust, 10 = complete trust). We want to compare typical values and how spread out the answers are for men and women.
This is what it looks like:
| gender | trust | 
|---|---|
| Men | 3.879049 | 
| Men | 4.539645 | 
| Men | 8.117417 | 
| Men | 5.141017 | 
| Men | 5.258576 | 
Suppose we surveyed people about their trust in government on a 1–10 scale (1 = no trust, 10 = complete trust). We want to compare typical values and how spread out the answers are for men and women.
This is what we had previously:
Suppose we surveyed people about their trust in government on a 1–10 scale (1 = no trust, 10 = complete trust). We want to compare typical values and how spread out the answers are for men and women.
ggarrange()Sometimes you don’t want facets (splitting one dataset).
Instead, you might want to combine different plots — for example:
library(ggpubr)
# Plot 1: voter turnout
p1 <- ggplot(eg5, aes(x = year, y = turnout, color = country, group = country)) +
  geom_line(linewidth=1.2) +
  labs(title = "Voter Turnout")
# Plot 2: trust in government
p2 <- ggplot(eg6, aes(x = trust, fill = gender)) +
  geom_histogram(bins = 10, color = "white", alpha = 0.7) +
  labs(title = "Trust in Government")
# Arrange them side by side
ggarrange(p1, p2, ncol = 2)Common functions:
coord_cartesian() – zoom without dropping datacoord_flip() – swap x & y axes (useful for long labels)Let us check out another toy example inspired by real data.
The data looks like this:
| side | support | 
|---|---|
| Leave | 52 | 
| Remain | 48 | 
This is how we can plot our data:
This is how we can truncate the axis (zoom the view) without dropping data:
Moral: Truncated axes exaggerate differences.
Use coord_cartesian() for transparency.
coord_flip() swaps x and y axes, rotating the entire plot.
Useful for bar charts with long category labels (improves readability).
Often makes rankings and comparisons easier to interpret.
Suppose we have survey data on average voter turnout rates (%) across different education groups. Here we don’t want to plot individual points — instead, we’re comparing aggregated values (categories on x, turnout on y).
| education | 
|---|
| Primary | 
| Primary | 
| High School | 
| High School | 
Suppose we have survey data on average voter turnout rates (%) across different education groups. Here we don’t want to plot individual points — instead, we’re comparing aggregated values (categories on x, turnout on y).
Suppose we have survey data on average voter turnout rates (%) across different education groups. Here we don’t want to plot individual points — instead, we’re comparing aggregated values (categories on x, turnout on y).
This is how we can use coord_flip
Raw plots ≠ finished plots.
Annotations direct the reader’s eye and add context.
Labels vs Annotations
geom_textWe learned to use geom_text in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
eg1 <- data.frame(
  democracy = c(-8, -7, -5, -3, 0, 2, 5, 8, 9),   # democracy score
  gdp = c(2, 9, 4, 7, 8, 20, 15, 25, 27),  # GDP per capita in $1,000s
  country = c(
    "North Korea",   # very autocratic, very poor
    "Saudi Arabia",  # autocratic, but richer due to oil
    "Zimbabwe",      # authoritarian, low GDP
    "Russia",        # hybrid regime, middle income
    "Nigeria",       # similar position
    "India",         # low–mid democracy, growing GDP
    "Brazil",        # democracy, mid GDP
    "Poland",        # consolidated democracy, higher GDP
    "South Korea"))   # rich democracygeom_textWe learned to use geom_text in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
| democracy | gdp | country | 
|---|---|---|
| -8 | 2 | North Korea | 
| -7 | 9 | Saudi Arabia | 
| -5 | 4 | Zimbabwe | 
| -3 | 7 | Russia | 
| 0 | 8 | Nigeria | 
geom_textWe learned to use geom_text in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
geom_labelWe could also use geom_label in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
geom_text_repelWe could also use geom_text_repel in this example to avoid clutter:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
geom_label_repelWe could also use geom_label_repel in this example to avoid clutter:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
geom_textWe learned to use geom_text in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
# Scatterplot with geom_point
ggplot(eg1, aes(x = democracy, y = gdp)) +
  geom_point(size = 5) +
  geom_text(aes(label = country), vjust = -1, size = 5) + 
  annotate("text", x = -7, y = 11, label = "Oil-rich outlier", 
           color = "red", size = 6, fontface = "bold") +
  annotate("segment", x = -7, xend = -7, y = 9, yend = 10.5, 
           arrow = arrow(length = unit(0.2, "cm")), color = "red")labs()?Every good plot needs context. labs() lets you set:
Previously, we plotted democracy score vs GDP with labels:
labs()Now, we can improve readability:
ggplot(eg1, aes(x = democracy, y = gdp)) +
  geom_point(size = 5) +
  geom_text(aes(label = country), vjust = -1, size = 5) +
  labs(
    title = "Democracy and Wealth",
    subtitle = "Higher democracy scores often align with higher GDP",
    x = "Democracy Score (-10 = Autocracy, +10 = Democracy)",
    y = "GDP per Capita (in $1,000s)",
    caption = "Toy dataset, inspired by real-world patterns"
  )Control non-data elements of the plot.
Fonts, background, gridlines, legend placement, margins.
Do not affect the data → only the presentation.
Good themes = clarity + professionalism.
theme_grey()Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
theme_bw()Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
theme_minimal()Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
theme_classic()Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
theme_void()Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
ggthemes - theme_economistTo access additional themes you should install the ggthemes package
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
ggthemes - theme_wsjTo access additional themes you should install the ggthemes package
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
# Scatterplot with geom_point
ggplot(eg1, aes(x = democracy, y = gdp)) +
  geom_point(size = 5) +
  geom_text(aes(label = country), vjust = -1, size = 5) + 
  theme_minimal()+
  theme(panel.background = element_rect(fill = "white"), # Set background color
        axis.title.x = element_text(color = "#BF0404", face = "bold"), # Set x-axis label color
        axis.title.y = element_text(color = "#BF0404", face = "bold"),
        axis.line = element_line(color = "#BF0404", size = 1.5), 
        panel.grid = element_blank(), 
        panel.border = element_blank(), 
        panel.grid.major.y = element_line(color = "#40403E", size = 0.5, linetype = "dotted"))Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
set.seed(123)
library(gganimate)
# Toy dataset with US and UK
eg5 <- data.frame(
  year = rep(c(2000, 2004, 2008, 2012, 2016, 2020), times = 2),
  turnout = c(
    # US presidential elections
    54, 60, 62, 58, 56, 65,
    # UK general elections (closest years aligned to US election years for teaching)
    59, 61, 65, 66, 68, 67
  ),
  country = rep(c("United States", "United Kingdom"), each = 6)
)Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
| year | turnout | country | 
|---|---|---|
| 2000 | 54 | United States | 
| 2004 | 60 | United States | 
| 2008 | 62 | United States | 
| 2012 | 58 | United States | 
| 2016 | 56 | United States | 
| 2020 | 65 | United States | 
| 2000 | 59 | United Kingdom | 
Suppose we have data on average voter turnout (%) in national elections over several years for the US and the UK. We want to see the trend in participation.
Let us first create a panel data.
library(dplyr)
library(tidyr)
# Base cross-section
eg1 <- data.frame(
  democracy = c(-8, -7, -5, -3, 0, 2, 5, 8, 9),   # democracy score
  gdp = c(2, 9, 4, 7, 8, 20, 15, 25, 27),  # GDP per capita ($1,000s)
  country = c(
    "North Korea",
    "Saudi Arabia",
    "Zimbabwe",
    "Russia",
    "Nigeria",
    "India",
    "Brazil",
    "Poland",
    "South Korea"
  )
)
# Add a fake panel (2000–2020 every 5 years)
set.seed(123)  # reproducible "wiggles"
eg_panel <- eg1 %>%
  slice(rep(1:n(), each = 5)) %>%           # repeat each country 5 times
  mutate(year = rep(seq(2000, 2020, 5), times = nrow(eg1))) %>%
  group_by(country) %>%
  mutate(
    # let democracy scores drift a bit
    democracy = democracy + cumsum(runif(5, -0.6, 0.3)),
    # let GDP grow with some noise
    gdp = gdp + cumsum(runif(5, 0, 2))
  )Let us first examine the data
| democracy | gdp | country | year | 
|---|---|---|---|
| -7.790158 | 3.330230 | North Korea | 2000 | 
| -8.168679 | 3.519912 | North Korea | 2005 | 
| -8.730825 | 4.287851 | North Korea | 2010 | 
| -9.035696 | 4.836618 | North Korea | 2015 | 
| -8.776643 | 6.465898 | North Korea | 2020 | 
| -6.733278 | 9.440238 | Saudi Arabia | 2000 | 
| -6.521209 | 10.199871 | Saudi Arabia | 2005 | 
We learned to use geom_text in this example:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
library(ggplot2)
library(sf)
library(rnaturalearth)
library(rnaturalearthdata)
library(ggiraph)
library(glue)
library(scales)
world <- ne_countries(scale = "medium", 
                      returnclass = "sf")
europe_bounds <- list(x = c(-10, 40),
                      y = c(35, 70))
# Mapping Them
# Interactive ggplot
p <- ggplot() +
  geom_sf_interactive(
    data = world,
    aes(
      fill = log(pop_est),
      tooltip = glue("{admin}: {comma(pop_est)}")  # hover text
    ),
    color = "white", linewidth = 0.1
  ) +
  coord_sf(
    xlim = europe_bounds$x,
    ylim = europe_bounds$y
  ) +
  theme_grey(base_size = 25) +
  scale_fill_viridis_c(option = "viridis")
# Render interactive plot with tooltips
girafe(ggobj = p)This was the first example we had:
Imagine we have data about 9 countries that record their level of democracy from -10 to +10 (x-axis) and their GDP per capita in $1,000s (y-axis).
We save the plot to an object and then print it:
The command to save a picture is ggsave
We can save it as:
When you save, you can tell R how wide and tall the picture should be.
When you save, you can tell R how wide and tall the picture should be.
When you save, you can tell R how wide and tall the picture should be.
Here’s a new word: dpi = dots per inch.
Here’s a new word: dpi = dots per inch.
Compose wisely:
Be honest with coordinates:
coord_cartesian();coord_flip().Guide the eye:
geom_text[_repel], geom_label[_repel], arrows) for readable stories.labs() for titles, subtitles, axes, captions, and clear legends.ggthemes to professionalize—data first, décor second.Combine & share:
ggarrange() to curate narratives;gganimate/ggiraph to add motion/interaction;ggsave to export with the right size & DPI.Popescu (JCU): Data Visualization 3