ggplot2: Scales, Guides, and Themes (How to Control What We See)
February 28, 2026
scale_*_*()scale_*_*()?
A scale control aesthetics mappings:
x/y position: scale_x_*(), scale_y_*()
color/fill: scale_color_*(), scale_fill_*()
size/shape/alpha: scale_size_*(), scale_shape_*(), scale_alpha_*()
Each deals with one combination of mapping and scale. Too many to memorize (131 distinct scale_*_*() functions)!
https://ggplot2tor.com/apps provides a complete guide to scales and themes, as well as aesthetics.
Continuous: numeric with many possible values
→ *_continuous(), *_log10(), *_sqrt(), *_date(), *_datetime()
Discrete: categories / factors
→ *_discrete(), *_manual()
Rule of thumb:
scale_x_continuous(): breaks & labelsgapminder |>
filter(year == 2007) |>
ggplot(aes(x = gdpPercap,
y = lifeExp,
color = continent)) +
geom_point(alpha = 0.8,
size = 3) +
geom_smooth(se = F) +
scale_x_continuous(
breaks = c(1000, 10000, 50000),
labels = scales::dollar
) +
labs(
x = "GDP per capita",
y = "Life expectancy",
color = "Continent")
What this controls:
breaks)labels)scale_*() Arguments You’ll Use a LotFor most scales, these are the “big four”:
name = legend/axis title (often we can do this in labs() too)limits = c(min, max) what values are shownbreaks = where tick marks / legend entries appearlabels = how ticks / legend entries display as textlimits vs coord_*Two different ideas:
limits =, ggplot fits the smooth using only data (\(\leq\) 20,000) (outliers removed).coord_cartesian(), ggplot fits the smooth using all data, then just shows the part of the line within the window.Common mistake:
year)
aes(color = factor(year)) or convert to factor
A single plot can have:
Each mapped aesthetic typically has one scale.
RColorBrewergender, country) → Distinct colors that won’t be easily confusedLevel of Education) → Graded color scheme running from less to more or earlier to laterRColorBrewerRColorBrewer provides a wide range of named color palettes.scale_color_brewer() or scale_fill_brewer() with the palette parameter.
Qualitative palettes do not imply magnitude differences between legend classes.
Qualitative schemes are best suited to representing unordered categorical data.
scale_color_brewer(palette = ...)
scale_color_brewer(palette = ...).RColorBrewer flags palettes that are color-blind friendly in the colorblind column."Set2", "Dark2") when designing for broad audiences.brewer.pal(n, name) to extract hex color codes from any named palette:
n — number of colors to extract (must be between 3 and the palette’s maximum)name — name of the palette (e.g., "Set2", "Blues", "RdBu")scale_*_manual() with brewer.pal()
scale_color_manual() or scale_fill_manual().guides()A guide is the display of a scale:
Guides answer:
If we don’t want a legend for an aesthetic:

Difference:
guides(color = "none") removes only that guidetheme(legend.position="none") removes all legendsSometimes ggplot guesses well; sometimes we want control:

Useful when moving legend to top/bottom.
order =)When we map multiple aesthetics, we can control the order:
override.aes)Sometimes the plot uses alpha/size that makes legend hard to read.
guides()?Use guides() when we want to:
If we only want to rename legends, start with labs().
theme()A theme controls everything that is not our data:
Themes do not change the data mapping.
theme_*()Try different base themes quickly:
The theme() function has 94 possible arguments!
Common ones:
plot.title, plot.subtitle, plot.captionaxis.title.x, axis.title.y, axis.text.x, axis.text.ypanel.grid.major, panel.grid.minor, panel.backgroundlegend.position, legend.title, legend.text, legend.keystrip.text, strip.background (facets)plot.marginThe only way to learn how to use theme() is to use it and tinker with it.
gapminder |>
filter(year == 2007) |>
ggplot(aes(gdpPercap, lifeExp,
color = continent)) +
geom_point(size = 3, alpha = 0.9) +
scale_x_log10(labels = scales::dollar) +
labs(
title = "Life expectancy vs GDP per capita (2007)",
subtitle = "Log x-axis; color = continent",
x = "GDP per capita (log scale)",
y = "Life expectancy",
color = "Continent",
caption = "Source: gapminder"
) +
theme(
legend.position = "top",
plot.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
gapminder |>
filter(year == 2007) |>
ggplot(aes(gdpPercap, lifeExp,
color = continent,
size = pop)) +
geom_point(alpha = 0.7) +
# SCALES: transform, breaks, labels
scale_x_log10(
breaks = c(500, 2000, 10000, 50000),
labels = scales::dollar
) +
scale_size_continuous(labels = scales::label_number(scale_cut = scales::cut_si(""))) +
# GUIDES: legend order + layout
guides(
color = guide_legend(order = 1, nrow = 1),
size = guide_legend(order = 2)
) +
# THEME: layout + typography
theme(
legend.position = "top",
plot.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
) +
# labels are not scales/guides/themes, but they coordinate everything
labs(
title = "Scales + Guides + Themes in one figure",
x = "GDP per capita (log scale)",
y = "Life expectancy",
color = "Continent",
size = "Population"
)
aes() + scale_*_*()labs() + guides()theme() (or theme_*())