R PROGRAMMING • R-SPECIFIC TOPICS (DATA WORKFLOWS)

Building ggplots — Build a ggplot with aesthetics (aes) and geoms

Master the layered grammar of graphics to compose precise, publication-quality visualizations in R.

Historical Context & Motivation

Data visualization has long been central to statistical computing, but for decades, the tools available in R and its predecessors relied on imperative, pen-on-paper metaphors: you told the computer exactly where to draw each line, bar, and label. The base graphics system in R, inherited from the S language of the 1970s, exemplified this approach—powerful but verbose, and difficult to compose or modify systematically. The fundamental problem was the absence of a coherent grammar that could decompose any visualization into reusable, interchangeable components, much the way a formal grammar decomposes natural language into parts of speech.

1999
The Grammar of Graphics
Leland Wilkinson publishes The Grammar of Graphics, proposing that every statistical graphic can be described as a composition of data, aesthetic mappings, geometric objects, scales, coordinate systems, and facets.
2005
ggplot1 Prototype
Hadley Wickham, then a PhD student at Iowa State, releases the first ggplot package for R, demonstrating that Wilkinson's grammar can be implemented as a practical API.
2007
ggplot2 Released
Wickham releases ggplot2, a complete rewrite using a layered grammar. It introduces the aes() function and the additive + operator for composing layers.
2012
Tidyverse Integration
ggplot2 becomes a cornerstone of the tidyverse ecosystem, gaining seamless interoperability with dplyr, tidyr, and the pipe operator, making data wrangling and visualization a single coherent workflow.
2020+
Modern Extensions
Hundreds of extension packages (ggridges, ggforce, patchwork) and ggplot2 3.x introduce new features like the after_stat() function, reinforcing the grammar's extensibility.

The core question ggplot2 answers is deceptively simple: how do you map abstract data columns onto the visual properties of graphical marks in a composable, declarative way? The answer lies in two interlocking concepts—aesthetic mappings (aes()) and geometric objects (geom_*())—that together form the backbone of every ggplot.

Core Principles & Definitions

Building a ggplot is fundamentally an act of declaring relationships between your data and the visual channels of a chart. Rather than writing imperative instructions like 'draw a bar from (0,0) to (1,5),' you state that a column named revenue should be mapped to the y-axis and quarter to the x-axis, and then you specify that the resulting marks should be bars. This declarative paradigm separates what you want to show from how the rendering engine draws it, a principle familiar to CS students who have studied declarative languages or frameworks like SQL or React.

1

Data (data frame)

Every ggplot begins with a tidy data frame passed to ggplot(data = ...). Each row is an observation; each column is a variable. The data frame is the single source of truth for all subsequent layers.
2

Aesthetics (aes)

The aes() function maps columns in the data to visual channels—position (x, y), color, size, shape, alpha, fill, and linetype. These mappings are data-driven and generate legends automatically.
3

Geoms (geometric objects)

Geom layers (geom_point(), geom_line(), geom_bar(), etc.) determine the geometric representation of the data. Each geom has required and optional aesthetics.
4

Layers & the + Operator

Layers are composed additively using the + operator. Each layer can introduce its own data, aesthetics, and geom, enabling multi-layer plots such as scatter plots overlaid with trend lines.
5

Scales, Coords & Themes

Scales control how data values map to aesthetic values (e.g., which colors represent which categories). Coordinate systems (Cartesian, polar) and themes (fonts, grid lines) complete the specification.
KEY TAKEAWAY
Think of building a ggplot like writing a SQL query for graphics: ggplot(data) is your FROM clause, aes() is your SELECT that picks which columns to visualize, and geom_*() is the output format. Just as SQL separates data selection from presentation, ggplot2 separates data mapping from rendering, and both systems compose small, orthogonal pieces into powerful expressions.

Visual Explanation — The Layered Architecture

The diagram below illustrates how a ggplot is assembled layer by layer. At the bottom sits the data frame. Above it, the aesthetic mapping layer connects data columns to visual channels. On top of that, one or more geom layers render the marks. Finally, scales, coordinates, and themes provide the finishing touches. Each layer is independent: you can swap a geom_point() for a geom_line() without changing the data or aesthetics.

Each layer of a ggplot is composed with the + operator. Layer 1 binds data; Layer 2 maps columns to visual channels via aes(); Layer 3 specifies geometry; Layers 4–5 refine scales, coordinates, and theme.

Notice the architectural similarity to a software stack: each layer depends on the one below it but is independently replaceable. This composability is the hallmark of the grammar of graphics and is what gives ggplot2 its remarkable flexibility compared to imperative plotting systems.

How aes() and geom_*() Work Under the Hood

When you call aes(x = displ, y = hwy, color = class), you are not evaluating those column names immediately. Instead, non-standard evaluation (NSE) captures the unevaluated expressions as quosures—closures that pair an expression with the environment in which it should be evaluated. This is the same tidy evaluation machinery that powers dplyr's filter() and mutate(). The practical consequence is that aesthetic mappings are resolved lazily, at render time, when the data frame is available. This design enables inheritance: aesthetics defined in the top-level ggplot() call are inherited by every subsequent geom unless explicitly overridden.

Aesthetic Inheritance Rules

Aesthetics defined inside ggplot(aes(...)) are global aesthetics. Every geom layer inherits them. Aesthetics defined inside a specific geom_*(aes(...)) are local aesthetics and apply only to that layer. A local mapping overrides a global one if they conflict. Meanwhile, setting an aesthetic to a constant outside aes()—for instance, geom_point(color = "red")—is a fixed setting, not a mapping, and does not generate a legend.

⚠️ Mapping vs. Setting — The #1 Beginner Mistake
Writing aes(color = "blue") does NOT make points blue. It creates a new categorical variable with the single value "blue" and maps it to the default color palette—often producing a salmon-pink color and a spurious legend entry. To set a fixed color, place it outside aes(): geom_point(color = "blue").

The Geom–Stat Duality

Each geom is paired with a default stat (statistical transformation). For example, geom_bar() uses stat = "count" to aggregate raw data into bar heights, while geom_point() uses stat = "identity" and plots values as-is. You can override the default stat—geom_bar(stat = "identity") expects pre-computed heights—or equivalently use geom_col(), which is syntactic sugar for the same thing. Understanding this duality helps you predict when ggplot will transform your data versus when it plots raw values.

Common Geoms & Their Required Aesthetics

ggplot2 ships with over 40 geom functions, but a handful account for the vast majority of everyday visualizations. The table below catalogs the most common geoms alongside their required aesthetics, optional aesthetics, and default statistical transformations. Knowing which aesthetics a geom requires prevents the most common class of ggplot errors.

Common ggplot2 geoms with their aesthetic requirements and default statistical transformations.
GeomRequired aesOptional aesDefault statUse Case
geom_point()x, ycolor, size, shape, alphaidentityScatter plots
geom_line()x, ycolor, linetype, linewidthidentityTime series, trends
geom_bar()xfill, color, widthcountFrequency distributions
geom_col()x, yfill, color, widthidentityPre-computed bar heights
geom_histogram()xfill, bins, binwidthbinContinuous distributions
geom_boxplot()x, yfill, color, outlier.shapeboxplotFive-number summaries
geom_smooth()x, ymethod, se, colorsmoothRegression / LOESS curves
geom_text()x, y, labelsize, angle, hjust, vjustidentityData labels
A compatibility matrix showing which aesthetic channels are required (solid fill), optional (semi-transparent), or not applicable (empty) for each common geom type.

The matrix above is invaluable when debugging. If ggplot throws an error like geom_bar requires the following missing aesthetics: x, you can cross-reference the chart to confirm that x is indeed a required aesthetic for geom_bar(). Similarly, remembering that geom_bar() does not require y (because its default stat computes counts) explains why supplying both x and y often produces errors—use geom_col() instead.

Worked Example — From Data to Multi-Layer Plot

Let us walk through building a complete ggplot using the built-in mpg dataset, which contains fuel economy data for 234 vehicles. Our goal is to create a scatter plot of engine displacement versus highway miles per gallon, colored by vehicle class, with a smoothed trend line overlay.

Building a Multi-Layer Scatter Plot with Trend Line
1
Step 1 — Load the library and inspect the dataCall library(ggplot2) to load the package. The mpg data frame is automatically available. Confirm the columns with glimpse(mpg). We need displ (numeric), hwy (numeric), and class (character/factor with 7 levels).
library(ggplot2) — 234 rows × 11 columns confirmed
2
Step 2 — Initialize the plot with data and global aestheticsWrite ggplot(data = mpg, aes(x = displ, y = hwy, color = class)). This creates the plot object and establishes three global aesthetic mappings: engine displacement on the x-axis, highway mpg on the y-axis, and vehicle class mapped to color. Running this line alone produces an empty coordinate system with axes labeled—no marks yet, because no geom has been added.
Empty plot canvas with axes: x = displ [1.6, 7.0], y = hwy [12, 44]
3
Step 3 — Add a point geom layerAppend + geom_point(size = 2.5, alpha = 0.7). The geom_point() layer inherits all three global aesthetics (x, y, color) and renders 234 points. Note that size and alpha are set as fixed constants outside aes(), so every point gets the same size and transparency.
Scatter plot: 234 points, 7 color categories, legend auto-generated
4
Step 4 — Overlay a smoothed trend lineAppend + geom_smooth(aes(color = NULL), method = "loess", se = TRUE, color = "gray40"). Here we override the inherited color aesthetic by setting aes(color = NULL) so that the smoother is not split by class—we want a single overall trend. The fixed color = "gray40" outside aes() sets the line color. se = TRUE renders a confidence ribbon.
LOESS curve overlaid with 95% confidence band in gray
5
Step 5 — Add labels and a themeAppend + labs(title = "Engine Size vs. Highway Fuel Economy", x = "Displacement (litres)", y = "Highway MPG", color = "Vehicle Class") + theme_minimal(). The labs() function sets human-readable titles and axis labels, while theme_minimal() applies a clean visual theme.
Final plot: titled scatter with LOESS overlay, minimal grid, and legend
💻 Complete Code
library(ggplot2) ggplot(data = mpg, aes(x = displ, y = hwy, color = class)) + geom_point(size = 2.5, alpha = 0.7) + geom_smooth(aes(color = NULL), method = "loess", se = TRUE, color = "gray40") + labs( title = "Engine Size vs. Highway Fuel Economy", x = "Displacement (litres)", y = "Highway MPG", color = "Vehicle Class" ) + theme_minimal()

Strengths, Pitfalls & Common Errors

ggplot2's grammar-based approach delivers enormous advantages in expressiveness and reproducibility, but it also introduces a specific class of pitfalls that trip up even experienced R programmers. The table below contrasts the key strengths against the most frequently encountered limitations.

Strengths and common pitfalls of building ggplots with aes() and geoms.
StrengthsPitfalls / Limitations
Declarative syntax: you describe the mapping, not the drawing procedure. Plots are self-documenting.Confusing mapping vs. setting — aes(color = "blue") vs. color = "blue" produces very different results.
Composability: layers, scales, and themes can be mixed and matched freely via the + operator.The + must appear at the end of a line, not the beginning of the next. Placing it on a new line causes a syntax error.
Automatic legends, axis labels, and scales are derived directly from the data and aesthetic mappings.Default stat transformations can surprise: geom_bar() counts by default; providing a y aesthetic without stat = "identity" throws an error.
Massive extension ecosystem (500+ packages on CRAN) provides specialized geoms, stats, and themes.Performance degrades with very large datasets (>100K points); consider geom_hex() or sampling.
Reproducible: a ggplot call is a complete specification, easy to save, share, and version-control.ggplot2 expects tidy (long-format) data; wide data must be pivoted with pivot_longer() first.
KEY TAKEAWAY
Most ggplot2 errors are not algorithmic but grammatical: they arise from misplacing an argument inside or outside aes(), choosing a geom whose default stat conflicts with your data shape, or breaking the + chain at a line boundary. Thinking of ggplot2 as a type system for visual channels—where aes() declares variable bindings and geoms consume them—helps you locate errors quickly.

Connection to Advanced Visualization Techniques

The aes() + geom_*() foundation extends seamlessly into advanced visualization paradigms. Understanding these connections situates today's concepts within the broader ggplot2 ecosystem and prepares you for more sophisticated analyses.

How today's aes() + geom concepts extend into advanced ggplot2 techniques.
Concept Learned TodayAdvanced ExtensionKey Difference
Single-panel plots with aes()Faceting with facet_wrap() / facet_grid()Splits data into small multiples by a categorical variable, creating one panel per level.
Fixed scales via defaultsCustom scales: scale_color_manual(), scale_x_log10()Manually control how data values map to aesthetic values—custom palettes, log transforms, date axes.
Standard geoms (point, line, bar)Extension geoms: ggridges, ggforce, gganimateCustom geometric objects for ridge plots, Voronoi diagrams, and animated transitions.
Cartesian coordinatesCoordinate transforms: coord_polar(), coord_sf()Polar coordinates turn bar charts into pie/donut charts; coord_sf() enables geographic maps.
Individual plotsComposition with patchwork packageCombine multiple ggplot objects into a single figure layout using +, |, and / operators.

The critical insight is that every advanced technique above still relies on the same aes() + geom backbone you learned today. Faceting adds a dimension without changing how aesthetics or geoms work. Custom scales modify the mapping function between data values and aesthetic values but do not alter the data or the geom. Even animation with gganimate simply adds a transition_*() layer that interpolates between aesthetic states over time. Mastering the foundational grammar therefore provides exponential returns as you explore the ecosystem.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain the difference between placing color = "red" inside aes() versus outside aes() but still inside geom_point(). What does ggplot2 do in each case, and why does the first often produce unexpected results?
PROBLEM 2BASIC CALCULATION
Given the code ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(), modify it so that point color represents the number of cylinders (cyl) and point size represents horsepower (hp). Write the complete code.
PROBLEM 3INTERMEDIATE
You want to create a bar chart of mpg$class (vehicle class) showing the mean highway fuel economy for each class. Should you use geom_bar() or geom_col()? Write the complete pipeline including any data wrangling.
PROBLEM 4APPLIED
You are analyzing server response times stored in a data frame logs with columns timestamp (POSIXct), latency_ms (numeric), and endpoint (character, 4 API endpoints). Build a ggplot that shows latency over time as lines (one per endpoint), with a horizontal reference line at 200 ms (the SLA threshold), and appropriate labels. Write the full code and explain each aesthetic decision.
PROBLEM 5CRITICAL THINKING
A colleague writes the following code and complains that all 7 trend lines have separate slopes, making the plot unreadable: ggplot(mpg, aes(x = displ, y = hwy, color = class)) + geom_point() + geom_smooth(method = "lm") Explain why 7 separate trend lines appear. Propose two different design solutions—one that shows a single overall trend line and one that retains per-class trends but improves readability—and write the modified code for each.

Summary — Building ggplots with aes() and geoms

Every ggplot is built by composing three essential elements with the + operator. First, a tidy data frame is passed to ggplot(). Second, aesthetic mappings via aes() bind data columns to visual channels like x, y, color, size, shape, fill, and alpha. Third, geom layers (geom_point(), geom_line(), geom_bar(), geom_col(), etc.) determine the geometric marks that represent the data. Global aesthetics defined in ggplot(aes(...)) are inherited by all layers and can be overridden locally within individual geom calls.

The most critical distinction to internalize is between mapping (inside aes) and setting (outside aes): mappings are data-driven and generate legends, while settings apply a constant value to all marks. Each geom is paired with a default stat that may transform your data before rendering—geom_bar() counts, geom_smooth() fits models, and geom_point() uses identity. Mastering these foundational concepts equips you to leverage the full power of ggplot2's layered grammar, from simple scatter plots to complex multi-panel, multi-layer visualizations.

Varsity Tutors • R Programming • Building ggplots — Build a ggplot with aesthetics (aes) and geoms