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 incomplete
  • type selects 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; mfcol fills 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)) and boxplot(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 graphicsggplot2
ModelPen and paper: each call draws on the current deviceDeclarative: build a plot object, then print it
Data shapeAny vectors you pass inLong, tidy data frame is required
Adding a layerplot() then points(), lines(), abline()Add with + geom_point(), + geom_line()
Panelspar(mfrow = c(2, 2))facet_wrap() or facet_grid()
Legendlegend() placed by handGenerated from the aesthetic mapping
Appearancepar(), col, pch, cextheme_*() and scale_*_*()
Savingpng(); plot(); dev.off()ggsave(), which knows the current plot
Best forQuick looks, unusual multi-panel layoutsPublication 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".

Data manipulation with base R Figures, axes and your first plot

Last refreshed 2026-09-18.