Skip to content
D
Documentation

Build a dashboard

how-to
3 min readUpdated

Declare widgets as data, mount the board, then use its handle for view changes, layout changes, programmatic moves and persistence. The result is a live board whose built-in widget painters draw KPI, line, donut, bar, funnel and table widgets; users can drag and resize it when the board is not static.

When to use this

Use the dashboard kit when your page needs a grid of independently movable widgets, more than one board view, or a saved layout. Give the mounted element a height: a dashboard in a zero-height container has no visible board.

1. Declare the board

dashboard takes DashboardOptions and returns a DashboardSpec. Each widget is a DashboardWidgetSpec; each view follows DashboardViewSpec. Omit renderWidget to use the shipped painters.

ts
import { dashboard, render } from '@grafloria/element';

const spec = dashboard({
  columns: 12,
  sizing: 'grow',
  views: [
    {
      id: 'overview',
      name: 'Overview',
      widgets: [
        {
          id: 'revenue',
          kind: 'kpi',
          span: 3,
          rows: 1,
          data: { label: 'Revenue', value: '$6.81M', delta: 12.4 },
        },
        {
          id: 'trend',
          kind: 'line',
          span: 9,
          rows: 2,
          title: 'Revenue trend',
          data: {
            series: [{ name: 'Revenue', values: [420, 455, 512] }],
            labels: ['Jan', 'Feb', 'Mar'],
          },
        },
      ],
    },
    {
      id: 'sales',
      name: 'Sales',
      widgets: [
        {
          id: 'regions',
          kind: 'donut',
          span: 6,
          rows: 2,
          title: 'By region',
          data: { slices: [{ label: 'EMEA', value: 1920 }, { label: 'APAC', value: 1340 }] },
        },
      ],
    },
  ],
});

const canvas = document.createElement('div');
canvas.id = 'dashboard';
canvas.style.display = 'block';
canvas.style.width = '1180px';
canvas.style.height = '660px';
document.body.append(canvas);
const instance = render(spec, canvas);
const handle = spec.handle;

handle.showView('sales');
handle.setLayout('grid');
handle.widget('regions')?.resize(8, 3);
localStorage.setItem('dashboard', JSON.stringify(handle.toJSON()));

window.addEventListener('unload', () => instance.dispose());

The render() call mounts the spec and returns a live diagram instance. The handle exposes the dashboard facade: showView() changes the on-camera view, setLayout() changes the active view between the cell grid and split layout, widget() finds one widget, and resize() accepts cell span and row count. The last line saves plain data, not a renderer object.

2. Mount it in each framework

The same views data drives all bindings. The examples below mount a board with a real height, switch between grid and split layouts, move and resize a widget through the typed handle, and save the snapshot.

js
import { dashboard, render } from '@grafloria/element';

const views = [{ id: 'main', name: 'Main', widgets: [
  { id: 'revenue', kind: 'kpi', span: 4, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
  { id: 'trend', kind: 'line', span: 8, rows: 2, title: 'Trend', data: { series: [[1, 2, 3]], labels: ['A', 'B', 'C'] } },
]}];
const spec = dashboard({ views, sizing: 'grow' });
const canvas = document.createElement('div');
canvas.id = 'dashboard';
canvas.style.display = 'block';
canvas.style.width = '1180px';
canvas.style.height = '660px';
document.body.append(canvas);
const instance = render(spec, canvas);
const handle = spec.handle;
handle.setLayout('split');
handle.widget('trend')?.moveTo(2, 0);
void handle.widget('trend')?.resize(6, 2);
localStorage.setItem('dashboard', JSON.stringify(handle.toJSON()));
window.addEventListener('unload', () => instance.dispose());

In every binding, the board paints the declared cards inside the sized container. Pointer drag and corner resize use the same live grid as moveTo() and resize(); a committed change is reported by onLayoutChange (or the Angular/Vue output). Use that callback instead of maintaining a second copy of the layout.

3. Persist and restore the board

DashboardSnapshot is the whole board as plain data. Save handle.toJSON() after a user change. To restore it in JavaScript, pass the parsed snapshot back to dashboard, supplying the rendering callback if you use custom widgets.

ts
import { dashboard, render } from '@grafloria/element';
import type { DashboardSnapshot } from '@grafloria/element';

const canvas = document.createElement('div');
canvas.id = 'dashboard';
canvas.style.display = 'block';
canvas.style.width = '1180px';
canvas.style.height = '660px';
document.body.append(canvas);
const source = dashboard({ widgets: [
  { id: 'revenue', kind: 'kpi', span: 4, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
] });
const snapshot: DashboardSnapshot = source.handle.toJSON();
localStorage.setItem('dashboard', JSON.stringify(snapshot));
const saved = localStorage.getItem('dashboard');
if (!saved) throw new Error('Dashboard snapshot was not saved');
const restored = dashboard(JSON.parse(saved) as DashboardSnapshot);
render(restored, canvas);

For a controlled framework board, keep the snapshot in application state and provide its views and options to the component. Do not recreate the board for each gesture; use the ready handle and the layout-change callback.

Options that matter

OptionTypeDefaultWhat it does
columnsnumber12Sets the column count for every view unless a view overrides it.
gapnumber8Sets the widget gap and board padding in pixels.
sizing'fit' | 'grow''grow'grow keeps row height and extends the board; fit keeps the board height and squeezes rows.
layout'grid' | 'split''grid'Selects the cell grid or splitter layout.
rowHeightnumber130Sets row height in grow mode.
floatbooleanfalseWhen false, gravity packs widgets upward; when true, gaps are allowed.
rtlbooleanfalseMirrors pixels so column zero renders at the right edge without changing cells.
responsiveDashboardResponsiveOptions—Derives the live column count from board width.
staticbooleanfalseDisables pointer drag, resize and handles while API and keyboard access remain available.
views / widgetsDashboardViewSpec[] / DashboardWidgetSpec[]—Use views for tabs, or widgets as shorthand for one unnamed view; they are mutually exclusive.

Pitfalls

  • Give the host a height. The kit cannot paint a useful board into a zero-height element.
  • fit is bounded: a drop, resize or addWidget() that needs another row is refused. Use grow or overflow: 'scroll' when the board must accommodate more rows.
  • static is a viewing mode. It removes drag, resize and handles; it does not remove API operations.
  • width and height are fixed-board dimensions. A fluid board follows its container instead.
  • A widget with pinned: true refuses the mover and survives reflow. Use movable: false or resizable: false when only one gesture must be disabled.

Live demos

  • Dashboard builder shows tabbed views, built-in widget kinds, palette additions, pinning, fit/grow, layout switching and versioned saves.

  • Fluid dashboard shows fluid sizing, grid and split layouts, drag-handle modes and live additions.

Was this page helpful?