ggplot2 Basics
ggplot2 is the most popular charting package in R and part of the tidyverse. Instead of one function per chart type, it gives you a small grammar of graphics: you say which data to use, which columns map to which visual properties (position, colour, size), and which shapes to draw. The same few pieces combine into almost any chart, and the defaults already look clean.
In this lesson you will build scatter plots, bar charts, histograms and box plots, add titles and themes, split a chart into panels and save it. The current version is ggplot2 4.0.3. ggplot2 cannot be installed inside this site's browser editor, so the examples are plain code; run them in RStudio or Positron after install.packages("ggplot2"), and read the description of each chart here.
The grammar: data, aesthetics, geoms
Every ggplot has three essential parts:
- Data: a data frame, given to
ggplot(). - Aesthetic mappings with
aes(): which column goes on the x-axis, which on the y-axis, which controls colour, and so on. - Geoms (geometric objects): the shapes that draw the data, added with
+, such asgeom_point()for points.
library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point()This draws 32 grey-black points on a light grey panel with white grid lines: weight on the x-axis, miles per gallon on the y-axis, sloping downwards. The axes are labelled with the column names automatically.
Notice the + at the end of the first line. ggplot2 builds a chart in layers joined by +, not by the pipe. If you start a new line with +, R treats the first line as complete and the second line fails.
Mapping more variables and adding layers
Map a column to colour, size or shape inside aes() and ggplot2 adds a legend for you. Set a fixed value outside aes(). Add layers with more geoms and polish with labs() and a theme:
ggplot(mtcars, aes(x = wt, y = mpg, colour = factor(cyl))) +
geom_point(size = 3) +
geom_smooth(method = "lm", se = FALSE) +
labs(title = "Heavier cars use more fuel",
x = "Weight (1000 lbs)", y = "Miles per gallon",
colour = "Cylinders") +
theme_minimal()`geom_smooth()` using formula = 'y ~ x'
The chart now shows large points in three colours (by default a red, a green and a blue for 4, 6 and 8 cylinders), each group with its own straight trend line, a legend titled "Cylinders" on the right, a title at the top and a clean white background from theme_minimal(). The message is only information: geom_smooth() tells you which formula it fitted.
factor(cyl)makes colour discrete (three separate colours). A numeric column such ashpwould give a continuous colour gradient instead.size = 3sits outsideaes(), so every point gets size 3. Writingaes(size = 3)by mistake would create a pointless legend.geom_smooth(method = "lm")adds regression lines;se = FALSEhides the shaded confidence band.- Other built-in themes include
theme_bw(),theme_classic()andtheme_light().
Bar charts, histograms and box plots
ggplot(mtcars, aes(x = factor(cyl))) +
geom_bar(fill = "#276dc3") +
labs(x = "Cylinders", y = "Number of cars")
sales <- data.frame(day = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"),
amount = c(4200, 3900, 4600, 5100, 6800, 9200, 8700))
sales$day <- factor(sales$day, levels = sales$day)
ggplot(sales, aes(x = day, y = amount)) +
geom_col(fill = "darkorange") +
labs(title = "Weekly sales (practice data)", x = NULL, y = "Sales (Rs)")geom_bar()counts rows for you: three blue bars of 11, 7 and 14 cars.geom_col()draws bars whose heights are values already in the data: seven orange bars rising to a Saturday peak. Makingdaya factor with the levels in week order stops ggplot2 from sorting the days alphabetically, the same idea you met in Factors.
ggplot(airquality, aes(x = Temp)) +
geom_histogram(binwidth = 5, fill = "grey70", colour = "white")
ggplot(airquality, aes(x = factor(Month, labels = month.abb[5:9]), y = Temp)) +
geom_boxplot(fill = "lightgreen") +
labs(x = "Month", y = "Temperature (F)")The histogram shows grey bars 5 degrees wide with white outlines, peaking around 80 °F. The box plot shows one green box per month from May to September, with July and August highest. binwidth controls the bin size; try a few values, because the shape of a histogram can change with it.
Facets: one panel per group
Facets split a chart into small panels, one for each value of a variable, all with the same axes. They make comparisons easy:
ggplot(iris, aes(x = Petal.Length, y = Petal.Width)) +
geom_point(colour = "steelblue") +
facet_wrap(~ Species) +
labs(x = "Petal length (cm)", y = "Petal width (cm)")Three panels appear side by side, labelled setosa, versicolor and virginica, each with its own small cloud of blue points. Because the axes are shared, you can see at a glance that setosa petals are much smaller. facet_grid(rows ~ cols) arranges panels by two variables.
Saving a chart
Store a chart in a variable, then save it with ggsave(). The file type comes from the extension:
p <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
ggsave("mpg-weight.png", p, width = 7, height = 4, dpi = 300)width and height are in inches by default. Use .pdf or .svg for sharp charts in reports, .png for slides and web pages.
ggplot2 or base graphics?
To see what ggplot2 automates, here is the coloured scatter plot from earlier drawn with base graphics, which does run in the browser. You choose the colours, look them up for every point and build the legend yourself:
cols <- c("4" = "#F8766D", "6" = "#00BA38", "8" = "#619CFF")
plot(mtcars$wt, mtcars$mpg, pch = 19, cex = 1.4,
col = cols[as.character(mtcars$cyl)],
main = "Heavier cars use more fuel",
xlab = "Weight (1000 lbs)", ylab = "Miles per gallon")
legend("topright", legend = names(cols), col = cols, pch = 19, title = "Cylinders")
table(mtcars$cyl)4 6 8 11 7 14
The chart shows the same red, green and blue points with a hand-made legend in the top right corner; the trend lines would need three more abline() calls. cols[as.character(mtcars$cyl)] looks up one colour per car by name. In ggplot2, colour = factor(cyl) inside aes() does all of this in one step.
| Base graphics | ggplot2 | |
|---|---|---|
| Needs a package | No | Yes |
| Legends | Drawn by hand with legend() | Automatic from aes() |
| Groups and panels | Manual (col =, par(mfrow)) | colour =, facet_wrap() |
| Style | Plain by default | Consistent themes |
| Best for | Quick looks while exploring | Polished, layered charts for reports |
Many analysts explore with base graphics and present with ggplot2. Both are in Base Graphics and this lesson.
Common mistakes
- Starting a line with
+. Put the+at the end of the previous line. - Setting a fixed colour inside
aes().aes(colour = "blue")makes a legend entry called "blue" and a red-ish default colour. Put fixed values outside:geom_point(colour = "blue"). - Using the pipe
|>between layers. Layers join with+; the pipe can only pass data intoggplot(). - Numeric codes as continuous colours. Wrap coded categories in
factor(). - Confusing
geom_bar()andgeom_col().geom_bar()counts rows;geom_col()uses a y value.
Key points
- A ggplot is data + aesthetic mappings (
aes()) + geoms, joined with+. geom_point(),geom_line(),geom_col(),geom_bar(),geom_histogram()andgeom_boxplot()cover everyday charts.- Map variables inside
aes(); set fixed values outside it. labs()sets titles and labels; themes such astheme_minimal()change the look;facet_wrap()makes panels.ggsave()writes a chart to PNG, PDF or SVG.
Exercise
Using airquality, draw a scatter plot of Temp (x) against Ozone (y), coloured by month (as a factor), with a single straight trend line for all the data, a title, axis labels with units, and theme_bw(). ggplot2 drops the rows with missing ozone and warns you; explain what the warning means.
Show solution
library(ggplot2)
ggplot(airquality, aes(x = Temp, y = Ozone)) +
geom_point(aes(colour = factor(Month))) +
geom_smooth(method = "lm", se = FALSE, colour = "black") +
labs(title = "Hotter days have more ozone",
x = "Temperature (F)", y = "Ozone (ppb)", colour = "Month") +
theme_bw()Putting colour in the aes() of geom_point() only, not in ggplot(), colours the points by month while geom_smooth() fits one line through all of them. ggplot2 warns that it removed 37 rows containing missing values: those are the days with no ozone reading, which cannot be placed on the y-axis.
Quick Quiz
1. Which part of a ggplot says that wt goes on the x-axis?
aes() maps data columns to visual properties such as position and colour.
2. How are layers joined in ggplot2?
Each layer is added with + at the end of the previous line.
3. You want bars whose heights are the values in a column. Which geom?
geom_col() uses the y values; geom_bar() counts rows.
4. What does facet_wrap(~ Species) do?
Facets split a chart into small multiples, one per group.
Learn R Programming with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
