Introduction to Themes in ggplot2

Themes in ggplot2 allow you to control the overall look of your plots. They can change the background color, gridlines, fonts, and many other elements. Using themes can help make your visualizations more readable and aesthetically pleasing. By default, ggplot2 uses the theme_grey(), but there are many other built-in themes you can use.

We’re going to work in the ggplot2 way: first, we set up a plot, then we add a different theme as a layer on top of the plot.

## Set up plot (monochrome base)
p.gap <- ggplot(data = gapminder %>% filter(year==1997), mapping = aes(x = gdpPercap, y=lifeExp)) +
    geom_point(alpha = 0.6) +
    scale_y_continuous(breaks=seq(20, 100, by = 10)) +
    geom_smooth(method = "lm", formula = "y ~ x", color = "black") + 
    scale_x_log10(labels = scales::dollar_format(accuracy = 1)) + 
    labs(x = "GDP Per Capita", y = "Life Expectancy in Years",
         title = "Economic Growth and Life Expectancy (1997)", 
         subtitle = "Data points are country-years",
         caption = "Source: Gapminder.")

Base ggplot2 theme examples

Default theme. You can see the classic ggplot2 default look that many/most use. It’s not bad, but we can do better.

p.gap + theme_gray()

In what ways does it fall short? Perhaps the grey background isn’t always the most visually appealing, or the gridlines might be too prominent for some tastes. Maybe you prefer different fonts or want to remove the box around the plot. These are the kinds of elements themes can help you control!

Built-in Themes

ggplot2 comes with several built-in themes that you can use to quickly change the appearance of your plots. Some commonly used themes include:

Let’s try a black and white theme with theme_bw() and theme_minimal on our plot.

p.gap + theme_bw()

p.gap + theme_minimal()

p.gap + theme_classic()

Adding External Themes with ggthemes

The ggthemes package provides a variety of additional themes that you can use to further customize your plots. These themes are inspired by various sources such as Tufte’s books, The Economist, and Excel.

The FiveThirtyEight theme is similar to that used by the FiveThirtyEight website. It’s a good theme for data journalism.

p.gap + theme_fivethirtyeight() 

And one with the Economist theme.

p.gap + theme_economist() + scale_x_log10(labels = scales::dollar_format(accuracy = 1)) 
## Scale for x is already present.
## Adding another scale for x, which will replace the existing scale.

Here, we emulate that other statistics package.

p.gap + theme_stata()

hrbrthemes: A Modern Take on Themes

The hrbrthemes (https://github.com/hrbrmstr/hrbrthemes) package offers modern and clean themes that are particularly good for publication and presentations. These themes are designed to be visually appealing and can help make your plots look more professional.

I used theme_ipsum in a recently published paper.

## hrbr theme examples 
# Displaying multiple hrbrthemes in one chunk
p.gap + theme_ipsum() 

p.gap + theme_ipsum_rc() 

p.gap + theme_modern_rc() 

Fine-Tuning Your Plot: Modifying Specific Theme Elements with theme()

While pre-built themes are great starting points, you often want to adjust individual parts of your plot’s appearance. The theme() function is your command center for this, allowing you to control almost every non-data element of your plot. Think of it as giving specific instructions to different parts of the plot like the title, axis labels, or legend.

Here’s how you can make some common adjustments using p.gap as our base. We’ll focus on a few key changes to keep it simple:

p.gap + 
  theme_minimal() + 
  labs(title = "Customized Plot Title (1997)", # We can still set labs
       x = "GDP Per Capita (log scale)", 
       y = "Life Expectancy (Years)") +
  theme(
    # Modify plot title: make it larger and centered
    plot.title = element_text(size = 18, hjust = 0.5),
    # Modify axis titles: change their size
    axis.title.x = element_text(size = 14),
    axis.title.y = element_text(size = 14),
    # Modify axis text (the numbers/labels on the axes): make y-axis text smaller
    axis.text.y = element_text(size = 9)
    # Note: legend.position was removed as p.gap doesn't have a legend here.
  )

Each argument inside theme() targets a specific plot element. You use functions like element_text() for text components (to control font size, face, color, alignment etc.). For example, hjust = 0.5 centers text.

Adding Style to Your Text: Rich Text with ggtext

Sometimes you want more than just plain text in your titles or labels. The ggtext package extends ggplot2 to allow you to use Markdown (and some HTML) for richer text formatting. This means you can easily add bold, italics, or even colors directly within your text strings.

First, make sure ggtext is loaded (it’s in the setup chunk).

Let’s define p.gap_color which will have a legend, making it a good candidate to show off ggtext on legend titles too.

# Define p.gap_color to include color aesthetic mapped to continent
p.gap_color <- ggplot(data = gapminder %>% filter(year==1997), 
                mapping = aes(x = gdpPercap, y=lifeExp, color = continent)) + 
    geom_point(alpha = 0.8, size = 2.5) +
    scale_y_continuous(breaks=seq(20, 100, by = 10)) +
    geom_smooth(method = "lm", formula = "y ~ x", se = FALSE, color = "gray20", linetype = "dashed", linewidth = 0.7) +
    scale_x_log10(labels = scales::dollar_format(accuracy = 1)) + 
    labs(x = "GDP Per Capita", y = "Life Expectancy in Years",
         title = "Economic Growth and Life Expectancy (1997)",
         subtitle = "Data points are countries, now colored by continent", 
         caption = "Source: Gapminder.",
         color = "Continent") # Legend title for color

Now, let’s use p.gap_color to see ggtext in action with a simple example:

p.gap_color + 
  theme_minimal(base_size = 11) + 
  labs(
    title = "Economic Growth and **Life Expectancy** (<span style='color:darkorange;'>1997</span>)",
    x = "GDP Per Capita (*log scale*)", 
    color = "**Continent** Group" # Legend title with Markdown
  ) +
  theme(
    plot.title = element_markdown(size = 16), # Apply Markdown rendering to title
    axis.title.x = element_markdown(),        # And X-axis title
    legend.title = element_markdown()         # And legend title
  )

To make ggtext work, you generally need to: 1. Write your text using Markdown (e.g., **bold**, *italics*) or simple HTML tags (e.g., <span style='color:blue;'>blue text</span>). 2. Tell ggplot2 to render that text element as Markdown/HTML by using element_markdown() (from ggtext) within the theme() function for the corresponding text element (like plot.title, axis.title.x, etc.).

This allows for more expressive and visually appealing text in your plots!

ggsci: Scientific and Sci-Fi Themed Color Palettes

The ggsci package provides scientific journal and sci-fi themed color palettes and themes, which can be very useful for creating visually appealing and publication-ready figures. These color palettes are inspired by the color schemes of various scientific journals, science fiction movies, TV shows, and even the Solarized color palette. They are designed to be visually striking and effective for distinguishing between different data groups.

Applying ggsci Palettes

The ggsci package provides several functions to apply its palettes to your ggplot2 visualizations. These functions include scale_color_* and scale_fill_* for different types of palettes. Here are some examples of how to use these palettes.

Example: D3 Palette

The D3 palette provides a set of colors inspired by the D3.js library, which is widely used for data visualization on the web.

# Applying D3 palette
p.gap <- ggplot(data = gapminder %>% filter(year == 2007), mapping = aes(x = gdpPercap, y = lifeExp, color = continent)) +
  geom_point(alpha = 0.8) +
  scale_y_continuous(breaks = seq(20, 100, by = 10)) +
  scale_x_log10(labels = scales::dollar) +
  labs(x = "GDP Per Capita", y = "Life Expectancy in Years",
       title = "Gapminder dataset (2007) with D3 palette")

p.gap + scale_color_d3()

For the Nerds: Creating a Custom Theme

Creating a custom theme allows you to define a consistent style for your plots that can be reused across different visualizations. This is particularly useful for maintaining a cohesive (“House Style”) look in papers or presentations. You can start with a base theme (like theme_minimal()) and then modify specific elements to suit your preferences.

# Define our custom theme by starting with a base theme and adding our tweaks.
# We can wrap this in a function for easy use, though saving it as an object also works.
theme_boris_special <- function(base_size = 12) {
  theme_minimal(base_size = base_size) +
  theme(
    # Customize plot title and subtitle
    plot.title = element_text(face = "bold", size = rel(1.2), hjust = 0.5),
    plot.subtitle = element_text(size = rel(1.0), hjust = 0.5, color = "gray40"),
    
    # Customize axis titles and text
    axis.title = element_text(face = "bold"),
    axis.text = element_text(color = "gray50"),
    
    # Customize legend
    legend.position = "bottom",
    legend.title = element_text(face = "bold"),
    
    # Customize panel and grid lines
    panel.grid.major = element_line(color = "grey90", linewidth = 0.5),
    panel.grid.minor = element_blank() # No minor grid lines
  )
}

# Now, let's apply our new custom theme to the plot with colored continents.
# Remember the `p.gap_color` object we created earlier.

p.gap_color

p.gap_color +
  theme_boris_special() + # Apply our theme with one simple command!
  labs(
    title = "Plot Styled with a Custom Reusable Theme",
    subtitle = "This style can be applied to any ggplot object"
  )

Customizing Colors with RColorBrewer

Beyond themes that change the overall plot appearance, you often need to control the specific colors used for your data points, lines, and areas. This is crucial for clarity, aesthetics, and accessibility (e.g., ensuring colorblind-friendly palettes). The RColorBrewer package is an excellent resource for this, offering a curated set of color palettes designed by Cynthia Brewer, a cartographer and color expert.

Why Custom Colors?

  • Clarity: Distinguish categories effectively.
  • Emphasis: Highlight key findings.
  • Aesthetics: Create visually appealing plots that align with your style.
  • Accessibility: Choose palettes that are perceivable by individuals with various forms of color vision deficiencies. Many RColorBrewer palettes are colorblind-safe.
  • Publication Standards: Match journal or organizational style guides.

Types of RColorBrewer Palettes

RColorBrewer palettes are divided into three types:

  1. Sequential Palettes (seq): For ordered data where values range from low to high (e.g., shades of blue from light to dark). Best for continuous variables or ordered categorical variables.
  2. Diverging Palettes (div): For data with a meaningful midpoint, where values diverge in two directions (e.g., temperature differences from a mean, with reds for hot and blues for cold).
  3. Qualitative Palettes (qual): For nominal or categorical data where there’s no inherent order, and the goal is to distinguish discrete categories (e.g., different countries, types of products). Our continent variable is a good candidate for this.

You can explore all available palettes visually with display.brewer.all().

display.brewer.all()

Using RColorBrewer Palettes in ggplot2

To use RColorBrewer palettes in ggplot2, you can use the scale_color_brewer() (for point/line colors) and scale_fill_brewer() (for area fills) functions. You specify the desired palette using the palette argument.

Let’s apply a qualitative palette to our plot, coloring points by continent. We will use the p.gap_color object defined earlier (it maps color to continent).

p.gap_color + theme_minimal() + labs(title="Plot with Continent Colors (1997, Default Palette)") 

Now that our points are colored by continent, we can explore different ways to customize these colors using p.gap_color.

Let’s apply a qualitative palette. The “Set2” palette is a good choice for distinct categories and is colorblind-friendly.

p.gap_color + 
  scale_color_brewer(palette = "Set2") +
  theme_minimal() + 
  labs(title = "Using RColorBrewer 'Set2' Palette (1997)", 
       subtitle = "A colorblind-friendly qualitative palette")

The “Accent” palette is another good qualitative option.

p.gap_color + 
  scale_color_brewer(palette = "Accent") +
  theme_bw() +
  labs(title = "Using RColorBrewer 'Accent' Palette (1997)") 

If you have more categories than a specific palette supports, ggplot2 might try to interpolate, or it might recycle colors, which is usually not ideal. It’s best to choose a palette designed for the number of categories you have. You can check the maximum number of colors for each qualitative palette using brewer.pal.info. For example, “Set2” has 8 colors, “Accent” has 8. “Paired” has 12, making it useful if you have more categories.

brewer.pal.info %>% dplyr::filter(category == "qual")
##         maxcolors category colorblind
## Accent          8     qual      FALSE
## Dark2           8     qual       TRUE
## Paired         12     qual       TRUE
## Pastel1         9     qual      FALSE
## Pastel2         8     qual      FALSE
## Set1            9     qual      FALSE
## Set2            8     qual       TRUE
## Set3           12     qual      FALSE

Our continent variable has 5 categories (5). Both “Set2” and “Accent” (and many others) will work well.

Let’s combine theme_ipsum_rc() from hrbrthemes with a RColorBrewer palette.

p.gap_color + 
  theme_ipsum_rc() + 
  scale_color_brewer(palette = "Dark2") + 
  labs(title = "ipsum_rc with RColorBrewer 'Dark2' (1997)") 

Example with a Sequential Palette (Illustrative)

To illustrate scale_fill_brewer() with a sequential palette, we need an ordered categorical variable. Let’s use the continent_counts data and categorize the number_of_countries into ordered groups.

# Count countries per continent in 1997
continent_counts <- gapminder %>%
  filter(year == 1997) %>%
  count(continent, name = "number_of_countries")

# Create ordered categories for the number of countries
continent_counts <- continent_counts %>%
  mutate(num_countries_category = cut(number_of_countries,
                                      breaks = c(0, 20, 40, Inf), 
                                      labels = c("Few (0-20)", "Some (21-40)", "Many (>40)"), 
                                      ordered_result = TRUE)) 

# View the new data with categories
print(continent_counts)
## # A tibble: 5 × 3
##   continent number_of_countries num_countries_category
##   <fct>                   <int> <ord>                 
## 1 Africa                     52 Many (>40)            
## 2 Americas                   25 Some (21-40)          
## 3 Asia                       33 Some (21-40)          
## 4 Europe                     30 Some (21-40)          
## 5 Oceania                     2 Few (0-20)
ggplot(continent_counts, 
       aes(x = reorder(continent, -number_of_countries), 
           y = number_of_countries, 
           fill = num_countries_category)) + 
  geom_col(color = "black") + 
  #scale_fill_brewer(palette = "Blues", name = "Country Count") + 
  scale_fill_ipsum() +
  labs(x = "Continent", y = "Number of Countries (1997)",
       title = "Number of Countries per Continent (1997)", 
       subtitle = "Bar fill by country count category") + 
  theme_ipsum_rc(grid = "Y") 

Note: In this example, we created discrete, ordered categories from a continuous variable to demonstrate scale_fill_brewer(). This function is designed for such categorical data, applying sequential (or diverging/qualitative) palettes appropriately.

hrbr itself has Color Themes

p.gap_color + theme_ipsum_rc() + scale_color_ipsum()

p.gap_color + theme_ipsum_rc() + scale_color_ft()

Or fill.

ggplot(continent_counts, 
       aes(x = reorder(continent, -number_of_countries), 
           y = number_of_countries, 
           fill = num_countries_category)) + 
  geom_col(color = "black") + 
  scale_fill_ipsum(name = "Country Count") + 
  labs(x = "Continent", y = "Number of Countries (1997)",
       title = "Number of Countries per Continent (1997)", 
       subtitle = "Bar fill by country count category") + 
  theme_ipsum_rc(grid = "Y") 

Manually Specifying Colors

Sometimes, you might want to specify colors manually, perhaps to match specific branding, because RColorBrewer doesn’t have exactly what you need, or you simply want full control. You can do this using scale_color_manual() or scale_fill_manual().

You can use named R colors (e.g., "red", "skyblue"; type colors() in your R console for a full list) or hexadecimal color codes (e.g., "#FF0000" for red). For this lesson, we’ll stick to R’s named colors as they are often easier to remember and use.

# First, let's see the order of continents to assign colors correctly.
# This helps ensure your manual colors map to the intended continents.
# levels(factor(gapminder$continent)) # "Africa" "Americas" "Asia" "Europe" "Oceania"

# Define a custom vector of R named colors.
# It's good practice to name the vector elements if you want to ensure specific colors go to specific categories.
my_custom_colors <- c("Africa" = "firebrick",
                      "Americas" = "steelblue",
                      "Asia" = "forestgreen",
                      "Europe" = "darkorchid",
                      "Oceania" = "darkorange")

p.gap_color + 
  scale_color_manual(values = my_custom_colors) +
  theme_ipsum_rc() +
  labs(title = "Manually Specified Colors for Continents (1997)", 
       subtitle = "Using scale_color_manual() with R named colors")

This approach gives you precise control over your plot’s color scheme.