Skip to contents

A vellumplot plot is a stack of marks. Each mark_*() appends one drawing layer to the spec, and every mark reads the same grammar: bare column names (or expressions) captured with tidy evaluation become encodings, while scalar values become constant aesthetics. color = hp maps the hp column through a scale; color = "red" paints every element red.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = hp, size = 3)
A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis), where colour shows hp. Based on 32 observations.152025302345mpgwthp100150200250300

You can stack as many marks as you like on a panel, and scales train across all of them at once.

Points, lines, and bars

mark_point() draws markers; mark_line() and mark_step() connect points in x order; mark_rule() draws reference lines. mark_point() also takes shape (one of "circle", "square", "triangle", "diamond", "plus", "cross") and a position = "jitter" adjustment.

vplot(pressure) |>
  mark_line(x = temperature, y = pressure) |>
  mark_point(x = temperature, y = pressure)
A plot combining line chart and scatter plot. It plots pressure (vertical axis) against temperature (horizontal axis). Based on 19 observations.2004006008000100200300pressuretemperature

shape also accepts an SVG icon — a path d string (what icon sets ship) or a .svg file — drawn as a crisp vector marker. Use a literal string for a constant icon, or map shape and give [scale_shape()] one icon per level (the legend shows the icons); size scales them.

star <- "M12 2l3 7h7l-5.5 4.5 2 7-6.5-4.5-6.5 4.5 2-7L2 9h7z"
heart <- "M12 21s-7-4.35-9.5-8.5C1 9 2.5 5 6 5c2 0 3.5 1.5 4 2.5C10.5 6.5 12 5 14 5c3.5 0 5 4 3.5 7.5C19 16.65 12 21 12 21z"
d <- data.frame(x = 1:6, y = c(2, 4, 3, 5, 4, 6), g = rep(c("a", "b"), 3))
vplot(d) |>
  mark_point(x = x, y = y, shape = g, color = g, size = 1.6) |>
  scale_shape(values = c(a = heart, b = star))
A scatter plot. It plots y (vertical axis) against x (horizontal axis), where colour shows g. Based on 6 observations.23456123456yxgab

mark_bar() draws bars from a zero baseline. Give it an explicit y for heights, or omit y and it counts rows per category (the count stat). When fill is mapped, groups stack by default; switch to side-by-side with position = "dodge" or normalise to 1 with position = "fill".

vplot(mtcars) |>
  mark_bar(x = factor(cyl), fill = factor(gear), position = "dodge")
A bar chart. It plots count (vertical axis) against factor(cyl) (horizontal axis), where colour shows factor(gear). Based on 32 observations.036912468countfactor(cyl)factor(gear)345

For finer control, the position argument also takes a parameterised position_*() object. position_dodge2() fills each category’s band by the groups actually present (so a ragged grouping stays centred); position_nudge() offsets by a constant; position_jitter(width=, seed=) and position_jitterdodge() control scatter for overplotted categorical points.

vplot(mtcars) |>
  mark_bar(x = factor(cyl), fill = factor(gear), position = position_dodge2(padding = 0.15))
A bar chart. It plots count (vertical axis) against factor(cyl) (horizontal axis), where colour shows factor(gear). Based on 32 observations.036912468countfactor(cyl)factor(gear)345

Areas and intervals

mark_area() fills between a y line and zero, mark_ribbon() fills between ymin and ymax, and the interval marks draw ranges: mark_errorbar() (with caps), mark_linerange() (without), and mark_segment() from (x, y) to (xend, yend).

vplot(pressure) |>
  mark_area(x = temperature, y = pressure, fill = "steelblue", alpha = 0.4) |>
  mark_line(x = temperature, y = pressure)
A plot combining area chart and line chart. It plots pressure (vertical axis) against temperature (horizontal axis). Based on 19 observations.02004006008000100200300pressuretemperature

mark_boxplot() summarises the raw y values per x category into a box-and-whisker (box from Q1 to Q3, median line, whiskers at 1.5 times the IQR, outliers as points).

vplot(mtcars) |>
  mark_boxplot(x = factor(cyl), y = mpg)
A box plot. It plots mpg (vertical axis) against factor(cyl) (horizontal axis). Based on 32 observations.15202530468mpgfactor(cyl)

Like points and bars, these summary marks are addressable: an error bar or line range keyed with data_id/tooltip carries that identity on every segment it draws, and a boxplot keys each box by its category — so they hover, tooltip, and select as units once rendered as an interactive widget.

mark_pointrange() and mark_crossbar() are the identity summary marks: you supply y, ymin, and ymax (e.g. a model’s estimate and interval) and they draw a point-with-range or a box-with-centre-line — no aggregation.

Reference lines and function curves

mark_abline() draws a sloped reference line y = slope * x + intercept, the diagonal companion to mark_rule()’s horizontal and vertical lines. Both slope and intercept may be vectors to draw a family of lines.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  mark_abline(slope = -5, intercept = 37, color = "firebrick")
A plot combining scatter plot and abline plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.152025302345mpgwt

mark_function() draws a curve y = fun(x) sampled across the panel’s x range — handy for eyeballing a theoretical distribution against the data. Overlay a normal density on a density histogram:

set.seed(1)
vplot(data.frame(z = rnorm(500))) |>
  mark_histogram(x = z, y = after_stat(density)) |>
  mark_function(fun = dnorm, color = "firebrick", linewidth = 1.5)
A plot combining bar chart and fun plot. It plots density (vertical axis) against z (horizontal axis). Based on 500 observations.0.00.10.20.30.4-202densityz

Both reuse the panel’s existing scales, so add them on top of a data layer. (mark_function() holds a live function, so it does not round-trip through as_spec().)

When many observations land on the same (x, y) — common with rounded or discrete data — mark_count() collapses them to one bubble sized by the overlap count, an honest alternative to invisible overplotting:

vplot(data.frame(cyl = mtcars$cyl, gear = mtcars$gear)) |>
  mark_count(x = cyl, y = gear)
A scatter plot. It plots gear (vertical axis) against cyl (horizontal axis), where size shows n. Based on 32 observations.3.03.54.04.55.045678gearcyln36912

Tiles and bins

mark_tile() draws a rectangle at each (x, y) coloured by fill; mark_raster() is the same thing drawn as a single raster image (a fast path that needs a complete regular grid). For continuous data, mark_bin2d() and mark_hex() bin x and y into a grid and colour each cell by count.

grid <- expand.grid(x = 1:8, y = 1:8)
grid$z <- with(grid, sin(x / 2) + cos(y / 2))
vplot(grid) |>
  mark_tile(x = x, y = y, fill = z) |>
  scale_fill_continuous(palette = "Batlow")
A heatmap. It plots y (vertical axis) against x (horizontal axis), where colour shows z. Based on 64 observations.24682468yxz-101

mark_contour() draws iso-density contour lines of a 2-D point cloud (and mark_contour_filled() fills the bands), coloured by level. See the statistical marks article for the details.

vplot(faithful) |>
  mark_point(x = eruptions, y = waiting, color = "grey70") |>
  mark_contour(x = eruptions, y = waiting)
A plot combining scatter plot and contour plot. It plots waiting (vertical axis) against eruptions (horizontal axis), where colour shows level. Based on 272 observations.50607080902345waitingeruptionslevel0.0050.0100.0150.020

Text

mark_text() draws the label aesthetic as text at each (x, y); mark_label() adds a filled background behind each label so it stays legible over busy marks. size is in points, and angle can be mapped or constant.

top <- mtcars[mtcars$mpg > 30, ]
vplot(top) |>
  mark_point(x = wt, y = mpg) |>
  mark_text(x = wt, y = mpg, label = rownames(top), vjust = "bottom", size = 9)
A plot combining scatter plot and text-label plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 4 observations.Fiat 128Honda CivicToyota CorollaLotus Europa3132331.61.82.02.2mpgwt

On a crowded scatter, labels collide. repel = TRUE moves them apart and draws a thin leader back to each point. Placement is solved by the engine in device pixels and applied as an absolute offset, so it does not drift with the data scale and is deterministic (no seed needed). Because the offset is coordinate agnostic, repel also works on faceted and polar plots — each panel is solved and its labels kept inside it.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  mark_text(
    x = wt, y = mpg, label = rownames(mtcars),
    repel = TRUE, size = 4
  )
A plot combining scatter plot and text-label plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.Mazda RX4Mazda RX4 WagDatsun 710Hornet 4 DriveHornet SportaboutValiantDuster 360Merc 240DMerc 230Merc 280Merc 280CMerc 450SEMerc 450SLMerc 450SLCCadillac FleetwoodLincoln ContinentalChrysler ImperialFiat 128Honda CivicToyota CorollaToyota CoronaDodge ChallengerAMC JavelinCamaro Z28Pontiac FirebirdFiat X1-9Porsche 914-2Lotus EuropaFord Pantera LFerrari DinoMaserati BoraVolvo 142E152025302345mpgwt

mark_text_path() sets a label along a curve instead of at a point – one label per group, its glyphs following the group’s path and rotated to the local tangent. It is the direct way to label a line without a legend, or to caption a contour or arc. Glyphs follow the tangent, so a path walked right-to-left reads upside-down – traverse it in the reading direction (here sin over a rising half-period).

t <- seq(0, pi, length.out = 60)
curve <- data.frame(x = t, y = sin(t), lab = "y = sin(x)")
vplot(curve) |>
  mark_text_path(x = x, y = y, label = lab, size = 9, vjust = "bottom", offset = 2)
A text_path plot. It plots y (vertical axis) against x (horizontal axis). Based on 60 observations.y=sin(x)0.000.250.500.750123yx

mark_series_label() is the other legend-free way to name lines: it puts each series’ name at its end (the point with the largest x), coloured to match and repelled apart. Map the same x/y/color as the lines — the label text and colour follow the series. Give the panel a little x-room and drop the now-redundant colour legend.

set.seed(1)
econ <- data.frame(
  year = rep(2000:2015, 3),
  value = c(
    cumsum(rnorm(16, 2)), cumsum(rnorm(16, 1)), cumsum(rnorm(16, 3))
  ),
  series = rep(c("north", "south", "east"), each = 16)
)
vplot(econ) |>
  mark_line(x = year, y = value, color = series) |>
  mark_series_label(x = year, y = value, color = series) |>
  xlim(2000, 2018) |>
  guides(color = "none")
A plot combining line chart and text-label plot. It plots value (vertical axis) against year (horizontal axis), where colour shows series. Based on 48 observations.northsoutheast102030402000200520102015valueyear

mark_outlier_label() labels only the points that stand out — it keeps the rows whose y is an outlier (Tukey’s IQR rule, or method = "sd") and labels just those, so a busy scatter names its extremes without a wall of text. Map a label to name each outlier; with a color/fill mapped, outliers are found within each group.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  mark_outlier_label(x = wt, y = mpg, label = rownames(mtcars))
A plot combining scatter plot and text-label plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.Toyota Corolla152025302345mpgwt

Images

mark_image() puts a bitmap at each (x, y) in place of a marker: a flag or a company logo. src is a column of file paths (one image per datum) or a single path reused at every point. size sets the height in millimetres, and the width follows each image’s own aspect ratio, so nothing stretches.

badge <- function(text, fill) {
  path <- tempfile(fileext = ".png")
  magick::image_blank(120, 120, color = fill) |>
    magick::image_annotate(text, size = 64, gravity = "center", color = "white") |>
    magick::image_write(path)
  path
}
d <- data.frame(
  x = 1:3,
  y = c(2, 3, 1),
  logo = c(badge("A", "tomato"), badge("B", "steelblue"), badge("C", "seagreen"))
)
vplot(d) |>
  mark_image(x = x, y = y, src = logo, size = 14)
A pictograph. It plots y (vertical axis) against x (horizontal axis). Based on 3 observations.1.01.52.02.53.01.01.52.02.53.0yx

Reading images needs the magick package (a suggested dependency), which decodes PNG, JPEG, SVG, and more. Because size is in millimetres rather than data units, images keep their physical size as the panel resizes.

Pie and donut

mark_pie() and mark_donut() are the part-of-whole shortcuts. Each value becomes a wedge; fill colours the slices. Under the hood they are a stacked bar projected through coord_polar(), which they set for you.

parts <- data.frame(part = c("a", "b", "c", "d"), n = c(3, 5, 2, 4))
vplot(parts) |>
  mark_donut(value = n, fill = part, inner_radius = 0.6)
A bar chart. It plots n (vertical axis) against factor(1) (horizontal axis), where colour shows part. Based on 4 observations.05101partabcd

For polar plots generally, coord_radial() extends coord_polar() with a central hole (inner_radius) and a partial startend arc — e.g. a semicircular coxcomb:

vplot(mtcars) |>
  mark_bar(x = factor(cyl), fill = factor(cyl)) |>
  coord_radial(theta = "x", start = -pi / 2, end = pi / 2, inner_radius = 0.2)
A bar chart. It plots count (vertical axis) against factor(cyl) (horizontal axis), where colour shows factor(cyl). Based on 32 observations.4680510factor(cyl)468

Layering is the point

Because scales train across every layer, mixing marks on one panel works. Here a point cloud and a fitted line share the same trained x and y axes.

vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = factor(cyl)) |>
  mark_smooth(x = wt, y = mpg, method = "lm")
A plot combining scatter plot and smoothed-trend plot. It plots mpg (vertical axis) against wt (horizontal axis), where colour shows factor(cyl). Based on 32 observations.10152025302345mpgwtfactor(cyl)468

From here: