| name | visuals |
| description | Use when adding charts, KPIs, tables, slicers, or any visual to a dashboard. Charts are authored as Graphein chart specs — one chart = one JSON `ChartSpec` (type + tidy `data` + `encoding`) dropped into `<ChartCard spec={…} />`, which owns loading / empty / error and the app theme. Covers the spec model and the per-type recipes (line/area/bar/scatter/pie/heatmap/funnel/combo/histogram/ treemap/gauge/bullet/waterfall/calendarHeatmap/slope/dumbbell/table/matrix), declarative features (transform/annotations/insights/trendline/facet), the validate→repair→report self-check (graphein 0.16), headless preview against live data, the DAX→rows helpers (toChartData / toTable / topN / deriveKpi), `KpiCard`, `DataTableCard` (Graphein table/matrix), slicers (dropdown/list/search/date-range/range + FilterBar) with shared filter state, interactivity, layout, value formatting, and color tokens.
|
Visuals — author a Graphein spec, drop it in a tile
One chart = one JSON spec. You don't hand-write SVG or wire a charting
library. You (1) map your DAX result into plain rows, (2) author a single Graphein
ChartSpec — a type, a tidy data
array, and an encoding that names the columns — and (3) drop it into
<ChartCard spec={…} />. The card owns the loading / empty / error states and
bridges the app theme, so a spec never needs a color or a size.
Charts are graphein 0.16. That means a broad chart catalog (combo/dual-axis,
histogram, treemap, gauge, bullet, waterfall, calendar-heatmap, slope, dumbbell
on top of the classics), in-spec transforms and annotations (reference
lines), and a self-correcting loop — validateSpec → repairSpec →
summarize plus a render report. Render each visual spec headlessly
against live data to a PNG + report before ship: see the headless-preview
skill. This is the agent validation loop; KPI/table/matrix/slicers/dashboard
rasterize to PNG too, so preview-validate every visual before shipping.
Three things are React surfaces around Graphein specs or state:
- KPIs →
<KpiCard> (big value + delta pill + sparkline).
- Tabular data →
<DataTableCard spec={tableOrMatrixSpec}> (Graphein
table / matrix — virtualized, sortable, conditional formatting, totals).
- Filters → the slicers (
FilterBar + DropdownSlicer/…) over shared
filter state; chart selections can bridge into the same state.
Everything is exported from one barrel: @/components/dashboard.
Fast path
Optimize time to wow: ship one real tile, preview, user review, iterate.
Phase 1 — Hero slice: render ONE real visual the simplest way — map the hero
query with toChartData(...), author a spec, pass it to a ChartCard. Pass
loading / error straight from the query hook. That's enough to ship.
import { ChartCard, toChartData } from "@/components/dashboard";
import { useSemanticModelQuery } from "@/hooks/use-semantic-model-query";
const { data, isLoading, error } = useSemanticModelQuery({ connection, query });
<ChartCard
title="Revenue"
subtitle="Last 12 months"
loading={isLoading}
error={error}
spec={{
type: "line",
data: toChartData(data, { columns: { month: "Date[Month]", revenue: "Total Revenue" } }),
points: true,
encoding: {
x: { field: "month", type: "temporal" },
y: { field: "revenue", type: "quantitative", format: "$,.0f" },
},
}}
/>
Phase 2 — Breadth: add the rest (metric band, more charts, a DataTableCard),
wrapped in PageShell + StatStrip + DashboardGrid/Tile. Preview each
visual as you add it before automatic ship.
Phase 3 — Polish: slicers, interactivity, multi-series, formatting, dark-mode
review.
The data flow (map → author → drop in)
Every tile follows the same shape:
- Fetch with
useSemanticModelQuery({ connection, query }) →
{ data, isLoading, error } (see the dax + fabric-data skills).
- Map the DAX result into the shape the visual wants. Helpers accept the
query result, a raw
QueryTable, or undefined — no status check:
- Charts want tidy/long rows →
toChartData(result, options?).
- Tables want a Graphein
table spec → toTable(result, { columns }).
Hand-author a matrix spec over toChartData(result) rows for pivots.
- Author + pass. Put rows in a spec's
data and hand the spec to the card
with loading + error. Don't pre-render skeletons/empty states — the cards do it.
const rows = toChartData(data, {
columns: { month: "Date[Month]", revenue: "Total Revenue" },
});
Keep data tidy — split with series, don't pre-pivot
Graphein wants long/tidy data: one row per observation. To show multiple
series (multi-line, grouped/stacked bars, stacked areas), add a series channel
that points at the category column — do not widen the table into one column
per category.
[{ "quarter": "Q1", "channel": "Online", "revenue": 210 },
{ "quarter": "Q1", "channel": "Retail", "revenue": 180 }]
toChartData already returns long rows, so a normal DAX result drops straight in.
(pivotChartData still exists for the rare case you truly need wide rows, but with
Graphein you almost never do.)
Authoring a spec
A spec is a plain JSON object — no functions, no colors, no sizes:
{
"type": "bar",
"data": [ ],
"encoding": {
"x": { "field": "quarter" },
"y": { "field": "revenue", "type": "quantitative", "format": "$,.0f" },
"series": { "field": "channel" }
},
"stack": true
}
encoding is required for line/area/bar/scatter (x+y), pie
(theta+color), heatmap (x+y+color), funnel/waterfall
(stage/value), treemap (category+value), calendarHeatmap
(date+color), dumbbell (category+value+group), and combo (x +
per-layer y). gauge/bullet take a value (not encoding).
FieldDef.type is quantitative | temporal | ordinal | nominal (inferred when omitted).
- Validate → repair → render.
validateSpec(spec) → { valid, errors, warnings }
catches field-name typos and bad shapes; repairSpec(spec) auto-fixes many of
them (returns the patched spec). Both are re-exported from the barrel. See
Self-check below and the headless-preview skill. When a spec still
renders wrong, set debug: true on it to swap the chart for a diagnostic view
(live preview + resolved spec + data sample + validation + render report);
clear the flag to render normally.
- Don't author
theme. ChartCard injects the app's CSS-token theme (brand
color + dark mode) automatically. Recolor via src/global.css tokens, never
per-spec hex.
Pick a type
| Goal | type | Key channels / options |
|---|
| Trend over time | line (area to emphasize volume) | x temporal, y, optional series; points, curve |
| Part-to-whole over time | area + stack: true | x, y, series |
| Compare categories | bar | x category, y, optional series; stack or grouped |
| Two measures, different scales | combo (dual-axis) | encoding.x + layers[] each { mark, encoding.y, axis: "left"|"right" } |
| Stage conversion | funnel | stage, value, optional percent: "first" | "previous" |
| Running total / bridge | waterfall | stage, value (signed); totals for absolute bars |
| Composition of a total | bar + stack, or pie/donut | bar: series; pie: theta + color, donut, labels |
| Nested part-to-whole | treemap | category, value, optional group, color |
| Correlation / 3rd dim | scatter | x, y, optional size, series; trendline |
| Distribution of one measure | histogram | x (binned); bin controls |
| Density across two categories | heatmap | x, y, color, scheme |
| Value over a calendar | calendarHeatmap | date, color, scheme |
| Single value vs target/range | gauge / bullet | value (+ min/max; bullet adds target) |
| Before/after, two points per row | dumbbell | category, value, group (2 levels) |
| Rank change between two periods | slope | x (2 values), y, series |
| Headline metric | KpiCard (React) | not a Graphein chart spec — see Cards |
| Raw / detail records | DataTableCard with table spec | toTable(result, { columns }); see Cards |
| Pivot / cross-tab | DataTableCard with matrix spec | rows, columns, values, totals, conditional formatting |
Rules of thumb: prefer bar over pie beyond ~6 slices; stack for
part-to-whole, grouped bars for direct comparison; combo only when two measures
genuinely share an x but need different y-scales (don't reach for it by default).
Recipes (mirror the gallery)
{ "type": "line", "data": rows, "points": true,
"encoding": { "x": { "field": "month", "type": "temporal" },
"y": { "field": "value", "type": "quantitative", "format": "$,.0f" },
"series": { "field": "metric" } } }
{ "type": "area", "data": rows, "stack": true,
"encoding": { "x": { "field": "quarter", "type": "ordinal" },
"y": { "field": "revenue", "type": "quantitative", "format": "$,.0f" },
"series": { "field": "channel" } } }
{ "type": "bar", "data": rows, "stack": true,
"encoding": { "x": { "field": "quarter" },
"y": { "field": "revenue", "type": "quantitative", "format": "$,.0f" },
"series": { "field": "channel" } } }
{ "type": "bar", "data": topN(rows, "revenue", 8),
"encoding": { "x": { "field": "region", "type": "nominal" },
"y": { "field": "revenue", "type": "quantitative", "format": "$,.2s" } } }
{ "type": "scatter", "data": rows,
"encoding": { "x": { "field": "price", "type": "quantitative", "format": "$,.0f" },
"y": { "field": "units", "type": "quantitative" },
"size": { "field": "margin", "title": "Margin" } } }
{ "type": "pie", "data": rows, "donut": 0.6,
"encoding": { "theta": { "field": "value", "type": "quantitative", "format": "$,.0f" },
"color": { "field": "category" } } }
{ "type": "pie", "data": rows,
"labels": { "placement": "outside", "content": "category-percent", "minShare": 0.03, "connector": "muted" },
"encoding": { "theta": { "field": "value", "type": "quantitative", "format": "$,.0f" },
"color": { "field": "category" } } }
{ "type": "funnel", "data": rows, "labels": true, "percent": "previous",
"encoding": { "stage": { "field": "stage" },
"value": { "field": "users", "type": "quantitative", "format": ",d" } } }
{ "type": "heatmap", "data": rows, "scheme": "teal",
"encoding": { "x": { "field": "quarter" }, "y": { "field": "region" },
"color": { "field": "revenue", "type": "quantitative", "format": "$,.2s" } } }
{ "type": "combo", "data": rows,
"encoding": { "x": { "field": "month", "type": "temporal" } },
"layers": [
{ "mark": "bar", "axis": "left", "encoding": { "y": { "field": "revenue", "format": "$,.0f" } } },
{ "mark": "line", "axis": "right", "encoding": { "y": { "field": "margin", "format": ".0%" } } } ] }
{ "type": "histogram", "data": rows, "bin": { "maxbins": 20 },
"encoding": { "x": { "field": "orderValue", "type": "quantitative", "format": "$,.0f" } } }
{ "type": "treemap", "data": rows,
"encoding": { "category": { "field": "product" }, "value": { "field": "revenue", "format": "$,.0f" },
"group": { "field": "category" } } }
{ "type": "waterfall", "data": rows, "totals": ["Start", "End"],
"encoding": { "stage": { "field": "stage" }, "value": { "field": "delta", "format": "$,.0f" } } }
{ "type": "gauge", "data": [row], "min": 0, "max": 100, "value": { "field": "score" } }
{ "type": "bullet", "data": [row], "value": { "field": "actual" }, "target": { "field": "goal" },
"encoding": { "label": { "field": "metric" } } }
{ "type": "dumbbell", "data": rows,
"encoding": { "category": { "field": "region" }, "value": { "field": "revenue", "format": "$,.0f" },
"group": { "field": "year" } } }
{ "type": "line", "data": rows, "insights": true,
"annotations": [ { "type": "line", "value": 100, "label": "Target" } ],
"encoding": { "x": { "field": "month", "type": "temporal" },
"y": { "field": "revenue", "type": "quantitative", "format": "$,.0f" } } }
Full field-by-field docs + every channel/option:
Graphein spec reference.
Declarative features (graphein 0.16)
Reshape and enrich a chart inside the spec — no pre-massaging the data, no
second chart. All are plain JSON and render headlessly:
transform — an in-spec pipeline run before the chart builds: aggregate
(group + sum/mean/…), bin, filter, fold (wide→long), timeUnit,
calculate. Lets encodings reference fields the pipeline produces.
{ "type": "bar", "data": rows,
"transform": [{ "aggregate": [{ "op": "sum", "field": "revenue", "as": "total" }], "groupby": ["region"] }],
"encoding": { "x": { "field": "region" }, "y": { "field": "total", "format": "$,.0f" } } }
annotations — reference lines, bands, threshold zones, and
point callouts overlaid on the plot. A y-axis line uses value (a band
uses from/to; a point uses x+y):
"annotations": [{ "type": "line", "value": 100, "label": "Target" }].
insights: true — auto-mark the notable points (max/min; opt into
outliers) so you never hardcode where the peak is.
trendline: true — overlay a linear line of best fit (on scatter/line).
facet: { field } — split into a trellis of small multiples, one panel per
category, on shared scales.
Self-check before ship
Graphein 0.16 can critique its own specs — use it to iterate before ship:
validateSpec(spec) → { valid, errors, warnings } — path-pointed errors +
soft warnings. repairSpec(spec) → { spec, applied, remaining } auto-fixes
many mistakes (apply applied to your source). summarize(spec) → string —
a one-line read of what the chart says (sanity-check the trend).
- Render it against live data —
npm run preview -- --spec s.json --query <alias> --dax-file q.dax writes a themed PNG and a report (ok, diagnostics for
clipping/overlap/contrast, mark/series/color counts). View the PNG, read the
report, fix, repeat — then drop the spec into a <ChartCard>. Full loop +
flags: the headless-preview skill. KPI/table/matrix/slicers/dashboard
rasterize to PNG too, so preview-validate them before shipping.
Gotchas
- Horizontal bars are supported — set
orientation: "horizontal" on a bar
spec (keep encoding.x = category, encoding.y = value; the renderer swaps the
axes). For "top N" / ranked breakdowns still sort rows by value
(topN(rows, key, n)); horizontal reads best when category labels are long. For
a category comparison of two points (e.g. before/after), use a dumbbell.
- Reference lines & combo charts now exist (0.15). Use
annotations: [{ type: "line", value }] for a target/threshold line, and the combo type for two
measures on different y-scales — don't fake either with stacked ChartCards.
- Temporal fields are ISO strings (
"2024-01", "2024-01-15") or epoch ms —
JSON has no Date. Mark the field type: "temporal" for a time axis.
- Empty
data → empty tile. A spec with data: [] makes ChartCard show
its empty state. Never ship mock/placeholder rows in the real app — the one
exception is the clearly-labeled bundled demo under src/demo/**, which you
delete when you wire the real model.
Cards
ChartCard
The card shell — rounded-2xl, hairline border, no shadow — in two modes:
<ChartCard title="Revenue" subtitle="Last 12 months"
loading={isLoading} error={error} spec={spec} />
<ChartCard title="Filters"><ListSlicer … /></ChartCard>
Props: eyebrow, title, subtitle, action (right-aligned header slot),
variant ("surface" | "feature" | "outline" | "ghost"), accent
(thin left spine; use chart tokens like "chart-1"), spec, height (omit
for responsive aspect-based height; table/matrix specs auto-use a fixed scroll
height), isEmpty (force empty; defaults to detecting empty spec.data),
store, onSelectionChange, footer, loading, error, emptyMessage,
onRetry, bodyClassName, children.
KpiCard
Hero metric tile: big formatted value, colored delta pill, optional accent dot /
badge / icon, an optional variant, and an inline trend sparkline. Prefer
StatStrip for the top KPI header; use KpiCard for standalone metrics.
<KpiCard
label="Revenue"
data={rows} valueKey="revenue"
valueFormat="currency"
delta={9.2}
deltaLabel="vs last month"
trend={rows.map((r) => r.revenue)}
invertDelta={false}
/>
Pass a literal value or data + valueKey (reads only the first row — feed a single-row measure result or a
precomputed value, not a multi-row table you expect it to aggregate). With no
value it renders the empty state — never a fake 0. delta is a percent
number (9.2 → +9.2%), not a fraction. Use deriveKpi(result, { valueKey })
to get { value, previous, delta, trend } from a time series in one call.
Empty card with data present? valueKey must match a mapped column name
exactly (case-sensitive). Alias columns in toChartData({ columns: … })
for stable keys; in dev the console prints the available keys.
DataTableCard
A Graphein table / matrix in the card shell — virtualized, sortable, themed,
with conditional formatting, groups, and totals. Build a table spec with
toTable(result, { columns }); hand-author a matrix over toChartData(result)
rows for a pivot/cross-tab.
const table = toTable(data, {
columns: [
{ field: "account", source: "Customer[Account]", title: "Account" },
{ field: "revenue", source: "Revenue", title: "Revenue", format: "$,.0f", align: "right",
conditionalFormat: { type: "bar", showValue: true } },
],
sort: { field: "revenue", order: "desc" },
totals: { label: "Total" },
});
<DataTableCard title="Top accounts" loading={isLoading} error={error}
spec={table} height={420} />
Props: spec (TableSpec | MatrixSpec), height (default 360), store,
onSelectionChange, isEmpty, plus the shared card state props (title,
subtitle, action, loading, error, emptyMessage, onRetry). See
formatting & color and the
Graphein spec reference for table/matrix
fields.
Interactivity
Graphein specs can publish and consume named selections:
params publishes a point or interval selection (click marks or brush).
highlight consumes a selection by emphasizing matches and dimming the rest.
filter consumes selections or literal predicates by subsetting rows.
{ "type":"bar", "data":rows,
"params":[{ "name":"pick", "select":{ "type":"point", "fields":["region"] } }],
"encoding":{ "x":{"field":"region"}, "y":{"field":"revenue"} } }
{ "type":"line", "data":rows, "highlight":{ "param":"pick" },
"encoding":{ "x":{"field":"month","type":"temporal"}, "y":{"field":"revenue"}, "series":{"field":"region"} } }
Use SelectionStoreProvider / useSelectionStore() and pass the same store to
several ChartCards or DataTableCards for cross-highlight/cross-filter. The
default is Power BI–style: useCrossHighlight(field) + spreading
crossHighlightParams(param, fields) into the source spec makes a click dim that
chart's own unpicked marks while every other tile re-queries — feed the source
applyFilters(rows, pick.own(selections)) and others toDaxFilters(selections).
For a manual bridge, call useSelectionFilterBridge(store, { fieldMap }): it maps
Graphein selections into useFilterState, which drives applyFilters and
toDaxFilters. React slicers + DAX re-query remain the primary filter path because
this app's tiles are independently DAX-aggregated per tile.
Shape helpers (DAX → rows/specs)
toChartData(result, { columns? }) → tidy rows for a spec's data. Alias
columns for stable keys.
toTable(result, { columns, sort, totals, density, striped, numeric, text })
→ a Graphein table spec for DataTableCard. columns are Graphein table
columns plus optional source (full Table[Col], short name, or index).
topN(rows, valueKey, n, { other?, ascending? }) — sort + slice mapped
rows for ranked bars / leaderboards, with an optional "Other" rollup.
deriveKpi(result, { valueKey }) → { value, previous, delta, trend } for
a KpiCard.
Layout
Default to the new flat, non-uniform dashboard path: PageShell → StatStrip →
DashboardGrid + Tile. Build hierarchy with layout, surfaces, borders, accent
edges, and typography — no gradients or shadows.
import {
PageShell, ThemeToggle,
StatStrip, Stat,
DashboardGrid, Tile,
ChartCard, DataTableCard,
} from "@/components/dashboard";
<PageShell eyebrow="Sales" title="Revenue overview" subtitle="FY24" actions={<ThemeToggle />}>
<StatStrip>
<Stat label="Revenue" data={rows} valueKey="revenue" valueFormat="currency" accent="chart-1" delta={12.4} />
<Stat label="Orders" data={rows} valueKey="orders" delta={3.1} />
<Stat label="Avg order" value={84.2} valueFormat="currency" delta={-1.2} />
</StatStrip>
<DashboardGrid>
<Tile size="hero"><ChartCard title="Revenue trend" className="h-full" variant="feature" accent="chart-1" spec={lineSpec} /></Tile>
<Tile size="md"><ChartCard title="By region" spec={barSpec} /></Tile>
<Tile size="md"><ChartCard title="Channel mix" spec={pieSpec} /></Tile>
<Tile size="full"><DataTableCard title="Detail" spec={tableSpec} /></Tile>
</DashboardGrid>
</PageShell>
- Frames:
PageShell is the default; SidebarShell adds an in-content
filter/context rail for filter-heavy analytics; AppShell is the flexible
lower-level frame for custom mastheads, toolbar, or rail composition.
- Metric header:
StatStrip + Stat is one bordered, hairline-divided band
of 2–5 metrics. Prefer it over four look-alike KpiCards at the top.
- Grid:
DashboardGrid is the responsive 12-col canvas. Use Tile size:
"sm" 3, "md" 4, "lg" 6, "wide" 8, "hero" 8×2, "full" 12.
Mix sizes for editorial rhythm; do not default to a uniform grid. A hero
tile needs className="h-full" on the card inside (it spans 2 rows; without it
the card sits at its natural height and leaves the lower row blank). Two md
tiles right after a hero stack to fill its remaining 4-col × 2-row corner.
- Zones/cards:
SectionBand creates alternate-surface (surface-2) zones.
Card, ChartCard, and KpiCard use flat variants ("surface" | "feature" | "outline" | "ghost"); Card/ChartCard also take accent for a thin left
spine. Use chart tokens such as "chart-1", not raw colors.
- Legacy:
KpiGrid, ChartGrid, BentoGrid, and BentoItem still exist for
back-compat, but avoid them by default in new dashboards.
Controls & slicers (interactivity)
Chart specs can now publish selections, but React slicers remain the primary
server-side filter path in this app: they update shared filter state, which then
re-filters client rows or re-queries DAX.
Lightweight controls — own the value in useState, filter your rows:
const [range, setRange] = useState("30d");
<SegmentedControl value={range} onChange={setRange}
options={[{ label: "7D", value: "7d" }, { label: "30D", value: "30d" }]} />
SegmentedControl<T> (single-select pills) · FilterChips<T> (multi-select chips).
Power BI-style slicers — wire one shared filter model. The starter
already mounts a FilterBar of slicers in the PageShell toolbar, wrapped in
<FilterStateProvider> — feed real options and apply the selections; you
rarely need to add the provider yourself. Every slicer reads/writes the same
selections. Apply them with applyFilters(rows, selections) (instant,
client-side) or toDaxFilters(selections) (re-query the model — see dax).
<FilterStateProvider>
<FilterBar>
<DropdownSlicer label="Category" field="Product[Category]" options={catOptions} />
<DateRangeSlicer label="Date" field="Date[Date]" />
<RangeSlicer label="Price" field="Product[Price]" min={0} max={1000} />
</FilterBar>
<RevenueByRegion /> {}
</FilterStateProvider>
Slicers: DropdownSlicer, ListSlicer, SearchSlicer, DateRangeSlicer,
RangeSlicer, FilterBar. Fetch distinct values with
useSlicerOptions({ connection, field, … }). Full guide:
slicers & filter state.
Formatting & color
- In a spec: format numbers/dates with Graphein's
format mini-language on a
FieldDef —
"$,.0f", ",d", ".1%", ".2s" (→ 1.2k), "%b %e, %Y" (dates).
- Pie labels: use
labels (placement: "outside" for callouts,
content: "category-percent", etc.).
- KpiCard:
valueFormat — "number" | "compact" | "currency" | "percent" (0–100) | "ratio" (0–1) or a (n) => string function.
- Table/matrix: column/value
format plus conditionalFormat (bar, icon,
colorScale, rules).
- Color/theme: never put hex in a spec —
ChartCard themes every chart from
src/global.css tokens (--color-chart-1..10, accent, dark mode). Restyle by
editing those tokens. See formatting & color.
State tiles
Used internally by the cards; use directly only for custom content.
EmptyTile (message, icon, height) · ErrorTile (error,
title, onRetry, height) · ChartSkeleton / KpiSkeleton ·
TileBody (error → loading → empty → children switchboard).
Import surface
import {
AppShell, PageShell, SidebarShell, DashboardGrid, Tile, StatStrip, Stat,
SectionBand, Section, Card, ThemeToggle, SegmentedControl, FilterChips,
KpiGrid, ChartGrid, BentoGrid, BentoItem,
FilterStateProvider, useFilterState, FilterBar,
DropdownSlicer, ListSlicer, SearchSlicer, DateRangeSlicer, RangeSlicer,
useSlicerOptions, applyFilters, toDaxFilters,
ChartCard, KpiCard, DataTableCard, Chart, validateSpec, createSelectionStore,
SelectionStoreProvider, useSelectionStore, useSelection, type ChartSpec,
useSelectionFilterBridge, selectionToFilters, filterToSelection,
useCrossHighlight, crossHighlightParams, selectionsExcept,
EmptyTile, ErrorTile, ChartSkeleton, KpiSkeleton, TileBody, Sparkline,
toChartData, toTable, topN, deriveKpi, pivotChartData,
formatNumber, formatCompact, formatCurrency, formatPercent, formatDate,
seriesColor, roleColor,
} from "@/components/dashboard";
References