Kanzo UI
Analytics

Charts

A thin composable layer over Mosaic/vgplot — marks, interactors and axes as JSX, over a bring-your-own DuckDB coordinator.

This is a grammar, not a set of chart types. ChartRoot compiles its children into one vg.plot(...), so a bar and a line share a plot, any interactor pairs with any mark, and the chart you need is the one you write — there is no BarChart to fork.

import { ChartRoot, ChartBarY, ChartToggleX, count } from "@kanzo-tech/ui/analytics";

Like CodeEditor with CodeMirror, the library wraps the engine and never owns the runtime: it is bring-your-own-coordinator. The package never imports DuckDB-WASM and never builds a Coordinator — you wire one in a "use client" island and pass it in, which is what keeps the analytics stack out of the base bundle and out of every React Server Component. @uwdata/vgplot, @uwdata/mosaic-core, @uwdata/mosaic-sql and @duckdb/duckdb-wasm are all optional peers, so import { Button } keeps working for everyone who has not installed them.

Four plots over one table, wired to one shared crossfilter Selection: drag an interval across the histogram or the hourly line, click a region bar, drag a box on the scatter — each publishes to the selection, so brushing any one filters the other three. Nothing above is a chart component; it is four ChartRoots with two marks and an interactor each.

Why a grammar

vgplot is ~40 marks, 19 interactors and ~250 attributes. A chart component — Histogram, BarChart, ScatterPlot — exposes that as one photograph of it, and assembles the plot inside itself, so the first thing it cannot do needs a fork: a line on top of bars, a brush where the preset hardcoded a toggle, two marks against a selection you own.

A grammar has no such edge. The proof that it is expressive enough is that every shape a preset would have covered is ten lines of JSX you can copy.

Two kinds of children

ChartRoot takes children of two different natures, and the distinction is worth internalising because it explains everything else on this page:

What they areIdiom
marks, interactors, axesinert descriptors — they render nullRecharts / Observable Plot
ChartRoot, ChartLegend, your own partsreal DOMark.*, data-slot, asChildArk

A descriptor has no DOM of its own. ChartRoot reads the compile function off the element type, in source order, and turns the lot into a single vg.plot(...) — the mark never renders.

Marks cannot be Ark parts, and that is not a shortcut. vgplot paints imperatively: it returns an SVG node that we mount with host.replaceChildren(...). There is no element for a part to own, no ref to forward, no data-slot to style. What is genuinely Ark here is ChartRoot plus useChartContext() — a context carrying the config, the selections and the coordinator, so you can build a legend, a tooltip or a readout of your own without forking anything.

The practical consequence: descriptors cannot read context, because they are compiled before React would render them. Pass values down from the component that owns the ChartRoot.

Anatomy

<ChartRoot table="sightings" config={config} height={220}>
  <ChartBarY x="host" y={count()} fill="host" tip />
  <ChartRuleY at={0} />
  <ChartAxisX tickRotate={45} />
  <ChartAxisY grid />
  <ChartToggleX />
  <ChartLegend />        {/* real DOM: reads the config off the context */}
</ChartRoot>

Three rules cover the ordering:

  1. An interactor binds to the last mark declared before it. Put ChartToggleX after the mark it should drive.
  2. Axes compile to plot attributes (xTickRotate, yGrid), not to vgplot's axisX/axisY decorator marks — an attribute cannot steal the binding from the interactor that follows it. ChartFrame, ChartGridX and ChartGridY are marks and do shift it.
  3. Fragments and arrays flatten; null and false vanish. Conditionals work, which is how you swap an interactor at runtime.

There is no ChartTip: the tooltip is a mark option, so it is the tip prop on any mark.

A descriptor must be a direct child, not a child of one of your components. The compiler walks the element tree — it never renders it — so it sees <YourThing /> and stops, exactly as Recharts does. That includes Show: a <Show> element's type is Show, not a Fragment, so whatever it wraps is silently never plotted. Conditional marks and interactors are the one place in these docs that stays && or a ternary — those compile to false, and rule 3 is what makes them vanish cleanly.

Installation

The charts live on the @kanzo-tech/ui/analytics subpath, never the root barrel, because the Mosaic/DuckDB stack is heavy and optional. Install the peers alongside the library:

pnpm add @uwdata/vgplot @uwdata/mosaic-core @uwdata/mosaic-sql @duckdb/duckdb-wasm

Bring your own coordinator

The library never touches DuckDB-WASM. You build the Coordinator — over wasmConnector(), a socket/REST connector, or a shared worker — and hand it to MosaicProvider, which registers it as vgplot's active coordinator and shares a pair of Selections with every chart beneath it: one the interactors publish into, one the marks filter by (why two). DuckDB-WASM is browser-only, so boot it in a "use client" island loaded with ssr: false, render a skeleton until it is ready, and let the chart hydrate:

"use client";

import { useEffect, useState } from "react";
import { Coordinator, MosaicProvider, loadObjects, wasmConnector } from "@kanzo-tech/ui/analytics";

export default function Analytics({ children }: { children: React.ReactNode }) {
  const [coordinator, setCoordinator] = useState<Coordinator | null>(null);

  useEffect(() => {
    const coord = new Coordinator(wasmConnector());
    coord.exec(loadObjects("sightings", rows)).then(() => setCoordinator(coord));
  }, []);

  if (!coordinator) return <Skeleton className="h-40 w-full" />;

  return <MosaicProvider coordinator={coordinator}>{children}</MosaicProvider>;
}

Coordinator, Selection and wasmConnector are re-exported from the subpath, along with the loaders (loadObjects, loadCSV, loadJSON, loadParquet, loadSpatial, loadExtension), so you wire the backend without a direct @uwdata import. vgplot's own coordinator() setter is not re-exported: MosaicProvider is the only thing that should ever call it.

One coordinator per page. MosaicProvider registers its coordinator as vgplot's active one, and that setter is process-wide: mount two providers with two different coordinators on the same page and the last one wins, leaving the other page's charts empty. Share a single instance (every example below does — see docs/examples/charts/mosaic-boot.tsx), or keep the demos on separate routes. Several MosaicProviders over the same coordinator are fine; each still gets its own crossfilter Selection.

The five charts

The five shapes a chart library is expected to ship, written in the grammar. Each is the same: a dimmed mark for the full relation (filterBy={null}), the crossfiltered mark on top, one interactor, two axes. Sixteen more shapes — waffle, ridgeline, hexbin, box plot, small multiples — are in the gallery.

Histogram

bin() a numeric or temporal column into rectY, and drag an interval into the selection.

Categorical bars

Group a string column, sort by descending count, click a bar to toggle that category. Give both layers the same sort (and limit, for a high-cardinality column) so they share one x domain. The ChartHighlight — no props — is what makes the clicked bar visible, and there is a reason it can be that terse: see feedback on the chart that filters.

Line over an ordered axis

A line is stroked, not filled, so the token lands on stroke. The area underneath is simply a second mark — "line + area" was never a chart type, it was two marks.

Scatter

Two numeric columns and a 2-D brush.

Stacked series

Map fill to a column and the bars stack by it — the layer copies a column-valued fill to z, which is the series key Plot stacks on and the one thing Mosaic cannot infer for it (the details). The config is what binds a hue to an entity: it pins the plot's colour domain, so filtering a series never repaints the survivors, and the same object feeds ChartLegend.

Feedback on the chart that filters

Click a bar and every chart filters, including a visible response on the chart you clicked<ChartToggleX /> and <ChartHighlight />, neither carrying a prop, exactly as in categorical bars above. That is the default. It is worth knowing what is underneath it, because one misplaced prop takes it away.

A crossfilter is defined by hiding a clause from the client that published it:

  • Selection.crossfilter() is created with cross: true.
  • The resolver skips a clause for any client listed in clause.clients, and a toggle's clause lists the marks of the plot it came from. That skip is what stops a chart filtering itself into a single bar the moment you click one.
  • ChartHighlight resolves its predicate with selection.predicate(mark) — without the noSkip flag that filterBy uses. So against a crossfilter the predicate comes back empty, the test degrades to "everything matches", and nothing is dimmed.

The chart that publishes the click is therefore the only one that cannot read it back. The fix is one selection more, not a fork — publish into a non-cross selection and relay it onward — and ChartRoot builds that selection for you, one per chart:

// inside every ChartRoot, for you
const own = Selection.union();     // interactors publish here; ChartHighlight reads it
// relayed on, into the provider's `selected`, and from there into the crossfilter the marks use

Per chart, not per page, and that part is load-bearing. ChartHighlight does not filter: it appends its predicate to the mark's own query as an extra output column, and on an aggregating mark that query has a GROUP BY. So a predicate naming a column this plot does not group by is Binder Error: column "provider" must appear in the GROUP BY clause — the query dies, vgplot swallows it, and the plot keeps its previous render. No error on screen, no empty state, just a chart frozen one state behind while its neighbours move.

A page-wide publish target guarantees that the moment a second chart filters by a second column, which is the ordinary case. A per-chart selection cannot: the only clauses in it are the ones this plot's own interactors published, over the columns its own marks group by.

useMosaic() gives you crossfilter (what marks filter by), selected (every chart clause on the page, flattened — a read model for a filter-chip row, not a publish target) and reset(). Use reset() for a "Clear filters" button: Selection.reset() travels downstream only, so resetting the crossfilter alone leaves every chart still holding its own pick.

Two legends, and only one of them is a control

ChartLegend is ours and never publishes anything: an ark.ul you place where you like, reading the root's config, with the label — not the swatch — carrying the accessible channel. ChartColorLegend is vgplot's, drawn inside the plot, and it both publishes on click and reads the selection back, dropping unselected swatches to 0.2 opacity.

The difference that looks like a detail and is not: its toggle is built with peers: false, so its clause is not hidden from the plot it belongs to. Click a swatch and that plot filters itself — where <ChartToggleColor /> on the mark is skipped by the crossfilter for its own marks, which is the whole of the section above.

Both legends only stay complete because the config pins the colour domain. Without it the scale is ordered by the data, so filtering to one verdict leaves the legend holding one swatch.

When you do wire it yourself

  • You pass your own crossfilter to MosaicProvider. include is constructor-only, so a selection handed in cannot be relayed into after the fact — the provider relays into it instead of constructing it, and everything below keeps working.
  • You pass your own as to a ChartRoot. Then you own its wiring: nothing is relayed for you, though reset() still clears it. Pair it with a matching by on the highlight, and keep to the rule above — one selection per plot, naming only columns that plot groups by.
  • A click should replace the previous pick, not add to it. Selection.single() keeps one clause where the default union accumulates (shift/⌘-click adding to a pick is Toggle's own behaviour, not ours).

Notes that hold either way:

  • ChartHighlight binds to the last mark declared before it, like every interactor. In the dual-layer idiom that is the crossfiltered layer, which is the one you want dimmed.
  • Interval brushes need none of this: the brush rectangle is its own feedback.

What the presets could not do

Bars and a line in one plot

Two marks, one x scale, one interactor. Under five preset components this needed a sixth component; here it is one more child.

The interactor is a ChartToggleX, and where it sits matters twice over. It binds to the last mark before it, so it goes between the bars and the line; and it is a toggle rather than a brush because ChartBarY makes x a band scale even for an integer hour — an interval over a band throws, and takes the rest of the page down with it (why). Bin the x, or toggle it.

Swap the interactor

Click-to-toggle and drag-to-brush differ by one child. A false child compiles to nothing, so the plot is rebuilt with the other interactor and the marks stay exactly as written. Removing an interactor does not withdraw the clause it already published, so the example calls selection.reset() on the swap.

Bring your own selection

The provider's crossfilter is a default, not a cage. Own the Selection and wire it explicitly: as on the chart that publishes, filterBy on the ones that read. Everything else on the page stays still.

An unwrapped mark

The layer wraps every mark vgplot ships but six: axisX, axisY, axisFx, axisFy, gridFx and gridFy, left out on purpose because axes compile to attributes and an axis mark would steal the binding from the interactor after it. A plot therefore draws one x axis — and the scale repeated on the opposite edge, which a long bar list wants, is exactly what ChartRaw is for.

Both anchors are spelled out in the example, because Plot adds its implicit axis only while the plot has no axis mark for that scale: declare one and you own them all. The scale stays ChartAxisX's — grid and label come from the descriptor, and the marks inherit them.

A raw directive is otherwise a mark like any other: it takes its place in source order (declare it after the interactor you did not want it to capture), and it is a function, so hoist it or the plot rebuilds on every render. An array is fine — spec takes one directive or several.

ChartConfig

Record<string, { label?, color?, icon? }> — series key to presentation, in one place:

const config = {
  ok: { label: "OK", color: "var(--chart-1)" },
  error: { label: "Error", color: "var(--chart-5)" },
} satisfies ChartConfig;

A non-empty config pins the plot's colour scale (colorDomain = the keys, colorRange = the colours) and feeds ChartLegend. Pinning matters: without it vgplot orders the colour domain by the data, so filtering out a series repaints the ones that survive.

color takes var(--chart-1), --primary, a hex or an rgb(). Tokens are resolved against the live DOM at mount and on every theme change, because Observable Plot cannot parse oklch, color-mix or color(srgb …). The same resolution applies to the fill and stroke props of any mark — those two channels only, and only when the value looks like a token, so fill="region" stays the column region. A series without a colour falls back to its slot in the categorical set — as the token var(--chart-N), not as a baked hex, so one answer follows both the light/dark flip and whichever client is being served.

chartSeriesColor(config, key) is exported for anything outside the plot that has to agree with it — a chip beside a row, a heading, a sparkline of your own. It answers what the chart answered: color if the series declared one, its categorical slot if it did not, and undefined for a key the config does not carry.

The categorical set belongs to the tenant's palette document, not to this layer: N hues written into --chart-1..8 by whoever authored the theme. A chart reads the token and that is the only copy. A second source of truth in this layer — eight literal hexes, or a table of them — makes the same series come out one colour through ChartConfig and another through a token.

A theme need not author them: tokens.css declares eight on :root, chosen to clear contrast and separation on every background the catalogue ships, so a theme that writes none still has a chart channel and one that writes some overrides them slot by slot.

CHART_SLOTS is 8 because a stylesheet cannot have a variable number of custom properties. How many of those slots carry a real category is the theme's --chart-capacity, which can be lower: past it compile writes var(--muted-foreground), because naming five categories honestly beats naming eight that a colour-blind reader sees as five. Past the eighth series, fold into "Other" — categoricalColor(i) returns the muted token rather than cycling, because a ninth series wearing slot 1 would claim to be the first one.

useChartContext()

The context behind any DOM part you add — a legend, a tooltip, a caption:

function Total({ rows }: { rows: number }) {
  const { color, formatNumber } = useChartContext();
  return <p style={{ color: color("hoax") }}>{formatNumber(rows)} sightings</p>;
}
FieldWhat it is
color(key)the series colour, already resolved to rgb(...) — safe in CSS and in Plot
formatNumber(value, options?)the chart's Intl.NumberFormat, from the root's locale / numberFormat — one number vocabulary for axes, legend and tooltip
config, tablewhat the root was given
filterBy, asthe selections the marks read and the interactors write
coordinatorfor a part that queries alongside the plot

It throws outside a ChartRoot; useChartContextOptional() returns null instead, which is how ChartLegend also works standalone. Descriptors cannot use it — see two kinds of children.

Escape hatches

Three, because wrapping vgplot must stay a convenience:

  • <ChartRaw spec={…} /> — a vg.* directive (or an array of them) in its source position, with the same interactor-binding semantics as a wrapped mark. What it is mostly reached for is one of the six axis marks the axis descriptors compile away from: axisX({ anchor: "top" }) to repeat the measure scale at the top of a long bar list, axisFy to label a facet. Those six are re-exported from the subpath. No token resolution happens inside a raw spec, so pass a Plot-safe colour — resolveTokenColor(host, "--chart-1") is exported for exactly this, and reads the live cascade rather than a frozen copy of it.

  • attributes on ChartRoot — raw directives applied last, so they win over anything the layer decided. For the ~225 plot attributes the axes do not cover.

  • The Mosaic client protocol, for a view that is not a plot at all — a WebGL canvas, a map, an imperative widget. Subclass MosaicClient or wrap makeClient, declare a query, and publish one of the five clauses: clausePoint, clausePoints, clauseInterval, clauseIntervals, clauseMatch. That is the whole of it, and it is what makes such a view a peer of the plots rather than a readout drifting beside them. All seven are re-exported from the subpath, so this costs no direct @uwdata import.

    Turning the answer back into values is the half the protocol does not give you, and it is where every client independently writes as { getChild(name: string): … } — a cast that asserts Arrow's shape rather than checking it, and is wrong the first time a query selects a string. column, fillColumn and numbers are exported for that: Arrow only offers a typed column when the type allows one, so the fallback is not a nicety.

Neither hatch needs a source builder: ChartRoot already owns the plot(…) call, and a mark's data / filterBy props already name its relation. Anything past that — an expression builder, a plot attribute — still comes from @uwdata/vgplot directly, which is what a hatch is.

Both are keyed by identity: a vgplot directive is a function, so building one inline rebuilds the plot on every render. Hoist it to module scope or useMemo it, and keep the attributes array referentially stable.

Limitations

  • A descriptor wrapped in your own component is invisible. ChartRoot inspects its children's element types, so <MyBar /> — even when it returns <ChartBarY /> — compiles to nothing. This is Recharts' limitation and it has the same cause. Write a function that returns descriptors and spread them, or make your component wrap the whole ChartRoot, as the examples do.
  • Descriptors cannot read React context, for the same reason.
  • The plot rebuilds when the grammar changes. Props are fingerprinted (SQL expressions by their SQL, so a fresh count() is free); functions and selections by identity.
  • An interval brush needs a continuous scale. ChartIntervalX over a band scale — bars grouped by a string, or by an integer Plot bands — throws Unrecognized scale type: band inside Mosaic's pre-aggregator the moment the pointer enters the plot. The pre-aggregator belongs to the coordinator, so that one mistake blanks the other charts on the page, which report nothing of their own. Brush binned or continuous marks; toggle categorical ones. ChartRoot warns to the console in development when it compiles the pairing, because the runtime symptom points anywhere but at the chart that caused it.

Dashboards

A dashboard is not only plots, and the pieces divide by whether they need the engine.

ChartFilter, ChartSearch and ChartSlider are real DOM controls that publish Mosaic clauses without being charts, so they filter every plot below them. ChartStat is a headline figure read from the relation under the same crossfilter, and useChartQuery is what it is built on — reach for it whenever something that is not a plot has to move with the brush. A widget that runs a plain coordinator.query() in an effect will sit there reporting unfiltered totals next to filtered charts, which reads as a bug in the crossfilter.

ChartFilter is a FacetFilter over a GROUP BY, and it takes that component's searchable. Know what the field covers before you turn it on: it narrows the fetched page — the limit most frequent values the control asked for, 50 by default — and asks nothing new of DuckDB. When the page is truncated, both the footnote and the no-match message say so, because a field that answers no matching values about a database it never queried is worse than no field. Searching the column itself is ChartSearch, which publishes a match clause and does run a query.

ChartSearch is a filter, not a picker — the clause is a match over whatever you typed, and the values it offers are completions. It asks for the autocompleteLimit distinct values of the column (50 by default), narrows those in the browser as you type, and says when that list is capped; picking one publishes exactly the clause typing it would have. Set autocompleteLimit={0} for a plain search field with no lookup at all.

Its presentational half, StatTile, is in the root barrel — it takes a number, so making it cost DuckDB-WASM would be absurd. That split is the engine rule, the same one behind Table and DataTableRoot.

The frame and the grid are not library components: a titled card and an auto-fitting grid add no behaviour, so they are arrangements you copy. The ones these docs use are in docs/lib/. The gallery wires the whole thing together.

Both halves of the pair, side by side, because that is the only way to see why there are two of them. Click a region bar and watch which numbers move — and which one does not:

Theming

vgplot hard-codes its fills; these charts read tokens off the live DOM instead, so a theme change re-colours the marks along with everything else — the vgplot analogue of how kanzoHighlighting re-skins CodeMirror. Axis text inherits --foreground through currentColor, so it stays legible in light and dark. Tokens are resolved against the chart's own host element, so a scoped theme override wins.

The compile surface, for a mark this layer does not ship

Every chart child is inert: it renders null and carries its compilation on a static. That shape is ChartDescriptor<P> and the function on it is ChartCompile<P>(props, ctx) in, directives out — so a host that needs a mark this layer does not wrap writes one child rather than a fork.

What comes out is a ChartDirective, and it is plain data in all four of its cases: a ChartMarkDirective, a ChartInteractorDirective, a ChartAttributeDirective, or a ChartRawDirective, which is the escape hatch — a vg.* value the layer does not wrap, passed through untouched. What goes in is ChartSpecContext, which carries the relation every table-backed mark reads from; ChartSpecOptions extends ChartFacetOptions with the rest of what a spec needs, and ChartMargin is the four numbers around it.

The mark's own shapes are ChartMarkProps and ChartMarkSource — data comes from the table, from a literal array, or from a query — and ChartInteractorProps is the interactor equivalent. A series is configured with ChartSeriesConfig and read back as a ChartSeriesEntry, a facet with ChartFacetOptions, and a filter offers ChartFilterOptions.

Two of these are not about charts at all and are exported because the inputs are. MosaicInputOptions<T> and MosaicInputState<T> are what a control needs to be a Mosaic client — the shape behind every filter input on this page — and IdSetClientOptions is the same for a client that publishes a set of ids rather than a range. ExprValue is anything Mosaic will accept where an expression is expected, and NumericArray is the union of typed arrays a column may arrive in: Float32Array | Float64Array | Uint32Array | Uint16Array | Int32Array.

API Reference

MosaicProvider

PropTypeDefault
coordinatorCoordinator
crossfilterSelectiona fresh Selection.crossfilter({ include: selected })

useMosaic() returns { coordinator, crossfilter, selected }; useCrossfilter() and useSelected() return one half each. All three throw outside a MosaicProvider. Passing your own crossfilter collapses selected onto it — what that costs.

ChartRoot

PropTypeDefault
tablestring— (omit it when every mark carries its own data)
filterBySelection | nullthe provider's crossfilter; null = unfiltered
asSelectionthe provider's selected, relayed into that crossfilter
configChartConfig{}
heightnumber200 (width is measured from the container)
marginnumber | { top, right, bottom, left }vgplot's default
aspectRationumber
attributesreadonly unknown[]
localestringthe runtime locale
numberFormatIntl.NumberFormatOptions
className / plotClassNamestring

Marks

Props are the vgplot / Observable Plot mark options (x, y, z, r, fill, stroke, opacity, sort, limit, inset, …) plus the common sugar: filterBy (this mark's selection, null for the full relation), data (literal rows instead of the table), at (position a rule or tick without a table), and tip.

One option is filled in for you. On the stacking marks — ChartBarY/X, ChartArea/Y/X, ChartRect/Y/X, ChartWaffleY/X — a column-valued fill or stroke is copied to z unless you pass one, because that is Plot's own default and Mosaic breaks it (why). z={null} opts out.

ChartBarY ChartBarX ChartWaffleY ChartWaffleX ChartLine ChartLineY ChartLineX ChartArea ChartAreaY ChartAreaX ChartDot ChartDotX ChartDotY ChartCircle ChartHexagon ChartImage ChartRect ChartRectY ChartRectX ChartCell ChartCellX ChartCellY ChartRuleY ChartRuleX ChartTickX ChartTickY ChartText ChartTextX ChartTextY ChartVector ChartVectorX ChartVectorY ChartSpike ChartArrow ChartLink ChartHeatmap ChartRaster ChartRasterTile ChartContour ChartDenseLine ChartDensity ChartDensityY ChartDensityX ChartHexbin ChartRegressionY ChartErrorbarY ChartErrorbarX ChartVoronoi ChartVoronoiMesh ChartDelaunayLink ChartDelaunayMesh ChartHull ChartGeo · decorators: ChartFrame ChartGridX ChartGridY ChartHexgrid ChartSphere ChartGraticule · escape hatch: ChartRaw.

Six of vgplot's marks are deliberately absent — axisX, axisY, axisFx, axisFy, gridFx, gridFy — because they are attributes here. ChartRaw still takes them.

Interactors

as chooses where the interaction publishes (default: the root's as, which is the provider's selected). Everything else is the vgplot interactor option — peers, brush, pixelSize, empty, …

ChartIntervalX ChartIntervalY ChartIntervalXY ChartToggleX ChartToggleY ChartToggleColor ChartNearestX ChartNearestY ChartRegion ChartPanZoom

Six of these answer to two names, and both are real. pick and brush say what the gesture is — click a category, drag a range — where vgplot's toggle and interval say how the machine models it. The pairs are the same object, not wrappers:

Gesturevgplot's spelling
ChartPickX ChartPickY ChartPickColorChartToggleX ChartToggleY ChartToggleColor
ChartBrushX ChartBrushY ChartBrushXYChartIntervalX ChartIntervalY ChartIntervalXY

Reach for the gesture name when you are writing a chart; the vgplot name is there so anyone arriving from Mosaic's own documentation lands on the same component. This table exists because the alias block claimed the docs taught these and they did not — both spellings were live in code, one of them undocumented, which is how a reader ends up believing they are two different interactors.

ChartHighlight is the odd one out: it reads a selection (by) and dims the mark before it rather than publishing. It cannot read a crossfilter's own clause, which is why by defaults to selected and not to the crossfilter (the whole story). ChartPanZoom carries no selection at all — it rewrites the scales.

Axes

ChartAxisX / ChartAxisY, each compiling to prefixed plot attributes:

PropType
anchor"top" | "bottom" | null (X) · "left" | "right" | null (Y) — null hides the axis, keeps the scale
labelstring | null
tickscount, interval name, or explicit values
tickFormat tickSize tickRotate
grid line zero percent nice reverseboolean
domain scale inset padding

Anything else goes through ChartRoot's attributes.

ChartFacetX and ChartFacetY are the same descriptor over the fx and fy scales — the ones a faceted plot puts its small multiples on. Same props, minus the four that only mean something on a measure: a facet is always a band, so scale, nice, zero and percent are not there. anchor still hides the axis while keeping the panels.

Legends

  • ChartLegend — our DOM (ark.ul, data-slot="chart-legend"). Reads the root's config, or takes its own config / series prop and works standalone. The label carries identity; the key is a Swatch, so it is aria-hidden — colour is never the only channel.
  • ChartColorLegend — vgplot's, drawn inside the plot and interactive: clicking a swatch publishes into the selection. Pass as={null} for a static key.

Helpers

Re-exported so a whole chart is written without a direct @uwdata import: the aggregates count sum avg min max median quantile stddev mode bin sql; the six axis marks the descriptors withhold on purpose, axisX axisY axisFx axisFy gridFx gridFy, which are what ChartRaw is for; the boot vocabulary Coordinator Selection wasmConnector and the load* loaders.

A set is re-exported when it is closed and named. vgplot's ~250 plot attributes and mosaic-sql's expression builders are open, so they stay a direct import — taking them raw is what an escape hatch is.

For a descriptor of your own, chartDescriptor(displayName, compile) mints one; compile returns plain data ({ kind: "mark", mark: "barY", source, options }), never a vg.* call.

Every part's props are exported as an interface — ChartAxisXProps, ChartAxisYProps, ChartColorLegendProps, ChartFacetXProps, ChartFacetYProps, ChartFilterProps, ChartHighlightProps, ChartLegendProps, ChartPanZoomProps, ChartRawProps, ChartRootProps, ChartSearchProps, ChartSliderProps, ChartStatProps and MosaicProviderProps — so a wrapper can take the same props without restating them.

On this page