Menu

ggplot2 in R: A Beginner's Guide to the Grammar of Graphics

How ggplot2 works - the data + aes() + geom mental model, mapping vs setting aesthetics, labs(), facet_wrap(), themes, and saving plots with ggsave().

ggplot2 Builds Plots from Layers

ggplot2 is R's most widely used plotting package, and the reason people swear by it is a design idea: the grammar of graphics. Base R gives you one function per chart - plot(), hist(), barplot() - each with its own arguments. ggplot2 instead gives you a small set of composable pieces - a dataset, a mapping from columns to visual properties, and drawing layers - that combine with + into any chart. Learn the grammar once and every new chart type is the same sentence with one word changed. As a bonus, the defaults (gray panel, sensible axes, automatic legends) look presentable without any styling effort.

It's an add-on package, so it needs a one-time install and a per-session load:

install.packages("ggplot2")   # once, on your machine
library(ggplot2)              # at the top of every script that uses it

One expectation to know upfront: ggplot2 wants a data frame (not loose vectors), ideally in tidy long format - one row per observation, one column per variable. If your data is wide, pivoting it longer first usually dissolves what looked like a plotting problem.

The Mental Model: Data, Mapping, Geometry

Every ggplot answers three questions, one per part:

  1. Data - which data frame? ggplot(mtcars, ...)
  2. Mapping - which columns drive which visual properties? aes(x = wt, y = hp)
  3. Geometry - how should the observations be drawn? + geom_point()

Here is the anatomy, token by token:

ggplot(mtcars, aes(x = wt, y = hp)) +
    geom_point()
  • ggplot(mtcars, ...) starts a plot whose default data is mtcars. On its own it draws an empty gray panel - no layers yet.
  • aes(x = wt, y = hp) is the aesthetic mapping: horizontal position comes from the wt column, vertical position from hp. Note you write bare column names, not mtcars$wt - the mapping is evaluated inside the data you supplied.
  • + geom_point() adds a layer that draws each row as a point. The + really is addition of layers: the plot is an object you build up, and you can keep adding - another geom, labels, a theme - each with another +.

Run it and you get the same weight-vs-horsepower scatter as the base-R version in the scatter plot guide, on a gray panel with white gridlines. Swap geom_point() for geom_line() and the same mapping becomes a line chart - that's the grammar doing its job.

The Geoms You'll Actually Use

A dozen geoms cover almost all day-to-day work:

geom_point()       # scatter plot: two numeric variables
geom_line()        # line chart: trends over an ordered variable
geom_col()         # bar chart from pre-computed heights (needs y)
geom_bar()         # bar chart that counts raw rows for you (no y)
geom_histogram()   # distribution of one numeric variable (set binwidth)
geom_boxplot()     # distributions compared across groups
geom_smooth()      # fitted trend line; method = "lm" for straight

Layers stack, and that's where ggplot2 starts beating base R for expressiveness: geom_point() + geom_smooth(method = "lm") is a scatter plot with a regression line and a confidence band, in one readable line. Because plots are objects, you can also save a base and branch from it:

p <- ggplot(mtcars, aes(x = wt, y = hp)) + geom_point()

p + geom_smooth(method = "lm")   # one variant
p + geom_smooth()                # another (loess curve)

Mapping vs Setting: the Classic aes() Confusion

The one mistake every ggplot2 beginner makes, usually in week one: putting a literal color inside aes(). The rule is short - inside aes() means "varies with the data"; outside means "fixed setting" - and the two placements do completely different things:

# MAPPING: color varies by a column - each cylinder count gets its own
# color, and a legend appears automatically
ggplot(mtcars, aes(x = wt, y = hp, color = factor(cyl))) +
    geom_point(size = 2)

# SETTING: every point is literally steelblue - no legend, no mapping
ggplot(mtcars, aes(x = wt, y = hp)) +
    geom_point(color = "steelblue", size = 2)

And the trap itself:

# WRONG: "blue" inside aes() is treated as data, not as a color
ggplot(mtcars, aes(x = wt, y = hp, color = "blue")) +
    geom_point()

This draws salmon-pink points with a legend whose single entry is labeled "blue". Why: aes() maps columns to properties, so color = "blue" creates a fake one-category column whose value is the word "blue" everywhere, and ggplot assigns that category its default first color - which happens to be a reddish salmon. If you meant "make the points blue", the color goes outside the mapping: geom_point(color = "blue"). The same rule governs size, shape, fill, and alpha. (Also note factor(cyl) in the mapping example: cyl is numeric, and wrapping it in factor() asks for distinct colors per group rather than a continuous color gradient.)

Labels, Facets, and Themes

labs() names everything in one place - and a plot isn't done until it has real labels:

ggplot(mtcars, aes(x = wt, y = hp, color = factor(cyl))) +
    geom_point(size = 2) +
    labs(title = "Horsepower vs. weight",
         x     = "Weight (1000 lbs)",
         y     = "Horsepower",
         color = "Cylinders")

Facets are ggplot2's headline feature: split one plot into small multiples, one panel per group, with shared axes so panels compare honestly:

ggplot(mtcars, aes(x = wt, y = hp)) +
    geom_point() +
    facet_wrap(~ cyl)

Three panels appear - one each for 4-, 6-, and 8-cylinder cars - laid out in a grid with the group value captioned above each panel. In base R this is a par(mfrow) juggling act with manual shared limits; here it's one line. Faceting rewards tidy data, which is why ggplot2 pairs so naturally with a dplyr pipeline - filter and summarize, then pipe the result straight into ggplot().

Themes restyle the non-data ink in one call: + theme_minimal() (clean white background), + theme_bw(), + theme_classic() (axes only, no grid), or the gray default. Pick one per project and stay consistent - a report where every figure shares a theme reads as one document rather than a scrapbook.

Saving with ggsave() - and When Base R Is Still Fine

ggsave() writes the most recent plot to disk, inferring the format from the file extension:

ggsave("hp-vs-weight.png", width = 8, height = 5)          # last plot shown
ggsave("hp-vs-weight.pdf", plot = p, width = 8, height = 5) # a saved object

width and height are in inches by default; set them explicitly, because font sizes scale with the output dimensions and the defaults rarely match where the figure is headed.

As for when to bother: for a five-second look at one variable during an analysis, base R's plot() and friends are less typing and zero dependencies - there's no prize for using ggplot2 everywhere. ggplot2 earns its keep the moment a figure involves groups, legends, facets, or an audience: the grammar scales to complexity that base plotting handles only with effort.

What You Take Away

  • ggplot2 composes every chart from data + aes() mapping + geom layers, joined by +; learn the grammar once, reuse it for every chart type.
  • aes() maps columns to visual properties - bare column names, evaluated in your data frame.
  • Inside aes() = varies with data (and gets a legend); outside = fixed setting. aes(color = "blue") is the classic bug.
  • labs() for titles, facet_wrap(~ group) for small multiples, theme_minimal() and friends for styling, ggsave() with explicit dimensions to export.
  • Quick solo look: base R. Groups, facets, or an audience: ggplot2.

That closes the plotting chapter - from here, the natural next step is descriptive statistics, putting numbers to the shapes these plots reveal.

Frequently Asked Questions

What is ggplot2 in R?

ggplot2 is R's most popular plotting package. Instead of one function per chart type, you compose every plot from the same three pieces - a data frame, an aes() mapping from columns to visual properties, and one or more geom_*() layers - joined with +. It's part of the tidyverse.

How do you install ggplot2?

Run install.packages("ggplot2") once, then library(ggplot2) at the top of every script that uses it. Installing puts the package on your machine; library() loads it into the current session.

What does aes() do in ggplot2?

aes() maps columns of your data to visual properties: aes(x = wt, y = hp, color = factor(cyl)) says horizontal position comes from wt, vertical position from hp, and point color from the cylinder count. Anything inside aes() varies with the data; anything outside it is a fixed setting.

Why are my ggplot points not the color I asked for?

You put the color inside aes(): aes(color = "blue") doesn't set a color, it maps a made-up category named "blue", so ggplot picks its own default color and adds a legend. To set a literal color, put it outside the mapping: geom_point(color = "blue").

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED