Common Code 03 — Basic ggplot2
Scatter plots, boxplots, labels, colour, shapes, axis limits, themes, and saving
The basics of using ggplot
Introduction to ggplot2
ggplot2 is built on the grammar of graphics — the idea that every plot can be described with the same small set of ingredients: data, mappings, and geometric shapes. Once you know the grammar, you can build almost any plot by combining the same pieces in different ways.
This vignette covers the essentials you need for basic exploratory and presentation-quality plots. More advanced topics — statistical summaries, custom themes stored in separate files, multi-panel layouts — are covered in later vignettes.
⬇️ Download the companion R script — all examples ready to run:
03_ggplot.R
💡 Following along? Examples use the
penguinsdataset from thepalmerpenguinspackage — a clean, well-structured dataset that shows real ecological patterns. Swap in your own data frame wherever you seepenguins.
Packages needed
library(tidyverse) # includes ggplot2
library(palmerpenguins) # penguins dataset1 · The grammar of graphics
Every ggplot is built from the same three required pieces, joined with +:
ggplot(data, aes(x = , y = )) + # 1. data + mappings
geom_*() + # 2. geometric shape
labs() # 3. labels (optional but always do it)
The minimal working plot — data, axes, and points:
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point()💡 Key idea: Think of
ggplot()as setting up the canvas and telling R which columns map to x and y. Thegeom_*()function then decides how to draw the data — as points, boxes, bars, lines, and so on. You can swap geoms without changing anything else.
⚠️ Watch out! The
+must sit at the end of a line, never the start of the next one. A+at the start of a line causes an error.
The concise style
R4DS (Wickham & Grolemund) uses shorthand once you know the argument order — you do not need to write data = and mapping = explicitly:
# Verbose (explicit argument names — good for learning)
ggplot(data = penguins, mapping = aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point()
# Concise (argument names dropped — tidyverse convention)
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point()
# With the pipe (also fine)
penguins |>
ggplot(aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point()All three produce identical output. We use the concise style throughout this vignette.
2 · Scatter plots
Basic scatter plot
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point()Map colour to a grouping variable
Place the mapping inside aes() and ggplot2 automatically assigns colours and draws a legend:
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = species)) +
geom_point()Map colour AND shape together
Encoding the same variable with two aesthetics makes the plot accessible to colour-blind readers — a good habit for any published figure:
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = species, shape = species)) +
geom_point()💡 Key idea: When you map both
colorandshapeto the same variable, ggplot2 merges them into a single legend automatically — no extra code needed.
Fixed vs. mapped aesthetics
This is one of the most common points of confusion in ggplot2:
- Inside
aes()= the value comes from the data (varies by row → legend appears) - Outside
aes()= fixed for all points (same for every row → no legend)
# color mapped to data — different colour per species
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = species)) +
geom_point()
# color fixed — all points the same tomato-red, no legend
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point(color = "tomato", size = 2, alpha = 0.7)⚠️ Watch out! Putting a colour name inside
aes()—aes(color = "tomato")— does not make all points red. It maps the literal string"tomato"as a data category, producing one colour and an unhelpful legend. Fixed values always go outsideaes().
3 · Box plots
Box plots are ideal when one variable is categorical (groups) and one is numeric (measurement).
Basic box plot
ggplot(penguins, aes(x = species, y = body_mass_g)) +
geom_boxplot()Fill colour mapped to the x variable
ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
geom_boxplot(alpha = 0.6)Overlay raw data points with jitter
A boxplot alone hides sample size. Adding jittered points shows both the distribution shape and how many observations went into each box:
ggplot(penguins, aes(x = species, y = body_mass_g)) +
geom_boxplot() +
geom_jitter(width = 0.15, alpha = 0.4, color = "steelblue")💡 Key idea: Layer order matters.
geom_boxplot()first andgeom_jitter()second draws the points on top of the boxes. Swap the order and the boxes cover the points.
Side-by-side box plots (two grouping variables)
Map a second grouping variable to fill to split each x-group:
ggplot(penguins, aes(x = species, y = body_mass_g, fill = sex)) +
geom_boxplot(alpha = 0.6)4 · Titles and axis labels with labs()
labs() controls every piece of text on the plot. Always label your axes — unlabelled axes are a common reason reviewers send figures back.
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = species, shape = species)) +
geom_point(size = 2.5, alpha = 0.8) +
labs(
title = "Body mass increases with flipper length",
subtitle = "Palmer Archipelago penguins, 2007–2009",
x = "Flipper length (mm)",
y = "Body mass (g)",
color = "Species", # rename the colour legend
shape = "Species", # rename the shape legend (must match color label)
caption = "Data: Gorman et al. / palmerpenguins package"
)💡 Key idea: The
colorandshapearguments insidelabs()rename the legend titles. When both are set to the same string (e.g."Species"), ggplot2 merges them into one legend. If they differ, you get two separate legends — usually not what you want.
labs() arguments at a glance:
| Argument | Controls |
|---|---|
title |
Main title above the plot |
subtitle |
Smaller text below the title |
caption |
Small text at bottom-right (source, notes) |
x |
x-axis label |
y |
y-axis label |
color / colour |
Colour legend title |
fill |
Fill legend title |
shape |
Shape legend title |
5 · Mapping colour and shape
Colour and shape from the data
Map to categorical variables for automatic discrete palettes:
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm,
color = species, shape = species)) +
geom_point(size = 2)Map to a continuous variable for a gradient palette:
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = bill_length_mm)) +
geom_point(size = 2)Fixed point shapes
Common shape codes for shape = outside aes():
| Code | Shape |
|---|---|
16 |
Filled circle (default) |
17 |
Filled triangle |
15 |
Filled square |
21 |
Circle with separate fill and colour |
22 |
Square with separate fill and colour |
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(shape = 17, color = "steelblue", size = 2.5)💡 Shapes 21–25 have both a
fill(inside) andcolor(border), which lets you map two aesthetics independently — useful when you want coloured outlines around differently filled points.
6 · Adjusting axis limits
Two approaches — they behave differently and that matters:
coord_cartesian() — zoom without removing data
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point(alpha = 0.6) +
coord_cartesian(
xlim = c(170, 220),
ylim = c(2500, 6500)
)xlim() / ylim() — remove data outside the range
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
geom_point(alpha = 0.6) +
xlim(170, 220) +
ylim(2500, 6500)⚠️ Watch out!
xlim()andylim()silently drop rows that fall outside the range before any computations happen. This changes the output of computed geoms likegeom_boxplot(),geom_smooth(), andgeom_histogram()— the statistics are recalculated on the reduced data, which may mislead. Prefercoord_cartesian()in almost every case. Only usexlim()/ylim()when you genuinely want to exclude those data points from the analysis, not just from the view.
7 · Simple themes
A theme controls everything that is not data — background, gridlines, text sizes, tick marks. ggplot2 ships with several complete themes. Try them with the same plot to see which suits your purpose:
p <- ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
geom_boxplot(alpha = 0.6) +
labs(x = "Species", y = "Body mass (g)", title = "Penguin body mass by species")
p + theme_grey() # default: grey background, white gridlines
p + theme_bw() # white background, grey gridlines, black border
p + theme_minimal() # no background or border, subtle gridlines
p + theme_classic() # white background, axis lines only, no gridlines
p + theme_light() # light grey lines and border💡
theme_classic()is a safe default for ecological publications — it is clean, uncluttered, and close to what most journals expect.
Remove the legend when it is redundant
When colour just repeats the x-axis information, drop the legend:
p + theme_classic() +
theme(legend.position = "none")Move the legend
p + theme_classic() +
theme(legend.position = "bottom") # "top", "left", "right", "bottom", "none"💡 Key idea: The built-in themes (
theme_classic()etc.) set defaults. Adding atheme()call afterwards lets you override individual elements on top of that base. Complex customisation — font sizes, axis text angle, background colours — is covered in the advanced themes vignette, where you will also learn to store your theme in a separate file andsource()it at the top of every script.
8 · Storing a plot as an object
Assign the plot to a named object with <-. This lets you print it, add more layers to it, or save it without retyping the whole block:
my_plot <- ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g,
color = species, shape = species)) +
geom_point(size = 2.5, alpha = 0.8) +
labs(
title = "Body mass and flipper length",
x = "Flipper length (mm)",
y = "Body mass (g)",
color = "Species",
shape = "Species"
) +
theme_classic() +
theme(legend.position = "bottom")
my_plot # print it
# Add an extra layer without retyping
my_plot + geom_smooth(method = "lm", se = FALSE, color = "grey40")9 · Saving plots with ggsave()
Save to your figures/ folder. Always save the plot object explicitly with plot = — do not rely on ggsave() grabbing the last printed plot, as that can cause hard-to-trace bugs in longer scripts.
PDF — vector format, scales to any size
ggsave("figures/penguin_flipper_mass.pdf",
plot = my_plot,
width = 6,
height = 5,
units = "in")PNG — raster, good for Word documents, presentations, web
ggsave("figures/penguin_flipper_mass.png",
plot = my_plot,
width = 6,
height = 5,
units = "in",
dpi = 300) # 300 dpi = publication quality; 96 dpi = screen onlyTIFF — required by some journals
ggsave("figures/penguin_flipper_mass.tiff",
plot = my_plot,
width = 6,
height = 5,
units = "in",
dpi = 300,
compression = "lzw")⚠️ Watch out!
ggsave()takes the filename first, thenplot =. Swapping the order is the most common saving mistake — R will not warn you, it will just silently save the wrong plot or throw a confusing error.
Format comparison:
| Format | Best for | Scales? | File size |
|---|---|---|---|
.pdf |
Print, journals | ✅ infinite | Small |
.png |
Word docs, web, slides | ❌ fixed pixels | Medium |
.tiff |
Journal submission | ❌ fixed pixels | Large |
.svg |
Web, Illustrator editing | ✅ infinite | Small |
Quick reference
| Task | Code |
|---|---|
| Scatter plot | geom_point() |
| Box plot | geom_boxplot() |
| Jittered raw data | geom_jitter(width = 0.15, alpha = 0.5) |
| Map colour (data) | aes(color = variable) |
| Map shape (data) | aes(shape = variable) |
| Fixed colour | geom_point(color = "steelblue") |
| Fixed size | geom_point(size = 2) |
| Transparency | geom_point(alpha = 0.6) |
| All labels | labs(title=, subtitle=, x=, y=, color=, caption=) |
| Zoom (safe) | coord_cartesian(xlim = c(a,b), ylim = c(c,d)) |
| Classic theme | theme_classic() |
| Remove legend | theme(legend.position = "none") |
| Save as PDF | ggsave("figures/plot.pdf", plot = p, width=6, height=5, units="in") |
| Save as PNG | ggsave("figures/plot.png", plot = p, width=6, height=5, dpi=300, units="in") |
End of Common Code 03 — Basic ggplot2. Next: Common Code 04 — Filtering and selecting data.