Releases: r-vellum/vellumwidget
Release list
vellumwidget 0.8.0
Requires vellum >= 0.6.6, which fixes the coordinate space
element_geometry() reports in — the exact hit-testing below is built on it.
-
The raster base image is now rendered at 2× for HiDPI screens. A widget is
always viewed at screen resolution, and a 1× image stretched by the browser is
visibly soft on a retina display.scene_png(scale = 2)multiplies dpi while
holding physical size, so the image carries two pixels per device pixel while
the SVG shell stays in device pixels — the space the element bounding boxes and
panel rectangles live in, so nothing about hit-testing or pan/zoom changes.No new argument: raster mode is chosen precisely when the SVG alternative would
be enormous, so the extra bytes are noise against what they replace (at 30,000
keyed points, 446 KB → 877 KB, versus a 14.7 MB per-element SVG). The dense end
is cheaper at 2×, not dearer — a saturated canvas compresses better.The shell's dimensions now come from the scene rather than from the image, via
vellum::vl_convert(). Halving a scaled PNG re-rounds and drifted by a pixel
on a fractional page size, which would have put a small but systematic offset
between the drawn frame and the bounding-box space picking uses. -
Exact hit-testing: picking now measures to the mark, not to its box. The
payload carries each keyed element's true vertices in device pixels
(vellum::element_geometry(), computed once at build time); the Flatbush
R-tree still shortlists candidates by bounding box — a box distance is never
greater than the true distance, so nothing in range is missed — and the
shortlist is then ranked by distance to the actual shape. What changes:- Graph edges are hoverable. An edge's box is the whole rectangle its
endpoints span, so a box-distance "nearest" matched it from anywhere inside
that rectangle; edges had to be excluded from the open-space snap entirely.
They are now measured to the line and compete on their merits. - A diagonal is no longer matched from the far corner of its box. On a
four-series line chart whose boxes each cover ~91% of the panel, a cursor on
a line picks that line — where previously two or more series were tied at
box-distance zero at 39 of 40 probe positions. - A click inside a filled region hits the region, at distance zero, rather
than snapping to whichever border happens to be nearest. - Round marks are measured to the disc, not to the square around it.
Only the kinds whose shape is not their box (segment, line, polygon, path) are
shipped; a point's disc and a label's box are reconstructed from the bounding
box the payload already carries, so the dense scatter — where payload size
actually bites — pays nothing for this. Above 500,000 vertices the block is
dropped with a message and picking falls back to boxes.Brush and lasso are unchanged: a box is the right answer for a rectangular
brush, which is what it was always for. - Graph edges are hoverable. An edge's box is the whole rectangle its
-
Raster-mode widgets no longer touch disk. The base image is now encoded
withvellum::scene_png(), which returns the PNG bytes, instead of rendering
to atempfile()and reading it back. Widget output is unchanged. -
Animated plots as widgets.
as_widget()now accepts a keyframe animation
fromvellumplot::animate()and embeds it as a self-contained animated SVG
— resolution-independent and honouringprefers-reduced-motion(both handled
inside the SVG by vellum). It plays on its own through the runtime's existing
inline-SVG mount; since the marks move every frame there is no per-element
interaction index (no hover/brush) and no raster canvas. -
Fix: click-to-source did nothing for single-row groups. A grob backed by
one data row had itsrowsauto-unboxed to a scalar in the payload, so the
runtime'srows.lengthcheck skipped it (no event, no popover) — e.g. every
single-county region of a choropleth. The payload now forces arrays, and the
runtime coerces a scalar defensively. -
Click-to-source. When a plot opts in with
vellumplot::inspect_source(),
clicking a mark surfaces the source data rows behind it: the widget reads the
compiled scene's provenance (keyed by each grob'sdata-vellum-id), fires a
bubblingvellum:sourceDOM event (detail=id,rows,fields, and
valueswheninspect_source(values = TRUE)), setsinput$<id>_sourceunder
Shiny, and shows a small values popover. Opt-in rides the spec, so it adds no
as_widget()argument and no payload weight to a plain widget.
vellumwidget 0.7.0
-
Bug fix: hover snapped to the wrong mark on a scaled plot. When the svg was
rendered at a different size than its viewBox implies (the widget's
max-width:100%, the RStudio Viewer pane, an embedded/saveWidgetpage), hover
highlighted a mark offset from the cursor (usually up-and-left); click was
unaffected because it uses the DOM target. The client→scene hit-test mapping
relied ongetScreenCTM()/ viewBox math, which can disagree with how the
content is actually laid out when the svg carrieswidth/heightattributes.
Hit-testing (hover, brush, lasso) now calibrates the transform from the real
rendered positions of the marks themselves versus their known scene bboxes —
exact under any viewBox / attribute / CSS-scale / zoom combination. Tooltip
anchoring uses the mark's own rendered position. -
Breaking: interaction is now declared in the plot, not on
as_widget().
The interaction-intent arguments have been removed —hover_color,
selected_color,dim_opacity(nowvellumplot::condition()), and the
hover/select/brush/lasso/tooltip/nearest/zoom/axis_zoom/
zoom_markstoggles (now on by default — interactive-by-default). The
signature drops from ~36 to 24 arguments. Per-plot interaction (highlight,
filter, cross-filter) travels with the spec via
vellumplot::select_point()/select_interval()/condition()/filter_by().
Requiresvellumplot (>= 0.6.0.9000). (hover_mode,crosshair,
legend_click,select_mode, and the tooltip-appearance args remain until
their spec equivalents land.) -
Declarative interactivity: cross-view filtering. A selection defined on one
cell of a composition and referenced byfilter_by()on another now
cross-filters — brush/select the source cell and the linked cell narrows to the
matching rows, matched by a cross-viewjoinid. The source cell stays fully
visible; only the target cell hides (scoped by per-cell-unique keys). All in
one widget runtime, no server round-trip. -
Declarative interactivity: filters. A
vellumplotplot that declares
filter_by(selection)now hides everything outside the selection when that
selection's gesture fires — reusing the cross-filter path. Driven entirely by
the plot spec (noas_widget()argument). Single-view for now; the cross-view
form (brush one panel, filter another) follows. -
Declarative interactivity: conditional encodings.
as_widget()now reads
the interaction model avellumplotplot declares (select_point()/
condition()) off the compiled spec and enacts it on the frozen scene. A
hover- or click-driven selection with acondition()on an aesthetic keeps
selection members at theirif_truestyle and appliesif_falseto
non-members (an explicit colour, or the theme dim as a spotlight) — reverting
when the selection empties. No newas_widget()argument: the behaviour
travels with the plot. (Filters, cross-view, and scale binds follow.)
vellumwidget 0.6.0
-
Bug fix: legend swatches no longer jump off-screen under axis-aware zoom.
Withaxis_zoom/zoom_marks = "fixed"(both default), the constant-size counter-
scale was applied to every glyph mark, including discrete legend swatches. Because
a swatch is positioned by atransformattribute (unlike data points, which carry
absolute coordinates), the inline counter-scale overrode that attribute and flung
the swatch to a corner. The counter-scale is now restricted to marks inside the
pannable data region, so legend swatches stay put. -
Axis-aware zoom (
axis_zoom, defaultTRUE). Wheel/drag zoom scales only
the plot's data region and re-ticks the axes for the visible range — holding the
frame (axes, titles, legend) in place the way a charting library zooms, rather
than scaling the whole scene like an image. Hit-testing (hover/brush/lasso), the
crosshair, and theinput$<id>_zoomdata range all follow the zoomed data region.
Applies to a single linear cartesian panel (continuousidentity/reverse
axes) rendered as SVG; plots with log/date/discrete axes, several panels, or in
raster mode silently fall back to the ordinary whole-scene zoom, so it is safe to
leave on. Setaxis_zoom = FALSEfor the plain whole-scene zoom. Built on vellum's
pannable-panel contract (vl_viewport(pannable=)) and the panel scale metadata
vellumplotemits (needs the current developmentvellum/vellumplot). -
Constant-size markers on zoom (
zoom_marks, default"fixed"). Under
axis-aware zoom, glyph marks (points, circles, hexagons, sector wedges) now keep
their original pixel size and only their positions re-map — so points stay round
and don't stretch into ellipses under the navigator's x-only zoom, the way a
charting library zooms. Positional marks (bars, error bars, lines, areas) still
scale with the data; their stroke width is held constant. Setzoom_marks = "scale"for the old behaviour where glyphs grow with the zoom (useful to read
density). Applies to SVG axis-aware zoom and the raster crisp-point layer. -
Interactive continuous colorbar filter (visualMap). When a
vellumplotplot
maps a continuouscolorscale, its colorbar becomes a range filter: drag the
two handles on the gradient bar to a value range and marks whose colour value
falls outside it fade out (and drop out of hover/brush hit-testing); double-click
the bar to reset. The selected range is reported to Shiny as
input$<id>_colorfilter = c(lo, hi)(NULLat the full range). Automatic — no
argument to set — whenever the plot has a continuous colorbar (needs the current
developmentvellumplot; SVG mode). Discrete/binned colour legends keep their
existing click-to-hide interaction. -
Tooltip polish.
as_widget()gainstooltip_delay(ms to wait before the
tooltip appears — the highlight is immediate, only the tooltip waits),
tooltip_follow(TRUE, the default, tracks the cursor;FALSEanchors above
the mark), andtooltip_sticky(the tooltip accepts pointer events and lingers
briefly on leave, so tooltips containing links/buttons are usable). The tooltip
also now auto-flips below the cursor when there isn't room above and clamps
horizontally so it doesn't overflow the widget. -
Overview navigator (
navigator = TRUE). An opt-in strip below the plot that
renders the whole scene in miniature with a draggable, resizable window marking
the visible x-range: drag the window to pan, drag a handle to zoom. Zoom is
x-only — the selected x-range fills the width while the full y-range stays on
screen (a time-series range selector). Withaxis_zoom(the default) it is
rendered through the axis-aware zoom, so the x-axis re-ticks crisply; otherwise
the view stretches horizontally. It stays two-way in sync with the main view —
wheel/keyboard/brush, the toolbar, and linked-group pan/zoom all move the window,
and moving the window drives them. Useful for scrubbing long series.
navigator_heightsets the strip height (default 56px). Client-side; off by
default. -
Linked pan/zoom across a
group. Widgets sharing agroupalready linked
selection and hover; now panning or zooming one moves the others to the same
view. The view is shared as a fraction of each widget's own extent (over the
same client-side bus, no Shiny/crosstalk), so linked plots of different sizes —
small multiples — stay aligned. Reset links too. -
Brush and view now report data-space coordinates, not just pixels. When the
plot carries a cartesian scale (anyvellumplotplot),input$<id>_brushgains
the brushed region's data-space boundsx0d,y0d,x1d,y1d(plus thepanelname)
alongside the existing device-pixel rectangle, andinput$<id>_zoomgains
data = list(x=, y=, panel=), the visible range in data coordinates. This reads
the per-panel scale descriptorsvellumplotnow attaches to the scene (requires
the current developmentvellum(>= 0.4.0.9000) andvellumplot); a rawvellumscene
or a non-cartesian coordinate system reports device-pixel fields only, as before.
Date/time axes report the numeric epoch (days forDate, seconds forPOSIXct),
which you map back withas.Date()/.POSIXct(). The fields describe the
visual axes (undercoord_flip(),x0dis the plot'syaesthetic); a
discrete axis reports fractional band positions; and an axis built with a custom
scales::transform_*()object (beyond identity / log10 / sqrt / reverse) is
omitted from the data-space fields rather than reported wrong. (Groundwork for
axis-aware zoom.) -
Fix: cross-filtered and legend-hidden marks were still hit-testable. A
display-tier cross-filter (crosstalk orvw_filter()) and a legend
legend_click = "hide"toggle set the marks todisplay:none, but they stayed
in the spatial index — so a nearest-mark hover could still tooltip a hidden
datum, and a brush or lasso could re-select filtered-out points (propagating the
selection back to linked/crosstalk views). Keyboard traversal already skipped
hidden marks; hover, brush, lasso, and raster click-snap now do too, via a single
"inert" guard. Muted (not hidden) legend series stay interactive. -
Freehand lasso-select. A third drag mode alongside brush and pan (default
on; disable withas_widget(lasso = FALSE)). The toolbar's mode button now
cycles brush → lasso → pan, and it appears whenever at least two drag modes are
enabled. Drag a loop and every mark whose centre falls inside it is selected —
hit-tested with a point-in-polygon check over the Flatbush spatial index, so it
stays fast on large scenes. It reports throughinput$<id>_brushlike the box
brush, with alasso = TRUEflag and the loop's bounding box. -
Widgets report their current view to Shiny (
input$<id>_zoom). After a
zoom/pan settles — wheel, drag-pan release, pinch, keyboard, reset,
zoom-to-selection, or avw_zoom()proxy call — the widget publishes
list(x=, y=, w=, h=, zoomed=): the currentviewBox(device-px) and whether
the view is zoomed in. A deduped state input, so a coordinated dashboard can
react to what the user is looking at. (Data-space limits await axis/scale
metadata in the scene contract.) -
Legend click-to-hide and double-click-to-isolate.
as_widget()gains a
legend_clickargument. The default"select"is unchanged (clicking a
discrete-legend swatch selects its series)."hide"turns the legend into a
visibility toggle — a single click hides or shows the series, a double-click
isolates it (hides every other series; double-click again to restore all) — the
reflexive legend interaction from plotly / ECharts / Highcharts."mute"is the
same but dims the series rather than removing it (keeping its layout). Hovering a
swatch still highlights its series under every policy, and hidden series drop out
of hover/tooltip hit-testing. Works with keyboard (Enter/Space on a focused
swatch toggles) and is independent of the crosstalk cross-filter (the two never
clobber each other). Entirely client-side. -
Shared (unified) hover tooltips and a crosshair.
as_widget()gains a
hover_modeargument. The default"closest"is unchanged (hover shows the
single nearest mark), but"x"(or"y") turns on a unified hover: every
mark sharing the hovered x (or y) position is highlighted at once and listed
together in one tooltip box — the shared readout multi-series line and
time-series charts expect, where you want every series' value at the cursor's
x rather than one point. A companioncrosshairargument (defaultFALSE)
draws a guide rule at the hovered position: a vertical rule at the shared x in
"x"mode, a horizontal one in"y"mode, and a full cross through the mark
in"closest"mode. Both are entirely client-side and need no extra scene
metadata — the mark positions come straight from the element index — so they
work on any keyedvellumplotplot or rawvellumscene, and in raster mode.
Unified hover snaps along its axis so the readout tracks the cursor anywhere in
the plot; thehoverShiny input still reports the single nearest (primary)
mark, so the read-back contract is unchanged. -
Fix: hover/selection feedback rings were drawn offset from the mark. When
the widget container was larger than the plot's rendered box — which happens
routinely, as htmlwidgets stamps an explicitheighton the container and a
fluid layout can stretch its width — the feedback overlay filled the whole
container while the base SVG only filled its aspect-locked box. The overlay's
viewBoxthen letterboxed (centred), so every hover/selection ring drew off
the real mark: downward when the container was taller (e.g. 68px low for a
520px-tall container over a 384px plot) and sideways when it was wider. It
looked like the wrong mark was pi...
vellumwidget 0.5.0
-
textargument onas_widget(). Choose how text is written into the SVG,
passed through tovellum::scene_svg():"native"(the default) emits
selectable<text>referencing system fonts — smaller when the page has the
font, post-processable, and better for accessibility and LLMs — while
"outline"emits pixel-faithful, font-independent glyph paths. Applies to the
per-element SVG path only; in raster mode text is baked into the base image and
the argument is ignored (with a warning if set explicitly) (#1). -
New articles. Two articles document the features added since 0.4.0:
Linking views with crosstalk (coordinating a widget with DT / plotly /
leaflet and crosstalk'sfilter_*inputs) and Very large scenes (raster
mode, the spatial index, the columnar payload, and crisp zoom). -
Crisp zoom in raster mode. When you zoom into a raster-mode plot, the base
image used to upscale and blur. The widget now redraws the points sharply on a
<canvas>overlay while zoomed in — sampling each point's colour straight from
the rendered image and its position/size from the element index, so the crisp
layer matches what vellum drew. It engages only when zoomed in (the faithful,
anti-aliased base image still shows at the full view), redraws just the points in
view, and degrades gracefully to the image alone where a 2D canvas context isn't
available. Entirely client-side; no change to the payload or to small/moderate
(SVG-mode) plots. -
Very large scatterplots are navigable (raster mode).
as_widget()gains a
modeargument ("auto"/"svg"/"raster"). In"auto"(the default), a
scene with more thanraster_thresholdkeyed elements (default20000) is drawn
once as a single embedded image instead of one SVG node per element, and all
interaction — hover tooltips + highlight, click/brush select, pan/zoom — is driven
client-side from the element index (bounding boxes + keys) rather than the DOM.
A 150k-point keyed scatter that previously produced a ~75 MB SVG with 150,000 DOM
nodes now ships as a ~0.8 MB image plus a compact index, with a handful of DOM
nodes, and stays smooth to hover and pan. Small and moderate plots are unchanged
(they keep the per-element SVG). Trade-offs in raster mode: per-element grammar
colours, per-mark screen-reader focus, and display-tier cross-filtering don't
apply (there are no per-element nodes), and a zoomed-in view is a scaled raster
until re-rendered. Adds a dependency onbase64enc. -
Smoother hover, brush, and pan on large plots. Two client-side changes lift
the per-interaction cost that made big scatterplots laggy:- Spatial index. Nearest-mark hover and rectangular brush now hit-test
against a Flatbush R-tree
(O(log n) / O(k)) instead of scanning every element each time. The nearest-mark
scan runs on every pointer move, so this is the change you feel most. - Cheaper hover dim. Above a threshold, hovering dims the plot once (via the
holder's opacity) and re-draws the hovered marks crisply in a small overlay —
O(hovered) — instead of restyling every element via CSS (O(n)), which forced
a full-scene style recalc on each hover. Small and moderate plots keep the exact
previous per-mark dim. (Phase 2 of vellum's big-data interactivity plan.)
- Spatial index. Nearest-mark hover and rectangular brush now hit-test
-
Much faster, smaller payload for large plots (columnar element table). The
keyed-element metadataas_widget()embeds is now serialised in a columnar
form (one array per field) instead of one JSON object per element. This is a
transparent wire-format change — the widget behaves identically — but it removes
the per-element serialisation cost that dominated at large N. On a 150,000-point
keyed scatter the payload build + serialise dropped from ~24 s / 89 MB to
~0.4 s / 12 MB (~60x faster), so a big interactive scatter is no longer choked
by payload generation. Small and moderate plots are unaffected. (This is Phase 1
of vellum's big-data interactivity plan; the browser-side DOM/hover work for
truly huge scatters follows in later phases.) -
Error bars and boxplots are interactive. Now that vellumplot keys these
statistical marks, the widget hovers, tooltips, clicks/selects, and brushes them
as units: an error bar's bar + caps, or a box's rect + median + whiskers, all
light up and select together because they share onedata-key(outliers stay
individually addressable). Brush selection no longer double-counts a mark whose
key spans several SVG elements —input$<id>_brush$keysreports each such key
once. No runtime change was needed for the core behaviour; it already grouped
every node sharing a key.
vellumwidget 0.4.0
-
Server-to-client proxy (
vellumwidget_proxy()). A Shiny app can now drive an
already-rendered widget from the server without re-rendering it — no
renderVellumwidget()round-trip, no lost pan/zoom. Get a handle with
vellumwidget_proxy(outputId)(inside anobserve()), then pipe it through the
verbs:vw_select()/vw_clear_selection()— set or clear the selection (projects
acrosshover_groupand propagates to linked / crosstalk widgets, exactly
like a user click, and updatesinput$<id>_selected);vw_filter()/vw_clear_filter()— cross-filter the widget (show only the
given keys, dim the rest; display tier, data untouched);vw_zoom()/vw_reset_zoom()— frame a set of keys, or restore the full view.
All keys are the element
data_ids — the same identifiers you read back through
input$<id>_selected. This completes the two-way Shiny story begun with the
input read-back in 0.3.0; see the expanded Using vellumwidget in Shiny article.
vellumwidget 0.3.0
-
Adopted vellum's renamed
vl_*graphics primitives (grid collision fix). -
Shiny input read-back. A widget rendered with
vellumwidgetOutput()now reports the
user's interactions to the server as reactive inputs keyed by the output id:
input$<id>_selected(selected keys, state),input$<id>_click(list(key=),
event),input$<id>_hover(hovered key orNULL, state), and
input$<id>_brush(list(keys=, x0=, y0=, x1=, y1=), event). Values are element
data keys, so they map straight back to your data. Emitted only in a live Shiny
session — a static render is unchanged and produces no input traffic. See the new
Using vellumwidget in Shiny article. (Driving the widget from the server — a
vellumwidget_proxy()— is a planned follow-up.) -
Accessibility (
a11y = TRUE, on by default). The interactive widget is now
keyboard- and screen-reader-navigable, not a mute image:- the SVG is announced as an interactive chart (
role="graphics-document"+
aria-roledescription), labelled from the scene's title/description (which
vellumplotsets automatically) or an explicitas_widget(alt =); - every mark is a focusable
graphics-symbolwith a roving tabindex — arrow
keys move between marks, Enter/Space toggles selection, Escape exits; - a polite
aria-liveregion announces the focused / selected mark; - a visually-hidden data table lists every mark for assistive tech.
All gated ona11y;a11y = FALSErestores the previous output exactly. See the
vellumplot Accessibility article.
- the SVG is announced as an interactive chart (
vellumwidget 0.2.0
vellumwidget 0.2.0
Interaction-depth release (ROADMAP §4).
- Rich tooltips. Tooltip text now renders as safe HTML: an author-built
tooltip =(e.g. viaglue()) may use<b>/<i>/<br>for bold/italic/
line breaks. Data values are escaped and only inert, attribute-free tags are
honoured — no scripts, handlers, or attributes (no XSS). New
as_widget(tooltip_style =)themes the tooltip box (background/color/fontsize/
max_width). - Touch + keyboard. The widget is driven by pointer events, so pan/brush/
hover work with mouse, touch, and pen from one path; a two-finger pinch
zooms. With the widget focused, arrow keys pan,+/-zoom, and0resets. - Configurable export.
as_widget(export_filename =, export_scale =)set the
download filename base and a hi-res PNG multiplier; exports capture the current
(zoomed/panned) view. A "copy PNG to clipboard" toolbar button appears where
the Clipboard API is available. - Large-N performance. Hover/selection highlighting uses a per-render
key→node cache instead of aquerySelectorAllper key, and the nearest-mark
hover scan is throttled to one per animation frame. - New "Interactive widgets: a tour" article.
Deferred: a spatial index for hit-testing (the DOM cost was the real bottleneck,
now cached; bbox scans are cheap and datashade collapses huge clouds to a
raster); coordinated zoom across linked widgets and URL deep-linking; PDF export.
vellumwidget 0.1.0
vellumwidget 0.1.0
First release. vellumwidget turns a vellum scene or a vellumplot plot into a
self-contained, client-side interactive HTML widget via a single terminal
as_widget() pipe — no Shiny, no server round-trip. Everything below ships in
this first release.
Features
as_widget()compiles avellumplotplot or a rawvellumscene and bundles its
SVG +scene_model()element table into an htmlwidget.- Hover — tooltip (from the declared
tooltip, falling back to the key) and
highlight with inverse-dim; a hovered element'shover_grouphighlights the
whole group. Hover snaps to the nearest mark when the cursor isn't directly
over one. - Click — select (single / multiple modes); every element sharing a key
toggles together. - Brush — drag a rectangle to select every element it covers.
- Pan / zoom — mouse wheel and pan-mode drag reframe the SVG
viewBox, plus
zoom-to-selection and reset. - Toolbar (on hover) — brush/pan mode toggle, zoom-to-selection, reset zoom,
download SVG, download PNG, fullscreen. - Everything is opt-outable via
as_widget()arguments
(tooltip/hover/select/brush/zoom/toolbar/nearest). - Legend interaction. For a discrete
color/shapescale on an interactive
plot, each legend swatch drives its whole data series: hovering a swatch
highlights the series (the swatch stays lit), clicking it selects the series
(and links across views / crosstalk). Automatic — no extra arguments. - Linked views.
as_widget(group=)links vellumwidget widgets client-side (no
dependency): selecting/brushing in one highlights the same data keys in the
others, projecting byhover_group(select one, select the series).
as_widget(crosstalk = SharedData)bridges to the crosstalk ecosystem
(plotly / leaflet / DT andfilter_*inputs) via aSelectionHandle+
FilterHandle; a crosstalk filter hides the non-matching marks (display-tier
cross-filter). crosstalk is a Suggests, loaded only when used. - Customisable interaction styling, at two composing levels:
- Widget theme —
as_widget(hover_color=, selected_color=, dim_opacity=)
sets the look for the whole plot (any R or CSS colour). - Per-element grammar —
vellumplotmarks'hover_color/selected_color
(constant or column-mapped) style each element individually and override the
theme. Both use CSS variables with a defaults ← theme ← per-element cascade.
- Widget theme —
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).