rguides

ggplot2::aes()

aes(x, y, ..., colour, fill, size, shape, alpha, linetype, linewidth)

Every ggplot2 plot starts with aes(). It maps data variables to visual properties, position, colour, size, shape, so the reader can see patterns in the data rather than raw numbers. Getting comfortable with aes() is what makes ggplot2 click.

aes() uses tidy evaluation (rlang formulas, not bare strings) to capture variable names and use them as plot labels automatically.

Syntax

aes(x, y, ..., colour, fill, size, shape, alpha, linetype, linewidth)

All arguments are optional. The mappings you omit fall back to defaults or nothing.

ArgumentVisual property
x, yPosition on axes
colourLine and point colour, fill colour for areas
fillFill colour for shapes and bars
sizePoint size, line thickness
shapePoint shape (0–25)
alphaTransparency (0–1)
linetypeLine type (solid, dashed, etc.)
linewidthLine width

You can map any aesthetic to any variable. aes(colour = group) colours points by group. aes(size = population) sizes points by population. The data drives the visual.

What gets captured

aes(x = mass, colour = sex) captures the column names mass and sex as symbols. ggplot2 converts these to strings for axis labels and legends automatically. You pass bare column names, not quoted strings, because aes() uses tidy evaluation to capture the expression before evaluating it:

ggplot(storms, aes(x = pressure, y = wind)) + geom_point()
# x-axis label becomes "pressure"
# y-axis label becomes "wind"

This is tidy evaluation in practice: the expression is captured as a quosure, not evaluated immediately. ggplot2 stores the mapping and waits until the plot is rendered to resolve column references against the data.

Mapping vs setting

aes() maps aesthetics to data columns, so each observation gets a visual property that depends on its data values. To set a constant visual property for all observations regardless of the data, pass the value as an argument to the geom layer (outside aes()) rather than inside it:

# Mapped: colour varies with drat
ggplot(mtcars, aes(x = wt, y = mpg, colour = drat)) + geom_point()

# Set: all points are red
ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point(colour = "red")

# Both: points coloured by cylinder, all points size 3
ggplot(mtcars, aes(x = wt, y = mpg, colour = cyl)) + geom_point(size = 3)

Setting goes in geom_*() or other layer arguments. Mapping goes in aes(). This separation makes the intent of each visual property clear when reading the code.

Grouping

When you want visual separation between groups without adding a colour or fill legend, use the group aesthetic. This tells geom layers to treat each group as a separate data series while keeping the default appearance. The example below draws smoothed trend lines per cylinder without adding a colour scale:

# Draw separate smoothed lines per cylinder without colour legend
ggplot(mtcars, aes(x = wt, y = mpg, group = cyl)) + geom_smooth()

Without group, geom_smooth() would draw one trend line through all points combined.

All available aesthetics

ggplot2 maintains a comprehensive list of supported aesthetics, and each geom documents which subset it accepts. You can inspect the full list programmatically to see what is available before writing your mapping:

ggplot2:::.all_aesthetics
# [1] "x"           "y"            "alpha"        "colour"      "fill"        
# [6] "linetype"    "shape"        "size"         "linewidth"    "stroke"     
# ...etc

Not all aesthetics work with every geom. linetype applies to line-based geoms but is ignored by point geoms. shape works for points but has no effect on lines. Check the geom’s documentation to confirm which aesthetics are supported before using them.

aes_string(), string-Based mapping

When your column names come from character strings — for example, from a function argument or a configuration file — use aes_string() instead of aes(). This accepts quoted column names and constructs the mapping from strings rather than symbols:

aes_string(x = "mass", y = "velocity")

You can also build the mapping programmatically when the column name is stored in a variable rather than known at write time. aes_string() accepts character strings directly, so you can pass a variable holding the column name and have it resolve to the correct column at runtime:

col_name <- "carat"
aes_string(y = col_name)
# Equivalent to aes(y = carat)

aes_string() is the recommended approach inside functions where column names arrive as character arguments rather than unquoted symbols. When your target column is determined at runtime, build the call with the variable holding the column name rather than hardcoding it:

aes_(), explicit quosures

For advanced metaprogramming, aes_() accepts pre-built quosures directly. Use the !! (bang-bang) operator to inject a symbol into the quosure context. Most users never need this level of abstraction; it exists for package developers building on top of ggplot2:

x_var <- sym("carat")
aes_(x = !!x_var, y = price)

Combining mappings

Aesthetic mappings defined in ggplot() serve as global defaults that apply to every layer that supports those aesthetics. Mappings in individual geom_*() calls add to or override the global mapping for that specific layer. This layered approach lets you share common mappings while customizing per-geom specifics:

ggplot(mtcars, aes(x = wt, y = mpg)) +          # global: x, y
  geom_point(aes(colour = factor(cyl))) +          # adds colour
  geom_smooth(aes(linetype = vs), se = FALSE)      # adds linetype

The legend system merges aesthetics intelligently. When two layers map the same aesthetic to different variables, ggplot2 produces separate legend entries for each, making the distinctions visible without manual configuration.

aes() and the pipe

When your data transformation pipeline is long, pipe the processed data directly into ggplot() to keep the code linear and readable. The pipe passes the data frame as the first argument, which is exactly where ggplot() expects it:

storms %>%
  filter(storm == "Amy") %>%
  ggplot(aes(x = pressure, y = wind)) +
  geom_line()

Rowwise aesthetics

Some visualizations benefit from per-row aesthetic decisions driven by data values. Using ifelse() or case_when() inside aes() maps a computed logical condition to a visual property, highlighting specific rows that meet a criterion without subsetting the data into separate layers:

ggplot(economics, aes(x = date, y = psavert)) +
  geom_point(aes(colour = ifelse(psavert < 0, "negative", "positive")),
             size = 1.5)

This highlights rows where the personal savings rate went negative.

Column names become labels

ggplot2 extracts the expression from the quosure and converts it to a string for axis labels and legend titles. aes(y = population_millions) produces an axis label of “population_millions”. To override this automatic label, add labs(y = "Population (millions)") to the plot.

See also