Skip to content

Repository files navigation

vellumplot

R-CMD-check

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.

The compile is a real pipeline (spec → resolve encodings → train scales → measure layout → compile guides → compile marks → vellum scene) and it runs without a graphics device, because vellum measures text itself. If you already know ggplot2 the grammar will feel familiar; the reason to reach for this stack is what the retained vector scene underneath makes possible.

What is a little different here

Most of these exist somewhere in R; having them in one grammar, from one spec, is what is unusual. None of it is a reason to switch on its own — but together they cover a few gaps.

  • One spec, one solved layout, several destinations — including a tagged PDF. render_plot() writes PNG, SVG, or PDF from the same compiled scene rather than re-solving the layout per device, and the PDF carries a real structure tree and alt text, so a screen reader can navigate it. Accessible (tagged) PDF output is uncommon for R graphics.
  • Accessibility is checkable, not just aspirational. Render through a colour-vision-deficiency simulation (render(cvd = "deutan")) to see the figure as a colour-blind reader would, and run plot_lint() to be told about tiny text, low contrast, or a single-level legend before you publish it.
  • The interactive widget uses the marks you declared. A compiled plot is a vellum scene, so each element keeps its data key and its resolved device-pixel box (vellum::scene_model()). vellumwidget's as_widget() reads those for tooltips, brushing, and linked selection — no *_interactive() twins, and no second engine (Vega, plotly) re-drawing the plot in the browser, so the static and interactive figures cannot drift.
  • 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.
  • Texture, not only hue. pattern_*() hatch fills stay legible in greyscale print and under colour-vision deficiency, and render on every backend including PDF.
  • Animation from the same grammar. transition_states() + animate() compile one keyframe per state (scales frozen, so the animation is non-reactive) and anim_save() encodes a GIF, an APNG, or a resolution-independent animated SVG that honours prefers-reduced-motion.
  • A hand-drawn mode that is exact. theme_sketch() (or a sketch = argument) gives a plot a wobbly, hand-drawn look generated natively in the engine, so it is identical across PNG, SVG, and PDF rather than a post-hoc filter.
  • The spec is plain data. summary() shows a plot's structure without drawing it, and the spec round-trips, so a plot is something you can inspect, store, and program against.

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()
plot of chunk example

plot of chunk example

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)
plot of chunk layers

plot of chunk layers

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)
plot of chunk facet

plot of chunk facet

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()
plot of chunk sf

plot of chunk sf

Draw a network: vgraph() lays out an igraph graph (stress majorization by default), then mark_edges() / mark_nodes() draw it — aspect-locked, no 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))
plot of chunk network

plot of chunk network

The spec is just data — 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

  • 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 an sf geometry column as a map layer, with coord_sf() to reproject and lock the aspect ratio.
  • Network: 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().
  • Encodings (tidy-eval): x, y, color/fill, size, shape, alpha.
  • Position scales (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.
  • Colour scales (scale_color_continuous() / scale_fill_continuous(), scale_color_discrete() / scale_fill_discrete(), _gradient(), _binned(), _manual()), scale_shape(), and a trained scale_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 the resolve_scale() lattice.
  • Statistical marks: mark_histogram(), mark_density(), mark_summary(), mark_smooth() (with after_stat()).
  • Coordinate systems: coord_cartesian(), coord_flip(), coord_fixed() / coord_equal(), coord_trans() (nonlinear display remap), coord_polar() (pie / coxcomb / radar), and coord_sf().
  • Position adjustments: stack / dodge / fill bars, jittered points.
  • Per-mark blend = modes (CSS mix-blend-mode: "multiply", "screen", ...).
  • mark_datashade() for million-point density rasters.
  • Annotations: annotate(), labs(), and md() 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 via scale_pattern() — greyscale- and colour-vision-safe, on every backend.
  • Boolean marks: vvenn() draws 2-/3-set Venn/Euler diagrams as solid geometry, and mark_sf(merge = TRUE) dissolves adjacent same-value regions into one.
  • SVG icon markers (shape = a d path string or a .svg file) 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(), and anim_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), and plot_svg() for an inline SVG string (e.g. a sparkline inside a gt table).
  • Hand-drawn rendering: 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.

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.

About

A declarative, pipe-first grammar of graphics for R that compiles an inspectable spec into a vellum scene.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages