Skip to Content
Course content

64: Advanced ggplot2: Custom Themes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, we've all used theme_minimal() or theme_bw() a thousand times. They're great for quick exploration, but when you're handing a report to a stakeholder or submitting a paper, "standard R output" isn't always the vibe you want. To get a truly professional look, you have to stop relying on the presets and start building your own theme objects.

I'm currently working on a dashboard for some simulated high-frequency trading data. The goal is a "Dark Mode" aesthetic—something that looks like a terminal window rather than a spreadsheet. Let's build this from the ground up.

Starting with a boring default

First, I'll whip up a basic plot. I'm using a simple time series of "Volatility" over a few days. Notice how the default ggplot2 theme, while functional, feels too "bright" for the executive dark-mode look I'm after.

library(ggplot2)

# Some dummy volatility data
df <- data.frame(
  time = 1:100, 
  volatility = cumsum(rnorm(100))
)

ggplot(df, aes(x = time, y = volatility)) +
  geom_line(color = "#00FFCC", size = 1) +
  labs(title = "Market Volatility Index", subtitle = "Real-time stream")

Painting the canvas dark

To customize a theme, we use the theme() function. Instead of changing the data, we're changing the non-data components. I want a deep charcoal background, white text for readability, and I want to strip away those distracting default gridlines.

ggplot(df, aes(x = time, y = volatility)) +
  geom_line(color = "#00FFCC", size = 1) +
  labs(title = "Market Volatility Index", subtitle = "Real-time stream") +
  theme(
    plot.background = element_rect(fill = "#1A1A1A"),
    text = element_text(color = "white"),
    panel.grid.major = element_line(color = "#333333"),
    panel.grid.minor = element_blank()
  )

The "Grey Box" glitch

If you run that code, you'll notice something annoying: there's a weird grey rectangle surrounding the actual plot area. I made this mistake a dozen times when I first started with ggplot2. I changed the plot.background (the entire image area), but I forgot that the panel.background (the area where the data actually sits) is a separate entity with its own default grey fill.

It looks like a poorly cropped photo. To fix this, I need to make sure the panel background matches the plot background exactly.

# Correcting the background mismatch
ggplot(df, aes(x = time, y = volatility)) +
  geom_line(color = "#00FFCC", size = 1) +
  labs(title = "Market Volatility Index", subtitle = "Real-time stream") +
  theme(
    plot.background = element_rect(fill = "#1A1A1A"),
    panel.background = element_rect(fill = "#1A1A1A"), # Fixes the grey box
    text = element_text(color = "white"),
    panel.grid.major = element_line(color = "#333333"),
    panel.grid.minor = element_blank(),
    axis.text = element_text(color = "#BBBBBB")
  )

Packaging it for the whole project

Writing that theme() block over and over again is a nightmare. In a real project, I'd never do that. Instead, I'll wrap these settings into a custom function. This allows me to apply my "Dark Mode" brand to twenty different plots with a single line of code.

I'll start by inheriting from theme_minimal() so I don't have to manually define every single tiny detail (like margin sizes), and then I'll override the specific elements I care about.

theme_dark_exec <- function() {
  theme_minimal() %+% 
    theme(
      plot.background = element_rect(fill = "#1A1A1A", color = NA),
      panel.background = element_rect(fill = "#1A1A1A", color = NA),
      text = element_text(color = "white"),
      axis.text = element_text(color = "#BBBBBB"),
      panel.grid.major = element_line(color = "#333333"),
      panel.grid.minor = element_blank(),
      plot.title = element_text(face = "bold", size = 14),
      plot.subtitle = element_text(color = "#888888")
    )
}

# Now it's a breeze to apply
ggplot(df, aes(x = time, y = volatility)) +
  geom_line(color = "#00FFCC", size = 1) +
  labs(title = "Market Volatility Index", subtitle = "Real-time stream") +
  theme_dark_exec()

Notice I used %+%. That's the "theme addition" operator. It's a handy way to add specific theme() modifications to an existing theme object without overwriting the whole thing. Now we have a reusable brand identity for our visuals.




📋 Practical Task

Build a "Botanical Journal" Custom Theme

You are preparing a series of plots for a botany journal. The journal requires a very specific "organic" look: a light cream background, dark olive green text, and no gridlines at all (only axis lines).

Using the iris dataset, create a scatter plot of Sepal.Length vs Sepal.Width. Then, write a custom theme function called theme_botanical() that implements the following:

  • Backgrounds: Both plot.background and panel.background should be a cream color (e.g., "#F5F5DC").
  • Text: All text should be a dark olive green (e.g., "#556B2F").
  • Grids: Remove all major and minor gridlines (set them to element_blank()).
  • Axes: Add a solid dark olive green line for the x and y axes using axis.line.

Apply your custom theme function to the plot to ensure it meets the journal's specifications.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.