R PROGRAMMING • R-SPECIFIC TOPICS (DATA WORKFLOWS)

Customizing ggplot — Customize labels, scales, and themes at a basic level

Transform default ggplot2 outputs into polished, publication-ready visualizations through labels, scales, and themes.

Historical Context & Motivation

The story of ggplot2 begins with a deeper theoretical question: how should we think about the components of a statistical graphic? Before ggplot2, R's base plotting system offered imperative commands — plot(), lines(), par() — that required the user to manually manage every aspect of a figure's appearance, from axis tick marks to legend positioning. While powerful in a low-level sense, this approach meant that even simple customizations involved verbose, procedural code with no consistent abstraction layer.

Hadley Wickham recognized that Leland Wilkinson's Grammar of Graphics — a formal framework decomposing every chart into data, aesthetics, geometries, scales, coordinates, facets, and themes — could serve as the foundation of a declarative plotting API. The result was ggplot2, first released in 2005, which treats customization not as an afterthought but as a first-class operation governed by composable layers. Understanding how labels, scales, and themes fit into this layered architecture is essential for producing visualizations that communicate clearly, whether in an academic paper, a Shiny dashboard, or an exploratory notebook.

1999
The Grammar of Graphics
Leland Wilkinson publishes The Grammar of Graphics, proposing a layered, algebraic framework for describing statistical plots independent of any specific rendering technology.
2005
ggplot2 Initial Release
Hadley Wickham releases ggplot2 as an R package implementing Wilkinson's grammar, introducing the + operator for composing plot layers declaratively.
2012
Theme System Overhaul
The ggplot2 theme system is refactored to expose a comprehensive set of element_*() functions, enabling fine-grained control over non-data ink such as panel backgrounds, grid lines, and text styling.
2016
ggplot2 2.0 & Tidyverse Integration
ggplot2 becomes a core member of the tidyverse, benefiting from pipe-based workflows. Scale functions receive consistent naming conventions like scale_<aes>_<type>().
2020+
Extension Ecosystem Matures
Hundreds of extension packages (ggthemes, ggrepel, patchwork) build on the customization primitives, demonstrating that labels, scales, and themes form a composable interface rather than a closed API.

The central question this lesson addresses is practical: given ggplot2's default output, how do you systematically modify what the viewer reads (labels), how data maps to visual properties (scales), and the overall look and feel (themes) — all without abandoning the declarative composability that makes ggplot2 elegant in the first place?

Core Principles of ggplot2 Customization

Customizing a ggplot2 figure operates within the grammar's layered architecture. Every aesthetic mapping — position, color, size, shape — is governed by a scale that translates between data space and visual space. Every piece of human-readable annotation — axis titles, legend names, subtitles — is a label. And every non-data element — background color, font family, grid lines — is controlled by the theme layer. Understanding these three categories is the key to moving beyond default plots.

1

Labels (labs())

The labs() function sets human-readable text for titles, subtitles, captions, and axis/legend names. Labels describe what the viewer is looking at without altering how data maps to visuals.
2

Scales (scale_*())

Scale functions control the mapping between data values and aesthetic properties. They define axis limits, breaks, transformations (e.g., log), and color palettes. Think of scales as the translator between raw data and what appears on screen.
3

Themes (theme())

The theme layer controls all non-data ink: backgrounds, grid lines, fonts, legend placement, and margins. Built-in themes like theme_minimal() provide presets, while theme() offers element-level overrides.
4

Layer Composition with +

Every customization is added as a layer using the + operator. Order matters for themes — later layers override earlier ones — but labels and scales combine additively. This composability is what distinguishes ggplot2 from imperative plotting systems.
5

Separation of Concerns

Labels handle semantics (naming), scales handle data-to-visual mapping, and themes handle presentation style. This clean separation means you can swap color palettes without changing labels, or apply a dark theme without touching your scale logic.
KEY TAKEAWAY
Think of a ggplot2 figure like a web application with a CSS-like separation: labels are like the content/text of your HTML, scales are like the data binding logic (mapping state to the DOM), and themes are like the stylesheet. You can swap any one independently without breaking the others, just as you'd swap a CSS file without rewriting your JavaScript.

Visual Explanation — The ggplot2 Customization Stack

The four customization layers of a ggplot2 figure: data/aesthetics form the foundation, scales control mapping, labels provide readable text, and themes style the non-data elements. Each layer is composed using the + operator.

The diagram above illustrates how each customization category occupies a distinct layer in the ggplot2 rendering pipeline. The base layer — your ggplot() call with aes() mappings and one or more geom_*() layers — produces a functional but unstyled chart using default scales, auto-generated labels (derived from column names), and the default gray theme. Each subsequent customization layer refines a specific aspect of the output without interfering with the others. This clean separation is a direct consequence of the Grammar of Graphics: because each component has a well-defined role, you can compose customizations independently rather than wrestling with tangled state, much like how composable middleware works in a web server framework.

How Labels, Scales, and Themes Work

Labels with labs()

The labs() function accepts named arguments corresponding to every aesthetic and plot-level text element. The most common arguments are title, subtitle, caption, x, y, and the name of any mapped aesthetic (e.g., color, fill, size). When you set labs(color = "Species"), you rename the legend heading for the color aesthetic. Convenience wrappers xlab(), ylab(), and ggtitle() exist but labs() is idiomatic because it consolidates all label changes into a single call.

Scales with scale_<aesthetic>_<type>()

Scale functions follow a consistent naming convention: scale_<aesthetic>_<type>(). For example, scale_x_continuous() governs a continuous x-axis, while scale_color_brewer() maps a discrete color aesthetic using ColorBrewer palettes. Key arguments shared across scale functions include limits (the data range to display), breaks (where tick marks appear), labels (what text appears at each break), and name (the axis or legend title, which labs() can also set). For color scales specifically, values (in scale_color_manual()) lets you specify exact hex codes for each factor level.

Themes with theme() and theme_*()

ggplot2 ships with several complete themes: theme_gray() (the default), theme_minimal(), theme_bw(), theme_classic(), and others. These set all non-data elements at once. For fine-grained control, the theme() function accepts dozens of named arguments, each set to an element function: element_text() for text properties (size, face, color, angle), element_line() for lines, element_rect() for rectangles (backgrounds, borders), and element_blank() to remove an element entirely. For instance, theme(axis.text.x = element_text(angle = 45, hjust = 1)) rotates x-axis tick labels by 45 degrees.

💡 Naming Convention Pattern
The scale naming formula is systematic: scale_ + aesthetic (x, y, color, fill, size) + _ + type (continuous, discrete, manual, log10, brewer). If you can name the aesthetic and know whether it's continuous or discrete, you can construct the function name without memorizing a list.

Detailed Breakdown — Common Functions and Arguments

Common ggplot2 customization functions and their key arguments
CategoryFunctionKey ArgumentsPurpose
Labelslabs()title, subtitle, caption, x, y, color, fillSet all text annotations in one call
Scale (axis)scale_x_continuous()limits, breaks, labels, name, transControl continuous x-axis range, ticks, and labels
Scale (axis)scale_y_log10()breaks, labelsApply log₁₀ transformation to y-axis
Scale (color)scale_color_manual()values, name, labelsAssign specific colors to discrete factor levels
Scale (color)scale_fill_brewer()palette, direction, nameUse ColorBrewer palettes for fill aesthetic
Theme (preset)theme_minimal()base_size, base_familyClean theme with no panel background
Theme (custom)theme()plot.title, axis.text.x, legend.position, panel.grid.majorOverride individual non-data elements
An annotated scatter plot showing where each customization function affects the final figure. Yellow annotations mark label functions, green marks axis labels, red marks the color scale, and orange marks the caption.

This annotated plot illustrates how each customization function maps to a specific region of the output. Notice that the labs() function covers five distinct text areas (title, subtitle, x-axis label, y-axis label, and caption), while the scale_color_manual() function affects both the point colors and the legend. The background, grid lines, and font choices are all governed by the theme layer, which is not annotated here but would affect every non-data pixel in the panel.

Worked Example — Customizing an mpg Scatter Plot

We will build a fully customized scatter plot step by step, starting from a bare ggplot() call and progressively adding labels, scales, and theme adjustments. The dataset is mpg from the ggplot2 package, which contains fuel economy data for 234 car models.

Building a Publication-Ready Scatter Plot
1
Step 1 — Create the base plotStart with the data and aesthetic mappings. We plot displ (engine displacement) on x, hwy (highway MPG) on y, and map factor(cyl) to color.
ggplot(mpg, aes(x = displ, y = hwy, color = factor(cyl))) + geom_point(size = 2.5, alpha = 0.7)
2
Step 2 — Add descriptive labelsReplace the default column-name labels with human-readable text. The labs() call sets the title, subtitle, axis names, legend name, and a data source caption.
+ labs(title = "Engine Size vs. Highway Fuel Economy", subtitle = "Larger engines tend to have lower MPG", x = "Engine Displacement (liters)", y = "Highway MPG", color = "Cylinders", caption = "Source: EPA fuel economy data via ggplot2::mpg")
3
Step 3 — Customize the x-axis scaleSet explicit limits, breaks, and label formatting for the x-axis. We restrict the range to 1–7 liters with breaks at each integer.
+ scale_x_continuous(limits = c(1, 7), breaks = seq(1, 7, by = 1))
4
Step 4 — Customize the color scaleAssign specific colors to each cylinder count using scale_color_manual(). This overrides the default palette and ensures accessibility through distinct hues.
+ scale_color_manual(values = c("4" = "#22d3ee", "5" = "#34d399", "6" = "#a78bfa", "8" = "#f472b6"))
5
Step 5 — Apply a theme and fine-tuneApply theme_minimal() as the base, then override specific elements: bold the plot title, rotate x-axis text slightly, move the legend to the bottom, and increase the base font size.
+ theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold", size = 16), plot.subtitle = element_text(color = "gray40"), legend.position = "bottom", panel.grid.minor = element_blank())
📋 Complete Code
Combining all steps: library(ggplot2) ggplot(mpg, aes(x = displ, y = hwy, color = factor(cyl))) + geom_point(size = 2.5, alpha = 0.7) + labs( title = "Engine Size vs. Highway Fuel Economy", subtitle = "Larger engines tend to have lower MPG", x = "Engine Displacement (liters)", y = "Highway MPG", color = "Cylinders", caption = "Source: EPA fuel economy data" ) + scale_x_continuous(limits = c(1, 7), breaks = seq(1, 7, by = 1)) + scale_color_manual(values = c("4" = "#22d3ee", "5" = "#34d399", "6" = "#a78bfa", "8" = "#f472b6")) + theme_minimal(base_size = 14) + theme( plot.title = element_text(face = "bold", size = 16), plot.subtitle = element_text(color = "gray40"), legend.position = "bottom", panel.grid.minor = element_blank() )

Strengths, Limitations, and Trade-offs

Strengths and limitations of ggplot2 customization
AspectStrengthLimitation
LabelsSingle-call consolidation via labs(); clean separation from data logicRich text (bold, italic, math) in labels requires the ggtext extension — not built in
ScalesConsistent naming convention makes discoverability easy; transformations like log₁₀ are one-linersOnly one scale per aesthetic; combining two color scales (e.g., fill and color with different palettes) can be tricky
ThemesComplete themes provide instant visual overhauls; theme() offers element-level precisionThe list of theme arguments is very large (60+); debugging inheritance between complete themes and overrides can be confusing
ComposabilityLayers can be stored as variables and reused across multiple plots; excellent for DRY principlesOrder of theme() calls matters — later calls override earlier ones, which can silently clobber customizations
Base R vs. ggplot2Declarative grammar avoids the 'spaghetti code' problem of imperative base R par() settingsMore verbose for quick one-off plots; initial learning curve is steeper than plot()
KEY TAKEAWAY
The ggplot2 customization API is analogous to a well-designed class hierarchy in object-oriented programming. Complete themes act like base classes providing sensible defaults, while theme() overrides function like method overrides in a subclass — you only specify what you want to change. Similarly, scales enforce a consistent interface (the naming convention) much like an abstract interface contract ensures uniform behavior across implementations.

Connection to Advanced ggplot2 Customization

The basic label, scale, and theme adjustments covered in this lesson form the entry point to a much deeper customization ecosystem. Once you are comfortable with labs(), scale_*(), and theme(), you can explore programmatic theme creation, custom scale transformations, and extensions that push ggplot2 well beyond its default capabilities.

From basic to advanced ggplot2 customization
Basic (This Lesson)Advanced
labs(title = "...")Rich-text titles with ggtext::element_markdown() supporting HTML/Markdown inside labels
scale_color_manual(values = ...)Custom continuous color scales with scale_color_gradient2() or perceptually uniform palettes via viridis
theme_minimal() presetsBuilding custom themes with theme_set() and distributing them as packages for organizational branding
scale_x_continuous(breaks = ...)Custom transformation functions via scales::trans_new() for domain-specific axes (e.g., probability scales)
Single plot customizationMulti-panel layouts with patchwork or cowplot, with shared themes and scales across subplots

The key insight is that ggplot2's customization primitives are extensible by design. The ggproto object-oriented system underlying ggplot2 allows package authors to define entirely new scales, geoms, and stats that slot into the existing grammar. This means the patterns you learn today — composing layers, overriding defaults, using consistent naming conventions — transfer directly to advanced use cases and the broader extension ecosystem.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain the difference between labs(color = "Species") and scale_color_manual(name = "Species"). Do they produce the same effect? When would you prefer one over the other?
PROBLEM 2BASIC
Write a labs() call that sets the plot title to "Temperature Over Time", the x-axis label to "Date", the y-axis label to "Temperature (°C)", and adds a caption "Data: NOAA".
PROBLEM 3INTERMEDIATE
You have a plot with a continuous y-axis representing revenue in dollars. Write the appropriate scale function call that sets the axis limits from 0 to 100000, places breaks at every 20000, and formats the labels as dollar amounts (e.g., "$20,000"). Hint: the scales package provides label_dollar().
PROBLEM 4APPLIED
You are preparing a scatter plot for a conference poster. The requirements are: (1) use theme_minimal() with base font size 18; (2) bold the title and set it to size 22; (3) position the legend at the bottom; (4) remove minor grid lines; (5) set the panel background to a very light gray (#f8f9fa). Write the complete theme layers to achieve this.
PROBLEM 5CRITICAL THINKING
A colleague writes the following code and complains that their custom theme settings are being ignored: ggplot(df, aes(x, y)) + geom_point() + theme(plot.title = element_text(face = "bold")) + theme_bw() Diagnose the problem and propose a fix. Then explain the general rule for ordering complete themes and theme() overrides.

Lesson Summary

Customizing ggplot2 visualizations revolves around three composable systems. Labels, set via labs(), control all human-readable text — titles, subtitles, axis names, legend headings, and captions. Scales, invoked through the scale_<aesthetic>_<type>() naming convention, govern the mapping from data values to visual properties — axis limits, tick breaks, label formatting, color palettes, and transformations. Themes control every non-data element — backgrounds, grid lines, fonts, and legend placement — via complete presets like theme_minimal() and element-level overrides with theme() paired with element functions (element_text(), element_line(), element_rect(), element_blank()).

The critical operational rule is layer ordering: complete themes must precede theme() overrides, because a complete theme resets all elements while theme() only patches specific ones. These three systems — labels, scales, themes — are orthogonal by design, meaning you can modify any one without affecting the others, enabling a clean separation of concerns that scales from quick exploratory plots to reusable, publication-quality figure templates.

Varsity Tutors • R Programming • Customizing ggplot — Customize labels, scales, and themes at a basic level