Skip to contents

Most plotting libraries draw as you call them: each function pushes graphics onto a device. vellumplot does not. When you build a plot, nothing is drawn. You are assembling a spec, a plain S7 object that records the data, the layers, the scales, the facet, the coord, and the theme. Drawing happens later, in one pass, when a compiler turns that spec into a vellum scene. This article is about that separation and why it is worth having.

The spec is just data

Building a plot returns a value you can hold, inspect, and modify before anything is rendered.

p <- vplot(mtcars) |>
  mark_point(x = wt, y = mpg, color = hp) |>
  mark_smooth(x = wt, y = mpg)

p is a spec, not a picture. summary() shows its structure without drawing it:

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

Because the spec is an ordinary object, a plot is a value like any other. You can build it in one function and return it, keep a list of specs and render them in a loop, add a layer to a plot someone else handed you, or save the spec and rebuild the exact figure later. The plot is decoupled from the act of drawing it.

What the compiler does

Printing the spec (or calling render_plot()) runs the compiler. It is a pipeline of distinct stages, each consuming the output of the last:

  1. Resolve encodings. Evaluate the tidy-eval expressions (x = wt, color = hp) against the data for every layer, producing concrete columns.
  2. Apply stats and positions. Marks with a statistical transform (histogram, density, smooth) compute their derived data here, and position adjustments (stack, dodge, jitter) are applied.
  3. Train scales. Scan the resolved values across all layers to work out each scale’s domain, then build the mapping from data to colour, size, shape, and position. This is why layered marks share axes without you aligning anything.
  4. Measure layout. Lay out the panel (or the grid of facet panels), reserving room for axes, strips, legends, and titles, and measure where everything goes.
  5. Compile guides. Emit the axes, gridlines, and legends the trained scales imply.
  6. Compile marks. Turn each layer into vellum grobs positioned in the panel.

The result is a vellum scene: a resolution-independent description of the drawing that the backend can render to PNG, SVG, or PDF. vellumplot never touches a graphics device itself; it produces a scene and hands it to vellum.

Why this design pays off

Scales that span layers. Training happens after every layer’s data is resolved, so a point cloud and a fitted line automatically land on the same axes. There is no drawing order to get wrong.

Faceting is a layout question, not a redraw. Because layout is its own stage over an already-trained spec, splitting into panels reuses the same resolved data and scales. Panels can share scales or, via the resolve_scale() lattice, train independently, and either way it is one compile.

Inspectability. You can look at a plot before committing to it. summary() reports the layers and page size; the spec itself carries the answer to “what will this draw?” without drawing it. That makes plots testable: you can assert on the spec.

One scene, many outputs. The compiler targets a vellum scene, not a device. The same spec renders to a raster or a vector file, and the same scene model is what the vellumwidget package turns into an interactive widget. Write the plot once; choose the output later.

Rendering

Two ways to draw. Print the spec to see it in the plots pane or embed it in a knitr or Quarto document, exactly like ggplot2:

p

Or write it to a file with render_plot(). The format follows the file extension, and because the scene is resolution-independent, .svg and .pdf are exact at any size while .png honours the plot’s dpi.

render_plot(p, "cars.png")
render_plot(p, "cars.svg")
render_plot(p, "cars.pdf")

To go deeper into the backend the compiler targets, see the vellum documentation. To turn a compiled scene into an interactive widget, see vellumwidget.