Skip to contents

vellumplot is a declarative, pipe-first grammar of graphics built on the vellum graphics backend. You describe a plot as an inspectable, serializable spec; nothing is drawn until the spec is compiled into a vellum scene and rendered.

Compiling walks a fixed pipeline: resolve encodings, train scales, measure layout, compile guides, compile marks, emit a vellum scene. It needs no graphics device, because vellum measures text itself. If you already know ggplot2 the grammar will feel familiar; the difference is that the compiled scene is kept around afterwards, and a lot follows from that.

What is different here

Most of the following exists somewhere in R already. Having it in one grammar, from one spec, is the unusual part.

  • render_plot() writes PNG, SVG, or PDF from one compiled scene, so the layout is solved once instead of once per device. The PDF carries a structure tree and alt text, which lets a screen reader navigate it; tagged PDF output is rare for R graphics.
  • Accessibility is something you can check. Render through a colour-vision-deficiency simulation (render(cvd = "deutan")) to see the figure the way a colour-blind reader will, and run plot_lint() to catch tiny text, low contrast, or a single-level legend before you publish.
  • A compiled plot is a vellum scene, so every element keeps its data key and its resolved device-pixel box (vellum::scene_model()). vellumwidget’s as_widget() reads both for tooltips, brushing, and linked selection. Nothing re-draws the plot in the browser and there are no *_interactive() twins to keep in sync, so the static and interactive figures cannot drift apart.
  • Effects and regions stay vector where they can. glow() / shadow() are real Gaussian blur and work on text; Venn/Euler diagrams and merged choropleth regions are computed as boolean geometry rather than alpha-composited overlaps, so they stay crisp in a PDF instead of being flattened to pixels.
  • pattern_*() hatch fills encode a mapping as texture instead of hue, so it survives greyscale print and colour-vision deficiency. They render on every backend, PDF included.
  • transition_states() plus animate() compile one keyframe per state, with scales frozen so the animation is non-reactive. anim_save() encodes a GIF, an APNG, or a resolution-independent animated SVG that honours prefers-reduced-motion.
  • theme_sketch() (or a sketch = argument) gives a plot a wobbly, hand-drawn look. The engine generates it, so it is exact and comes out the same in PNG, SVG, and PDF, unlike a post-hoc filter.
  • The spec is plain data. summary() shows a plot’s structure without drawing it, and the spec round-trips, so you can inspect a plot, store it, and program against it.

Installation

# install.packages("pak")
pak::pak("r-vellum/vellumplot")

vellumplot needs the vellum backend, which compiles a Rust crate, so you also need a Rust toolchain (cargo/rustc); pak pulls vellum in automatically.

Usage

Building a plot returns a spec; printing it draws into the Plots pane (and embeds in a knitr/Quarto chunk), like ggplot2. Use render_plot() to write a file.

library(vellumplot)

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

Layer marks on a single panel; scales train across every layer:

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  mark_smooth(x = wt, y = mpg)
A plot combining scatter plot and smoothed-trend plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations.1015202530352345mpgwt

Facet into a grid of panels (facet_wrap() / facet_grid()), with shared or free scales:

vplot(mtcars) |>
  mark_point(x = wt, y = mpg) |>
  facet_wrap(~cyl)
A scatter plot. It plots mpg (vertical axis) against wt (horizontal axis). Based on 32 observations. Faceted by cyl.152025301520253023452345468mpgwt

Draw spatial data: mark_sf() renders an sf geometry column and coord_sf() reprojects and locks the map aspect ratio:

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
vplot(nc) |>
  mark_sf(fill = BIR74) |>
  coord_sf()
A map. Based on 100 observations.34.034.535.035.536.036.5-84-82-80-78-76yxBIR745000100001500020000

Draw a network: vgraph() lays out an igraph graph (stress majorization by default), then mark_edges() / mark_nodes() draw it, aspect-locked and without axes, edges under nodes:

g <- igraph::make_graph("Zachary")
g <- igraph::set_vertex_attr(
  g,
  "grp",
  value = as.factor(igraph::cluster_louvain(g)$membership)
)
g <- igraph::set_vertex_attr(g, "deg", value = igraph::degree(g))
vgraph(g, layout = "stress") |>
  mark_edges(alpha = 0.4) |>
  mark_nodes(size = deg, fill = grp) |>
  scale_size(range = c(2, 8))
A network graph. It has 34 nodes and 78 edges, where colour shows grp and size shows deg.grp1234deg481216

The spec is just data, so summary() shows its structure without drawing:

summary(vplot(mtcars) |> mark_point(x = wt, y = mpg, color = hp))
#> <PlotSpec> 32x11 (11 columns), page 6x4 in
#> 
#> ── layers
#> • mark_point(x = wt, y = mpg, color = hp)

Write to a file with render_plot() (the format follows the extension):

p <- vplot(mtcars) |> mark_point(x = wt, y = mpg)
render_plot(p, "cars.png")

What’s included

The vellum ecosystem

vellumplot is the grammar layer of a small ecosystem of packages that share the vellum scene model:

  • vellum, the parchment: the low-level graphics backend (Rust scene graph, PNG/SVG/PDF renderer).
  • vellumplot, the pen: this package.
  • vellumwidget, the annotation: turns a vellumplot plot (or a raw vellum scene) into a client-side interactive HTML widget via as_widget().
  • vellumverse installs and loads the whole ecosystem in one step.