API Reference
The full set of exports, organised by use case. For walkthroughs see Getting Started, Translation, Gallery, and the FAQ.
Building specs
The same algebra as AlgebraOfGraphics — data * mapping * visual composed with * (intersection) and + (union).
AlgebraOfVega.config Function
config(; kwargs...)Create a Config with Vega-Lite properties. Common options:
width,height,title— spec dimensions and titleencoding— deep-merged with auto-generated encodings (add aggregate, scale, axis, etc.). ADictor the equivalent NamedTuple spelling:encoding=(; x=(; scale=(; type="log"))). On one summand of a+sum it applies to that summand's layers only (a * config(encoding=E) + b, e.g. a tooltip fora's marks); apply it to the whole sum,(a + b) * config(encoding=E), to reach every layer.row/column/facetoverrides always configure the shared facet.config— raw Vega-Lite top-levelconfig(aDictor NamedTuple), deep-merged into AoV's defaults rather than replacing themparams,transform— VL interactivity parameters and data transformsselect— field(s) for client-side dropdown filtering (e.g.select=:origin)frame— animate the figure over a column's values, one frame at a time, with a play/pause button and a slider (e.g.frame=:year); the long form isframe=(; field=:year, interval=0.5, loop=false, start=:first). Frames play in sorted order, or in asorterorder (frame=:month => sorter("Jan", "Feb", …)). Colour, size and x/y scales, and a map's projection, stay fixed across frames. Interactive path only.scales— an AoGscales(...)object, mirroringdraw(spec, scales(...)). Currently translates X/Y/Zscale=log/log2/log10/sqrt/symlog/identityto VL scale types. Example:config(scales=scales(Y=(; scale=log10))). Usescale=symlogfor a symlog axis (linear near 0, log beyond — keeps genuine zeros a plainlogaxis cannot render), optionally withconstant=to tune the linear-region half-width (VL default 1):config(scales=scales(X=(; scale=symlog, constant=2))).facet— an AoG-style NamedTuple passed todraw(; facet=...).linkxaxes=:none/linkyaxes=:nonetranslate to VLresolve.scale.<axis>="independent". Example:config(facet=(; linkxaxes=:none, linkyaxes=:none)).axis— an AoG-style NamedTuple passed todraw(; axis=...).limits=((xlo, xhi), (ylo, yhi))(each side may benothing) translates to VLencoding[x/y].scale.domain;clamp=trueadditionally setsscale.clamp=trueon the limited axes (VL-only extension, stripped on the Makie path). Example:config(axis=(; limits=((1, 100), nothing), clamp=true)).independent_scales— deprecated: usefacet=(; linkxaxes=:none, linkyaxes=:none).
Config is applied to a spec via *: data(df) * mapping(:x, :y) * visual(Scatter) * config(width=500).
Successive terms accumulate: spec * config(a...) * config(b...) keeps every property of both. encoding, config, facet, axis and scales merge key by key (encoding and config recursively, scales scale by scale), with the later term winning on a conflicting key; any other property (width, title, params, transform, …) takes the later term's value.
| Function | Use |
|---|---|
data(df) / vdata(df) | Wrap a tabular source. vdata is an alias for use when data clashes with a local field. |
mapping(:x, :y; color=:g) | Map columns to channels (x, y, color, linestyle, row, col, …). |
visual(Mark; kwargs...) | Pick the mark type (Scatter, Lines, BarPlot, …) and per-mark properties. |
dims(i) | Use the i-th group/dim as a channel value (AoG passthrough). |
pregrouped(...) | Pass a pre-grouped data source (AoG passthrough). |
Marks (re-exported from Makie/AoG)
Scatter, Lines, ScatterLines, BarPlot, Heatmap, BoxPlot, Band, HLines, VLines, Hist, Errorbars, Stairs, Contour, Violin, RainClouds, Rangebars, CrossBar, ECDFPlot.
Statistical transforms (re-exported from AoG)
density, histogram, linear, smooth, expectation, frequency.
Scale modifiers (re-exported from AoG)
renamer, sorter, nonnumeric, verbatim, presorted, direct, scale, scales. See AoG's scale-modifier docs for behaviour.
Rendering and output
AlgebraOfVega.to_node Function
to_node(spec; id, width, height, actions, signals)Convert a spec to an HTMX Node containing a div + script that calls vegaEmbed. Requires vega/vega-lite/vega-embed scripts to be loaded (use vega_head() in page head).
Keyword arguments
id: element ID (auto-generated if not provided)width,height: override spec dimensionsactions: show Vega action links (default false)signals: vector of signal→HTMX wirings. Each entry is a NamedTuple or Dict:(signal="brush", url="/on_brush", target="#detail")or with optionalswap,debounce.fit_width: whentrue(default) setwidth: "container"for single-view specs and apply numeric responsive widths for layered/faceted specs. An explicitwidth(kwarg orconfig(width=…)) orfit_width=falsekeeps the authored width: the runtime then neither resizes the plot to its container nor re-embeds it when the container resizes.
AlgebraOfVega.to_html Function
to_html(spec; id, width, height, source, base)Return a standalone HTML string with embedded vega-embed. source selects the script origin — :cdn (default), :vendor, or :inline, as in vega_head. source=:inline inlines AoV's vendored Vega builds, so the returned page renders with no network access at all.
to_html(node::HTMX.Node; texts=plot_texts(), title=texts.untitled, head_extra="", source, base, runtime) -> StringSerialize a rendered plot node (e.g. to_node, auto_remap_node, or a with_plot_caption fragment) as ONE standalone .html document string: <!DOCTYPE html> + <head> + <body>.
The <head> is the exact vega_head() set — Vega/Vega-Lite/Vega-Embed scripts plus the inlined window.AoV.* runtime — rendered by construction from vega_head() itself, so versions can never drift. source selects the script origin (:cdn default, :vendor, :inline) and runtime how AoV's runtime is loaded (:inline default, :linked), as in vega_head; source=:inline inlines the vendored builds, so the saved file renders with no network at all. head_extra appends additional rendered head HTML (the with_plot_caption methods use it for caption CSS + table sorting). The node body carries picker controls, embed scripts, and inlined spec/data JSON, so the saved file keeps working with no server: picker re-facets, CSV/PNG/SVG download, caption/summary render. Only signals=-wired plots degrade (their htmx.ajax callback has no server), and they do so silently — see signalToHtmx.
texts (a PlotTexts) sets the page's <html lang> and the title used when title is not given; pass the same value as the node's own texts= (plot_texts(:de) for a German page).
AlgebraOfVega.to_json Function
to_json(spec; interactive=true, auto_remap=nothing, kwargs...) -> StringConvert a spec to a Vega-Lite JSON string. Non-finite floats (NaN, ±Inf) are written as null, which Vega-Lite renders as a gap. interactive and auto_remap select the lowering exactly as in to_vegalite; the other kwargs pass through to JSON.json.
AlgebraOfVega.to_vegalite Function
to_vegalite(spec; interactive=true, auto_remap=nothing) -> Dict{String,Any}Convert an AoG Layer, Layers, or VegaSpec to a Vega-Lite JSON dictionary. Handles all translation: mark types, encodings, statistical transforms, config merging, and, by default, auto-interactivity. Set interactive=false to suppress only the parameters generated automatically by AoV; explicit config(params=...) and config(select=...) remain in the output.
For a plot rendered through the channel picker, pass the picker's keywords as auto_remap — the same (; dims, fixed, pinned, off, …) given to auto_remap_node, with_plot_caption(...; auto_remap=…) or update_spec. The result is the Vega-Lite that picker embeds before the reader changes anything (resolved channels, combined facet columns, broadcast facet fields), without building the picker controls or a plot node — e.g. for a JSON, markdown or static export of that figure. An already-lowered Vega-Lite Dict cannot be remapped: it no longer carries the layer tables.
to_vegalite(spec, scales; interactive=true, auto_remap=nothing) -> Dict{String,Any}Mirror of AlgebraOfGraphics.draw(spec, scales(...)): lower spec (a Layer, Layers, or VegaSpec) to a Vega-Lite dict, then apply the AoG Scales override to it. This is the second-positional-argument form; config(scales=scales(...)) applies the identical override inline.
Handles X/Y/Z axis scales (log/log2/log10/sqrt/symlog + nice/zero/domain/ clamp/constant) and a categorical/continuous Color scale — palette (explicit colours or a named scheme), categories (domain/group ORDER), colormap (named scheme), colorrange (continuous value range; values outside it take the colormap's end colours, or lowclip/highclip), scale (log/sqrt/symlog) and nan_color. Any other AlgebraOfGraphics colour attribute warns that it is not lowered, and a key AoG rejects (including clamp) is an ArgumentError. The Color override is merged only into colour encodings that carry a field, so a layer with a deliberate field-less colour (e.g. a pointinterval() median dot) is never given a bare, VL-dropped colour encoding.
AlgebraOfVega.sdraw Function
sdraw(spec, path::AbstractString; kwargs...)Render an AoG spec via AlgebraOfGraphics.draw → Makie → file. Saves the figure to path (format inferred from extension) and returns an h.img node pointing to path.
Requires a Makie backend (e.g. CairoMakie) to be loaded.
Extra kwargs are passed to Makie.save (e.g. px_per_unit=2 for retina).
AlgebraOfVega.sdraw_file Function
sdraw_file(spec, path::AbstractString; kwargs...)Render an AoG spec to a file via AlgebraOfGraphics.draw → Makie.save. Returns the path. Extra kwargs are passed to Makie.save.
AlgebraOfVega.sdraw! Function
sdraw!(position, spec)Render an AoV spec into an existing Makie figure, layout position, or axis via AlgebraOfGraphics.draw!. This is the static composition counterpart to sdraw_file: create a Figure, place independently configured specs with sdraw!(fig[row, col], spec), then save the complete figure with Makie.save.
Each spec keeps its own config(axis=...), config(facet=...), and config(scales=...) settings. config(width=...) and config(height=...) are standalone-figure settings and are therefore ignored when drawing into an existing layout. mapping display labels (:x => "Label"), including lineribbon/ribbon(bands=...) x/y/colour/facet labels, are preserved on the rebuilt static layers.
Like AlgebraOfGraphics.draw!, this does not add legends or colorbars — call AlgebraOfGraphics.legend!(position, grid) with the returned grid afterwards (see aov-use §1a for the composition pattern).
Requires a Makie backend (e.g. CairoMakie) to be loaded. Returns the result of AlgebraOfGraphics.draw!.
AlgebraOfVega.vlspec Function
vlspec(drawable; kwargs...)Wrap an AoG drawable in a VegaSpec with optional config properties. Equivalent to drawable * config(; kwargs...).
| Function | Output |
|---|---|
to_node(spec) | An HTMX.Node — embed in HTMX/Oxygen apps |
vdraw(spec) | Alias for to_node — use when you want a "draw"-shaped name without clashing with AoG's draw |
to_html(spec) | A standalone HTML string with the Vega tags embedded (source=:inline for fully offline) |
to_json(spec) | Pretty-printed Vega-Lite JSON |
to_vegalite(spec; interactive=true, auto_remap=nothing) | Raw Vega-Lite spec as a Dict{String,Any}; pass interactive=false to omit AoV-generated parameters, and a channel-picker figure's auto_remap keywords to get the Vega-Lite that picker embeds |
sdraw(spec, path; kwargs...) | Save through AoG/Makie and return an HTMX.Node image pointing to path |
sdraw_file(spec, path; kwargs...) | Save through AoG/Makie and return path; keywords pass to Makie.save |
sdraw!(position, spec) | Draw into an existing Makie figure/layout position for static composition |
The static functions require a loaded Makie backend such as CairoMakie. Compose panels with independently configured axes by drawing each spec into its own layout position and saving the parent figure:
using CairoMakie
fig = Figure(size=(900, 400))
sdraw!(fig[1, 1], mean_spec)
sdraw!(fig[1, 2], sd_spec) # may carry its own log-y scale
save("adaptive-centering.png", fig; px_per_unit=2)interactive=false disables only automatic zoom, legend, and nearest-point parameters. Explicit config(params=...) or config(select=...) remains explicitly requested output.
A VegaSpec also has Base.show(io, MIME"text/html"(), spec) and Base.show(io, MIME"application/vnd.vegalite.v5+json"(), spec) methods, so you can render it implicitly via any host that picks a MIME type (Pluto, IJulia, HTMX response handlers, …) without calling any of the functions above.
Vega / HTMX runtime
AlgebraOfVega.vega_head Function
vega_head(; vega_version, vegalite_version, vega_embed_version, source, base, runtime, zoom, max_width, actions, theme)Return a vector of h.script/h.style/h.link nodes to include in htmx(; extra_head=vega_head()).
source selects where the Vega/Vega-Lite/Vega-Embed scripts come from: :cdn (default) emits exact-pinned CDN tags with subresource integrity; :vendor emits same-origin <base>/vega.min.js?v=<content hash> tags — serve vega_vendor_dir() at base from your app; :inline inlines the vendored bytes. Version overrides apply only to :cdn.
runtime selects how AoV's own runtime and stylesheet (~96 KB) reach the page. :inline (default) inlines them. :linked — which needs source=:vendor — references <base>/aov-runtime.js and <base>/aov.css from the same mount instead, so the browser caches them rather than receiving them in every full page. Every :vendor URL carries a ?v= content hash that changes exactly when the file's bytes do, so the mount may serve them with a far-future immutable cache lifetime. The page settings below (zoom, max_width, actions, theme) stay an inline per-page script either way.
zoom uniformly scales all plots (chart area, fonts, axes, legend). Responsive plots are sized to containerWidth / zoom so they don't overflow their container.
max_width caps plot width at max_width px: a plot in a wider container is sized as if the container were max_width px (layered/faceted specs), or fills at most max_width px of its container (single-view specs). A per-plot config(max_width=...) overrides this page-level value for that plot.
theme selects how plots follow the page's light/dark choice. :host (the default) draws axes, legends, facet headers, titles and unencoded text in the CSS color each plot element inherits, on a transparent background, and re-renders the plots when the page's colour scheme changes (a prefers-color-scheme change, or a class / data-theme / style change on <html> or <body>). Authored palette and literal mark colours are unchanged; visual(...; color=:ink) uses the plot's inherited foreground colour instead. A spec's own config(config=Dict(...)) values still win. PNG/SVG downloads of such a plot are painted on the page background behind it. :none keeps Vega's own defaults (white background, black text). A per-plot config(theme=...) overrides this page-level value for that plot.
AlgebraOfVega.vega_runtime Function
vega_runtime()Return a h.script node with the AlgebraOfVega JS runtime, inlined. Its source is the file aov-runtime.js in vega_vendor_dir(), which vega_head(source=:vendor, runtime=:linked) references by URL instead. Manages Vega views by ID and provides helpers for HTMX integration.
Client-side API:
AoV.views[id]— access Vega views by element IDAoV.embed(id, spec, opts)— embed and register a view; calling it again for the sameid(e.g. with a spec that has more layers) replaces the viewAoV.whenReady(id, fn)— callfn(view)now, or once the view has been embeddedAoV.updateData(id, data)— swap a view's data without re-creating itAoV.appendData(id, data, name, maxRows)— insert rows into a view's data, optionally keeping only the most recentmaxRowsAoV.replaceData(id, data, key, name)— replace the rows of the groupsdatacarries (rows whosekeyfield values match a row ofdata), keeping the other groups' rowsAoV.removeData(id, values, key, name)— remove one explicitly named keyed group without replacement rows.datais a row array or the columnar{n, columns}formupdate_data/append_data/replace_datasend;AoV.embed/AoV.updateSpecexpand inline datasets in that form too (AoV._rowsFromColumns)AoV.onSignal(id, signal, callback)— listen to a Vega signalSignal→HTMX wiring is set up automatically by
to_node(; signals=...)aov:click— a DOM event every plot element dispatches (bubbling) when the reader clicks a data mark, on every view the runtime embeds for it;event.detailis{id, datum, event}: the plot id, a copy of the clicked mark's row, and the browser's mouse event. Legend, axis, title and empty-space clicks send none. Listen withdocument.addEventListener('aov:click', …)or htmxhx-trigger="aov:click from:#<id>"AoV.refreshTheme()— re-render host-themed plots whose inherited text colour changed (automatic onprefers-color-schemeand<html>/<body>class,data-themeorstylechanges; call it after other theme switches)AoV.hostThemeConfig(el)/AoV.hostBackground(el)— the Vega-Lite config a host-themed plot inelembeds with, and the background its image downloads useAoV.dispose(id)— tear a plot down: finalize its Vega view and drop all per-plot runtime state (DOM untouched)AoV.disposeWithin(root)— dispose every plot whose element isrootor inside it (works on detached subtrees too)
A Vega view registers window/document listeners (width: "container" resize, zoom/pan drags, the actions menu) that keep it — its data, scenegraph and canvas — reachable until it is finalized, so the runtime finalizes it. A plot whose element leaves the document is disposed automatically: one MutationObserver sweeps the registry after DOM removals, once the removing script yields. A node removed and re-inserted synchronously stays live, and a same-id re-render already embedded into its new element is untouched; a plot detached and re-attached later is not revived (re-run its embed). dispose finalizes through vega-embed's own finalize, as does every re-embed (View.finalize alone leaves the actions menu's document listener, which keeps the replaced view alive). Only the latest embed of a plot registers: one superseded by a newer embed or by dispose is finalized when it resolves, and resize/fit re-embeds scheduled by a superseded embed are inert.
Plots whose first render has an empty dataset (the incremental-plot pattern) hide legends bound to empty scale domains until the first data change: Vega renders an empty legend as a zero-item group whose inverted bounds collapse the whole canvas to 0x0. The first appendData/updateData re-embeds once with the accumulated rows, restoring the legend bound to the real domain.
A coloured lineribbon/ribbon draws each colour group as its own layers (tagged _lr_group; an empty first render has _lr_proto template layers instead), so the groups paint in order. A data change whose rows bring a group without layers re-embeds once with layers for every group of the rows; rows of groups that already have layers are swapped into the live view.
AoV.embed sizes the plot element of a width: "container" spec to its container itself, so single-view plots fill their container on any page — without it vega-embed's display: inline-block shrink-wraps the element and the view measures 0px wide wherever no page stylesheet widens it.
AlgebraOfVega.update_data Function
update_data(id, table; name="source_0")Return an h.script node that replaces an existing Vega view's dataset with the rows of table, verbatim. Useful for HTMX responses that should update a plot without re-rendering.
Raw rows only fit a plot whose embedded dataset is raw rows. A plot that AoV lowered server-side — an interval/ribbon summary (pointinterval, lineribbon, …), a faceted density, a merged multi-layer dataset, picker combo columns — embeds derived rows, so refresh it with update_spec instead. The runtime refuses (console error, data left unchanged) when the new rows lack a field that its current rows carry and that places or selects the plot's marks (a positional/facet channel field or a filter field — e.g. a summary's lo_*/hi_* bounds or a merged dataset's __src).
AlgebraOfVega.append_data Function
append_data(id, table; name="source_0", max_rows=nothing)Return an h.script node that inserts the rows of table into an existing Vega view's dataset, keeping the rows already there; scales and axes grow to fit. With max_rows, only the most recent max_rows rows are kept (a sliding window).
table is any Tables.jl source, read column by column (lazy columns stay lazy) and sent in a compact columnar form: a regular coordinate as a sequence, a constant column once, sorted labels as runs, other columns as flat arrays. The runtime rebuilds the identical rows.
For plots whose data arrives incrementally, e.g. one append_data fragment per chunk pushed over an HTMXObjects @ws route or returned by an HTMX poller. Calls that arrive before the view has finished embedding are applied once it is ready, and appended rows survive the view's responsive re-embeds.
Like update_data, this inserts raw rows: refresh an AoV-lowered plot (interval/ribbon summaries, merged multi-layer datasets) with update_spec, which recomputes the lowering.
AlgebraOfVega.replace_data Function
replace_data(id, table; key, name="source_0")Return an h.script node that replaces, in an existing Vega view's dataset, the rows of the groups table carries: every row whose key values (one column, or a vector of columns for a composite key) equal those of some row of table is removed, and the rows of table are inserted, in one change. Rows of every other group stay as they are, so a group that is refined in stages — one scenario's provisional posterior bands, re-sent as its draws accumulate — costs only its own rows on the wire and in the view (sent in the same columnar form as append_data):
vdraw(data(rows) * mapping(:x, :y; color=:scenario) *
lineribbon(bands=[:lower => :upper]); id="bands")
replace_data("bands", scenario_rows; key=:scenario) # one scenario's rows, each stageA group that table does not carry cannot be removed this way; send update_data for that. Like append_data/update_data, the rows survive the view's responsive re-embeds, calls arriving before the view has embedded are applied once it is ready, and raw rows are refused for a dataset AoV lowered server-side (refresh those with update_spec). A coloured lineribbon/ribbon draws each colour group as its own layers; a group first brought by a data change gets its layers then (the plot re-embeds once) — except in a ribbon layered with other layers, which logs a console warning instead: send update_spec when a group first appears there.
AlgebraOfVega.update_spec Function
update_spec(id, spec; auto_remap=nothing, width, height, actions, fit_width)Return an h.script node that refreshes the existing plot id (created by to_node, vdraw, auto_remap_node or with_plot_caption) from a new spec — new data, an additional layer, or both. spec is lowered server-side exactly like a first render, so analysis summaries, merged multi-layer datasets and facet densification are recomputed from the new raw rows: this is how an AoV-lowered plot (pointinterval, lineribbon, a faceted density, …) is refreshed as data arrives.
For a plot rendered through the channel picker, pass the picker's keywords as auto_remap — the same (; dims, fixed, pinned, off, …) given to auto_remap_node (or with_plot_caption(...; auto_remap=…)) — so the new spec is lowered through the same channel resolution.
The reader's state carries over: the picker's current channel assignment is re-applied to the new spec, and when only the data changed the view's datasets are swapped in place, keeping zoom/pan, legend selection and the canvas. A structural change (new facet panels, a new layer, changed encodings or size) re-embeds the plot in place. Signal listeners wired via to_node(; signals) carry over either way; rows added earlier by append_data/update_data are replaced by the new spec's data.
| Helper | Purpose |
|---|---|
vega_head() | The head tags for Vega/Vega-Lite/Vega-Embed plus AoV's runtime and stylesheet — drop in your <head> (source=:cdn/:vendor/:inline; runtime=:linked with :vendor loads AoV's runtime by cacheable URL too; theme=:host/:none, see the FAQ's light/dark section) |
vega_runtime() | The AoV JS runtime, inline — handles signal binding, update_data, etc. (source: aov-runtime.js in vega_vendor_dir()) |
vega_controls() | Optional HTML controls block (legend toggles, view reset, …) |
vega_cdn_urls() | The exact-pinned CDN URLs (Vega, Vega-Lite, Vega-Embed in order) |
vega_sri_hashes() | The subresource-integrity hashes for the pinned CDN URLs, in the same order |
vega_vendor_dir() | Directory holding the vendored trio and AoV's runtime/stylesheet — serve it from your app for source=:vendor |
update_data(id, new_rows) | Replace a rendered plot's raw rows by id (HTMX server-side handler returns update_data(...)) |
append_data(id, rows; max_rows) | Add rows to a rendered spec by id, keeping the existing ones (optionally a sliding window of max_rows) |
replace_data(id, rows; key) | Replace the rows of the groups rows carries (same key column values), keeping every other group's rows |
update_spec(id, spec; auto_remap) | Refresh a rendered plot from a new spec — new data and/or layers — re-lowered like a first render; keeps the picker assignment, and zoom when only data changed |
Incremental plots
Plots whose data or layers arrive over time are rendered once with a stable id and then updated by fragments pushed from the server. With an HTMXObjects @ws route and htmx's WebSocket extension, each message is an HTML fragment that htmx swaps in by id:
# In the page (with the htmx-ext-ws script loaded):
to_node(spec; id="live-plot"),
h.div(; hx_ext="ws", ws_connect="/feed")(h.div(; id="live-sink")),
# In the @htmx struct:
@ws feed() = for chunk in chunks
fragment = h.div(; id="live-sink")(append_data("live-plot", chunk))
HTTP.WebSockets.send(__ws__, repr(MIME"text/html"(), fragment))
endSend update_spec("live-plot", new_spec) instead to add layers. Rows added with append_data/update_data/replace_data survive the plot's responsive re-embeds; a new spec from update_spec brings its own data. The gallery's Streaming Data and Layers One by One demos show both.
When one group of the rows is refined in stages — one scenario's provisional bands, re-sent as its draws accumulate — send only that group's rows with replace_data(id, rows; key=:scenario): the rows whose key values rows carries are replaced and the other groups' rows stay in the view (key may be a vector of columns). A coloured lineribbon/ribbon draws each colour group as its own layers; a group that a data change brings first gets its layers then (the plot re-embeds once, resetting zoom), so an empty first render can be filled group by group. A ribbon layered with other layers (lineribbon(...) + visual(Scatter)) cannot grow layers this way and logs a console warning: send update_spec when a group first appears there.
The rows travel column by column, not as one JSON object per row: a regular coordinate (a range such as range(0, 28, length=6721), also repeated once per series in a long table) is sent as three numbers, a constant column (a series label) once, sorted labels as runs, and every other column as one flat array. The runtime rebuilds the same rows with the same values — a coordinate is sent as a sequence only when the browser's arithmetic reproduces every value exactly. Any Tables.jl source works, lazy columns included (a Fill label, a range, a TreeData reduction); nothing builds a row table on the server. The inline datasets to_node/update_spec embed use the same form. For a one-series time course of 6721 points this is 126 KB instead of 454 KB.
append_data/update_data insert rows verbatim, so they fit only plots whose embedded dataset is raw rows. A plot AoV lowered server-side — an interval or ribbon summary (pointinterval, lineribbon, …), a faceted density, a merged multi-layer dataset — is refreshed from new raw rows with update_spec, which recomputes the lowering. For a channel-picker plot pass the picker's keywords as auto_remap (the (; dims, fixed, pinned, off) given to auto_remap_node or with_plot_caption):
update_spec("ladder-plot", data(new_rows) * mapping(:value; y=:model, color=:family, col=:metric) *
pointinterval(); auto_remap=(; dims=["family" => "Family"], fixed=Dict(:column => "metric")))The reader's picker assignment is re-applied to the refreshed spec. When only the data changed, the new datasets are swapped into the live view (zoom/pan, legend selection and canvas survive); a structural change — new facet panels, layers, encodings or size — re-embeds the plot in place. Raw rows that lack a field placing or selecting the plot's marks (e.g. a summary's lo_*/hi_* bounds or a merged dataset's __src) are refused with a console error and the plot is left unchanged.
To export a channel-picker figure without the picker — as JSON, in a markdown report, or through a static Vega-Lite renderer — lower it with the same keywords: to_vegalite(spec; auto_remap=remap) (or to_json(spec; auto_remap=remap)) returns the Vega-Lite the picker embeds before the reader changes anything, without building the picker controls or a plot node.
Dynamic dashboard helpers
For interactive dashboards where the user picks columns, channels, or facets at runtime. auto_remap_node is the high-level entry point; the others are the building blocks.
AlgebraOfVega.auto_remap_node Function
auto_remap_node(plot_id, spec; dims, fixed=Dict(), pinned=:row, axes=false, texts=plot_texts())Fully automatic interactive plot. Takes a "normal" AoG spec and produces an h.div(picker, plot_node). The user does not call resolve_channels, refine_channels, apply_combos!, mapping_controls, or to_node; this function does all of it.
How channel info is sourced
Defaults are read from each layer's existing
.namedmapping. If a layer saysmapping(:x, :y; color=:source, row=:study), thencolor_default = "source"androw_default = "study"automatically.dimsis the user-supplied list of remappable dimensions, asVector{Pair{String,String}}(field => human label). Labels here are the single source of truth for picker labels and combo titles.fixedassigns fields to channels that the picker shouldn't expose as remappable. Same shape as inresolve_channels. Labels for fixed fields are picked up fromdimsif present, otherwise snake_case → Title Case.pinnedis the catch-all channel.textsis thePlotTextswhose channel labels the picker shows (default English;plot_texts(:de)for German).axes=trueopts in to X/Y axis channels in the picker. The layer's positional fields (mapping(x, y, …)) are automatically added to the picker's dim set — they become X/Y defaults AND are selectable on the other channels. Off by default because X/Y are single-select (no combo semantics) and usually the positional fields are not meant to be re-routed. When any layer carries aTidybayesAnalysis, X/Y stay hidden regardless ofaxes— analyses bind axes to computed columns.
What it does internally
Walks
specto enumerate layers and their dataframes.Reads channel defaults from layers' existing mappings.
Calls
resolve_channels(dims; defaults..., fixed, pinned).refine_channels(resolved, dfs...)— strips dims absent or single-valued across every layer.For each layer's df: cartesian-product broadcasts the df across the union of unique values for every row/column field it doesn't have,
fixed=facet fields included (so partial-overlap layers like observation scatters land in every relevant pred panel), thenapply_combos!builds__aov_row/etc. Color fields never broadcast: a layer missing the color field keeps its rows and gets no color encoding instead of fabricated copies.Rebuilds each layer with the new (broadcast) df and resolved channel kws merged into its mapping. lineribbon/pointinterval/etc. detail is patched on the transformation.
Reassembles layers via
+, preserves the originalVegaSpecconfig, and wraps withmapping_controls+to_nodein anh.div.
Example
spec = data(filtered_pred) * mapping(:dose_mg => "Dose (mg)", :qoi => "";
group=:draw, color=:source, row=:study) * lineribbon() +
data(obs_rows) * mapping(:dose_mg => "Dose (mg)", :qoi => "") *
visual(Scatter; size=30, opacity=0.8)
spec *= config(height=300, facet=(; linkxaxes=:none, linkyaxes=:none))
auto_remap_node("dose-response-plot", spec;
dims=["source"=>"Source", "study"=>"Study", "outcome"=>"Outcome", "method"=>"Method"],
fixed=Dict(:column => "assay"),
pinned=:row)AlgebraOfVega.mapping_controls Function
mapping_controls(id, dimensions; kwargs...)
mapping_controls(id, resolved::NamedTuple; table=nothing, spec=nothing)Client-side multi-select picker that remaps Vega-Lite encoding channels on an already-rendered plot. Each channel (color, row, column, ungrouped/detail) is a multi-select; selecting 2+ fields for one channel synthesises a combo column on spec.data.values. One channel is always "pinned" — the catch-all that auto-absorbs any dims not in another channel.
The first form (vector of dimensions) calls resolve_channels under the hood; the second form accepts a pre-resolved NamedTuple (e.g. from refine_channels) for callers that need to inspect / mutate the resolution before rendering.
Keyword arguments
id: must match theidkwarg passed toto_node(spec; id=...)dimensions: vector ofPair{String,String}(field => label) or bare strings/symbolscolor_default,row_default,column_default,detail_default: initial selections (vector or single string)channels: which channels to show (default[:color, :row, :column, :detail])pinned: which channel is the catch-all (default:color)fixed:Dictof channel => field(s) that are always applied but not user-editable, e.g.Dict(:column => :cylinders)table: optional table for field validationspec: optional AoG spec — if provided, validates that all dimension fields survive into the VL data (warns if a field was dropped during AoG summary)texts: thePlotTextswhose channel labels the picker shows (default English;plot_texts(:de)for German)
Example
id = "my-plot"
h.div()(
mapping_controls(id, ["origin" => "Origin", "cylinders" => "Cylinders"];
color_default="origin", fixed=Dict(:column => :cylinders)),
to_node(data(df) * mapping(:x, :y, color=:origin, col=:cylinders) * visual(Scatter); id=id),
)For fully-automatic picker + plot construction, prefer auto_remap_node.
AlgebraOfVega.resolve_channels Function
resolve_channels(dimensions; color_default, row_default, column_default, detail_default, pinned, fixed, channels)Compute which dimension field maps to which visual channel, returning kwargs ready to splice into mapping() and lineribbon(). This is the single source of truth for channel resolution — both mapping_controls and server-side spec construction should use this.
Returns a NamedTuple with:
color_kw: NamedTuple to splat intomapping(; color_kw...)row_kw: NamedTuple to splat intomapping(; row_kw...)column_kw: NamedTuple to splat intomapping(; column_kw...)(from non-fixed column defaults)fixed_kw: NamedTuple to splat intomapping(; fixed_kw...)(from fixed channels)detail:Vector{Symbol}forlineribbon(; detail)color_fields,row_fields,column_fields,detail_fields: raw field name vectorsdim_label_map:Dict{String,String}field → human labeldims: filtered dimension pairsdefaults:Dict{String,Vector{String}}channel → field names (including pinned auto-fill)
AlgebraOfVega.refine_channels Function
refine_channels(resolved, tables...)Return a new resolved with dimensions stripped if they are absent from every table or have ≤1 unique value in every table that has them. Same treatment for fixed-channel assignments. Use this once before building your spec so that:
the static server-side render's auto-generated row/col/color labels only reference dims that actually vary,
the pinned catch-all channel doesn't auto-absorb meaningless dims,
mapping_controls' picker reflects the same set.
Per-layer combo construction is still done by apply_combos! against the returned (refined) resolved. Layers that don't have every component of a combo column will have it left unset, and the broadcast pass will replicate them across the surviving facet panels.
Captioned plots (requires using HTMXObjects)
AlgebraOfVega.with_plot_caption Function
with_plot_caption(plot_node, caption; plot_id, kwargs...)Wrap an AoV plot node (built via to_node or auto_remap_node) in a <figure> with a caption header that includes a CSV-download button (and optionally a lazy "Show data" details). Implementation lives in the AlgebraOfVegaHTMXObjectsExt extension and requires using HTMXObjects to be loaded; see that extension's docstring for the full kwargs.
AlgebraOfVega.draws_summary_table Function
draws_summary_table(table; value, outcome, group_cols=Symbol[], ci_level=0.95, digits=2, caption=nothing, kwargs...)Build a "median [lo, hi]" summary table from long-format draws data. Implementation lives in the AlgebraOfVegaHTMXObjectsExt extension (requires using HTMXObjects).
The words of a captioned plot's chrome (the caption's "More" toggle, its buttons, the data preview and the channel picker) come from texts=: with_plot_caption(spec, caption; plot_id, texts=plot_texts(:de)) shows them in German, and keywords replace single words.
AlgebraOfVega.plot_texts Function
plot_texts(locale::Symbol=:en; texts...) -> PlotTextsThe PlotTexts for locale (:en or :de), with any of its texts replaced by keyword:
const DE = plot_texts(:de; show_data="Kennzahlen anzeigen")
with_plot_caption(spec, "Kapazität"; plot_id="kapazitaet", texts=DE)An unknown locale or text name, or a placeholder the text does not take ("{fromat}"), throws an ArgumentError.
AlgebraOfVega.PlotTexts Type
PlotTextsThe words AoV's own plot chrome shows: the caption's "More" toggle, the caption row's download and Share buttons, the "Show data" preview and its summary table, the channel picker's labels, a frame player's play/pause button (config(frame=…)), and standalone HTML exports. Build one with plot_texts and pass it as texts= to with_plot_caption, to_html, auto_remap_node, mapping_controls, caption_share_button, with_caption_share or summary_share_button.
A text in braces is a placeholder AoV fills in: image takes {format} (PNG, SVG); interval takes {point} (the median text) and {width} (95%).
Tidybayes-style uncertainty analyses
AlgebraOfVega.pointinterval Function
pointinterval(; probs=[0.95, 0.8, 0.5], point=:median, orientation=:horizontal)
pointinterval(; bands=[:q025 => :q975, :q25 => :q75], orientation=:horizontal)Nested credible intervals with varying stroke width + a point estimate.
With probs (default): expects draw-level data. Computes quantiles in Julia, then generates layered rule + point marks.
data(draws) * mapping(:value, y=:parameter) * pointinterval()With bands: expects pre-aggregated data with interval columns. The first positional is the point/median column. Each band is a lo => hi pair, outermost first. Skips Julia-side quantile computation.
data(summary) * mapping(:median, y=:parameter) *
pointinterval(bands=[:q025 => :q975, :q25 => :q75])With orientation=:vertical: value column on y-axis, category on x-axis. Requires two positional mappings (category, value):
data(draws) * mapping(:parameter, :value) * pointinterval(orientation=:vertical)When color identifies several intervals at the same category/facet position, those intervals are dodged automatically. A one-to-one metadata color remains centered. Map dodge_y explicitly for horizontal intervals, or dodge_x for vertical intervals, to choose a dodge field directly:
data(draws) * mapping(:value, y=:parameter, color=:dose, dodge_y=:model) *
pointinterval()A fixed colour colours every interval rule in place of the per-category palette colour; the median point keeps its white fill and dark ring. color=:ink follows the page's light/dark foreground. A color= mapping wins over it, as for plain marks. Other visual attributes are not applied (the rules keep their graduated widths) and are reported with a warning.
data(draws) * mapping(:value, y=:parameter) * pointinterval() * visual(; color="black")AlgebraOfVega.gradient_interval Function
gradient_interval(; probs=[0.95, 0.8, 0.5], point=:median, orientation=:horizontal)Nested credible intervals with uniform width and varying opacity + a point estimate. Color and dodge_y/dodge_x mappings, and a fixed visual(; color=…), follow pointinterval semantics.
data(draws) * mapping(:value, y=:parameter) * gradient_interval()
data(draws) * mapping(:parameter, :value) * gradient_interval(orientation=:vertical)AlgebraOfVega.lineribbon Function
lineribbon(; probs=[0.95, 0.8, 0.5])
lineribbon(; bands=[:q025 => :q975, :q25 => :q75])Uncertainty ribbons (area marks) + median line.
With probs (default): expects draw-level data. Groups by x, computes quantiles of y across draws. Requires group=:draw in mapping.
data(preds) * mapping(:x, :y, group=:draw) * lineribbon()With bands: expects pre-aggregated data with quantile columns. The second positional in mapping is the median column. Each band is a lo => hi pair, outermost first.
data(summary) * mapping(:x, :median => "Response") *
lineribbon(bands=[:q025 => :q975, :q25 => :q75])single_x_glyph (default true): any group/facet whose data collapses to a single x value (where an area/line would render nothing) is instead drawn as a vertical pointinterval glyph (rule-bands + a stroked median point) so it stays visible. Set false to opt out (single-x groups revert to invisible area/line).
A fixed colour colours every band, the median line and the single-x glyph rules, on vdraw and sdraw alike; color=:ink follows the page's light/dark foreground (the Makie theme's text colour under sdraw). A color= mapping wins over it, as for plain marks. Other visual attributes are not applied (the bands keep their graduated opacities) and are reported with a warning.
data(summary) * mapping(:x, :median) *
lineribbon(bands=[:q025 => :q975, :q25 => :q75]) * visual(; color="black")AlgebraOfVega.ribbon Function
ribbon(; probs=[0.95, 0.8, 0.5])
ribbon(; bands=[:q025 => :q975, :q25 => :q75])Uncertainty ribbons (area marks) without a median line. Same as lineribbon but omits the central line. Accepts the same bands kwarg for pre-aggregated data, and a fixed visual(; color=…) the same way.
data(preds) * mapping(:x, :y, group=:draw) * ribbon()
data(summary) * mapping(:x, :median) * ribbon(bands=[:q025 => :q975])AlgebraOfVega.dotinterval Function
dotinterval(; probs=[0.95, 0.5], n_dots=50, point=:median, orientation=:horizontal)Quantile dotplot with nested interval overlay. Color and dodge_y/dodge_x mappings follow pointinterval semantics. A fixed visual(; color=…) colours the dots as well as the rules (replacing the rules' default ink).
data(draws) * mapping(:value, y=:parameter) * dotinterval()
data(draws) * mapping(:parameter, :value) * dotinterval(orientation=:vertical)High-level recipes
AlgebraOfVega.ecdf_grid Function
ecdf_grid(table, columns; group=nothing, width=250, height=180)Render a grid of ECDF plots, one per column, colored by group. Returns an HTMX h.div node with flex-wrap layout.
table: any Tables.jl-compatible table (DataFrame, NamedTuple, etc.)columns: vector of column names (Symbols) to plotgroup: optional column name for color grouping (e.g.:chain,:model)width,height: per-plot dimensions
Example (posterior parameter ECDFs colored by chain):
ecdf_grid(draws_df, [:alpha, :beta, :sigma]; group=:chain)AlgebraOfVega.ppc_overlay Function
ppc_overlay(obs, pred; x, y, col=nothing, row=nothing, group=nothing,
color=nothing, truth=nothing, obs_mark=Scatter, obs_size=30,
truth_color="red", truth_strokeWidth=2, truth_strokeDash=[4,4])Build a posterior predictive check overlay: observations + prediction draws + optional truth. Returns composable AoG layers — combine with * config(...) and pipe to |> vdraw.
obs: observation data tablepred: prediction draws table (one row per draw × observation)x,y: column names for the axescol,row: optional faceting columnsgroup: draw identifier column (e.g.:draw_id) for prediction ribbonscolor: optional color column for predictions (e.g.:modelfor comparisons)truth: optional ground-truth data table (rendered as dashed lines)
Example:
ppc_overlay(obs_df, pred_df;
x=:time_h, y=:value, col=:assay_name, row=:subject_name,
group=:draw_id,
) * config(width=250, height=100, facet=(; linkxaxes=:none, linkyaxes=:none)) |> vdrawSample datasets
Useful when writing examples or exploring the API:
sample_cars, sample_tips, sample_stocks, sample_temperatures, sample_population (+ melt_population), sample_monthly_sales (+ melt_sales), sample_posterior_draws, sample_regression_predictions, sample_grouped_regression_predictions, sample_faceted_regression_predictions, sample_faceted_observations, classify_columns, table_to_rows, preaggregate.
AlgebraOfVega.preaggregate Function
preaggregate(table; y=:y, group_keys=Symbol[], probs=[0.025, 0.10, 0.25, 0.5, 0.75, 0.90, 0.975])Group table by group_keys, compute y-column quantiles per group, return a DataFrame-shaped NamedTuple of vectors with group_keys plus quantile columns.
Column naming: q<NNN> for fractional quantiles (e.g. q025 for 0.025, q10 for 0.10) and median for 0.5. Compatible with lineribbon(bands=[:q025 => :q975, ...]) call patterns.
Returns a NamedTuple of column vectors (the AoV-canonical lightweight table shape — data(...) accepts it directly via Tables.jl). If you need a true DataFrame for downstream select / transform! chains, wrap with DataFrame(preaggregate(...)) at the call site.
Explorer widget
A self-contained "click-to-build" UI for AoV specs, useful as a documentation entry point:
default_explorer_datasets, explorer_widget, write_explorer_assets, explorer_controls_html, explorer_js, explorer_data_init_js.