vellumwidget turns a vellum scene — or a vellumplot plot — into a self-contained, client-side interactive HTML widget: hover tooltips + highlighting, click selection, rectangular brush-select, pan/zoom, and a toolbar, with no Shiny and no server round-trip. It is the host adapter of the vellum interactivity stack: vellum emits per-element data-keys, bounding boxes, and a scene_model() element table, vellumplot declares what is interactive, and vellumwidget hosts it.
Interactions: hover (tooltip + highlight, with nearest-mark snapping and hover_group linking) · click-select (single/multiple) · drag a rectangle to brush-select · wheel / pan-drag to pan-zoom · discrete-legend interaction (hover a swatch to highlight its series, click to select it) · toolbar (mode toggle, zoom-to-selection, reset, save SVG/PNG, fullscreen) · keyboard + screen-reader access (Tab in, arrow keys move between marks, Enter/Space to select; a11y, on by default). Each is opt-outable via an as_widget() argument.
Installation
# install.packages("pak")
pak::pak("r-vellum/vellumwidget")vellumwidget builds on the vellum backend (and works with vellumplot plots); pak pulls them in automatically. vellum compiles a Rust crate, so a Rust toolchain (cargo/rustc) is needed to build it.
Usage
library(vellumplot)
library(vellumwidget)
df <- data.frame(wt = mtcars$wt, mpg = mtcars$mpg, model = rownames(mtcars))
vplot(df) |>
mark_point(x = wt, y = mpg, tooltip = model, data_id = model) |>
as_widget()as_widget() is terminal: it compiles the plot to a vellum scene, emits the SVG and the element table, and returns an htmlwidget. It also accepts a bare vellum scene. Declare interactivity in vellumplot with the reserved mark arguments data_id (the join key), tooltip, and hover_group (links elements for shared highlighting); discrete color/shape legends then become interactive automatically. A plot that declares none renders as a static — but still embeddable — SVG.
How it depends
vellumwidget depends only on vellum’s scene_svg() / scene_model() contract, so it wraps any vellum scene, whoever produced it. vellumplot is a Suggests (for the examples/tests).
Development
The JS runtime is TypeScript in srcts/, bundled by esbuild into the committed inst/htmlwidgets/vellumwidget.js (so the R package installs with no Node):
Styling the interactions
Hover/selection appearance is customisable at two levels that compose — a widget-wide theme and per-element grammar colours. Both accept any R or CSS colour; per-element colours win over the theme, which wins over the built-in defaults.
1. Widget theme — as_widget() arguments set the look for the whole plot:
library(vellumplot)
library(vellumwidget)
df <- data.frame(wt = mtcars$wt, mpg = mtcars$mpg, model = rownames(mtcars))
vplot(df) |>
mark_point(x = wt, y = mpg, tooltip = model, data_id = model) |>
as_widget(
hover_color = "steelblue", # outline on hover (default: none, just dim-others)
selected_color = "orange", # outline on click-select
dim_opacity = 0.15 # how much the non-hovered marks fade (default 0.28)
)2. Per-element (data-driven) grammar — declare hover_color / selected_color on the mark, just like tooltip/data_id. They can be constants or mapped from a column, so different marks highlight differently:
df$cyl <- factor(mtcars$cyl)
vplot(df) |>
mark_point(
x = wt, y = mpg, data_id = model,
hover_color = ifelse(mtcars$cyl == 8, "firebrick", "steelblue"),
selected_color = "black"
) |>
as_widget() # a per-element colour overrides any widget themeBoth layers use the same CSS-variable mechanism, so a plot can set a theme default and override it per element in the same pipe. Anything not set falls back to the built-in look. (You can also override the raw CSS classes — .vellumwidget-hl, [data-key].vellumwidget-selected, .vellumwidget-tip, … — from the host document, but the arguments above are the supported API.)
Linking views
Selection can be linked across views by data key — select or brush in one plot and the same data highlight everywhere.
Own bus (vellumwidget ↔︎ vellumwidget, no dependency). Give the widgets a shared group:
df$hp <- mtcars$hp
p1 <- vplot(df) |> mark_point(x = wt, y = mpg, data_id = model) |> as_widget(group = "cars")
p2 <- vplot(df) |> mark_point(x = hp, y = qsec, data_id = model) |> as_widget(group = "cars")
# in an HTML doc, selecting a point in p1 highlights the same car in p2Selection projects by field: if the marks declare hover_group, clicking one element selects the whole series (select one cylinder count → all its cars).
crosstalk (interop with plotly, leaflet, DT, and filter_* inputs). Wrap the data in a crosstalk SharedData (its key must match your data_id) and pass it to as_widget():
library(crosstalk)
sd <- SharedData$new(df, key = ~model, group = "cars")
bscols(
vplot(sd$origData()) |> mark_point(x = wt, y = mpg, data_id = model) |> as_widget(crosstalk = sd),
DT::datatable(sd) # selecting rows / points links both ways
)
filter_slider("hp", "Horsepower", sd, ~hp) # crosstalk's filter inputs hide non-matching marksvellumwidget uses its own selection engine and layers crosstalk on top as an optional bridge (a SelectionHandle + FilterHandle), so a crosstalk filter hides the non-matching marks (display-tier cross-filter) and selection round-trips with the other widgets. The crosstalk client library is pulled in only when you pass a SharedData.
Legend interaction
When a plot maps a discrete color or shape scale and declares any interactivity (e.g. a data_id), each legend swatch becomes a handle for its whole data series — no extra arguments needed:
df <- data.frame(
wt = mtcars$wt, mpg = mtcars$mpg,
model = rownames(mtcars), cyl = factor(mtcars$cyl)
)
vplot(df) |>
mark_point(x = wt, y = mpg, color = cyl, data_id = model) |>
as_widget()
# hover the "6" swatch -> all six-cylinder cars highlight (the swatch stays lit);
# click it -> the whole series is selected (respecting single/multiple mode).vellumplot tags each swatch with the series it drives and each mark with its series membership; vellumwidget projects a swatch event onto every mark in that series, reusing the same highlight/select machinery as hover_group. Selecting via a swatch also links across views and into crosstalk, exactly like selecting a mark.
The vellum ecosystem
vellumwidget is the interactivity 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: a pipe-first grammar of graphics that compiles a plot spec into a vellum scene.
- vellumwidget — the annotation: this package.
- vellumverse — installs and loads the whole ecosystem in one step.
