Skip to contents

vellumplot (development version)

Bug fixes

  • mark_area() now stacks. mark_area(position = ...) was silently ignored; it gains a position argument ("stack" default, plus "fill" and "identity"), so areas sharing an x with a mapped fill/color combine 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 fill on mark_label() colours the label background (previously it always fell back to white); a mapped or constant alpha is honoured per category/tick by mark_violin(), mark_ridgeline(), mark_contour(), mark_contour_filled() and mark_rug() (previously collapsed to one value or ignored), and mark_rug() also honours a mapped color.

  • British spelling works everywhere. hover_colour/selected_colour are now recognised as interactivity arguments (previously only the American spelling was), and mark_bin2d()/mark_hex() no longer add a default count fill when a British colour = is supplied.

  • vgraph() warns on a layout-column clash. A vertex/edge attribute named x/y (or xend/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 (unlike scale_*(trans=), which rescales the data and relabels in transformed space). Each of x/y takes a transform name ("log10", "sqrt", "identity") or a scales::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 under coord_trans().

  • mark_violin() and mark_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’s x variable along the top edge and of its y variable along the right edge, each sharing the scatter’s axis so they line up (the vellumplot analogue of ggExtra::ggMarginal()). Pick the distribution with type = "density" / "histogram", the edges with sides ("t", "r", "tr"), and the extent with size; group = TRUE splits each marginal by a discrete color/fill mapping in the matching palette. It reads x/y from 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. Previously y = 0 was 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 channels x, 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 one evenodd path_grob, lines into one NA-separated lines_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 declaring data_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 one evenodd rule, two same-fill features that overlap would cancel in the overlap region (the same property a self-overlapping MULTIPOLYGON already 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 mapped size or shape aesthetic whose data contains missing values now shows an “NA” key (as the colour scales already did). Also fixes a crash: NA in a shape mapping 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 and mark_contour_filled() fills the bands between them (coloured by level automatically). By default the field is a kernel density estimate (via MASS::kde2d()); map a z aesthetic to contour a supplied surface over a regular x/y grid instead. Contour tracing uses the isoband package (both isoband and MASS are 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 explicit breaks). 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: map label = 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 new labs(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, so render_plot() output carries role="img" + <title>/<desc> in SVG and a tagged Figure with Alt in 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 for color (previously the mapping was silently dropped and no colour scale was trained). Added scale_colour_*() aliases for the scale_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() gain nudge_x / nudge_y (in millimetres, +x right / +y up) 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()’s x / y offset 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

Marks & stats

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 an id that matches the grob’s data-vellum-id in SVG and the id column of vellum::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 rows resolve to the actual data rows an element draws rather than the whole layer. See the scene-contract vignette in vellum.

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 args data_id, tooltip, and hover_group (tidy-eval expressions). They flow into the vellum scene as per-element keys/metadata — data_id becomes the SVG data-key and both surface in vellum::scene_model() — the foundation a companion widget uses for hover/select/linking. A plot without them compiles and renders exactly as before. Applies to stat = "identity" marks (points, bars, tiles, segments, edges, hexbins, polar bars) and to mark_sf() — each sf feature (polygon/linestring/point) becomes an addressable, keyed element.

  • Per-element interaction styling: marks also accept hover_color and selected_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/shape scale 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, serializable PlotSpec.

  • Output resolution: vplot(dpi =) sets the authored resolution and render_plot(dpi =) overrides it per render, so an exported PNG’s pixel dimensions are width * dpi by height * dpi. Compositions inherit the first sub-plot’s dpi (or take an explicit concat(dpi =)).

  • Marks: mark_point(), mark_line(), mark_rule() (reference lines via xintercept / yintercept), and mark_bar() (uses explicit y heights, or counts rows per category when y is 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 and log10) with auto-trained domains and ggplot-style expansion; discrete (band) position scales are trained automatically for categorical x/y (bars); scale_color_continuous() (perceptual ramp), scale_color_discrete() (qualitative palette), and a trained size scale for a mapped size.

  • 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 + shape become coloured shape keys; continuous colour + size become colour-graded, size-graduated points. Merging follows ggplot2’s rule (same title and breaks/levels); give one scale a different name= to keep them separate.

  • Layering: multiple marks on one panel, with scales trained across all layers.

  • Faceting: facet_wrap(~var) and facet_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) and mark_smooth() (an "lm" fit drawn as a line with an optional confidence ribbon). mark_bar() with no y uses the count stat. Map computed variables with after_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 (via vellum::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() (aggregate y per category), in addition to the histogram / binning / smooth stats. Map computed variables with after_stat().

  • Coordinate systems: coord_cartesian() (view-window zoom), coord_flip(), coord_fixed() / coord_equal() (aspect lock), and coord_polar() (pie / coxcomb / radar).

  • Themes: theme_gray() (default), theme_minimal(), theme_bw(), theme_classic(), theme_void(), and theme() 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), and shadow() (drop / ambient). linear_gradient() / radial_gradient() can be used as a fill value (area / ribbon / bar), and theme_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 a sketch = 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’s sketch =, an element_line() / element_rect() sketch = slot, or the plot-wide theme_sketch() one-liner (hand-drawn gridlines, axes, ticks, marks and legend keys on a paper background). sketch = NA forces an element crisp; text is never sketched (pair a handwriting font).

  • Composition: hconcat(), vconcat(), concat(), and wrap_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, and compose_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 CSS mix-blend-mode set — "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 an sf object as a map layer (polygons / lines / points), with coord_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 an igraph graph — it runs a layout (stress majorization by default, via graphlayouts; "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, optional arrow), mark_nodes() the vertices, and mark_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 / graphlayouts are optional (Suggests). See _docs/DESIGN-igraph.md.

  • Output: render_plot(plot, path); vellum::render(plot, path) and print(plot) also work. The compiler is registered on vellum’s as_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 as data-vellum-id in 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 as attr(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 the vellum backend needs), including a nightly run against vellum’s main to 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).