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.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

Layered Architecture of a ggplotggplot(data = my_df)Tidy data frame — rows = observations, cols = variablesaes(x = col_a, y = col_b, color = col_c)Aesthetic mapping — data → visual channelsgeom_point() + geom_smooth()Geom layers — choose mark type (point, line, bar, …)scale_color_brewer() + coord_flip()Scales & coordinates — refine axis ranges, color palettestheme_minimal() + labs(title = ...)Layer 1Layer 2Layer 3Layer 4Layer 5++++
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.

SECTION 4

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.

SECTION 5

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.

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

Common ggplot2 geoms with their aesthetic requirements and default statistical transformations.

aes() Channels × Geom Types — Visual MapAesthetic ChannelsxycolorsizefillshapealphaGeomgeom_point()geom_line()geom_bar()geom_boxplot()geom_histogram()geom_smooth()RequiredOptionalNot applicable
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.

SECTION 6

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. 1

    Step 1 — Load the library and inspect the data

    Call 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. 2

    Step 2 — Initialize the plot with data and global aesthetics

    Write 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. 3

    Step 3 — Add a point geom layer

    Append + 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. 4

    Step 4 — Overlay a smoothed trend line

    Append + 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. 5

    Step 5 — Add labels and a theme

    Append + 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()

SECTION 7

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.

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.

Strengths and common pitfalls of building ggplots with aes() and geoms.

✦

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.

SECTION 8

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.

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.

How today's aes() + geom concepts extend into advanced ggplot2 techniques.

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.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

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?
Show answer
When color = "red" is placed inside aes(), ggplot2 treats "red" as a data value—a single-level categorical variable—and maps it through the default color scale, typically producing a salmon or coral color and generating a spurious legend entry labeled "red". When color = "red" is placed outside aes() (e.g., geom_point(color = "red")), it is interpreted as a fixed graphical parameter: all points become literally red, and no legend is created. The fundamental distinction is between mapping (data-driven, inside aes) and setting (constant, outside aes).

Question 2: BASIC 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.
Show answer
The solution is ggplot(data = mtcars, aes(x = wt, y = mpg, color = factor(cyl), size = hp)) + geom_point(alpha = 0.7). Wrapping cyl in factor() is important because cyl is numeric (4, 6, 8) and would otherwise be treated as a continuous variable, producing a gradient rather than discrete colors. Adding alpha = 0.7 outside aes() as a fixed setting helps with overplotting. Two legends are automatically generated: one for color (cyl) and one for size (hp).

Question 3: INTERMEDIATE

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.
Show answer
Use geom_col() because the bar heights are pre-computed means, not raw counts. The pipeline is: library(dplyr) mpg |> group_by(class) |> summarise(mean_hwy = mean(hwy)) |> ggplot(aes(x = reorder(class, mean_hwy), y = mean_hwy, fill = class)) + geom_col() + coord_flip() + labs(x = "Class", y = "Mean Highway MPG") + theme_minimal(). Using geom_bar() would fail here because its default stat = "count" would try to count rows rather than use the pre-computed mean. Alternatively, geom_bar(stat = "identity") works but geom_col() is more idiomatic.

Question 4: APPLIED

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.
Show answer
ggplot(logs, aes(x = timestamp, y = latency_ms, color = endpoint)) + geom_line(linewidth = 0.8) + geom_hline(yintercept = 200, linetype = "dashed", color = "red") + annotate("text", x = min(logs$timestamp), y = 210, label = "SLA threshold (200 ms)", hjust = 0, color = "red", size = 3.5) + labs(title = "API Latency Over Time", x = "Timestamp", y = "Latency (ms)", color = "Endpoint") + theme_minimal() The color = endpoint aesthetic inside aes() produces separate lines per endpoint with an auto-generated legend. geom_hline() adds a reference line that does not depend on the data—it uses a fixed y-intercept. annotate() places a text label at a fixed position rather than mapping data. This combination of data-driven (geom_line) and annotation-based (geom_hline, annotate) layers is a common real-world pattern.

Question 5: CRITICAL 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.
Show answer
Seven trend lines appear because geom_smooth() inherits the global color = class aesthetic, which causes ggplot2 to group the data by class and fit a separate model per group. Solution 1: Override the inherited color within geom_smooth: geom_smooth(aes(color = NULL), method = "lm", color = "black"). Setting color = NULL inside aes() removes the grouping, producing one overall regression line. Solution 2: Keep per-class trends but use faceting for readability: ggplot(mpg, aes(x = displ, y = hwy)) + geom_point(aes(color = class)) + geom_smooth(method = "lm", se = FALSE) + facet_wrap(~class, scales = "free_y"). This moves the color mapping to geom_point only and adds a single trend per facet panel, avoiding visual clutter. The key principle is that aesthetic inheritance can be controlled at the layer level to manage grouping behavior.

SUMMARY

Summary — Building ggplots with aes() and geoms

Summary

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