Dot Plot in R: A ggplot2 Guide

Draw, summarize, and group a ggplot2 dot plot — live in your browser

Data Visualization

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.

Published

June 20, 2026

Modified

July 7, 2026

TipKey takeaways
  • A dot plot stacks one dot per observation, so you see the raw data and its distribution shape in one compact column.
  • In ggplot2 you map a group to x and the values to y, then draw with geom_dotplot(binaxis = "y", stackdir = "center").
  • Tune the dots with dotsize (radius) and stackratio (spacing between stacked dots).
  • Add a measure of centre — mean or median — with stat_summary(), and show spread with mean_sdl as a crossbar or pointrange.
  • Layer the dots over a box plot or violin plot to combine raw points with a distribution summary.
  • Fill by one grouping variable with a colourblind-safe palette (scale_fill_viridis_d()); add a second variable with position_dodge() to compare groups side by side.
  • Every plot below is rendered for you — then tweak any example live in the sandbox.
Get the book — GGPlot2 Essentials (PDF)

Introduction

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).

Build a basic dot plot

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.

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()

ggplot2 dot plot of tooth length (len) by vitamin C dose; each dose is a centred column of stacked dots that climbs with dose.

Control dot appearance: dotsize and stackratio

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.

library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
  geom_dotplot(
    binaxis = "y", stackdir = "center",
    dotsize = 0.8, stackratio = 0.9,
    fill = "#3a86d4"
  ) +
  labs(x = "Dose (mg/day)", y = "Tooth length") +
  theme_minimal()

ggplot2 dot plot of tooth length by dose with larger, tightly packed dots set via dotsize and stackratio.

Add mean or median points

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.

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") +
  labs(x = "Dose (mg/day)", y = "Tooth length") +
  theme_minimal()

ggplot2 dot plot of tooth length by dose with a red point marking the mean of each group.

Add mean ± SD as a crossbar or pointrange

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()

ggplot2 dot plot of tooth length by dose with a crossbar showing the mean plus or minus one standard deviation for each group.

Combine with a box plot

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.

library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
  geom_boxplot(notch = TRUE, outlier.shape = NA, fill = "white") +
  geom_dotplot(binaxis = "y", stackdir = "center", dotsize = 0.7, fill = "#3a86d4") +
  labs(x = "Dose (mg/day)", y = "Tooth length") +
  theme_minimal()

ggplot2 notched box plot of tooth length by dose with the individual observations stacked as dots on top of each box.

Combine with a violin plot

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.

library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len)) +
  geom_violin(trim = FALSE, fill = "grey90", colour = "grey60") +
  geom_dotplot(binaxis = "y", stackdir = "center", dotsize = 0.7, fill = "#3a86d4") +
  labs(x = "Dose (mg/day)", y = "Tooth length") +
  theme_minimal()

ggplot2 violin plot of tooth length by dose with individual observations stacked as dots inside each violin.

Fill by one grouping variable

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.

library(ggplot2)
ggplot(ToothGrowth, aes(x = factor(dose), y = len, fill = factor(dose))) +
  geom_dotplot(binaxis = "y", stackdir = "center") +
  scale_fill_viridis_d() +
  labs(x = "Dose (mg/day)", y = "Tooth length", fill = "Dose") +
  theme_minimal()

ggplot2 dot plot of tooth length by dose, each column of dots filled with a colourblind-safe viridis colour.

Compare multiple groups side by side

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()

ggplot2 grouped dot plot of tooth length by dose, with orange-juice and ascorbic-acid columns of dots side by side at each dose.

Try it live

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.

🟢 With an AI agent

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 →

Frequently asked questions

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.

Test your understanding

Complete the code so each column of dots also shows its group mean as a single point.

# Use stat_summary() with fun = mean and geom = "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")
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")

Conclusion

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.

Reuse

Citation

BibTeX citation:
@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}
}
For attribution, please cite this work as:
“Dot Plot in R: A Ggplot2 Guide.” 2026. June 20. https://www.datanovia.com/learn/data-visualization/ggplot2/dot-plot.