Base graphics and ggplot2
The painter's model behind base plots, the grammar behind ggplot2, and how to write a chart to a file without losing it.
Base graphics draw onto a device
Base graphics is a pen-and-paper model: the first call opens a plot, and every later call draws on top of it. A device — the screen, a PNG file, a PDF — is where the drawing goes, and it must be closed before the file is complete.
x <- seq(0, 2 * pi, length.out = 100)
png("wave.png", width = 900, height = 500)
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1)) # one row, two panels
plot(x, sin(x), type = "l", col = "steelblue", lwd = 2,
xlab = "radians", ylab = "sin(x)", main = "Sine")
lines(x, cos(x), col = "firebrick", lty = 2)
abline(h = 0, col = "grey60")
legend("topright", c("sin", "cos"), col = c("steelblue", "firebrick"),
lty = c(1, 2), bty = "n")
hist(rnorm(1000), breaks = 30, col = "grey80", main = "Normal sample")
dev.off() # required: without it the PNG stays incompletetypeselects the geometry:"p"points,"l"lines,"b"both,"h"histogram-like stems.par(mfrow = c(2, 2))tiles subsequent plots into a grid, filling row by row;mfcolfills by column.par(mar = c(bottom, left, top, right))controls the margin in lines of text — the fix for a clipped axis label.- The formula interface (
plot(y ~ x, data = df)) andboxplot(y ~ group, data = df)read far better than positional arguments. - The base palette is limited;
hcl.colors(8, "Viridis")gives a perceptually uniform set with no extra package.
ggplot2 is a grammar, not a drawing
library(ggplot2)
df <- data.frame(
day = rep(1:14, 2),
kind = rep(c("signups", "cancellations"), each = 14),
n = c(120, 132, 141, 128, 150, 162, 155, 170, 168, 180, 175, 191, 188, 200,
10, 12, 9, 14, 11, 15, 13, 16, 12, 18, 14, 15, 19, 17)
)
p <- ggplot(df, aes(day, n, colour = kind)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
facet_wrap(~ kind, scales = "free_y") +
scale_colour_manual(values = c(signups = "#2c7fb8", cancellations = "#d95f02")) +
labs(title = "Daily totals", x = "Day", y = "Count", colour = NULL) +
theme_minimal(base_size = 12)
ggsave("trend.png", p, width = 8, height = 4, dpi = 150)| Base graphics | ggplot2 | |
|---|---|---|
| Model | Pen and paper: each call draws on the current device | Declarative: build a plot object, then print it |
| Data shape | Any vectors you pass in | Long, tidy data frame is required |
| Adding a layer | plot() then points(), lines(), abline() | Add with + geom_point(), + geom_line() |
| Panels | par(mfrow = c(2, 2)) | facet_wrap() or facet_grid() |
| Legend | legend() placed by hand | Generated from the aesthetic mapping |
| Appearance | par(), col, pch, cex | theme_*() and scale_*_*() |
| Saving | png(); plot(); dev.off() | ggsave(), which knows the current plot |
| Best for | Quick looks, unusual multi-panel layouts | Publication figures and many similar charts |
⚠️
In base R the classic mistake is forgetting
dev.off(): the process may exit with a truncated PNG, and a second plot() without par(mfrow) overwrites the first instead of adding to it. In ggplot2 the equivalent is building a plot object inside a function and never printing it — the object is only rendered when it is printed or handed to ggsave().FAQ
My ggplot has a wide data frame. How do I plot it?
Reshape it long first:
tidyr::pivot_longer(df, cols = -day) gives one row per observation with a name column you can map to colour or facets. ggplot2 has no wide-data shortcut by design.Why do my colours come out wrong?
A colour mapped inside
aes() is a data-driven scale and gets a legend; a colour set outside aes() — geom_point(colour = "red") — is a fixed constant. Using colour = "red" inside aes() maps a constant string as data and produces one legend entry labelled "red".Related
Data manipulation with base R Figures, axes and your first plot
Last refreshed 2026-09-18.