Lattice Grid Buy a licence

api reference

The KPI (stat-tile) view

createKPI and the stat tiles, the aggregations, formatting, baselines and delta, threshold bands, sparklines, and the keyed-diff surface a data router drives.

API reference › The KPI (stat-tile) view

The KPI (stat-tile) view

modules/kpi is an opt-in view of a dataset as a panel of stat tiles - each tile an aggregate over the routed rows: a sum, an average, a min/max, a count, a distinct count, or a host reducer. It is the fourth first-class viewer beside the grid, the kanban and the gantt, in the same shape: a separate bundle that adds no weight to a page that does not load it, changes nothing in grid core, and pulls in no dependency. A KPI panel is just another dataset viewer: it consumes data through the same keyed-diff contract a grid exposes, kpi.rows.apply({ add, update, remove }), so a Data Router can attach(value, kpi) and drive a KPI panel beside a grid, a kanban and a chart off one feed.

import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';

const kpi = createKPI(document.querySelector('#kpis'), {
  rows,                        // or { grid } to read a live grid's rows
  rowKey: 'id',
  columns: 4,                  // responsive tile columns
  tiles: [
    { id: 'total', label: 'Revenue', aggregation: 'sum', field: 'amount', format: 'currency' },
    { id: 'avg', label: 'Avg deal', aggregation: 'avg', field: 'amount',
      format: { type: 'currency', decimals: 0 }, baseline: 5000 },   // delta vs a baseline
    { id: 'open', label: 'Open deals', aggregation: 'count',
      filter: (r) => r.stage !== 'won',
      thresholds: { warn: 10, critical: 25, direction: 'lowerIsBetter' } },  // good/warn/critical bands
  ],
  onTileClick: ({ tile }) => drillInto(tile.id),
});

Each tile names an aggregation (sum, avg, min, max, count, countDistinct, or a custom reducer (rows, tile) => value), the field it reads, and an optional filter predicate. Formatting (number/currency/percent/compact, with decimals, currency and locale), a baseline for a delta, and semantic threshold bands (two cut points with a direction, or an explicit bands list) are all per tile; the band is a semantic name (good/warn/critical), separate from any accent colour. An optional sparkline plots a { x, y } series in sequence order. Each tile is a labelled <figure>, focusable and keyboard-activatable, its value announced; the sparkline transition respects prefers-reduced-motion.

Incremental, not recomputed. Each tile keeps a running accumulator, so a routed rows.apply delta adjusts only the rows it carries - an add contributes, a remove reverses, an update reverses the old row and contributes the new one - rather than re-reading the whole dataset per delta. The two bounded exceptions are honest: an extreme (min/max) removed at its current value triggers a rescan of that tile's own value multiset, and a custom reducer is recomputed over the (filtered) store because an arbitrary function has no inverse.

MemberDescription
createKPI(el, config)Create a KPI panel. Pass a DOM element to render into, or null for a headless panel that computes the same tile model without a DOM.
rows.apply({ add, update, remove })The keyed-diff consumer contract a grid shares, so the panel is a drop-in Data Router target and updates each tile incrementally. Also rows.forEach and rows.count.
tiles() / tile(id) / value(id)Every computed tile model, one tile by id (value, formatted, status, delta, deltaPercent, count, sparkline), or a tile's raw value.
setRows(rows) / refresh()Replace the source rows, or re-seed and recompute.
getState() / setState(snapshot)Serialise and restore the panel's row set, so a headless panel round-trips.
on(name, fn) / off(name, fn)Events: tile:click, tile:dblclick, tile:contextmenu, and change (after every update).
destroy()Empty the element and drop the listeners. The host still owns any bound grid.

Interaction is light and host-driven. A tile emits tile:click (also from the keyboard) carrying the tile model, so a host can drill down or, in a demo, filter a routed grid - the wiring lives in the host, not the module. This is deliberately not a dashboard layout engine (that is the parked dashboard generator) and charting beyond a minimal sparkline belongs to the charts module.

Live, driven by a Data Router alongside a grid, executed

One feed fans out (overlap) to a KPI panel through the same keyed-diff contract a grid uses: a snapshot seeds the tiles, then a delta removes the current max and the min/max rescans. Run headless on every build.

const { createKPI } = await import('../packages/modules/kpi/index.js');
const { createDataRouter } = await import('../packages/modules/data-router/index.js');

const kpi = createKPI(null, {
  rowKey: 'id',
  tiles: [
    { id: 'total', label: 'Revenue', aggregation: 'sum', field: 'amount', format: 'currency' },
    { id: 'max', label: 'Biggest', aggregation: 'max', field: 'amount' },
    { id: 'open', label: 'Open', aggregation: 'count', filter: (r) => r.stage === 'open',
      thresholds: { warn: 1, critical: 3, direction: 'lowerIsBetter' } },
  ],
});

const router = createDataRouter({ key: 'kind', rowKey: 'id', overlap: true });
router.attach(kpi, 'deal');                  // a KPI panel is a drop-in router target
router.load([
  { id: 'd1', kind: 'deal', amount: 100, stage: 'open' },
  { id: 'd2', kind: 'deal', amount: 250, stage: 'won' },
]);
const seeded = kpi.value('total') + ',' + kpi.value('max');       // 350,250

router.apply([{ op: 'delete', row: { id: 'd2' } }]);              // removes the current max
const afterRemove = kpi.value('total') + ',' + kpi.value('max'); // 100,100 (max rescanned)
const band = kpi.tile('open').status;                          // 'good' - 1 open, lowerIsBetter

router.destroy();
return [seeded, afterRemove, band].join(' | ');