# Build a dashboard

Use the dashboard kit when your board is data: declare views and widgets, mount
the board in a sized element, then use its live handle for layout changes and
persistence. The result is a responsive board with KPI and chart cards that the
user can rearrange.

## Declare the board

[`DashboardWidgetSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-dashboardwidgetspec)
is the data for one widget. Give it an `id`, a `kind`, a cell size, and the
payload consumed by the kit's renderer. A
[`DashboardViewSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-interfaces) groups
widgets into one view; supplying several views gives you tabs.

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

export const views: DashboardViewSpec[] = [{
  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: 'bar', span: 9, rows: 2,
      title: 'Revenue trend',
      data: {
        bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }],
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
      },
    },
  ],
}];
```

The shipped `kpi` and `line` widget painters render these cards. A widget with
`widgets` instead becomes a section with its own inner grid; use that shape for
nested boards rather than constructing grid nodes yourself.

## Mount it

Give the dashboard element a height. Without one, the board has no drawing
space. The following front doors produce the same board.

### JavaScript

The [`dashboard`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-functions) function
creates a [`DashboardSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-interfaces);
[`render`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-core) mounts that spec and returns the
diagram instance.

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

const views: DashboardViewSpec[] = [{
  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: 'bar', span: 9, rows: 2, title: 'Revenue trend',
      data: { bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }] } },
  ],
}];
const target = document.getElementById('dashboard') ?? document.body.appendChild(document.createElement('div'));
target.id = 'dashboard';
target.style.display = 'block';
target.style.height = '660px';
target.style.width = '1180px';

const spec = dashboard({ views, columns: 12, gap: 8, sizing: 'fit', responsive: { columnWidth: 96 } });
const instance = render(spec, target);
const handle = spec.handle;

handle.setLayout('grid');
localStorage.setItem('dashboard', JSON.stringify(handle.toJSON()));

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

```html
<div id="dashboard" style="height: 660px"></div>
```

`handle.toJSON()` returns the complete board as plain data, including the live
layout. Feed that object back to `dashboard()` when you load a saved board.

### Angular

[`GrafloriaDashboardComponent`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-angular-components-grafloriadashboardcomponent)
uses `[views]` for the board and emits its typed handle through `ready`.

```ts
import { Component } from '@angular/core';
import { GrafloriaDashboardComponent } from '@grafloria/angular';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';

@Component({
  standalone: true,
  imports: [GrafloriaDashboardComponent],
  template: `
    <button (click)="changeLayout()">Split</button>
    <button (click)="save()">Save</button>
    <grafloria-dashboard [views]="views" (ready)="handle = $event"
      (layoutChange)="dirty = true" style="display:block;height:660px" />
  `,
})
export class DashboardComponent {
  handle?: DashboardHandle;
  dirty = false;
  views: DashboardViewSpec[] = [{ id: 'overview', name: 'Overview', widgets: [
    { id: 'revenue', kind: 'kpi', span: 3, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
    { id: 'trend', kind: 'bar', span: 9, rows: 2, title: 'Revenue trend',
      data: { bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }] } },
  ] }];

  changeLayout() { this.handle?.setLayout('split'); }
  save() {
    const snapshot = this.handle?.toJSON();
    if (snapshot) localStorage.setItem('dashboard', JSON.stringify(snapshot));
    this.dirty = false;
  }
}
```

The `ready` output arrives after the first paint. `layoutChange` marks the
board dirty after a committed drag, resize, add, or remove; `save()` stores the
current arrangement rather than the original `views` array.

### Qwik

[`GrafloriaDashboard`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-qwik) takes `views`, `layout`, and
`onReady$`. Keep the live handle non-serializable and call it from a Qwik event.

```tsx
import { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaDashboard } from '@grafloria/qwik';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';

export default component$(() => {
  const handle = useSignal<NoSerialize<DashboardHandle>>();
  const views: DashboardViewSpec[] = [{ id: 'overview', widgets: [
    { id: 'revenue', kind: 'kpi', span: 3, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
     { id: 'trend', kind: 'bar', span: 9, rows: 2, data: { bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }] } },
  ] }];
  const save = $(() => {
    const snapshot = handle.value?.toJSON();
    if (snapshot) localStorage.setItem('dashboard', JSON.stringify(snapshot));
  });
  return <>
    <button onClick$={save}>Save</button>
    <GrafloriaDashboard views={views} onReady$={$((value) => {
      handle.value = noSerialize(value);
    })} style={{ display: 'block', height: '660px' }} />
  </>;
});
```

### React

The React binding follows the same data-first props. Its `onReady` callback
provides the live handle used by the toolbar.

```tsx
import { useState } from 'react';
import { GrafloriaDashboard } from '@grafloria/react';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';

export default function Dashboard() {
  const [handle, setHandle] = useState<DashboardHandle>();
  const views: DashboardViewSpec[] = [{ id: 'overview', widgets: [
    { id: 'revenue', kind: 'kpi', span: 3, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
    { id: 'trend', kind: 'bar', span: 9, rows: 2, data: { bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }] } },
  ] }];
  return <>
    <button onClick={() => handle?.setLayout('split')}>Split</button>
    <button onClick={() => { const s = handle?.toJSON(); if (s) localStorage.setItem('dashboard', JSON.stringify(s)); }}>Save</button>
    <GrafloriaDashboard views={views} onReady={setHandle} style={{ display: 'block', height: 660 }} />
  </>;
}
```

### Vue

Vue exposes the same board through `v-model:active-view`; `@ready` captures the
handle while the component owns mounting and repainting.

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaDashboard } from '@grafloria/vue';
import type { DashboardHandle, DashboardViewSpec } from '@grafloria/element';

const handle = ref<DashboardHandle>();
const tab = ref('overview');
const views: DashboardViewSpec[] = [{ id: 'overview', widgets: [
  { id: 'revenue', kind: 'kpi', span: 3, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
  { id: 'trend', kind: 'bar', span: 9, rows: 2, data: { bars: [{ label: 'Jan', value: 42 }, { label: 'Mar', value: 51 }, { label: 'May', value: 65 }] } },
] }];
function save() {
  const snapshot = handle.value?.toJSON();
  if (snapshot) localStorage.setItem('dashboard', JSON.stringify(snapshot));
}
</script>

<template>
  <button @click="handle?.setLayout('split')">Split</button>
  <button @click="save">Save</button>
  <GrafloriaDashboard :views="views" v-model:active-view="tab" @ready="handle = $event"
    style="display:block;height:660px" />
</template>
```

## Change the live board

The [`DashboardHandle`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-dashboardhandle)
is the shared runtime handle. Use it for live layout changes. `addWidget(spec)`
creates and mounts a widget and
returns its handle, or `undefined` when a bounded board has no room. Use
`widget(id)` to call `pin(true)`, `resize()`, `moveTo()`, or `update()` on one
widget.

| Option | Type | Default | What it does |
|---|---|---:|---|
| `columns` | `number` | `12` | Sets the board column count. |
| `gap` | `number` | `8` | Sets widget gaps and board padding in pixels. |
| `sizing` | `'fit' \| 'grow'` | — | `fit` bounds the board; `grow` lets rows extend it. |
| `layout` | `'grid' \| 'split'` | — | Selects the cell grid or splitter layout. |
| `responsive` | `DashboardResponsiveOptions` | — | Derives the live column count from board width. |
| `static` | `boolean` | — | Turns pointer dragging, resizing, and handles off. |

Use the `layout` prop or input for the initial state and the handle for a live
toolbar switch. Do not mutate a saved snapshot while the board is mounted;
serialize the handle's current `toJSON()` result after the user's changes.

## See it running

- [Dashboard builder](https://grafloria.com/demos/dashboard/dashboard-builder.html)
  shows tabbed views, palette additions, pinning, layout switching, and save/load.

- [Fluid board](https://grafloria.com/demos/dashboard/fluid-board.html) shows
  fit versus grow sizing and grid versus split layout.

- [Grid options](https://grafloria.com/demos/dashboard/grid-options.html) shows
  responsive columns, float, RTL, sections, and pinned widgets.

- [Nested containers](https://grafloria.com/demos/dashboard/nested-containers.html)
  shows a widget containing its own board.

## Related

- [Dashboard options](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-dashboardoptions)
- [Dashboard widget spec](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-dashboardwidgetspec)
- [Dashboard handle](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-dashboard-kit-dashboardhandle)
