Skip to content

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
julia
config(; kwargs...)

Create a Config with Vega-Lite properties. Common options:

  • width, height, title — spec dimensions and title

  • encoding — deep-merged with auto-generated encodings (add aggregate, scale, axis, etc.). A Dict or 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 for a's marks); apply it to the whole sum, (a + b) * config(encoding=E), to reach every layer. row/column/facet overrides always configure the shared facet.

  • config — raw Vega-Lite top-level config (a Dict or NamedTuple), deep-merged into AoV's defaults rather than replacing them

  • params, transform — VL interactivity parameters and data transforms

  • select — 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 is frame=(; field=:year, interval=0.5, loop=false, start=:first). Frames play in sorted order, or in a sorter order (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 AoG scales(...) object, mirroring draw(spec, scales(...)). Currently translates X/Y/Z scale=log/log2/log10/sqrt/symlog/identity to VL scale types. Example: config(scales=scales(Y=(; scale=log10))). Use scale=symlog for a symlog axis (linear near 0, log beyond — keeps genuine zeros a plain log axis cannot render), optionally with constant= to tune the linear-region half-width (VL default 1): config(scales=scales(X=(; scale=symlog, constant=2))).

  • facet — an AoG-style NamedTuple passed to draw(; facet=...). linkxaxes=:none / linkyaxes=:none translate to VL resolve.scale.<axis>="independent". Example: config(facet=(; linkxaxes=:none, linkyaxes=:none)).

  • axis — an AoG-style NamedTuple passed to draw(; axis=...). limits=((xlo, xhi), (ylo, yhi)) (each side may be nothing) translates to VL encoding[x/y].scale.domain; clamp=true additionally sets scale.clamp=true on the limited axes (VL-only extension, stripped on the Makie path). Example: config(axis=(; limits=((1, 100), nothing), clamp=true)).

  • independent_scales — deprecated: use facet=(; 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.

source
FunctionUse
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
julia
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 dimensions

  • actions: 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 optional swap, debounce.

  • fit_width: when true (default) set width: "container" for single-view specs and apply numeric responsive widths for layered/faceted specs. An explicit width (kwarg or config(width=…)) or fit_width=false keeps the authored width: the runtime then neither resizes the plot to its container nor re-embeds it when the container resizes.

source
AlgebraOfVega.to_html Function
julia
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.

source
julia
to_html(node::HTMX.Node; texts=plot_texts(), title=texts.untitled, head_extra="", source, base, runtime) -> String

Serialize 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).

source
AlgebraOfVega.to_json Function
julia
to_json(spec; interactive=true, auto_remap=nothing, kwargs...) -> String

Convert 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.

source
AlgebraOfVega.to_vegalite Function
julia
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.

source
julia
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.

source
AlgebraOfVega.sdraw Function
julia
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).

source
AlgebraOfVega.sdraw_file Function
julia
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.

source
AlgebraOfVega.sdraw! Function
julia
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!.

source
AlgebraOfVega.vlspec Function
julia
vlspec(drawable; kwargs...)

Wrap an AoG drawable in a VegaSpec with optional config properties. Equivalent to drawable * config(; kwargs...).

source
FunctionOutput
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:

julia
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
julia
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.

source
AlgebraOfVega.vega_runtime Function
julia
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 ID

  • AoV.embed(id, spec, opts) — embed and register a view; calling it again for the same id (e.g. with a spec that has more layers) replaces the view

  • AoV.whenReady(id, fn) — call fn(view) now, or once the view has been embedded

  • AoV.updateData(id, data) — swap a view's data without re-creating it

  • AoV.appendData(id, data, name, maxRows) — insert rows into a view's data, optionally keeping only the most recent maxRows

  • AoV.replaceData(id, data, key, name) — replace the rows of the groups data carries (rows whose key field values match a row of data), keeping the other groups' rows

  • AoV.removeData(id, values, key, name) — remove one explicitly named keyed group without replacement rows.

  • data is a row array or the columnar {n, columns} form update_data / append_data / replace_data send; AoV.embed/AoV.updateSpec expand inline datasets in that form too (AoV._rowsFromColumns)

  • AoV.onSignal(id, signal, callback) — listen to a Vega signal

  • Signal→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.detail is {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 with document.addEventListener('aov:click', …) or htmx hx-trigger="aov:click from:#<id>"

  • AoV.refreshTheme() — re-render host-themed plots whose inherited text colour changed (automatic on prefers-color-scheme and <html>/<body> class, data-theme or style changes; call it after other theme switches)

  • AoV.hostThemeConfig(el) / AoV.hostBackground(el) — the Vega-Lite config a host-themed plot in el embeds with, and the background its image downloads use

  • AoV.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 is root or 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.

source
AlgebraOfVega.update_data Function
julia
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).

source
AlgebraOfVega.append_data Function
julia
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.

source
AlgebraOfVega.replace_data Function
julia
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):

julia
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 stage

A 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.

source
AlgebraOfVega.update_spec Function
julia
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.

source
HelperPurpose
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:

julia
# 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))
end

Send 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):

julia
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
julia
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 .named mapping. If a layer says mapping(:x, :y; color=:source, row=:study), then color_default = "source" and row_default = "study" automatically.

  • dims is the user-supplied list of remappable dimensions, as Vector{Pair{String,String}} (field => human label). Labels here are the single source of truth for picker labels and combo titles.

  • fixed assigns fields to channels that the picker shouldn't expose as remappable. Same shape as in resolve_channels. Labels for fixed fields are picked up from dims if present, otherwise snake_case → Title Case.

  • pinned is the catch-all channel.

  • texts is the PlotTexts whose channel labels the picker shows (default English; plot_texts(:de) for German).

  • axes=true opts 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 a TidybayesAnalysis, X/Y stay hidden regardless of axes — analyses bind axes to computed columns.

What it does internally

  1. Walks spec to enumerate layers and their dataframes.

  2. Reads channel defaults from layers' existing mappings.

  3. Calls resolve_channels(dims; defaults..., fixed, pinned).

  4. refine_channels(resolved, dfs...) — strips dims absent or single-valued across every layer.

  5. 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), then apply_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.

  6. 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.

  7. Reassembles layers via +, preserves the original VegaSpec config, and wraps with mapping_controls + to_node in an h.div.

Example

julia
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)
source
AlgebraOfVega.mapping_controls Function
julia
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 the id kwarg passed to to_node(spec; id=...)

  • dimensions: vector of Pair{String,String} (field => label) or bare strings/symbols

  • color_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: Dict of channel => field(s) that are always applied but not user-editable, e.g. Dict(:column => :cylinders)

  • table: optional table for field validation

  • spec: 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: the PlotTexts whose channel labels the picker shows (default English; plot_texts(:de) for German)

Example

julia
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.

source
AlgebraOfVega.resolve_channels Function
julia
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 into mapping(; color_kw...)

  • row_kw: NamedTuple to splat into mapping(; row_kw...)

  • column_kw: NamedTuple to splat into mapping(; column_kw...) (from non-fixed column defaults)

  • fixed_kw: NamedTuple to splat into mapping(; fixed_kw...) (from fixed channels)

  • detail: Vector{Symbol} for lineribbon(; detail)

  • color_fields, row_fields, column_fields, detail_fields: raw field name vectors

  • dim_label_map: Dict{String,String} field → human label

  • dims: filtered dimension pairs

  • defaults: Dict{String,Vector{String}} channel → field names (including pinned auto-fill)

source
AlgebraOfVega.refine_channels Function
julia
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.

source

Captioned plots (requires using HTMXObjects) ​

AlgebraOfVega.with_plot_caption Function
julia
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.

source
AlgebraOfVega.draws_summary_table Function
julia
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).

source

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
julia
plot_texts(locale::Symbol=:en; texts...) -> PlotTexts

The PlotTexts for locale (:en or :de), with any of its texts replaced by keyword:

julia
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.

source
AlgebraOfVega.PlotTexts Type
julia
PlotTexts

The 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%).

source

Tidybayes-style uncertainty analyses ​

AlgebraOfVega.pointinterval Function
julia
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.

julia
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.

julia
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):

julia
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:

julia
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.

julia
data(draws) * mapping(:value, y=:parameter) * pointinterval() * visual(; color="black")
source
AlgebraOfVega.gradient_interval Function
julia
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.

julia
data(draws) * mapping(:value, y=:parameter) * gradient_interval()
data(draws) * mapping(:parameter, :value) * gradient_interval(orientation=:vertical)
source
AlgebraOfVega.lineribbon Function
julia
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.

julia
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.

julia
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.

julia
data(summary) * mapping(:x, :median) *
    lineribbon(bands=[:q025 => :q975, :q25 => :q75]) * visual(; color="black")
source
AlgebraOfVega.ribbon Function
julia
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.

julia
data(preds) * mapping(:x, :y, group=:draw) * ribbon()
data(summary) * mapping(:x, :median) * ribbon(bands=[:q025 => :q975])
source
AlgebraOfVega.dotinterval Function
julia
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).

julia
data(draws) * mapping(:value, y=:parameter) * dotinterval()
data(draws) * mapping(:parameter, :value) * dotinterval(orientation=:vertical)
source

High-level recipes ​

AlgebraOfVega.ecdf_grid Function
julia
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 plot

  • group: optional column name for color grouping (e.g. :chain, :model)

  • width, height: per-plot dimensions

Example (posterior parameter ECDFs colored by chain):

julia
ecdf_grid(draws_df, [:alpha, :beta, :sigma]; group=:chain)
source
AlgebraOfVega.ppc_overlay Function
julia
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 table

  • pred: prediction draws table (one row per draw × observation)

  • x, y: column names for the axes

  • col, row: optional faceting columns

  • group: draw identifier column (e.g. :draw_id) for prediction ribbons

  • color: optional color column for predictions (e.g. :model for comparisons)

  • truth: optional ground-truth data table (rendered as dashed lines)

Example:

julia
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)) |> vdraw
source

Sample 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
julia
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.

source

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.

You are viewing the dev branch. This branch may include code written with Claude Code with less human supervision. Only human-approved code is merged into main.