Kanzo UI
Analytics

Chart gallery

Sixteen chart types written with the charts grammar — each one a ChartRoot and a handful of marks, none of them a component.

Every chart on this page is built from the charts grammar: the same ChartRoot, the same marks, the same interactors. There is no HeatmapChart and no WaffleChart — a chart type is a shape you write, and this page is the proof, one shape at a time.

They all read the one sample relation the charts page uses, sightings: a numeric column (bounty), a second one that correlates with it (leagues), a category (region), an ordered axis (hour) and a series (verdict).

Everything here imports from @kanzo-tech/ui/analytics, not the root barrel, and that subpath needs four optional peers installed alongside the library:

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

You also bring the coordinator — see Charts.

Sparkline

The smallest useful plot: no axes, no margin, no legend. anchor={null} removes an axis but keeps its scale, which is the difference between hiding the ticks and losing the layout.

Stacked area

Map fill to a column and the areas stack by it — the same rule that stacks bars. curve is one of the mark options that passes through untouched, so the shape is Observable Plot's.

A stacked mark still wants an order; the z it needs is now derived for you. Plot stacks by z and normally infers it from fill — but through Mosaic the fill arrives as { value, scale: "color" }, maybeZ cannot read a series name out of that object, and stackY falls back to piling every row on the one before it. The chart renders, looks plausible and is wrong, which is worse than one that throws.

So the layer restores Plot's own default: on ChartBarY / ChartBarX, ChartAreaY / ChartAreaX, ChartRectY / ChartRectX and ChartWaffleY / ChartWaffleX, a column-valued fill (or stroke) becomes z unless you set one yourself. z={null} opts out, the way it does in Plot.

What is still yours is the stacking order, because the row order a GROUP BY returns is not one:

<ChartAreaY fill="status" order={["ok", "slow", "error"]} x="hour" y={count()} />

100% stacked area

The chart above with offset="normalize" added, which is the whole diff: Plot's stack transform normalises each column to 1, and percent on the y scale reads it back out as 0–100%.

Horizontal bars

ChartBarX swaps the roles: the category sits on y, the measure on x. The band scale moves with it, so the interactor becomes ChartToggleY — and the left margin has to be widened by hand, because Plot sizes the plot, not the labels.

Grouped bars

Grouped bars are faceted bars. fx on the mark splits the plot into one panel per region and x lays the series out side by side inside each panel; ChartFacetX configures the resulting facet scale exactly as ChartAxisX configures the position scale.

Ridgeline

One distribution per row, panels deliberately overlapping. fy makes the rows; the overlap is the y scale's range overshooting its own band, which is the one attribute ChartAxisY does not wrap — so it goes through attributes, hoisted to module scope to keep the plot from rebuilding.

ChartDensityY would smooth this properly, and it cannot be faceted: Mosaic's density mark groups its kernel grids by fill / stroke / z only, so an fx or fy channel survives into the plot spec with no column behind it and the panels come out empty. Facet a binned area (as above); reach for the density mark when the categories can share one pair of axes.

Heatmap

Two categorical axes and a measure in the cell. The magnitude rides on fillOpacity rather than on a colour ramp, which keeps the hue a token: a colorScheme would be a fixed palette in both themes, and ChartConfig only pins categorical scales.

Hexbin

A scatter that does not overplot. DuckDB assigns each pair to a hexagon and returns one row per bin, so the transfer is bounded by the lattice rather than by the table. ChartHexgrid is a decorator mark — no data source — so it draws the empty lattice behind everything.

2-D density

Kernel density over two columns, drawn as iso-lines above the raw points. The smoothing happens in the database: bandwidth sets the kernel, thresholds the number of contours. ChartDensity and ChartRaster draw the same grid as a cloud or a raster — map a channel to the literal string "density" (fillOpacity="density") to encode it.

Regression with a confidence band

The fit is a SQL aggregate — DuckDB returns slope, intercept and sums of squares, and the mark turns them into a line plus its band. ci is the level, ci={0} drops the band, and a stroke bound to a column fits one regression per group.

Box plot

Plot has no box-plot mark and does not need one: a box plot is three marks over four quantile aggregates. quantile() and median() run in DuckDB, so the distribution never leaves the database — the plot receives five numbers per category.

ChartErrorbarY is the other half of this family, and it is not a box plot: it computes the mean and a confidence interval from avg and stddev, not quartiles. Use it for uncertainty about a mean, and the quantile marks above for the shape of a distribution.

Waffle

A bar chart that counts out loud — one cell per unit rows. Mapping fill to a column stacks the cells by series inside each column, which is where a waffle earns its keep: parts of a whole, countable.

Small multiples

fx on any mark is the whole feature: one panel per distinct value, one shared pair of scales, so the four curves are genuinely comparable. The interval brush spans the panels and publishes one clause, as it would on a single plot.

Bars, a line, and a third variable

Three variables in one plot. Bar height is the sighting count, bar opacity is the mean bounty, and the line is the failing subset. The line shares the count scale on purpose: a plot has exactly one y scale, so the honest way to add a variable with a different unit is another channel, not a second axis.

The interactor is a toggle, not a brush: ChartBarY makes x a band scale even for an integer hour, and an interval over a band throws. ChartRoot warns in the console in development if you pair the two, because the failure is a runtime throw on hover that blanks the other charts on the page — see the limitation.

There is no dual-axis combo chart. Observable Plot exposes one y scale per plot, and neither the grammar nor attributes can add a second one — a right-hand axis in Plot means rescaling the second series into the first one's units by hand and relabelling the ticks. Stack two ChartRoots sharing an x domain instead, or encode the second variable on colour, opacity or radius.

Stacked histogram

bin() keeps the x scale continuous, which is exactly what makes the interval brush legal here. The same chart with x="region" would be a band scale, and brushing it throws inside Mosaic's pre-aggregator — see the limitation.

Dashboard

The composition pieces, wired to one crossfilter. ChartFilter, ChartSearch and ChartSlider are real DOM controls that publish Mosaic clauses without being charts — they connect as clients, ask the coordinator for their own values, and filter every plot below. StatTile, ChartCard and DashboardGrid are the frame around them.

The tiles show how a DOM part queries alongside the plots: useMosaic() hands you the coordinator, and one SELECT fills all three. They report the unfiltered totals — a widget only follows the crossfilter if it is a MosaicClient, which the inputs are and a plain useEffect is not.

What the grammar cannot draw

The honest edges, found by building the page:

Why
Dual y axesPlot has one y scale per plot. Not a wrapping gap — an engine constraint.
Faceted densityChartDensityY / ChartDensityX group their kernel grids by fill/stroke/z; an fx/fy channel yields empty panels. Facet a binned ChartAreaY instead.
A theme-aware sequential rampChartConfig pins a categorical colour domain. A continuous scale needs vg.colorScheme(…) through attributes, and that palette is fixed in both themes. Encode magnitude on fillOpacity with a token hue when it matters.
An opacity or symbol legendChartColorLegend is hard-wired to the colour channel; vg.opacityLegend() goes through ChartRaw.
A scale rangeChartAxisX / ChartAxisY cover the axis, not the whole scale — range, interpolate and the rest are attributes. The ridgeline above needs one.
Geographic marksChartGeo, ChartSphere and ChartGraticule are wrapped, but a projection is a plot attribute, so a map needs attributes={[vg.projectionType(…)]}. Untested here.

On this page