Learn / Dashboards in React
Dashboards in React
<GrafloriaDashboard> is the dashboard kit with React idioms on top: views declare the whole board as data, the kit wires the 12-column pack grid, drag/resize gestures and undo, and widgetTypes is the nodeTypes pattern applied to widgets.
Declare the board, mount the component
A widget is { id, kind, span, rows, data } — plus optional
title, pinned, and an explicit cell (x/y).
Omit the coordinates and widgets flow in declaration order, wrapping at the column
count, so the common case needs no geometry at all. span defaults to 3
columns, rows to 1:
import { GrafloriaDashboard } from '@grafloria/react';
const views = [
{ id: 'overview', name: 'Overview', widgets: [
{ id: 'kpi-revenue', kind: 'kpi', span: 3, rows: 1,
data: { label: 'Total revenue', value: '$6.81M', delta: 12.4, deltaLabel: 'vs last qtr',
spark: [42, 45, 47, 51, 50, 55, 59, 61, 60, 65, 71, 76] } },
{ id: 'trend', kind: 'line', span: 6, rows: 2, title: 'Revenue trend',
data: { series: [{ name: 'Revenue', values: [42, 45, 47, 51, 50, 55, 59, 61, 60, 65, 71, 76] }],
labels: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'] } },
{ id: 'mix', kind: 'donut', span: 6, rows: 2, title: 'Revenue by region',
data: { slices: [
{ label: 'EMEA', value: 2.9, color: '#3B52D9' },
{ label: 'AMER', value: 2.4, color: '#94A5F0' },
{ label: 'APAC', value: 1.5, color: '#059669' },
], centerLabel: '$6.8M' } },
{ id: 'bars', kind: 'bar', span: 6, rows: 2, title: 'Deals by quarter',
data: { bars: [
{ label: 'Q1', value: 210 }, { label: 'Q2', value: 260 },
{ label: 'Q3', value: 245 }, { label: 'Q4', value: 292 },
] } },
] },
];
export default function Board() {
return (
<div style={{ height: '100vh' }}>
<GrafloriaDashboard views={views} activeView="overview" />
</div>
);
}
That renders a working board — drag widgets between cells, resize them from the
corner, undo any of it — with real KPI, line, donut and bar widgets drawn from the
data above and not a charting library in sight. views is the multi-view
(tabbed) form; widgets is the single-view shorthand, and the two props are
mutually exclusive.
The six built-in kinds
When a widget's kind has no entry in widgetTypes, the
kit's built-in painters draw it from your own data — hand-rolled inline
SVG, zero dependencies, no sample dataset. The shapes they read:
kind: 'kpi' data: { label?, value?, delta?, deltaLabel?, spark?: number[] }
kind: 'line' data: { series?: number[] | { name?, values: number[] }[], labels?: string[] }
kind: 'bar' data: { bars?: { label?, value? }[] }
kind: 'donut' data: { slices?: { label?, value?, color? }[], centerLabel?, centerCaption? }
kind: 'funnel' data: { stages?: { label?, value? }[] }
kind: 'table' data: { columns?: string[], rows?: (string | number)[][] }
Every field is optional because a widget renderer must never throw — it paints into
a live board, mid-gesture, on every reflow — so missing or partial data degrades to an
empty-state note instead. A kind nobody knows falls back to a titled
placeholder frame, which makes a layout testable before any chart exists. Colors cycle
a built-in categorical palette; a per-slice color wins. KPI
value is a string on purpose: you pre-format it, so units and currency
stay yours.
Custom widgets: widgetTypes and WidgetProps
widgetTypes maps a widget kind to a real React component.
Components are portal-mounted into the kit's host elements, so hooks, context and
state work inside — the exact custom-node idiom, applied to boards. The component
receives { widget, data }: the full spec, and its data
payload:
import { useState } from 'react';
import type { WidgetProps } from '@grafloria/react';
function OrdersCard({ widget, data }: WidgetProps<{ title: string }>) {
const [expanded, setExpanded] = useState(false); // real component: state survives gestures
return (
<div onClick={() => setExpanded(!expanded)}>
<b>{data.title}</b> {expanded ? '▾' : '▸'}
</div>
);
}
<GrafloriaDashboard
views={views}
widgetTypes={{ orders: OrdersCard as never }} />
Kinds with a component render through React; kinds without one fall back to the built-in painters. Mixing both on one board is the normal case, not a trick.
widgetTypes — not
options.renderWidget. The wrapper installs its own
renderWidget to power the portal mechanism, so a function you pass inside
options is shadowed. Reserve options for board behavior and
geometry.Props: options, activeView, and the mount-once rule
options carries the board's physics — columns (default 12),
gap (default 8), sizing ('fit' squeezes rows into
the board height, 'grow' extends the board at rowHeight,
default 130), width/height, float (off means
gravity packs upward), rtl (mirrors pixels, never cells),
responsive (width-driven column count, via columnWidth or
named breakpoints) and binder, the escape hatch to the
gesture layer below.
activeView is the tab pattern: a controlled prop that drives
showView, so switching tabs is just re-rendering with a different id —
inactive views park off-camera, they are not unmounted.
views, widgets and options at mount and does not
rebuild on prop changes — activeView is the one prop that stays live.
After mount, data flows through the handle: widget updates, adds, removals,
sizing switches. Changing the views array on a later render does
nothing.Live: gravity packing, pinning, RTL, responsive columns →
The handle: your runtime API
onReady hands you the typed DashboardHandle once the board
is live; onLayoutChange mirrors every committed gesture — drag, resize,
add, remove — with the affected view's widgets, ready to persist:
import type { DashboardHandle } from '@grafloria/element';
const handleRef = useRef<DashboardHandle | null>(null);
<GrafloriaDashboard
views={views}
onReady={(h) => { handleRef.current = h; }}
onLayoutChange={({ viewId, widgets }) => persist(viewId, widgets)} />
// later — everything is a method on the handle:
const h = handleRef.current!;
h.widget('kpi-revenue')?.update({ data: { label: 'Total revenue', value: '$7.02M', delta: 3.1 } });
h.addWidget({ id: 'funnel', kind: 'funnel', span: 4, rows: 2,
data: { stages: [{ label: 'Leads', value: 900 }, { label: 'Won', value: 210 }] } });
h.setSizing('grow');
h.showView('ops');
The highlights: widget(id) returns a per-widget handle
(update, repaint, resize, moveTo,
pin, remove — the destructive ones are single undoable
steps, re-pack included). addWidget creates the node and its board
membership as one undo step. setSizing / setFloat /
setColumns / setRtl flip board behavior live, and
refresh() re-reads the boards after an undo/redo or any out-of-band model
change. binderOf() is the documented escape hatch to the raw grid
binder.
Persistence: the toJSON round-trip
// save — the whole board as plain data (views, cells, columns, gap, sizing, float, rtl)
localStorage.setItem('board', JSON.stringify(handle.toJSON()));
// restore — the function seams cannot be serialized, so supply them again:
import type { DashboardSnapshot } from '@grafloria/element';
const { views, ...options } = JSON.parse(saved) as DashboardSnapshot;
<GrafloriaDashboard views={views} options={options} widgetTypes={{ orders: OrdersCard as never }} />
toJSON() reads the live board, not the authored literal — a
sizing mode or column count the user changed after mount is what you get back. And it
serializes from the engine's widest cached column layout, so a board squeezed to one
column on a phone still saves the 12-column layout its user authored. It is also what
JSON.stringify(handle) calls.
export() frames the whole model — a two-view board can silently write
a ~21,000px document that is almost entirely empty. Scope it:
api.export('pdf', { includeIds: handle.exportIds() }). The set includes
the view's group, which hand-rolling from toJSON() would drop.Dashboard or flow?
Use <GrafloriaDashboard> when the thing you are building is a
board: cells, spans, gravity packing, pinning — and no wires. Widgets are
deliberately not connectable (their ports are stripped), and the board has no edges at
all. Use <GrafloriaFlow> when nodes connect
to nodes — free positions, edges, routing, validation. And if you are rendering any kit
spec — erDiagram(...), umlDiagram(...),
dashboard(...), DSL text — <GrafloriaDiagram spec={...}>
is the one generic host component; <GrafloriaDashboard> is that,
plus the React widget idioms.
Where next
- Kits — the data-first authoring layer the dashboard kit belongs to.
- Custom nodes in React — the same portal mechanism, one level down.
- Commands and undo — why widget add/remove is one Ctrl-Z, not two.
- Export — PNG, SVG and real vector PDF, with
includeIdsscoping.