Skip to contents

Everything here is a real ggplot2 layer, so it composes with aes(), stats, scales, facets, and coords — and renders on any device. Every example sets a seed so the wobble is reproducible.

Every plot uses a handwriting font throughout via options(ggsketch.base_family = "auto") in setup; without it, theme_sketch() keeps the device default font and only the labels that geoms draw themselves (geom_sketch_text(), geom_sketch_bracket()) are hand-drawn.

Bars and columns

geom_sketch_col() draws a roughened outline with a hachure (pencil-shading) fill.

sales <- data.frame(product = c("Alpha", "Bravo", "Charlie", "Delta", "Echo"),
                    units   = c(34, 51, 22, 47, 39))

ggplot(sales, aes(product, units)) +
  geom_sketch_col(fill = "#7BAFD4", seed = 1L) +
  labs(title = "Units sold", x = NULL) +
  theme_sketch()

Map fill to a variable like any ggplot2 bar. Each bar gets its own seed offset, so no two bars wobble identically.

ggplot(sales, aes(product, units, fill = product)) +
  geom_sketch_col(seed = 2L, show.legend = FALSE) +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "Mapped fill", x = NULL) +
  theme_sketch()

geom_sketch_bar() counts rows for you (like geom_bar()):

ggplot(mpg, aes(class)) +
  geom_sketch_bar(fill = "#C39BD3", seed = 3L) +
  labs(title = "Vehicle count by class", x = NULL) +
  theme_sketch() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

Bars flip and stack just like the originals:

ggplot(sales, aes(reorder(product, units), units)) +
  geom_sketch_col(fill = "#F1948A", seed = 1L) +
  coord_flip() +
  labs(title = "Horizontal bars", x = NULL) +
  theme_sketch()

Histograms and frequency polygons

geom_sketch_histogram() bins a continuous variable and draws hand-drawn bars; geom_sketch_freqpoly() draws the same counts as a roughened line.

ggplot(faithful, aes(eruptions)) +
  geom_sketch_histogram(fill = "#7BAFD4", bins = 20, seed = 1L) +
  labs(title = "Old Faithful eruption times") +
  theme_sketch()

ggplot(mpg, aes(hwy, colour = drv)) +
  geom_sketch_freqpoly(bins = 15, linewidth = 0.9, seed = 2L) +
  labs(title = "Highway mpg by drivetrain", x = "hwy") +
  theme_sketch()

Lines, paths, and points

econ <- economics[economics$date > as.Date("2000-01-01"), ]
ggplot(econ, aes(date, unemploy)) +
  geom_sketch_line(colour = "steelblue", linewidth = 0.8, seed = 1L) +
  labs(title = "US unemployment", x = NULL, y = "thousands") +
  theme_sketch()

geom_sketch_point() draws each point as a small roughened ellipse:

ggplot(mtcars, aes(wt, mpg, colour = factor(cyl))) +
  geom_sketch_point(size = 4, seed = 1L) +
  scale_colour_brewer("cylinders", palette = "Dark2") +
  labs(title = "Fuel economy vs weight") +
  theme_sketch()

Lines and points compose like any layers:

df <- data.frame(x = 1:12, y = c(3, 5, 4, 7, 6, 9, 8, 11, 9, 12, 11, 14))
ggplot(df, aes(x, y)) +
  geom_sketch_line(colour = "grey40", seed = 3L) +
  geom_sketch_point(size = 4, colour = "firebrick", seed = 8L) +
  labs(title = "Trend with markers") +
  theme_sketch()

Multiple groups, one seed per group:

ggplot(ggplot2::economics_long, aes(date, value01, colour = variable)) +
  geom_sketch_line(seed = 5L) +
  labs(title = "Five series, hand-drawn", x = NULL, y = NULL) +
  theme_sketch()

Point sizes

size behaves like any ggplot2 point size. Set it to a constant for bigger or smaller markers:

sz <- data.frame(x = 1:5, y = 1, s = c(2, 4, 6, 9, 13))
ggplot(sz, aes(x, y)) +
  geom_sketch_point(size = sz$s, colour = "#2E86C1", seed = 1L) +
  labs(title = "Fixed point sizes (2 → 13)", x = NULL, y = NULL) +
  theme_sketch()

Or map size to a variable for a bubble chart — pair it with scale_size_area() so the area (not radius) encodes the value:

ggplot(mtcars, aes(wt, mpg, size = hp, colour = factor(cyl))) +
  geom_sketch_point(alpha = 0.9, seed = 2L) +
  scale_size_area("horsepower", max_size = 14) +
  scale_colour_brewer("cylinders", palette = "Dark2") +
  labs(title = "Bubble chart: size = horsepower") +
  theme_sketch()

A small-multiples sweep of a single size aesthetic:

grid <- expand.grid(x = 1:6, y = 1:3)
grid$s <- seq(1.5, 11, length.out = nrow(grid))
ggplot(grid, aes(x, y, size = s)) +
  geom_sketch_point(colour = "#884EA0", show.legend = FALSE, seed = 3L) +
  scale_size_identity() +
  labs(title = "Increasing point size", x = NULL, y = NULL) +
  theme_sketch()

Point roughness

For geom_sketch_point(), roughness is a mappable aesthetic. As a constant it sets how wobbly every marker is — from clean circles up to very shaky:

rg <- data.frame(x = 1:4, y = 1, r = c(0, 0.4, 0.9, 1.6))
ggplot(rg, aes(x, y)) +
  geom_sketch_point(aes(roughness = I(r)), size = 14, colour = "#2E86C1",
                    seed = 1L) +
  geom_sketch_text(aes(label = r), nudge_y = -0.5, size = 6) +
  labs(title = "roughness 0 → 1.6 (constant per point)",
       x = NULL, y = NULL) +
  ylim(0.3, 1.3) +
  theme_sketch() +
  theme(axis.text = element_blank())

Map it to a variable and the values are rescaled to a legible band by scale_roughness_continuous() (applied automatically, default c(0.01, 0.75)), so points can encode a third variable through how shaky they look:

ggplot(mtcars, aes(wt, mpg, roughness = hp, colour = factor(cyl))) +
  geom_sketch_point(size = 4, seed = 1L) +
  scale_colour_brewer("cylinders", palette = "Dark2") +
  labs(title = "roughness mapped to horsepower") +
  theme_sketch()

Use I() to pass raw roughness through unscaled, or scale_roughness_continuous(range = ...) to widen the band.

Jitter and count

geom_sketch_jitter() spreads overplotted points; geom_sketch_count() sizes a single point by how many observations sit there.

ggplot(mpg, aes(class, hwy)) +
  geom_sketch_jitter(width = 0.2, height = 0, colour = "#5D6D7E",
                     size = 2, seed = 1L) +
  labs(title = "Jittered highway mpg", x = NULL) +
  theme_sketch() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

ggplot(mpg, aes(cty, hwy)) +
  geom_sketch_count(colour = "#C0392B", seed = 2L) +
  scale_size_area(max_size = 8) +
  labs(title = "Overplot count") +
  theme_sketch()

Rectangles and tiles

rects <- data.frame(xmin = c(1, 3, 5), xmax = c(2, 4, 6),
                    ymin = 0, ymax = c(2, 4, 3))
ggplot(rects) +
  geom_sketch_rect(aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax,
                       fill = factor(xmin)),
                   seed = 1L, show.legend = FALSE) +
  labs(title = "geom_sketch_rect()") +
  theme_sketch()

A sketchy heatmap with geom_sketch_tile():

td <- expand.grid(x = 1:8, y = 1:6)
td$z <- td$x + td$y
ggplot(td, aes(x, y, fill = z)) +
  geom_sketch_tile(seed = 2L, hachure_gap = 0.18) +
  scale_fill_viridis_c() +
  labs(title = "geom_sketch_tile()") +
  theme_sketch()

A sketchy 2-D bin heatmap with geom_sketch_bin2d() (cells default to a hachure fill, shaded by count):

ggplot(faithful, aes(eruptions, waiting)) +
  geom_sketch_bin2d(bins = 12, seed = 3L) +
  scale_fill_viridis_c() +
  labs(title = "geom_sketch_bin2d()") +
  theme_sketch()

geom_sketch_hex() bins into hexagons instead (needs the hexbin package):

ggplot(faithful, aes(eruptions, waiting)) +
  geom_sketch_hex(bins = 12, seed = 4L) +
  scale_fill_viridis_c(option = "magma") +
  labs(title = "geom_sketch_hex()") +
  theme_sketch()

Polygons, ribbons, areas, and densities

Concave polygons fill correctly (the hachure respects every notch):

ang  <- seq(0, 2 * pi, length.out = 11)[-11]
r    <- rep(c(1, 0.45), length.out = 10)
star <- data.frame(x = r * cos(ang), y = r * sin(ang))
ggplot(star, aes(x, y)) +
  geom_sketch_polygon(fill = "tomato", seed = 1L) +
  coord_equal() +
  labs(title = "A concave star") +
  theme_sketch()

band <- data.frame(x = 1:20)
band$y  <- 10 + 5 * sin(seq(0, 3 * pi, length.out = 20))
band$lo <- band$y - 2
band$hi <- band$y + 2

ggplot(band, aes(x)) +
  geom_sketch_ribbon(aes(ymin = lo, ymax = hi), fill = "plum", seed = 2L) +
  geom_sketch_line(aes(y = y), seed = 3L) +
  labs(title = "Ribbon + line") +
  theme_sketch()

ggplot(band, aes(x, y)) +
  geom_sketch_area(fill = "lightgreen", seed = 3L) +
  labs(title = "geom_sketch_area()") +
  theme_sketch()

ggplot(faithful, aes(eruptions)) +
  geom_sketch_density(fill = "khaki", seed = 4L) +
  labs(title = "Old Faithful eruptions") +
  theme_sketch()

Violins

geom_sketch_violin() mirrors a kernel density into a closed polygon and hachure-fills it.

ggplot(mpg, aes(class, hwy, fill = class)) +
  geom_sketch_violin(seed = 1L, show.legend = FALSE) +
  scale_fill_brewer(palette = "Set3") +
  labs(title = "Highway mpg distribution by class", x = NULL) +
  theme_sketch() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

Smooths

A hand-drawn fit with a roughened confidence band:

ggplot(mtcars, aes(wt, mpg)) +
  geom_sketch_point(seed = 1L) +
  geom_sketch_smooth(method = "lm", formula = y ~ x, seed = 2L) +
  labs(title = "Linear fit with CI band") +
  theme_sketch()

ggplot(mpg, aes(displ, hwy)) +
  geom_sketch_point(colour = "grey50", seed = 1L) +
  geom_sketch_smooth(seed = 2L, colour = "darkorange") +
  labs(title = "loess fit") +
  theme_sketch()

Function curves

geom_sketch_function() sketches an analytic curve over the x range, for example to overlay a theoretical density.

ggplot(data.frame(x = c(-4, 4)), aes(x)) +
  geom_sketch_function(fun = dnorm, colour = "#2E86C1", linewidth = 0.9,
                       seed = 1L) +
  geom_sketch_function(fun = dnorm, args = list(sd = 1.6),
                       colour = "#C0392B", linewidth = 0.9, seed = 2L) +
  labs(title = "Two normal densities", y = "density") +
  theme_sketch()

Q-Q plots

geom_sketch_qq() draws the quantile-quantile points and geom_sketch_qq_line() the reference line. Map data to the sample aesthetic.

ggplot(mtcars, aes(sample = mpg)) +
  geom_sketch_qq(size = 2.5, seed = 1L) +
  geom_sketch_qq_line(colour = "#C8553D", linewidth = 0.8, seed = 2L) +
  labs(title = "Normal Q-Q plot of mpg", x = "theoretical", y = "sample") +
  theme_sketch()

Quantile regression

geom_sketch_quantile() fits and draws quantile regression lines (requires the optional quantreg package).

ggplot(mpg, aes(displ, hwy)) +
  geom_sketch_point(colour = "grey60", seed = 1L) +
  geom_sketch_quantile(quantiles = c(0.1, 0.5, 0.9), colour = "#6C3483",
                       linewidth = 0.9, seed = 2L) +
  labs(title = "10th / 50th / 90th percentile fits") +
  theme_sketch()

Circles and ellipses

Radii are in data units, so use coord_equal() for true circles:

cdf <- data.frame(x = c(1, 3, 2), y = c(1, 1, 2.5),
                  r = c(0.6, 0.9, 0.5), grp = c("a", "b", "c"))
ggplot(cdf, aes(x, y, r = r, fill = grp)) +
  geom_sketch_circle(seed = 1L, show.legend = FALSE) +
  coord_equal() +
  labs(title = "geom_sketch_circle()") +
  theme_sketch()

edf <- data.frame(x = c(1, 3), y = c(1, 2), a = c(1.4, 0.8), b = c(0.6, 1.2))
ggplot(edf, aes(x, y, a = a, b = b, fill = factor(x))) +
  geom_sketch_ellipse(seed = 2L, show.legend = FALSE) +
  coord_equal() +
  labs(title = "geom_sketch_ellipse()") +
  theme_sketch()

Segments and steps

sdf <- data.frame(x = 1:4, y = c(1, 3, 2, 4),
                  xend = 2:5, yend = c(3, 1, 4, 2))
ggplot(sdf) +
  geom_sketch_segment(aes(x = x, y = y, xend = xend, yend = yend),
                      colour = "darkgreen", linewidth = 1, seed = 3L) +
  labs(title = "geom_sketch_segment()") +
  theme_sketch()

stp <- data.frame(x = 1:8, y = c(1, 3, 2, 5, 4, 6, 5, 8))
ggplot(stp, aes(x, y)) +
  geom_sketch_step(colour = "purple", linewidth = 1, seed = 4L) +
  geom_sketch_point(seed = 5L) +
  labs(title = "geom_sketch_step()") +
  theme_sketch()

Curves and spokes

geom_sketch_curve() is a curved connector (a quadratic Bézier); curvature sets how much it bends.

cdf <- data.frame(x = c(1, 1, 1), y = c(1, 2, 3),
                  xend = c(4, 4, 4), yend = c(1, 2, 3))
ggplot(cdf, aes(x, y)) +
  geom_sketch_curve(aes(xend = xend, yend = yend), curvature = 0.4,
                    colour = "#1A5276", linewidth = 0.9, seed = 1L) +
  geom_sketch_point(seed = 2L) +
  geom_sketch_point(aes(x = xend, y = yend), seed = 3L) +
  labs(title = "geom_sketch_curve()") +
  theme_sketch()

geom_sketch_spoke() draws a segment from each point by angle and radius — useful for vector fields.

field <- expand.grid(x = 1:6, y = 1:6)
field$angle  <- with(field, atan2(y - 3.5, x - 3.5))
field$radius <- 0.6
ggplot(field, aes(x, y)) +
  geom_sketch_spoke(aes(angle = angle, radius = radius),
                    colour = "#117A65", seed = 1L) +
  geom_sketch_point(size = 1.5, seed = 2L) +
  coord_equal() +
  labs(title = "geom_sketch_spoke()") +
  theme_sketch()

Rugs

geom_sketch_rug() adds marginal ticks along the panel edges (sides).

ggplot(mtcars, aes(wt, mpg)) +
  geom_sketch_point(colour = "#2C3E50", seed = 1L) +
  geom_sketch_rug(sides = "bl", colour = "#7B241C", seed = 2L) +
  labs(title = "Scatter with marginal rug") +
  theme_sketch()

Intervals and uncertainty

The interval family draws hand-drawn ranges: geom_sketch_linerange(), geom_sketch_pointrange(), geom_sketch_errorbar(), and geom_sketch_crossbar().

est <- data.frame(
  group = c("A", "B", "C", "D"),
  mean  = c(4.1, 5.6, 3.2, 6.0),
  lo    = c(3.2, 4.9, 2.4, 5.1),
  hi    = c(5.0, 6.4, 4.1, 6.8)
)
ggplot(est, aes(group, mean)) +
  geom_sketch_pointrange(aes(ymin = lo, ymax = hi), colour = "#1F618D",
                         seed = 1L) +
  labs(title = "Point estimates with 95% intervals", x = NULL) +
  theme_sketch()

ggplot(est, aes(group, mean)) +
  geom_sketch_col(fill = "#AED6F1", width = 0.6, seed = 1L) +
  geom_sketch_errorbar(aes(ymin = lo, ymax = hi), width = 0.3, seed = 2L) +
  labs(title = "Bars with error bars", x = NULL) +
  theme_sketch()

ggplot(est, aes(group, mean)) +
  geom_sketch_crossbar(aes(ymin = lo, ymax = hi), fill = "#FCF3CF",
                       fill_style = "hachure", seed = 3L) +
  labs(title = "geom_sketch_crossbar()", x = NULL) +
  theme_sketch()

Reference lines

geom_sketch_abline(), geom_sketch_hline(), and geom_sketch_vline() span the panel with a gentle wobble.

ggplot(mtcars, aes(wt, mpg)) +
  geom_sketch_point(seed = 1L) +
  geom_sketch_hline(yintercept = 20, colour = "#C0392B", seed = 2L) +
  geom_sketch_vline(xintercept = 3.3, colour = "#2471A3", seed = 3L) +
  geom_sketch_abline(slope = -5, intercept = 37, colour = "#117864",
                     linetype = 2, seed = 4L) +
  labs(title = "Reference lines") +
  theme_sketch()

Contours and 2-D density

geom_sketch_contour() draws contour lines of a surface (needs z); geom_sketch_density2d() contours a 2-D kernel density estimate.

ggplot(faithfuld, aes(waiting, eruptions, z = density)) +
  geom_sketch_contour(colour = "#2E4053", seed = 1L) +
  labs(title = "geom_sketch_contour()") +
  theme_sketch()

ggplot(faithful, aes(eruptions, waiting)) +
  geom_sketch_point(colour = "grey70", seed = 1L) +
  geom_sketch_density2d(colour = "#884EA0", linewidth = 0.7, seed = 2L) +
  labs(title = "geom_sketch_density2d()") +
  theme_sketch()

Text

The sketch of text is a handwriting font, not roughened glyphs. geom_sketch_text() uses the first installed handwriting face (and falls back to the device default otherwise).

lab <- data.frame(x = c(2, 4, 3), y = c(3, 4, 1.5),
                  txt = c("hand", "drawn", "labels"))
ggplot(lab, aes(x, y, label = txt)) +
  geom_sketch_point(size = 3, colour = "#C0392B") +
  geom_sketch_text(size = 7, nudge_y = 0.4) +
  labs(title = "geom_sketch_text()") +
  theme_sketch()

Boxplots

A composed geom: rough IQR box, thick median, whiskers, and sketchy outliers.

ggplot(mpg, aes(class, hwy)) +
  geom_sketch_boxplot(seed = 1L) +
  labs(title = "Highway mpg by class", x = NULL) +
  theme_sketch() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

By default the box is outline-only (its fill is NA). Give it a fill for a solid box, or map fill and switch on a fill style for coloured, shaded boxes:

ggplot(mpg, aes(class, hwy, fill = class)) +
  geom_sketch_boxplot(fill_style = "hachure", seed = 1L, show.legend = FALSE) +
  scale_fill_brewer(palette = "Pastel1") +
  labs(title = "Hachure-filled boxes", x = NULL) +
  theme_sketch() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

Annotations

annotate_sketch() adds one-off hand-drawn marks (no aes() inheritance):

ggplot(mtcars, aes(wt, mpg)) +
  geom_sketch_point(seed = 1L) +
  annotate_sketch("rect", xmin = 3, xmax = 4, ymin = 15, ymax = 22,
                  fill = NA, colour = "red", seed = 2L) +
  annotate_sketch("segment", x = 2, y = 32, xend = 3.4, yend = 21,
                  colour = "blue", linewidth = 1, seed = 3L) +
  annotate_sketch("circle", x = 5, y = 30, r = 0.4,
                  colour = "darkgreen", fill = "green", seed = 4L) +
  labs(title = "Highlighting with annotate_sketch()") +
  theme_sketch()

Significance brackets

geom_sketch_bracket() draws a hand-drawn comparison bracket with an optional handwriting label, for marking pairwise comparisons (a sketchy ggsignif).

brackets <- data.frame(
  xmin  = c(1, 2),
  xmax  = c(2, 3),
  y     = c(40, 45),
  label = c("p = 0.03", "n.s.")
)
ggplot(mpg, aes(drv, hwy)) +
  geom_sketch_boxplot(seed = 1L) +
  geom_sketch_bracket(
    data = brackets,
    aes(xmin = xmin, xmax = xmax, y = y, label = label),
    seed = 2L
  ) +
  labs(title = "Pairwise comparisons", x = "drivetrain") +
  theme_sketch()

Composition: facets, scales, coords

Sketch geoms respect the full grammar.

ggplot(mpg, aes(displ, hwy)) +
  geom_sketch_point(size = 2.5, colour = "#34495E", seed = 9L) +
  geom_sketch_smooth(method = "lm", formula = y ~ x, seed = 10L) +
  facet_wrap(~drv, labeller = label_both) +
  labs(title = "Faceted by drivetrain") +
  theme_sketch()

Dark mode

Every example above works with theme_sketch(dark = TRUE):

ggplot(sales, aes(product, units, fill = product)) +
  geom_sketch_col(seed = 1L, show.legend = FALSE) +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "Dark preset", x = NULL) +
  theme_sketch(dark = TRUE)

A hand-drawn frame

By default theme_sketch() keeps the gridlines, panel border, and axis ticks crisp. Pass rough_frame = TRUE and the frame is roughened too, so it matches the marks.

ggplot(sales, aes(product, units)) +
  geom_sketch_col(fill = "#7BAFD4", seed = 1L) +
  labs(title = "Everything wobbles", x = NULL) +
  theme_sketch(rough_frame = TRUE, seed = 1L)

The roughened elements are real theme elements — element_sketch_line() and element_sketch_rect() — so you can also drop them into any theme yourself and tune their roughness, bowing, and seed:

ggplot(mtcars, aes(wt, mpg)) +
  geom_sketch_point(seed = 1L) +
  theme_sketch() +
  theme(
    panel.grid.major = element_sketch_line(roughness = 0.8, seed = 7L),
    axis.ticks       = element_sketch_line(roughness = 0.6, seed = 8L)
  )

A matching palette

scale_colour_sketch() / scale_fill_sketch() use a qualitative palette (sketch_palette()) chosen to suit the hand-drawn look:

ggplot(mpg, aes(displ, hwy, colour = drv)) +
  geom_sketch_point(size = 2.5, seed = 1L) +
  scale_colour_sketch() +
  labs(title = "scale_colour_sketch()") +
  theme_sketch(rough_frame = TRUE, seed = 2L)

For continuous data the *_sketch_c() variants give an ink-on-paper gradient:

ggplot(faithful, aes(eruptions, waiting, colour = waiting)) +
  geom_sketch_point(size = 2.5, seed = 1L) +
  scale_colour_sketch_c() +
  labs(title = "scale_colour_sketch_c()") +
  theme_sketch()

The scribble fill

"scribble" is one continuous winding stroke that overshoots the boundary, like scribbling to fill a shape:

ggplot(sales, aes(product, units, fill = product)) +
  geom_sketch_col(fill_style = "scribble", seed = 3L, show.legend = FALSE) +
  scale_fill_sketch() +
  labs(title = "fill_style = \"scribble\"", x = NULL) +
  theme_sketch()

It works anywhere a fill_style is accepted. The eight styles:

styles <- c("hachure", "cross_hatch", "zigzag", "zigzag_line",
            "scribble", "dots", "dashed", "solid")
grid <- expand.grid(col = 1:4, row = 1:2)
grid$style <- styles
ggplot(grid) +
  lapply(seq_len(nrow(grid)), function(i) {
    geom_sketch_rect(
      data = grid[i, ],
      aes(xmin = col - 0.45, xmax = col + 0.45,
          ymin = row - 0.4,  ymax = row + 0.4),
      fill = "#7BAFD4", fill_style = grid$style[i], seed = i
    )
  }) +
  geom_sketch_text(aes(col, row - 0.55, label = style), size = 4) +
  coord_equal() +
  labs(title = "The eight fill styles", x = NULL, y = NULL) +
  theme_sketch() +
  theme(axis.text = element_blank())

Reproducible handwriting fonts

geom_sketch_text() picks up a handwriting face preinstalled on your OS, but for results that reproduce on any machine or CI runner, register a font file explicitly with register_sketch_font() and a font-aware device (ragg, svglite, cairo):

register_sketch_font("Caveat", "path/to/Caveat-Regular.ttf")

ggplot(lab, aes(x, y, label = txt)) +
  geom_sketch_text(family = "Caveat", size = 8) +
  theme_sketch()