Charts
The intelligence layer ships the data — typed, drillable, optionally live.
@semilayer/charts ships the pixels: a tiny vanilla-TypeScript SVG
renderer with zero framework dependencies. @semilayer/react-charts
wraps the same engine with a <AnalyzeChart> component and a hook for React
apps.
Pick the shape, hand it the result, and it renders. Click a bucket and the
chart calls back with the signed bucketKey that round-trips to
drill-down. Subscribe over the WS and bucket nodes
animate in place as analyze.diff frames arrive.
React — the easy path
The component owns the lifecycle: opens the chart, fetches once on mount,
subscribes to analyze.subscribe when liveUpdates is set, applies
analyze.diff frames in place via CSS transitions, and tears down the
socket on unmount. No render churn — bucket nodes update opacity / fill
without remounting.
The useAnalyzeChart hook gives you the same wiring with a ref-based API
when you need to pin a chart to a div you control:
Vanilla — createChart
For non-React apps (Vue / Svelte / vanilla / web components), the imperative API mounts a chart into any DOM node:
The same createChart is also available as a custom element so you can drop
charts into HTML without any JS framework:
Showcase — every shape
The gallery above shows 4 of the 14 shapes the renderer ships. Same data
shape (AnalyzeResult<D, M>); different shape prop. You can swap shapes
at runtime without re-fetching the analyze — chart.update({ shape }) or
the <AnalyzeChart shape> prop resugars the existing data into the new
visual.
| Shape | Best for |
|---|---|
table | The fallback view. HTML-in-foreignObject, sortable, copyable. |
bar / stackedBar | Categorical counts; stacked when you have 2+ measures sharing a baseline. |
line / area / stackedArea | Ordered dimensions — time buckets, numeric breaks. |
scatter | 2-measure dim × dim matrix; e.g. price vs rating. |
pie / donut | Share-of-total, ≤ 6 segments. Donut for "and the total is…" use case. |
heatmap | Two-dim grid: cohort matrices, hour-of-day × day-of-week. |
funnel | The funnel kind's natural shape — step counts + dropoff bands. |
cohort | The cohort kind's natural shape — cohorts × intervals retention matrix. |
treemap | Categorical share-of-total when you have ≥ 6 buckets and want everything visible. |
radar | Multi-measure profile — e.g. one product's score across 5 axes. |
geo | Geohash / H3 cells — vertical-bar fallback per cell. |
Every shape consumes the same headless reshape utilities from
@semilayer/headless — toLineSeries, toStackedSeries, toScatterPoints,
toHeatmapMatrix, toFunnelSteps, toCohortMatrix, etc. — so the data math
is canonicalized in one place. If you want the math without the rendering
layer (e.g. driving a third-party library like Recharts or D3), import
those utilities directly:
Theming
Themes are first-class. lightTheme + darkTheme ship out of the box;
resolveTheme(partial) shallow-merges any overrides; themeFromCssVars(host, mapping)
wires the chart palette to your existing CSS variables for a pixel-perfect
match with the rest of the app:
Switch themes at runtime with chart.setTheme(partial) — the renderer
re-runs the layout and re-paints in place.
Animations
Bucket nodes animate in / out via CSS transitions on opacity + fill — no
canvas churn, no render-thrash. The renderer respects
prefers-reduced-motion automatically: effectiveAnimationMs(theme)
returns 0 when the OS-level reduced-motion preference is set, so users who
disable animations get static updates without any opt-in code on your side.
When liveUpdates is on and a analyze.diff frame arrives, only the
added / changed / removed buckets transition. Unchanged buckets sit
still — the chart never thrashes.
Exports
Every chart can dump its current state in four formats:
The CSV export is union-keyed across sparse buckets — if some buckets have a
top_k measure that others don't, the header still shows every column. PNG
exports are rendered through Image + canvas; the dimensions default to the
chart's current size, multiplied by scale for retina output.
Streaming exports — the row-level dump
Charts dump their aggregated state. To dump the contributing rows behind a chart, reach for the streaming row export instead:
See Analyze → Exports for the full streaming contract,
tier caps, and the X-SemiLayer-Export-Truncated trailer.
Source adapters — beyond Beam
@semilayer/charts works with four source shapes:
kind | What it does | Right for |
|---|---|---|
'data' | Pass an AnalyzeResult you already have | Server-rendered pages, fixtures, demos |
'beam' | Hand a typed Beam handle; the chart owns the fetch + WS | React apps with codegen |
'http' | Hand a URL; the chart polls on pollMs | Static sites, CDN-cached results |
'ws' | Hand an open socket; chart sends analyze.subscribe | Apps with their own socket lifecycle |
Where to start
- Analyze Overview — what the underlying data shape looks like before it hits a chart.
- Drill-down — the bucketKey round-trip that
every chart's
onBucketClickhandler hooks into. - Exports — pull the rows behind a bucket as streaming NDJSON or CSV.
- demo.semilayer.com/analyze —
the dogfood. Four shapes, click-to-drill, search-inside-bucket, all
rendered through the vanilla
@semilayer/chartspackage (no React on this demo).