Skip to content
D
Documentation

Build a data-first dashboard

how-to
3 min readUpdated

Use the dashboard kit when your UI is a board of widgets arranged in cells. You declare views and widgets as data; the kit mounts the board, paints the built-in widget kinds, and owns drag, resize, and undo.

Before you start

Install the package for your framework and the shared engine and renderer. The examples below use the package versions supported by Grafloria 0.4:

bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer

For React, Vue, Angular, or Qwik, also install that framework binding. The bindings support React 17–19, Vue 3.4 or later, and Angular 18.1 or later.

1. Declare the board

Start with DashboardViewSpec objects. A view contains widgets; a widget needs an id and can specify a kind, span, rows, title, and data. If you omit x and y, widgets flow in declaration order. The built-in renderers draw kpi, line, bar, donut, funnel, and table widgets from their data.

The browser shows Revenue and Customers KPI cards, a Revenue trend chart, and a Revenue by region chart. Give the mounted element a height; a board in a zero-height container has no space to draw.

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

const views = [{
  id: 'overview',
  name: 'Overview',
  widgets: [
    { id: 'revenue', kind: 'kpi', span: 3, rows: 1,
      data: { label: 'Revenue', value: '$6.81M', delta: 12.4, spark: [42, 45, 51, 61, 76] } },
    { id: 'customers', kind: 'kpi', span: 3, rows: 1,
      data: { label: 'Customers', value: '1,284', delta: 8.1, spark: [980, 1010, 1090, 1150, 1284] } },
    { id: 'trend', kind: 'line', span: 6, rows: 2, title: 'Revenue trend',
      data: { series: [{ name: 'Revenue', values: [42, 45, 51, 61, 76] }], labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'] } },
    { id: 'regions', kind: 'donut', span: 6, rows: 2, title: 'Revenue by region',
      data: { slices: [{ label: 'EMEA', value: 1.92 }, { label: 'APAC', value: 1.34 }] } },
  ],
}];

const spec = dashboard({ views, columns: 12, sizing: 'fit' });
const container = document.getElementById('dashboard');
if (!container) throw new Error('Missing #dashboard');
container.style.height = '500px';
const instance = render(spec, container);

The mounted board shows the KPI cards and charts, and the user can drag or resize them. views and widgets are mutually exclusive: use views for tabs, or widgets as the shorthand for one unnamed view.

The Overview board shows KPI cards above the trend and regional charts.

2. Change the live layout

Keep the typed DashboardHandle returned by the JavaScript dashboard() spec or emitted by a framework binding. The handle changes the live board without rebuilding it:

ts
import type { DashboardHandle } from '@grafloria/element';

function configure(handle: DashboardHandle): void {
  handle.setSizing('grow');
  handle.setLayout('split');
  handle.setColumns(6);
  handle.setRtl(true);
  handle.setStatic(false);
}

'fit' keeps the board bounded and squeezes rows; 'grow' keeps the row height and extends the board. setLayout('split') changes the cell grid to the splitter layout. setColumns() changes the live column count, and setRtl(true) mirrors pixels without changing cell data. A static board has no pointer drag, resize, or handles.

The component props layout, sizing, and static are live. Other declaration data is read at mount; after mount, use the handle for changes.

3. Persist the resulting board

Use DashboardSnapshot data from toJSON() as the persisted value. It contains the live views, cells, columns, gap, sizing, float, and direction.

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

const saved = localStorage.getItem('sales-board');
const snapshot = saved ? JSON.parse(saved) as DashboardSnapshot : undefined;
const { views, ...options } = snapshot ?? {
  views: [{ id: 'overview', widgets: [{ id: 'revenue', kind: 'kpi', data: { label: 'Revenue', value: '$6.81M' } }] }],
  columns: 12,
  sizing: 'fit' as const,
};
const spec = dashboard({ ...options, views });
const container = document.getElementById('dashboard');
if (!container) throw new Error('Missing #dashboard');
container.style.height = '500px';
render(spec, container);
const handle = spec.handle;

localStorage.setItem('sales-board', JSON.stringify(handle.toJSON()));

The save call reads the live board, so a user’s moves, resizes, view changes, and layout switches are included. In a framework binding, connect its layout-change event to the same persistence call:

ts
import type { DashboardHandle } from '@grafloria/element';

function persist(handle: DashboardHandle): void {
  localStorage.setItem('sales-board', JSON.stringify(handle.toJSON()));
}

Do not serialize the render functions. On restore, pass the saved data back to dashboard() and provide renderWidget again if you supplied one.

Options that matter

OptionTypeDefaultWhat it does
columnsnumber12Sets the column count for every view.
gapnumber8Sets widget spacing and board padding in pixels.
sizing'fit' | 'grow''grow'Bounds the board or lets it extend by row height.
layout'grid' | 'split''grid'Selects cell packing or splitter layout.
rowHeightnumber130Sets row height in grow mode, in pixels.
staticbooleanfalseDisables pointer dragging, resizing, and handles.
rtlbooleanfalseMirrors rendered columns while keeping cell coordinates.
responsiveDashboardResponsiveOptions—Derives live columns from board width.

Pitfalls

  • Give the dashboard element an explicit height.
  • Use DashboardHandle after mount; changing views later does not rebuild the board.
  • In fit mode, an add, move, or resize that needs more capacity is refused. Use grow mode or overflow: 'scroll' when the board must retain its row height.
  • views and widgets cannot be supplied together.
  • Use widgetTypes for custom React widgets and grafloriaWidget templates for custom Angular widgets. Otherwise the shipped built-in painters handle the six built-in kinds.

See it running

Related: Dashboards in JavaScript, Dashboards in React, and Dashboards in Angular.

Was this page helpful?