Learn / Dashboards in Angular

Dashboards in Angular

A dashboard is data: [views] declares the whole board and the kit wires the 12-column pack grid, drag/resize gestures and undo. <grafloria-dashboard> adds the Angular idioms on top — a two-way active view, a typed handle, and ng-template widgets.

A board in one binding

board.component.ts
import { Component } from '@angular/core';
import { GrafloriaDashboardComponent } from '@grafloria/angular';

@Component({
  selector: 'app-board',
  imports: [GrafloriaDashboardComponent],
  template: `
    <grafloria-dashboard [views]="views" [(activeView)]="tab"
      style="display:block; height:100vh" />
  `,
})
export class BoardComponent {
  tab: string | undefined = 'overview';
  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 },
        ] } },
    ]},
  ];
}

That is a complete interactive dashboard: widgets drag and resize on a gravity-packed grid, every committed gesture is one undo step, and each kind above is painted by a built-in renderer. Widgets flow in declaration order, wrapping at the column count — the common case needs no coordinates at all (add x/y to pin a cell explicitly, pinned: true for a tile no reflow may move). As with the canvas: give the element a real height.

Live: the dashboard builder →

The component surface

  • [views]DashboardViewSpec[]: a multi-view (tabbed) board. Each view is { id, name?, widgets, columns?, width?, height? } — the last three override the board geometry per view.
  • [widgets]DashboardWidgetSpec[]: single-view shorthand. Mutually exclusive with views.
  • [options]Partial<DashboardOptions>: columns (default 12), gap (default 8), sizing: 'fit' | 'grow', rowHeight, float, rtl, and responsive (derive the live column count from board width — by columnWidth or named breakpoints).
  • [(activeView)] — two-way active view id; the tab pattern.
  • (ready) — emits the typed DashboardHandle once the board is live.
  • (layoutChange){ viewId, widgets }, mirroring the kit's committed gestures: drag, resize, add, remove.
  • Methods: getHandle() (undefined before first paint) and snapshot().

A widget spec is small: { id, kind?, span?, rows?, x?, y?, pinned?, data?, title? }span defaults to 3 columns, rows to 1, and data is your payload, handed back untouched to whatever renders the widget.

Built-in widget kinds

Six kinds render with zero code of yours — their data shapes, from the kit's own type declarations:

kind: 'kpi'    → { label?, value?, delta?, deltaLabel?, spark?: number[] }
                 // headline number; delta paints up/green or down/red; spark = trend line
kind: 'line'   → { series?: number[] | { name?, values }[], labels?: string[] }
                 // a bare number[] is the single-series shorthand
kind: 'bar'    → { bars?: { label?, value? }[] }
kind: 'donut'  → { slices?: { label?, value?, color? }[], centerLabel?, centerCaption? }
kind: 'funnel' → { stages?: { label?, value? }[] }   // each stage scaled vs the first
kind: 'table'  → { columns?: string[], rows?: (string | number)[][] }
                 // numbers right-align on their own

Any other kind string gets a titled placeholder frame — useful while a board's data contracts are still settling.

Custom widgets: ng-template grafloriaWidget

The node-template idiom, applied to boards. A widget whose kind matches a template renders through it — full Angular change detection, components, pipes and event handlers; kinds without a template fall back to the built-in painters above. grafloriaWidget with no value is the wildcard for any kind without an exact template.

orders-board.component.ts
import { GrafloriaDashboardComponent, GrafloriaWidgetDefDirective } from '@grafloria/angular';

@Component({
  imports: [GrafloriaDashboardComponent, GrafloriaWidgetDefDirective, OrdersCardComponent],
  template: `
    <grafloria-dashboard [views]="views" style="display:block; height:100vh">
      <ng-template grafloriaWidget="orders" let-widget let-data="data">
        <app-orders-card [title]="widget.title" [orders]="data['orders']" />
      </ng-template>
    </grafloria-dashboard>
  `,
})

The context mirrors the node one: $implicit (let-widget) is the full DashboardWidgetSpec, let-data="data" is its payload as Record<string, unknown> — index syntax again. Your template is stamped into a wrapper the component sizes to 100% × 100% of the widget's cell, so a root with height: 100% fills the tile.

A widget template runs once per widget when it mounts — the host element is reused across re-renders, not repainted per frame. Design widget components to own their data flow (signals, observables, inputs) rather than expecting the dashboard to re-stamp them.

Tabs — the active-view pattern

Multiple views park all but one off-camera; showView frames the chosen one. The component folds that into [(activeView)], so tab buttons are just writes to your own property:

<nav>
  @for (v of views; track v.id) {
    <button [class.on]="tab === v.id" (click)="tab = v.id">{{ v.name }}</button>
  }
</nav>
<grafloria-dashboard [views]="views" [(activeView)]="tab"
  style="display:block; height:calc(100vh - 48px)" />

On mount, a pre-set activeView is applied; if you left it undefined the component reflects the board's boot view back into the binding (deferred a microtask — writing it in the same change-detection pass would be NG0100).

Live: grids, float, RTL, responsive columns and pinning — one tab each →

Persistence: layoutChange + the snapshot round-trip

<grafloria-dashboard [views]="views" (ready)="handle = $event"
  (layoutChange)="persistView($event.viewId, $event.widgets)" />

(layoutChange) fires after any committed gesture with the view whose layout changed — the natural autosave hook. For whole-board persistence, snapshot() returns the handle's toJSON(): every DashboardOptions field except the function seams (renderWidget, onLayoutChange), read from the live board — a column count or sizing mode the user changed after mount is what you get back. Feed it back to [views]/[options] and the identical board rebuilds; that is the kit's round-trip contract.

Beyond that, the DashboardHandle from (ready) is the live-control surface: showView, addWidget (creates the node and commits it as one undoable step), setColumns / setRtl / setSizing / setFloat live switches, metrics(), refresh() after out-of-band mutations, and exportIds(viewId) — the node ids one view occupies, for scoping an export to a single board instead of the whole multi-view model.

Dashboard or canvas?

Both render through the same engine — which is why dashboard gestures are undoable and boards serialize like any diagram. Choose by what the user manipulates:

  • <grafloria-dashboard> when the content is widgets on a grid: cells, spans, gravity packing, pinning, RTL, responsive columns. You give up free placement and get layout discipline for free.
  • <grafloria-diagram-canvas> when the content is a graph: nodes anywhere, edges, ports, routing, layout algorithms. See Custom nodes in Angular.

The kit family — dashboards, ER, UML — shares this data-first shape; the framework-agnostic story is in Kits.

Where next

  • Kitsdashboard(), erDiagram(), umlDiagram() from plain data, in any framework.
  • State, signals & tooling — the canvas-side binding surface this page builds on.
  • The model — what those widget nodes really are underneath.