Lattice Grid Buy a licence

developer guide

Build a Dashboard from a JSON Spec

A dashboard used to mean a page of wiring: a layout of windows, a grid, charts, KPI tiles, a map, and the listeners that kept them in step. createDashboard builds all of it from one JSON spec, and hands the same spec back so an application can store and version a dashboard the way it already stores anything else.

One spec, a grid, a chart and a KPI tile

A spec names a layout, the panels it holds and the sources they read. Each panel is a grid, chart, kpi, map or html viewer, and its options pass straight through to that viewer's own factory, unchanged. Hand createDashboard the factories your page already loaded (on a script-tag page they are found on LatticeGrid, LatticeGridLayout and LatticeGridKPI without being named).

import { createDashboard } from '@toclocoinc/lattice-grid/modules/dashboard';

const dashboard = createDashboard(el, {
  layout: { columns: 3, rows: 2 },
  sources: { sales: { kind: 'rows', rows, rowKey: 'id' } },
  panels: [
    { id: 'table', kind: 'grid', title: 'Sales', source: 'sales', options: { selection: 'multiple' } },
    { id: 'byRegion', kind: 'chart', source: 'grid:table', options: { type: 'bar', x: 'region', y: 'sales' } },
    { id: 'total', kind: 'kpi', source: 'grid:table', options: { tiles: [{ id: 'sum', field: 'sales', aggregation: 'sum' }] } },
  ],
}, { createGrid, createHeadlessGrid, createLayout, createChart, createKPI });
// A chart or KPI panel reads "grid:table": the table's own grid, filtered.
// Filtering the table narrows the chart and the tile with it.

See it running, with the spec printed back on demand: a dashboard from one JSON spec.

A source per panel: rows, pushdown, a live route, or another panel's grid

A named source under sources can hold rows already in the page, a pushdown adapter (every key but kind is handed to createPushdownSource unchanged), or a Data Router route, attached once the dashboard is built so it sees only what arrives from then on. A panel can also read grid:<panel id> instead of a named source, viewing another panel's own grid directly, so a chart or a KPI tile narrows the moment the table beside it is filtered.

sources: {
  // Rows already in the page.
  memory: { kind: 'rows', rows, rowKey: 'id' },
  // A pushdown adapter; every key but "kind" goes to createPushdownSource.
  live: { kind: 'pushdown', adapter: duckdbAdapter({ connection, from: "read_parquet('data.parquet')" }) },
  // A Data Router route; attached once the dashboard is built.
  ticks: { kind: 'router', router, predicate: 'AAPL' },
},
panels: [
  { id: 'a', kind: 'grid', source: 'memory', options: {} },
  // A panel can also read another panel's own grid directly, inline.
  { id: 'b', kind: 'chart', source: 'grid:a', options: { type: 'line', x: 'date', y: 'close' } },
]

A map panel: a map chart, Leaflet or deck.gl

A map panel takes a binding: 'chart' (the default, a map chart type such as markermap or choropleth), 'leaflet' (bindLeaflet) or 'deckgl' (bindDeck), each needing its own module loaded on the page. See the Leaflet guide, the deck.gl guide and the geospatial guide for what each one draws.

A link names a from and a to panel and the column relating them. Over one Data Router it becomes a router.relate() edge; otherwise a named filter predicate on the target's grid. Two pushdown sources with no router between them, or two routers, are refused rather than silently ignored.

links: [
  // Selecting rows in "table" filters "detail" on a shared column.
  { from: 'table', to: 'detail', on: 'accountId' },
  // Different column names either side:
  { from: 'orders', to: 'lines', on: { from: 'orderId', to: 'order_id' } },
]
// Over one Data Router this becomes a router.relate() edge; two pushdown
// sources with no router between them, or two routers, are refused by name.

spec(), saved views, and storing a dashboard

dashboard.spec() hands back the dashboard exactly as built, every window's placement included, wherever a reader dragged it, so rebuilding from it gives the same dashboard back. That makes a dashboard something an application can store, version and restore, the same as any other record. saveView/applyView go further, keeping every grid panel's own state (sort, filter, column order) alongside the layout under one saved name.

const saved = dashboard.spec();          // every window's live placement, included
localStorage.setItem('sales-dashboard', JSON.stringify(saved));

// Later, or on another visit:
const stored = JSON.parse(localStorage.getItem('sales-dashboard'));
dashboard.apply(stored);                  // tears down and rebuilds exactly this
dashboard.saveView('Q3 review');          // the spec AND every grid's own state
dashboard.applyView('Q3 review');

propose(): drafting a spec with your own AI model

With the AI module and a host ask() callback (the same shape createAI takes), dashboard.propose(prompt, { source }) asks your model for a spec built over one source's column names and types alone. It never sees the rows. A panel naming a column the source does not have, or a viewer kind this page cannot draw, is dropped and named in the returned warnings, and nothing is built until you call dashboard.apply(spec) on what comes back, so a proposal is always a draft a reader (or your own code) can check first.

const ask = (payload) => model.complete(payload); // your own model call

const dashboard = createDashboard(el, { panels: [] }, { createGrid, createHeadlessGrid, createLayout, createChart, createKPI, ai: { ask } });

const { spec: proposed, warnings } = await dashboard.propose('sales by region with a total tile', { source: 'sales' });
// The model sees the source's column names and types, never the rows. A panel
// naming a column the source lacks, or a kind this page cannot draw, is
// dropped and named in "warnings". Nothing is built yet.
if (proposed) dashboard.apply(proposed);  // accept it, or let the reader edit it first

Every refusal, named

Whatever a spec asks for that cannot be built, a bad link, a panel kind with no factory, a source that does not exist, is refused rather than silently dropped: dashboard.problems() lists every one, by a catalogued dashboard:* id, the path into the spec, and a plain-language reason. validateDashboardSpec(spec) checks a spec the same way without building anything, for validating a proposal or a stored dashboard before it reaches the page.

dashboard.problems();
// [{ id: 'dashboard:link:orders->missing', path: 'links[0].to',
//    message: 'links[0] points at panel "missing", which this spec does not declare.' }]
// Every refusal is catalogued and named, never a silent drop.

See it running: a dashboard from one JSON spec, with a Show spec button that prints dashboard.spec() and rebuilds the same dashboard from it. The dashboard API reference covers every option in full, and the layout API reference covers the windows a dashboard's panels sit in on their own.