p <- vplot(mtcars, width = 6.5, height = 4) |>
mark_point(x = wt, y = mpg, color = factor(cyl), size = 2.4) |>
labs(title = "Weight against mileage", x = "weight (1000 lbs)", y = "mpg",
color = "cylinders")The plot is a document
A vellumplot plot is a specification rather than a picture, and that says more than when the drawing happens. The spec is plain data: a nested list of encodings, scales, coordinates, facets, labels, and page size. So it can be written to a file, sent over a wire, diffed, generated by a program, and compiled somewhere else.
summary() reports the structure without drawing anything:
summary(p)
#> <PlotSpec> 32x11 (11 columns), page 6.5x4 in
#>
#> ── layers
#> • mark_point(x = wt, y = mpg, color = factor(cyl)) [size = 2.4]
#>
#> ── labels
#> • title: Weight against mileage
#> • x: weight (1000 lbs)
#> • y: mpg
#> • color: cylindersRound-tripping through JSON
spec_to_json() writes it out and spec_from_json() reads it back:
json <- spec_to_json(p)
substr(json, 1, 320) |> cat()
#> {
#> "$schema": "https://r-vellum.github.io/vellumplot/spec.json",
#> "version": "1",
#> "width": 6.5,
#> "height": 4,
#> "dpi": 96,
#> "data": {
#> "name": "data",
#> "hash": "33f1aa2876d0ad2a1232bcd25a9a350e",
#> "nrow": 32,
#> "columns": [
#> "mpg",
#> "cyl",
#> "disp",
#> "hp",
#> "drat",
#> "wt",
#> p2 <- spec_from_json(json)
identical(plot_manifest(p)$spec_hash, plot_manifest(p2)$spec_hash)
#> [1] TRUEThe spec hash is unchanged, so the recompiled plot is the same plot rather than one that resembles it.
What matters more than the round-trip working is what happens when it cannot work. The spec covers the subset of the grammar a portable document can carry faithfully. Anything outside it (a custom transform function, a pattern fill, hand-drawn sketch() geometry, a secondary axis, per-layer data) is refused with an error naming the exact slot, rather than dropped on the floor and discovered later by looking at the picture:
vplot(mtcars) |>
mark_bar(x = factor(cyl), fill = pattern_stripe()) |>
as_spec()
#> Error in `.values_to_ir()`:
#> ! Cannot serialize params$fill.
#> ✖ a vellum_pattern value is not serializable (paint/pattern/function/opaque
#> object).
#> ℹ The spec serializer supports a documented subset; see
#> `?vellumplot::as_spec()`.Small data is inlined. Larger data is stored by reference (name, content hash, column schema), and from_spec(spec, data = ) supplies it back at compile time.
Checking a spec before compiling it
spec_diagnose() validates a spec against a dataset and returns a verdict instead of throwing. Every referenced field is checked against the data, then the spec is dry-run compiled:
broken <- sub('"field": "wt"', '"field": "weihgt"', json, fixed = TRUE)
d <- spec_diagnose(broken, data = mtcars)
d$ok
#> [1] FALSE
str(d$diagnostics, max.level = 2)
#> List of 1
#> $ :List of 4
#> ..$ severity: chr "error"
#> ..$ field : chr "weihgt"
#> ..$ message : chr "Unknown field 'weihgt'."
#> ..$ hint : chr "Available fields: mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb."A misspelled field comes back as a record giving the severity, the offending field, and the fields that do exist, rather than as a traceback. Whatever wrote the spec, a person or a program, gets told what to fix. vplot_from_spec() is the strict form, raising a classed error that carries the same diagnostics.
Handing it to another grammar
Vega-Lite is also a layered grammar of graphics, so most of a spec maps across directly. This plot does not translate cleanly, and the export says so:
vl <- spec_to_vegalite(p)
#> Warning: Vega-Lite export dropped 1 unsupported feature:
#> ✖ expression channel 'factor(cyl)'
vl$mark
#> $type
#> [1] "point"A Vega-Lite field is a column name, and color = factor(cyl) is an R expression evaluated at compile time, so there is nothing on the other side to point the channel at. Materialise the expression as a column and the same export goes through without a word:
cars <- transform(mtcars, cyl = factor(cyl))
vl2 <- vplot(cars, width = 6.5, height = 4) |>
mark_point(x = wt, y = mpg, color = cyl, size = 2.4) |>
spec_to_vegalite()
str(vl2$encoding, max.level = 2)
#> List of 4
#> $ x :List of 1
#> ..$ field: chr "wt"
#> $ y :List of 1
#> ..$ field: chr "mpg"
#> $ color:List of 1
#> ..$ field: chr "cyl"
#> $ size :List of 1
#> ..$ value: num 2.4That is the rule at this boundary too: what the bridge cannot express is reported rather than quietly dropped. Some things have no equivalent at all to materialise. A polar coordinate system is one:
vplot(mtcars) |>
mark_bar(x = factor(cyl)) |>
coord_polar() |>
spec_to_vegalite() |>
names()
#> Warning: Vega-Lite export dropped 2 unsupported features:
#> ✖ expression channel 'factor(cyl)' and coord 'polar'
#> [1] "$schema" "data" "mark" "encoding"The spec is data you can inspect, store, and program against, and every place it loses something it says so. See A figure that carries its own provenance for the hashing side of the same substrate.
