vellumplot (development version)
Bug fixes
mark_area()now stacks.mark_area(position = ...)was silently ignored; it gains apositionargument ("stack"default, plus"fill"and"identity"), so areas sharing anxwith a mappedfill/colorcombine into a band instead of overlapping from the zero baseline. An area with no fill mapping is unchanged.Mapped aesthetics that were silently dropped now take effect. A mapped
fillonmark_label()colours the label background (previously it always fell back to white); a mapped or constantalphais honoured per category/tick bymark_violin(),mark_ridgeline(),mark_contour(),mark_contour_filled()andmark_rug()(previously collapsed to one value or ignored), andmark_rug()also honours a mappedcolor.British spelling works everywhere.
hover_colour/selected_colourare now recognised as interactivity arguments (previously only the American spelling was), andmark_bin2d()/mark_hex()no longer add a default count fill when a Britishcolour =is supplied.vgraph()warns on a layout-column clash. A vertex/edge attribute namedx/y(orxend/yend) is overwritten by the layout coordinates; that used to happen silently and now emits a warning naming the attribute.
New features
coord_trans()— nonlinear display transforms. Warps the display of one or both position axes after the scale has trained, so gridlines bunch up and straight lines curve while the axis keeps its original data-value labels (unlikescale_*(trans=), which rescales the data and relabels in transformed space). Each ofx/ytakes a transform name ("log10","sqrt","identity") or ascales::transform_*()object, e.g.coord_trans(y = "log10"). Supports the common marks (points, lines, areas/ribbons, bars, tiles, smooths, text, …); segment/interval, boxplot, edges, and raster/datashade marks are not warped yet and raise a clear error undercoord_trans().mark_violin()andmark_ridgeline()no longer clip at the edges. The position scales are now trained to include each mark’s drawn footprint – the full kernel-density support for a violin, and the ridge height (scale) above the top category for a ridgeline – so the tallest ridge and the density tails are no longer cropped by the panel. Explicit axis limits still take precedence.Marginal distributions with
add_marginal(). A new plot modifier draws a density or histogram of the panel’sxvariable along the top edge and of itsyvariable along the right edge, each sharing the scatter’s axis so they line up (the vellumplot analogue ofggExtra::ggMarginal()). Pick the distribution withtype = "density"/"histogram", the edges withsides("t","r","tr"), and the extent withsize;group = TRUEsplits each marginal by a discretecolor/fillmapping in the matching palette. It readsx/yfrom the first plain (identity-stat) layer and reuses the existing density/bin stats, so no axes or legends are duplicated. This version supports a single panel only (an error is raised with facets, a non-Cartesian coordinate system, or a locked aspect ratio).Positional constants now render. A bare literal on a coordinate channel (e.g.
mark_segment(x = i, y = 0, xend = i, yend = value)for a lollipop baseline) is now treated as a constant-valued coordinate rather than a style param, so it populates the layer’s values and trains the position scale. Previouslyy = 0was dropped from scale training, collapsing each segment’s start onto its end (zero-length, invisible), and a segment-only plot errored with “Every layer needs an x and y encoding”. Segment/edge endpoints (xend/yend) now also extend the axis domain, so a segment-only plot derives its range from both ends. Affects the positional channelsx,y,xend,yend,xmin,xmax,ymin,ymax.Faster
mark_sf()on large maps. Non-interactive sf layers now batch all features that share a resolved fill/colour into a single grob per style group (polygons into oneevenoddpath_grob, lines into one NA-separatedlines_grob), instead of one grob per feature, and the layer’s bounding box is computed in a single linear pass rather than by growing coordinate vectors. On a 40k-polygon grid this cut scene compilation from ~24s to ~0.8s. Interactive layers (those declaringdata_id/tooltip/hover_group) are unchanged — they still emit one keyed grob per feature so every feature stays addressable. Note: because a batched polygon group is filled with oneevenoddrule, two same-fill features that overlap would cancel in the overlap region (the same property a self-overlappingMULTIPOLYGONalready has); disjoint features — every real choropleth and coverage map — are unaffected.
vellumplot 0.3.0
Adopted vellum’s renamed
vl_*graphics primitives (vl_gpar(),vl_unit(),vl_viewport()), which no longer mask grid.NA legend keys for
size/shape. A mappedsizeorshapeaesthetic whose data contains missing values now shows an “NA” key (as the colour scales already did). Also fixes a crash: NA in ashapemapping previously errored (Unknown point shape: NA) — it now draws as a neutral circle.2-D density contours.
mark_contour()draws iso-density contour lines of a 2-D point cloud andmark_contour_filled()fills the bands between them (coloured by level automatically). By default the field is a kernel density estimate (viaMASS::kde2d()); map azaesthetic to contour a supplied surface over a regularx/ygrid instead. Contour tracing uses theisobandpackage (bothisobandandMASSare Suggests).Binned position scales.
scale_x_binned()/scale_y_binned()cut a continuous axis into bins — ticks at the bin boundaries, each datum drawn at its bin centre — reusing the binned-colour classification (style/n, or explicitbreaks).mark_bar()sizes to the bin width.Rich and multi-line text labels.
mark_text()now carries rich labels through to the renderer instead of flattening them: maplabel = md(<expr>)for a per-datum styled label (bold/italic/super-/subscript/colour), and plain labels may contain newlines (\n) to wrap onto stacked lines. Requires vellum’s development version. (mark_label()’s background box does not yet support rich labels.)Accessibility (alt text). Every compiled plot is now an accessible image by default.
plot_alt()returns a plot’s text alternative — an author-written string from the newlabs(alt = ), or a prose summary vellumplot generates from the spec (chart type, x/y/colour/size mappings, observation count, faceting). At the compile seam the plot title becomes the scene’s accessible name and this alt text becomes its description, sorender_plot()output carriesrole="img"+<title>/<desc>in SVG and a taggedFigurewithAltin PDF (via vellum). See the new Accessibility article. Requires vellum’s development version (>= 0.1.1.9000) for the accessible SVG/PDF backend.British spelling.
mark_*(colour = )is now honoured as an alias forcolor(previously the mapping was silently dropped and no colour scale was trained). Addedscale_colour_*()aliases for thescale_color_*()family.Faithful alt text. The auto-generated description now matches what the plot actually renders: it honours
scale_*(name = )axis/legend titles, names the implicit “count” axis of a count bar, and describes a network graph by its node and edge counts instead of its (meaningless) layout x/y axes.
vellumplot 0.2.1
Consumes vellum’s new compound native + mm unit (requires vellum >= 0.1.1).
-
Label nudges:
mark_text()/mark_label()gainnudge_x/nudge_y(in millimetres,+xright /+yup) that shift a label by an exact absolute distance, device-resolved so the nudge is constant regardless of scale or panel aspect. -
Device-exact drop shadows:
shadow()’sx/yoffset is now an absolute distance in millimetres (was a panel-relative npc fraction), so a drop shadow stays the same physical distance and is isotropic on non-square panels. Defaults changed accordingly (a small down-right drop). Replaces the previous npc-fraction workaround.
Device-space dodge (mark_bar) and jitter (mark_point) remain data-space for now; converting them to the compound unit is deferred (it changes existing rendered output and needs snapshot review).
vellumplot 0.2.0
Grammar-breadth release: new scales, mapped aesthetics, legend control, and distribution marks, closing the most conspicuous gaps versus ggplot2.
Scales & axes
-
Date/time scales:
scale_x_date()/scale_x_datetime()/scale_x_time()(andytwins) withdate_breaks(interval strings like"3 months") anddate_labels(strftime formats).Date/POSIXctcolumns still get a sensible date axis automatically. -
New mapped aesthetics:
scale_alpha()(continuous opacity) andscale_linetype()(discrete line types), each drawn per element with its own legend.alphamapped to data now varies opacity (previously constant-only);linetypeapplies tomark_line()/mark_step(). -
Identity scales:
scale_color_identity(),scale_fill_identity(),scale_size_identity(),scale_shape_identity(),scale_alpha_identity(),scale_linetype_identity()— use data values verbatim, no legend. -
Limit shortcuts:
xlim(),ylim(), andlims(). -
Legend control:
guides()withguide = "none"(hide a legend),guide_legend(reverse = TRUE)(reverse key order), andguide_legend(title=).
Marks & stats
-
Distribution marks:
mark_ecdf()(empirical CDF step),mark_rug()(marginal ticks),mark_qq()+mark_qq_line()(quantile-quantile plot). -
Density-shape marks:
mark_violin()(mirrored density per category),mark_ridgeline()(overlapping per-category densities), andmark_dotplot()(binned, stacked dots). Violin and ridgeline carry per-category provenance.
Not yet implemented (still planned)
Secondary axes (sec_axis/dup_axis); 2-D density / contour (mark_contour/stat_density_2d); position-binned scales (scale_x_binned); coord_trans() and free non-position scales across facets; triple-merge legends (colour+shape+size) and NA keys for size/shape.
vellumplot 0.1.1
- New exported
plot_provenance(): returns the compiled-scene provenance table — one record per emitted mark grob, tying each low-level primitive back to the data rows and trained scales that produced it, with anidthat matches the grob’sdata-vellum-idin SVG and theidcolumn ofvellum::scene_model(). This is the first public consumer of the provenance metadata (previously internal), the substrate for interactivity, linked views, and accessibility. - Row-key provenance is now refined per element for the line, area, ribbon, step, text, and boxplot marks (previously only point/bar/tile/segment/sf/ edges), so a record’s
rowsresolve to the actual data rows an element draws rather than the whole layer. See the scene-contract vignette invellum.
vellumplot 0.1.0
First release. vellumplot is a declarative, pipe-first grammar of graphics that compiles an inspectable spec into a vellum scene, with faceting, coordinate systems, and multi-plot composition. Everything below ships in this first release.
Features
Interactivity declarations (host-agnostic; inert on a static render). Any
mark_*()accepts reserved per-row argsdata_id,tooltip, andhover_group(tidy-eval expressions). They flow into the vellum scene as per-element keys/metadata —data_idbecomes the SVGdata-keyand both surface invellum::scene_model()— the foundation a companion widget uses for hover/select/linking. A plot without them compiles and renders exactly as before. Applies tostat = "identity"marks (points, bars, tiles, segments, edges, hexbins, polar bars) and tomark_sf()— each sf feature (polygon/linestring/point) becomes an addressable, keyed element.Per-element interaction styling: marks also accept
hover_colorandselected_color(constant or column-mapped), carried into the scene so a host (vellumwidget) outlines each element in its own colour on hover/select — overriding the widget-wide theme.Interactive discrete legends: when a plot maps a discrete
color/shapescale and declares interactivity, each legend swatch is tagged with the data series it represents, and every mark carries its series membership. A host (vellumwidget) uses this to make swatches highlight/select their whole series. Inert on a static render and when no interactivity is declared.Auto-display: printing a plot (or composition) draws it into the active graphics device — the RStudio / Positron Plots pane, or a knitr/Quarto chunk — like ggplot2 (via
vellum::display()).summary()shows the inspectable spec tree instead;render_plot()still writes a file.vplot()starts an inspectable, serializablePlotSpec.Output resolution:
vplot(dpi =)sets the authored resolution andrender_plot(dpi =)overrides it per render, so an exported PNG’s pixel dimensions arewidth * dpibyheight * dpi. Compositions inherit the first sub-plot’sdpi(or take an explicitconcat(dpi =)).Marks:
mark_point(),mark_line(),mark_rule()(reference lines viaxintercept/yintercept), andmark_bar()(uses explicityheights, or counts rows per category whenyis omitted).Encodings captured with tidy evaluation:
x,y,color/fill,size,shape,alpha. Scalar values become constant aesthetics.Scales:
scale_x_continuous()/scale_y_continuous()(linear andlog10) with auto-trained domains and ggplot-style expansion; discrete (band) position scales are trained automatically for categoricalx/y(bars);scale_color_continuous()(perceptual ramp),scale_color_discrete()(qualitative palette), and a trained size scale for a mappedsize.Guides: trained x/y axes (breaks, labels, titles), a grey panel with white gridlines, and a legend area that stacks multiple guides — a colour legend (continuous gradient bar or discrete swatches) and a size legend.
Legend layout is measured in millimetres: each guide is sized to its content (a title line above one row per key), the row pitch is driven by the key’s drawn size so large bubble keys never overlap, and the guide block is centred in the legend track. Titles sit directly above their keys, horizontal (top/bottom) legends pack keys tightly instead of spreading them across the full width, and a vertical legend uses the full figure height.
Legend keys match the mark: a colour legend draws the glyph the plot actually uses — a filled circle for point layers, a short line for line-like layers (line/step/smooth/segment/…), and a filled square for bar/area/tile/ polygon layers — instead of always a square swatch.
Legend geometry is themeable:
theme(legend.key.size=, legend.spacing=, legend.margin=)set the key/swatch side, the gap between stacked guides, and the inset around the legend block (all in millimetres).Continuous colour bars now carry white break ticks aligned to their labels, on both vertical and horizontal (top/bottom) legends.
Merged legends: mapping one variable to two aesthetics draws a single legend whose keys carry both encodings — discrete
colour+shapebecome coloured shape keys; continuouscolour+sizebecome colour-graded, size-graduated points. Merging follows ggplot2’s rule (same title and breaks/levels); give one scale a differentname=to keep them separate.Layering: multiple marks on one panel, with scales trained across all layers.
Faceting:
facet_wrap(~var)andfacet_grid(rows ~ cols)split the data into a panel grid with facet strips and aligned, shared axes.Scale resolution (
resolve_scale()/facet_*(scales=)): position scales are shared across panels by default; opt into"free_x"/"free_y"/"free"(independent per panel) for per-panel ranges and axes.Statistical transforms:
mark_histogram()(bin a continuous variable into count bars) andmark_smooth()(an"lm"fit drawn as a line with an optional confidence ribbon).mark_bar()with noyuses the count stat. Map computed variables withafter_stat(), e.g.y = after_stat(density).Position adjustments (
position =): grouped bars stack by default;"dodge"places them side by side and"fill"normalises each group to 1.mark_point(position = "jitter")spreads overlapping points.Datashading:
mark_datashade()aggregates a large point cloud into a density raster (viavellum::datashade()) that fills the panel — cost independent of point count.mark_point(auto = TRUE)switches to this automatically above ~50k rows.More marks: areas/ribbons (
mark_area(),mark_ribbon()), steps (mark_step()), intervals (mark_errorbar(),mark_linerange(),mark_segment()),mark_boxplot(), tiles/heatmaps (mark_tile(),mark_raster()), 2-D binning (mark_bin2d(),mark_hex()),mark_text()/mark_label(), and pie/donut shortcuts (mark_pie(),mark_donut()).More statistical transforms:
mark_density()(kernel density),mark_summary()(aggregateyper category), in addition to the histogram / binning / smooth stats. Map computed variables withafter_stat().Coordinate systems:
coord_cartesian()(view-window zoom),coord_flip(),coord_fixed()/coord_equal()(aspect lock), andcoord_polar()(pie / coxcomb / radar).Themes:
theme_gray()(default),theme_minimal(),theme_bw(),theme_classic(),theme_void(), andtheme()for ad-hoc overrides (panel background, gridlines, text, strip background, legend position, margins).Effects & paints: stroked and point marks take a layer
effects = list(...)argument —glow()(neon halo),outline()(contrasting halo for legibility), andshadow()(drop / ambient).linear_gradient()/radial_gradient()can be used as afillvalue (area / ribbon / bar), andtheme_cyberpunk()ties glow + gradients + a neon palette into the mplcyberpunk look.Hand-drawn (“sketch”) rendering:
sketch()(re-exported from vellum) gives any geometry mark a wobbly, hachure-filled Rough.js look via asketch =argument. Unlike a layer effect it is a geometry property, generated natively in the engine, so it is exact, cross-backend, and works in PDF. It rides three levels, most-specific-wins: a mark’ssketch =, anelement_line()/element_rect()sketch =slot, or the plot-widetheme_sketch()one-liner (hand-drawn gridlines, axes, ticks, marks and legend keys on a paper background).sketch = NAforces an element crisp; text is never sketched (pair a handwriting font).Composition:
hconcat(),vconcat(),concat(), andwrap_plots()arrange several plots on a grid; the aligned path lines up panel edges and can collect guides across sub-plots.inset()overlays a plot, andcompose_annotation()adds figure-level titles and auto-tags (A,B, …).Repeat:
repeat_()replicates a plot across a set of fields, zipping one or more encodings to produce a composition.Blend modes: marks take a
blend =argument (the CSSmix-blend-modeset —"multiply","screen","darken", …). The layer composites as one isolated group against the panel and earlier layers, e.g. two overlapping translucent layers under"multiply".Spatial:
mark_sf()draws the geometry column of ansfobject as a map layer (polygons / lines / points), withcoord_sf()to reproject to a target CRS and lock the map aspect ratio;scale_fill_binned()/scale_color_binned()bin a continuous fill/colour into discrete classes for choropleths.Network:
vgraph()starts a node-link diagram from anigraphgraph — it runs a layout (stress majorization by default, viagraphlayouts;"sparse_stress","backbone","fr","circle", … or a matrix/function), builds a node and an edge table, and locks the aspect with a void theme.mark_edges()draws edges (straight, batched; reciprocal/parallel edges offset off the centre line, self-loops nested, optionalarrow),mark_nodes()the vertices, andmark_node_text()the labels — with a fixed edges-under-nodes draw order. Edges are capped exactly at each endpoint’s node boundary (per vertex, at any resolution), so arrowheads land on the node edge.scale_edge_width()maps a weight to edge width with its own legend.igraph/graphlayoutsare optional (Suggests). See_docs/DESIGN-igraph.md.Output:
render_plot(plot, path);vellum::render(plot, path)andprint(plot)also work. The compiler is registered on vellum’sas_vellum_scene()seam.
Under the hood
-
Compiled-scene provenance (foundation for interactivity / accessibility / linked views): every emitted mark grob now carries a globally-unique, stable
id(surfacing asdata-vellum-idin SVG), and the compiler builds a serializable row-key / scale-ref table — one record per grob tying it back to the data rows and trained scales that produced it — carried on the compiled scene asattr(scene, "vellumplot_provenance"). Populated on every compile; additive metadata only (raster/PDF output is byte-for-byte unchanged). See_docs/DESIGN.md§4. -
Continuous integration: GitHub Actions run
R CMD check(R release/devel/oldrel on Linux + macOS, with the Rust toolchain thevellumbackend needs), including a nightly run againstvellum’smainto catch cross-layer breaks early, plus a pkgdown build.
Not yet implemented (planned)
Reactivity, 2-D contour stats, and the algebraic * / + layer combinators. Independent non-position (colour/size) scales across facets are not yet supported (those legends stay shared). On the network side, community hulls and alternative idioms (arc/matrix/hive) are deferred (see _docs/DESIGN-igraph.md).
