About the vellum ecosystem

What vellum is, how the pieces fit together, and how this gallery is built.

One ecosystem, four packages

vellum is a family of R packages for drawing, built around a shared scene model and a common design. The metaphor runs through the names: vellum is the parchment, vellumplot is the pen, and vellumwidget is the annotation revealed on the page.

Package Role Analogue
vellum Low-level graphics framework: units, viewports, grobs, layout, and PNG/SVG/PDF rendering, with the scene graph and renderers implemented in Rust. grid
vellumplot A declarative, pipe-first grammar of graphics that compiles an inspectable plot spec into a vellum scene. ggplot2
vellumwidget Client-side interactive HTML widgets over a vellum scene: hover, select, brush, and zoom, read from the scene’s own geometry. plotly / htmlwidgets
vellumverse A meta-package that installs and loads the whole ecosystem together. tidyverse

One seam

The three packages are worth using together because they share a single joint. vellum measures text and solves layout without opening a device, so a compiled scene can report where everything landed: scene_model() gives one row per drawn element with its data key and resolved device-pixel box, and element_geometry() gives the true vertices behind that box. vellumplot compiles a plot spec into such a scene. vellumwidget hosts that same scene in a browser, reading the table rather than re-drawing anything.

So one specification yields a static figure, a vector file, and an interactive widget that cannot disagree about geometry, because there is one solved layout behind all three.

What that buys you

Everything below needs the geometry to exist before the draw, which a device-driven stack cannot offer:

  • Read the figure back: ask the scene where every mark, label, and panel landed, with nothing drawn.
  • Fit text to a box: wrap a label to a measure and shrink it until the block fits, re-wrapping at each probe. Long titles and long discrete axis labels wrap on the same rule.
  • Move colliding labels apart in the engine. Placement is solved in device pixels and applied as an absolute offset, so faceted, polar, and warped panels are handled in one pass.
  • Lint a figure before anyone sees it: text below a legibility floor, colour contrast under the WCAG threshold, a mark that will never be painted.
  • Treat a scene as a value. Hash it, diff it, nest one inside another. A structural diff beats an image diff, because it is immune to the font stack.
  • Ship accessible output: simulate colour-vision deficiency in the render, encode redundantly with real hatch geometry, and emit a tagged PDF a screen reader can navigate.

Some capabilities have no device-driven equivalent at all: text set along a curve, boolean path operations on real geometry, contours chained into polylines, SVG icon paths as crisp vector markers, animated SVG, multi-page PDF, and font pinning that makes “identical pixels everywhere” checkable rather than claimed.

Rendering is deterministic, so the same scene is byte-identical across platforms and figures can be snapshot-tested. The paint model covers gradients, tiling patterns, alpha and luminance masks, group opacity, blend modes, and real Gaussian blur. Large point clouds go through built-in aggregation with datashade(), and sketch() gives a hand-drawn look that is generated in the engine, so it is identical on every backend.

Pipe-first, ggplot2-adjacent

If you know ggplot2 the vocabulary will feel familiar (mark_point(), scale_color_continuous(), facet_wrap(), theme_minimal()), but plots are composed with the native pipe |> rather than +, and aesthetics are plain named arguments on each mark, with no aes():

library(vellumplot)

vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = hp) |>
  scale_color_continuous()
Back to top