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.
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.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.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.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.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.# 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.
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()
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)
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)
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()
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))
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")
mark_point(), mark_line(), mark_step(), mark_rule(),
mark_bar() (explicit heights, or row counts per category),
mark_area() / mark_ribbon(), intervals (mark_errorbar(),
mark_linerange(), mark_segment()), mark_boxplot(),
tiles/heatmaps (mark_tile(), mark_raster()), 2-D binning
(mark_bin2d(), mark_hex()), 2-D density contours
(mark_contour(), mark_contour_filled()), text (mark_text(),
mark_label()), and pie/donut shortcuts (mark_pie(),
mark_donut()).mark_sf() draws an sf geometry column as a map layer,
with coord_sf() to reproject and lock the aspect ratio.vgraph() starts a node-link diagram from an igraph graph
(stress layout by default, via graphlayouts), with mark_edges(),
mark_nodes(), mark_node_text(), and scale_edge_width().x, y, color/fill, size, shape,
alpha.scale_x_continuous(), scale_y_continuous();
linear and log10) with auto-trained, expanded domains; discrete band
scales (scale_x_discrete(), scale_y_discrete()) for categorical
axes.scale_color_continuous() / scale_fill_continuous(),
scale_color_discrete() / scale_fill_discrete(), _gradient(),
_binned(), _manual()), scale_shape(), and trained
scale_size() / scale_linewidth(), with stacked legends.facet_wrap(), facet_grid()) with shared or free scales,
via the resolve_scale() lattice.mark_histogram(), mark_density(),
mark_summary(), mark_smooth() (with after_stat()).coord_cartesian(), coord_flip(),
coord_fixed() / coord_equal(), coord_trans() (nonlinear display
remap), coord_polar() (pie / coxcomb / radar), and coord_sf().blend = modes (CSS mix-blend-mode: "multiply",
"screen", …).mark_datashade() for million-point density rasters.annotate(), labs(), and md() markdown titles.theme_gray() default, theme_minimal(), theme_bw(),
theme_classic(), theme_void(), theme_cyberpunk(), theme() /
set_theme()) and multi-plot composition (hconcat(), vconcat(),
concat(), wrap_plots(), inset(), repeat_()).glow(), outline(), shadow(), motion(),
echo()) and gradient fills (linear_gradient(),
radial_gradient()).pattern_stripe(), pattern_crosshatch(),
pattern_grid(), pattern_dot(), pattern_checker(), mapped via
scale_pattern(), greyscale- and colour-vision-safe, on every
backend.vvenn() draws 2-/3-set Venn/Euler diagrams as solid
geometry, and mark_sf(merge = TRUE) dissolves adjacent same-value
regions into one.shape = a d path string or a .svg file) and
repelled labels (mark_text_repel(), over the engine’s overlap
solver).plot_lint() for
legibility/contrast problems, colour-vision-deficiency simulation at
render time, and font pinning for reproducible text.transition_states() / transition_time() /
transition_reveal(), ease_aes(), animate(), and anim_save() to
GIF / APNG / animated SVG.render_plot() (PNG/SVG/PDF), pdf_pages() (multi-page
reports or one page per facet), render_all() (parallel batch), and
plot_svg() for an inline SVG string (e.g. a sparkline inside a gt
table).sketch() gives any geometry mark a wobbly,
hachure- filled Rough.js look (a sketch =
argument on marks, an element_line() / element_rect() sketch =
slot, or the plot-wide theme_sketch() one-liner). Generated natively
in the engine, so it is exact and works across PNG / SVG / PDF.vellumplot is the grammar layer of a small ecosystem of packages that share the vellum scene model:
as_widget().