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.- 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 runplot_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’sas_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()plusanimate()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 honoursprefers-reduced-motion.theme_sketch()(or asketch =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.
# 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()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")- Marks:
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()). - Spatial:
mark_sf()draws ansfgeometry column as a map layer, withcoord_sf()to reproject and lock the aspect ratio. - Network:
vgraph()starts a node-link diagram from anigraphgraph (stress layout by default, viagraphlayouts), withmark_edges(),mark_nodes(),mark_node_text(), andscale_edge_width(). - Encodings (tidy-eval):
x,y,color/fill,size,shape,alpha. - Position scales (
scale_x_continuous(),scale_y_continuous(); linear andlog10) with auto-trained, expanded domains; discrete band scales (scale_x_discrete(),scale_y_discrete()) for categorical axes. - Colour scales (
scale_color_continuous()/scale_fill_continuous(),scale_color_discrete()/scale_fill_discrete(),_gradient(),_binned(),_manual()),scale_shape(), and a trainedscale_size(), with stacked legends. - Trained axes, a panel with gridlines, and layering on one panel.
- Faceting (
facet_wrap(),facet_grid()) with shared or free scales, via theresolve_scale()lattice. - Statistical marks:
mark_histogram(),mark_density(),mark_summary(),mark_smooth()(withafter_stat()). - Coordinate systems:
coord_cartesian(),coord_flip(),coord_fixed()/coord_equal(),coord_trans()(nonlinear display remap),coord_polar()(pie / coxcomb / radar), andcoord_sf(). - Position adjustments: stack / dodge / fill bars, jittered points.
- Per-mark
blend =modes (CSSmix-blend-mode:"multiply","screen", …). mark_datashade()for million-point density rasters.- Annotations:
annotate(),labs(), andmd()markdown titles. - Themes (
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_()). - Layer effects (
glow(),outline(),shadow(),motion(),echo()) and gradient fills (linear_gradient(),radial_gradient()). - Pattern (hatch) fills:
pattern_stripe(),pattern_crosshatch(),pattern_grid(),pattern_dot(),pattern_checker(), mapped viascale_pattern(), greyscale- and colour-vision-safe, on every backend. - Boolean marks:
vvenn()draws 2-/3-set Venn/Euler diagrams as solid geometry, andmark_sf(merge = TRUE)dissolves adjacent same-value regions into one. - SVG icon markers (
shape =adpath string or a.svgfile) and repelled labels (mark_text_repel(), over the engine’s overlap solver). - Accessibility: tagged PDF output,
plot_lint()for legibility/contrast problems, colour-vision-deficiency simulation at render time, and font pinning for reproducible text. - Animation:
transition_states()/transition_time()/transition_reveal(),ease_aes(),animate(), andanim_save()to GIF / APNG / animated SVG. - Output:
render_plot()(PNG/SVG/PDF),pdf_pages()(multi-page reports or one page per facet),render_all()(parallel batch), andplot_svg()for an inline SVG string (e.g. a sparkline inside agttable). - Hand-drawn rendering:
sketch()gives any geometry mark a wobbly, hachure- filled Rough.js look (asketch =argument on marks, anelement_line()/element_rect()sketch =slot, or the plot-widetheme_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:
- 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.
