Sorted dot plot
numeric vs categorical dot plot
Median highway economy per class as a Cleveland dot plot: dots over bars for point estimates, categories sorted by value, and no legend. Built with summarise and geom-point.
Compare magnitudes across categories, or rank them.
Reach for these charts when the question is “which is bigger?”: one value per category, or one value per category within groups. Bars remain the most accurate encoding for magnitude, and dot plots do the same job with less ink when you only show a point estimate. Sort categories by value rather than alphabetically, flip to horizontal bars when labels are long, and label bars directly instead of forcing readers through a legend. Every example ships its full source: click View source to copy it.
numeric vs categorical dot plot
Median highway economy per class as a Cleveland dot plot: dots over bars for point estimates, categories sorted by value, and no legend. Built with summarise and geom-point.
one categorical horizontal bar chart
Counts per category flipped horizontal via coord-flip with the value printed at each bar end by geom-text, so no value axis guesswork remains.
three or more variables grouped bar chart
City and highway means per class reshaped with pivot-longer and dodged via position-dodge; classes sorted by value and coloured with the Okabe-Ito palette.
one categorical bar chart
Discrete x counted with geom-bar.
one categorical bar chart
geom-bar backed by stat-count to tally observations per category.
two categoricals grouped bar chart
Side-by-side bars per group via position-dodge on geom-col.
two categoricals grouped bar chart
Per-row width column lets position-dodge give one product a wider slot than another.
numeric vs categorical horizontal bar chart
coord-flip swaps x and y so vertical bars read horizontally.
// Showcase: sorted Cleveland dot plot of median highway economy per class.
// Dots over bars for point estimates; categories ordered by value, no legend.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let accent = okabe-ito.at(5)
#let by-class = summarise(
mpg,
hwy-med: rows => median(rows.map(row => row.hwy)).y,
by: "class",
).sorted(key: row => row.hwy-med)
#plot(
data: by-class,
mapping: aes(x: "hwy-med", y: "class"),
layers: (
geom-point(size: 4pt, fill: accent),
),
scales: scales(
x: scale-continuous(limits: (10, 35)),
y: scale-discrete(limits: by-class.map(row => row.class)),
),
labels: labels(
title: "Pickups and SUVs trail every car class on fuel economy",
subtitle: "Median highway miles per gallon by vehicle class",
x: "Median highway mpg",
y: none,
caption: "Source: bundled mpg dataset.",
),
theme: theme-minimal(),
width: 12cm,
height: 7cm,
)// Showcase: horizontal count bars with direct value labels; long category
// names stay readable and the labels replace a value axis.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let accent = okabe-ito.at(2)
#let species-counts = count(penguins, "species", sort: true)
#plot(
data: species-counts,
mapping: aes(x: "species", y: "n"),
layers: (
geom-col(fill: accent),
geom-text(
mapping: aes(label: "n"),
nudge-x: 0.25cm,
size: 9pt,
),
),
scales: scales(
x: scale-discrete(limits: species-counts.map(row => row.species)),
),
coord: coord-flip(),
labels: labels(
title: "Adelie penguins dominate the sample",
subtitle: "Penguins measured per species, 2007-2009",
x: none,
y: "Penguins measured",
caption: "Source: bundled Palmer penguins dataset.",
),
theme: theme-minimal(),
width: 12cm,
height: 6cm,
)// Showcase: dodged bars comparing city and highway economy per class; two
// dodge groups keep the within-class comparison easy, Okabe-Ito keeps it safe.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let long = pivot-longer(
mpg,
("cty", "hwy"),
names-to: "metric",
values-to: "mpg",
)
#let by-class = summarise(
long,
mean-mpg: rows => rows.map(row => row.mpg).sum() / rows.len(),
by: ("class", "metric"),
)
#let class-order = (
summarise(
mpg,
mean-hwy: rows => rows.map(row => row.hwy).sum() / rows.len(),
by: "class",
)
.sorted(key: row => row.mean-hwy)
.map(row => row.class)
)
#let metric-names = (cty: "City", hwy: "Highway")
#plot(
data: by-class.map(row => (..row, metric: metric-names.at(row.metric))),
mapping: aes(x: "class", y: "mean-mpg", fill: "metric"),
layers: (geom-col(position: "dodge"),),
scales: scales(
x: scale-discrete(limits: class-order),
fill: scale-okabe-ito(),
),
labels: labels(
title: "Highway economy beats city economy in every class",
subtitle: "Mean miles per gallon by vehicle class and driving condition",
x: none,
y: "Mean mpg",
fill: "Condition",
caption: "Source: bundled mpg dataset.",
),
theme: theme-minimal(),
width: 13cm,
height: 7.5cm,
)// Simple bar chart with discrete x.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let fruits = (
(fruit: "apple", count: 12),
(fruit: "banana", count: 19),
(fruit: "cherry", count: 7),
(fruit: "date", count: 15),
)
#plot(
data: fruits,
mapping: aes(x: "fruit", y: "count", fill: "fruit"),
layers: (geom-col(),),
guides: guides(fill: none),
labels: labels(title: "Counts per Fruit", x: "Fruit", y: "Count"),
theme: theme-grey(),
width: 12cm,
height: 9cm,
)// geom-bar: counts observations per category.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let items = (
(cat: "A"),
(cat: "A"),
(cat: "A"),
(cat: "B"),
(cat: "B"),
(cat: "C"),
(cat: "C"),
(cat: "C"),
(cat: "C"),
(cat: "D"),
(cat: "D"),
(cat: "D"),
(cat: "D"),
(cat: "D"),
)
#plot(
data: items,
mapping: aes(x: "cat", fill: "cat"),
layers: (geom-bar(),),
scales: scales(y: scale-continuous(expand: (0%, 20%))),
guides: guides(fill: none),
labels: labels(
title: "Category Counts via Stat-Count",
x: "Category",
y: "Count",
),
theme: theme-minimal(axis-ticks: element-tick(length: 0.12cm)),
width: 12cm,
height: 9cm,
)// Dodged bars: products shown side-by-side per quarter.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let sales = (
(q: "Q1", product: "A", revenue: 10),
(q: "Q1", product: "B", revenue: 20),
(q: "Q1", product: "C", revenue: 15),
(q: "Q2", product: "A", revenue: 12),
(q: "Q2", product: "B", revenue: 18),
(q: "Q2", product: "C", revenue: 22),
(q: "Q3", product: "A", revenue: 8),
(q: "Q3", product: "B", revenue: 25),
(q: "Q3", product: "C", revenue: 30),
)
#plot(
data: sales,
mapping: aes(x: "q", y: "revenue", fill: "product"),
layers: (geom-col(position: "dodge"),),
scales: scales(
x: scale-discrete(expand: false),
y: scale-continuous(
expand: (0%, 10%),
labels: format-currency(symbol: "$", digits: 0)
)
),
labels: labels(
title: "Revenue by Quarter, Dodged",
subtitle: "Side-by-side bars compare products within each quarter",
x: "Quarter",
y: "Revenue (M)",
fill: "Product",
),
theme: theme-minimal(),
width: 12cm,
height: 9cm,
)// Mixed-width dodge: per-row `width` column makes one product wider than the others.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let sales = (
(q: "Q1", product: "A", revenue: 10, width: 0.6),
(q: "Q1", product: "B", revenue: 20, width: 0.4),
(q: "Q2", product: "A", revenue: 12, width: 0.6),
(q: "Q2", product: "B", revenue: 18, width: 0.4),
(q: "Q3", product: "A", revenue: 8, width: 0.6),
(q: "Q3", product: "B", revenue: 25, width: 0.4),
)
#plot(
data: sales,
mapping: aes(x: "q", y: "revenue", fill: "product"),
layers: (geom-col(position: "dodge"),),
scales: scales(y: scale-continuous(labels: format-currency(symbol: "$", digits: 0))),
labels: labels(
title: "Revenue with Mixed-Width Dodge Slots",
subtitle: "Each row supplies its own dodge slot width via the width column",
x: "Quarter",
y: "Revenue (M)",
fill: "Product",
),
theme: theme-minimal(),
width: 12cm,
height: 9cm,
)// coord-flip swaps x and y so vertical bars read as horizontal.
#import "@preview/gribouille:0.7.0": *
#set page(width: auto, height: auto, margin: 0cm)
#let revenue = (
(q: "Q1", revenue: 10),
(q: "Q2", revenue: 18),
(q: "Q3", revenue: 25),
(q: "Q4", revenue: 22),
)
#grid(
columns: 1,
row-gutter: 0.5cm,
plot(
data: revenue,
mapping: aes(x: "q", y: "revenue", fill: "q"),
layers: (geom-col(),),
guides: guides(fill: none),
scales: scales(y: scale-continuous(labels: format-currency(symbol: "$", digits: 0))),
labels: labels(
title: "Default Cartesian",
x: "Quarter",
y: "Revenue (M)",
),
theme: theme-minimal(),
width: 12cm,
height: 9cm,
),
plot(
data: revenue,
mapping: aes(x: "q", y: "revenue", fill: "q"),
layers: (geom-col(),),
coord: coord-flip(),
guides: guides(fill: none),
scales: scales(y: scale-continuous(labels: format-currency(symbol: "$", digits: 0))),
labels: labels(title: "coord-flip()", x: "Quarter", y: "Revenue (M)"),
theme: theme-minimal(),
width: 12cm,
height: 9cm,
),
)