library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
geom_dotplot(binaxis = "y", stackdir = "center", fill = "#3a86d4") +
labs(x = "Dose (mg/day)", y = "Tooth length") +
theme_minimal()
Draw, summarize, and group a ggplot2 dot plot — live in your browser
Learn to build a dot plot in R with ggplot2 — draw it with geom_dotplot(), add mean and median summaries, combine it with box and violin plots, and compare groups. Edit and run the code live, no install needed.
June 20, 2026
July 7, 2026
x and the values to y, then draw with geom_dotplot(binaxis = "y", stackdir = "center").dotsize (radius) and stackratio (spacing between stacked dots).stat_summary(), and show spread with mean_sdl as a crossbar or pointrange.scale_fill_viridis_d()); add a second variable with position_dodge() to compare groups side by side.A dot plot is the most honest way to show a small distribution: instead of summarising the data into a box, it draws one dot per observation and stacks them, so the reader sees every value and the shape they form. In R, ggplot2 draws one with a single layer — geom_dotplot() — and makes it easy to add summary statistics and combine it with box or violin plots. This lesson builds one step by step; the figures are rendered right here, and you can re-run or edit any of them live.
We use the built-in ToothGrowth dataset: the length of odontoblast cells (len) in guinea pigs given vitamin C at three doses (dose: 0.5, 1, or 2 mg/day) via two delivery methods (supp: orange juice OJ or ascorbic acid VC).
Map dose to the x-axis and len to the y-axis. Set binaxis = "y" so dots are binned along the value axis, and stackdir = "center" so each group’s dots stack symmetrically into a column. Because dose is stored as a number, wrap it in factor() so ggplot2 draws one column per group.
Two arguments tune the look of the dots. dotsize scales the dot radius (relative to the bin width), and stackratio sets the vertical spacing between stacked dots — values below 1 pack them tighter, above 1 spread them out.
The dots show the raw data; add a marker for the centre. Overlay stat_summary() with fun = mean (or fun = median) to drop a single point on each column — useful when you want the typical value at a glance.
To show spread as well as centre, summarise each group as the mean ± standard deviation. stat_summary() with fun.data = mean_sdl (and fun.args = list(mult = 1) for ±1 SD) returns the mean and its bounds; draw them as a crossbar or a pointrange.
library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
geom_dotplot(binaxis = "y", stackdir = "center", fill = "#3a86d4", alpha = 0.6) +
stat_summary(
fun.data = mean_sdl, fun.args = list(mult = 1),
geom = "crossbar", width = 0.5, colour = "grey30"
) +
labs(x = "Dose (mg/day)", y = "Tooth length") +
theme_minimal()
A dot plot pairs naturally with a box plot: the box gives the median and quartiles, the dots show every observation behind it. Draw the box first (with outlier.shape = NA so points aren’t drawn twice), then stack the dots on top. Add notch = TRUE for a rough visual test of whether the medians differ — see the box plot lesson for more.
Swap the box for a violin to show the full density shape behind the dots. Draw geom_violin() first, then the dots on top — the dots prove how many observations sit inside each curve. See the violin plot lesson for the density details.
To colour each column by its group, map the grouping variable to fill. Use a colourblind-safe palette like viridis so the figure stays readable for everyone, in colour or in print.
Map a second variable to fill and dodge the columns to compare groups within each dose. Set stackgroups = TRUE and pass position = position_dodge() so each supplement gets its own column, placed side by side.
library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len, fill = supp)) +
geom_dotplot(
binaxis = "y", stackdir = "center",
stackgroups = TRUE, position = position_dodge(0.8),
dotsize = 0.7
) +
scale_fill_viridis_d() +
labs(x = "Dose (mg/day)", y = "Tooth length", fill = "Supplement") +
theme_minimal()
The plots above were rendered at build time. Want to experiment? Edit the code and press Run — it executes in your browser via webR (no server, no install).
Working in Python? A matplotlib/seaborn dot-plot guide is coming to the Python series.
Ask Prova “is a dot plot or a box plot the better choice for this ToothGrowth data?” — it answers with code you can run, so the explanation is reproducible. The runtime is the judge. Ask Prova →
Map your group to x and your values to y, then add geom_dotplot(binaxis = "y", stackdir = "center"). Setting binaxis = "y" bins the dots along the value axis and stackdir = "center" stacks each group’s dots into a symmetric column.
Tune the two appearance arguments: dotsize scales the dot radius relative to the bin width (use a value below 1 to shrink them), and stackratio sets the vertical spacing between stacked dots (below 1 packs them tighter, above 1 spreads them out).
Overlay stat_summary(fun = mean, geom = "point") on top of geom_dotplot() to drop one point on each column; swap to fun = median for the median instead. To also show spread, use fun.data = mean_sdl with fun.args = list(mult = 1) and geom = "crossbar" (or "pointrange") for the mean ± 1 SD.
Draw the summary layer first, then stack the dots on top. For a box plot use geom_boxplot(outlier.shape = NA) (so points aren’t drawn twice) followed by geom_dotplot(); for a violin use geom_violin() then geom_dotplot().
Map a second variable to fill, then in geom_dotplot() set stackgroups = TRUE and position = position_dodge() so each group gets its own column placed side by side. Pair it with scale_fill_viridis_d() for a colourblind-safe fill.
Complete the code so each column of dots also shows its group mean as a single point.
library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
geom_dotplot(binaxis = "y", stackdir = "center", fill = "#3a86d4", alpha = 0.7) +
stat_summary(fun = mean, geom = "point", shape = 18, size = 5, colour = "red")
You built a ggplot2 dot plot, tuned the dots with dotsize and stackratio, added mean and mean ± SD summaries, combined the dots with box and violin plots, coloured the columns by group with a colourblind-safe palette, and compared groups side by side with position_dodge(). Next, explore the box plot and violin plot for compact distribution summaries, and the rest of the ggplot2 grammar.
Prefer a book? GGPlot2 Essentials is available as a downloadable PDF — every lesson in this series, offline and yours to keep.
Prove you can do it. Master the whole ggplot2 series — track your path, build projects, and earn a certificate.
Go Pro — unlimited Prova on your own data and a verifiable certificate that proves the skill.
from $15/mo billed yearly
✓ You're Pro — keep going. The runtime is the judge.
Ready to level up?
Get new R & Python lessons by email
Practical, reproducible, no spam. Unsubscribe anytime.
Double opt-in. We never share your email.
This lesson is reproducible: the figures are executed at build time (if they render, the code works), and the sandbox + quiz re-run live in your browser. The runtime is the judge.
@online{2026,
author = {},
title = {Dot {Plot} in {R:} {A} Ggplot2 {Guide}},
date = {2026-06-20},
url = {https://www.datanovia.com/learn/data-visualization/ggplot2/dot-plot},
langid = {en}
}