Lattice Grid

developer guide

KPI Tiles Above a JavaScript Data Grid

A tile puts one figure above the rows and keeps it in step with them: bands that colour it good or bad, an icon, the record behind the maximum rather than just the maximum, and a scope that reads either the filtered view or the whole dataset.

Developer guide › Statistics and capability › KPI Tiles Above a JavaScript Data Grid

Statistic tiles

A headline figure over a grid, with the change since a baseline, a tone from thresholds, and the interval underneath.

A tile that follows the grid

import { createStat } from '@toclocoinc/lattice-grid';

createStat({
  grid,
  container: tile,
  title: 'Total capacity',
  // A leading icon beside the title and value. Same three forms as a menu
  // item: a sprite name, a character/emoji, or your own markup.
  icon: '<i class="fa-light fa-gauge-high"></i>',
  value: { of: 'capacity', fn: 'sum' },
  baseline: (g) => lastMonth,
  bands: { good: 5000, warn: 3000, direction: 'up' },
  interval: (v, g) => g.statistics.interval('capacity'),
});

icon is optional and lays out to the side without disturbing the change indicator, threshold bands or confidence interval; omit it for the plain tile. It uses the same icon contract as a menu item - a sprite name, a single character or emoji, or author-supplied markup such as a Font Awesome glyph or an <img>.

A tile bound to a grid on a pushdown, paged or remote source - one that holds only a window of its matching set, not every row - never reduces the loaded page. A fn: 'count' tile reads grid.rows.matchCount(), the same matching total the grid's own footer and pagination already read, and follows every header filter, quick filter and filter removal to it. A sum, avg, min or max tile asks the source's engine for the one number instead - one aggregate query, scoped by the grid's current filters and quick search, through the same source.aggregate() a KPI tile and a chart already ask. When the engine cannot answer it - the column is not a plain one, the filter or the quick search did not fully push, or the source's own aggregates config (or the adapter itself) keeps that statistic client-side - the tile shows "Not available on this source" and warns once, by name, rather than silently reducing the page it happens to have loaded. Over an in-memory source, or a tile with scope: 'selected' (the selection is loaded rows by definition), nothing here changes.

A tile can say more than a number. footer (a string, or a function of the value) is the caption under it. tooltip (a string, or a function of the value and the grid) describes the tile: it is the hover title and the tile's aria-description, so assistive technology hears it too. deltaWording replaces the arrow and percentage with a sentence - a function of the change, or a template per direction using {change}, {percent} (signed), {value} and {baseline}: { up: 'Up {change} on the previous period ({percent})' } reads "Up 12 on the previous period (+8%)". The same tooltip and deltaWording (plus a literal value and a caption) are fields of a KPI panel's stat tile, so a literal figure no longer needs compute: () => value.

visual draws the value as well as printing it, so a screen stops hand-building an SVG gauge inside a stat tile. 'ring' is a donut arc of the value over its target with the number in the centre; 'gauge' is a semicircle with the bands thresholds as coloured segments and a needle at the value. It works on a createStat tile and on every entry of the grid's kpis strip. It is vector, so crisp at any device-pixel ratio; it is coloured from theme tokens, so light and dark follow the theme; it sweeps to the new value when a grid filter changes it; and the tile's accessible name gives the value and the target.

OptionMeaning
visual'ring' or 'gauge'. Omit it for the plain number.
targetWhat the value is measured against: a number, or a function of the grid. A ring fills to value / target; it is also said in the accessible name.
maxThe top of the scale. Defaults to target, else 100. A gauge with a max also ticks the target.
minThe bottom of a gauge's scale. 0 by default; a ring always starts at 0.
bandsThe gauge's coloured segments, from the same { good, warn, direction } that tones the tile. A ring takes its colour from the band the value is in.

A ring and a gauge on a stat tile

const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createStat } = await import('../packages/dom/src/stat.js');

resetTestDom();
const dom = createTestDom({ width: 400, height: 200 });
const box = () => { const el = dom.document.createElement('div'); dom.root.appendChild(el); return el; };

// 72 against a target of 100: a ring with the number in its centre.
const ring = createStat({ container: box(), title: 'Fill', value: 72, target: 100, visual: 'ring' });

// The same reading as a gauge on a 0-100 scale, banded: below 50 bad, 50-80 warn, 80+ good.
const gauge = createStat({
  container: box(), title: 'Health', value: 72, visual: 'gauge', min: 0, max: 100, target: 90,
  bands: { good: 80, warn: 50 },
});

const r = ring.element().querySelector('.lat-stat__visual').getAttribute('data-fraction');
const g = gauge.element().querySelector('.lat-stat__visual').getAttribute('data-band');
return `ring ${r} | gauge ${g} | name ${ring.element().getAttribute('aria-label')}`;

Trend line, fixed status and accent

Three more options on a createStat tile and on every entry of the grid's kpis strip, so a screen does not hand-build them. sparkline draws a small trend line under the number with the same drawing createKPI's tiles use, coloured from theme tokens and redrawn whenever the tile refreshes. status gives the tile a fixed tone (its leading edge and its value take the theme's variant colour) when no bands decide one; bands win when both are set. The meter bar's own tone is a different option, bar.tone, and is unchanged. accent colours the tile's accent marker through a public custom property, --lat-stat-accent, so a host never restyles an internal class: set it on the tile with accent, or from your own stylesheet. A threshold tone or a fixed status takes the edge over the accent.

OptionMeaning
sparklineAn array of numbers, or { values, field, points, goodWhen }. values is a literal series; field reads that field from the grid's rows in the order the grid shows them, keeping the last points; goodWhen: 'up' | 'down' colours the line good or bad by whether the series ended up or down. Fewer than two numbers draws nothing.
status'neutral', 'info', 'success', 'warning' or 'danger', or a function of the value returning one. Overridden by bands.
accentA CSS colour or var() token, applied as --lat-stat-accent on the tile.

A sparkline, a status that follows the value, and an accent

const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createStat } = await import('../packages/dom/src/stat.js');

resetTestDom();
const dom = createTestDom({ width: 400, height: 200 });
const box = () => { const el = dom.document.createElement('div'); dom.root.appendChild(el); return el; };

const tile = createStat({
  container: box(), title: 'Orders', value: 8,
  sparkline: [3, 5, 4, 8],
  status: (value) => (value > 5 ? 'danger' : 'success'),
  accent: '#d6336c',
});
const root = tile.element();
const points = root.querySelector('polyline').getAttribute('points').split(' ').length;
return `points ${points} | status ${root.getAttribute('data-status')} | accent ${root.style.getPropertyValue('--lat-stat-accent')}`;

In the grid's kpis strip the same options sit on each entry, for example kpis: [{ title: 'Orders', of: 'qty', fn: 'sum', sparkline: { field: 'qty', points: 12 }, status: 'info' }].

A tile that computes its own value

Give a tile compute and no aggregation and it uses compute: createKPI tiles take compute: (rows, tile) => value, and createStat and the grid's kpis strip take compute: (rows) => value over the rows in scope. Name a built-in aggregation (aggregation: 'sum', or fn: 'sum' on a stat) as well and the aggregation wins: compute never runs and one warning names the tile and the conflict. aggregation: 'custom' (or fn: 'custom') with compute is the same as naming neither.

compute with no aggregation

const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createStat } = await import('../packages/dom/src/stat.js');
const { createKPI } = await import('../packages/modules/kpi/index.js');

// No aggregation: compute is the tile's value, not a row count.
const kpi = createKPI(null, { rows: [{ id: 1 }, { id: 2 }], rowKey: 'id', tiles: [{ id: 'answer', label: 'Answer', compute: () => 42 }] });

resetTestDom();
const dom = createTestDom({ width: 400, height: 200 });
const el = dom.document.createElement('div');
dom.root.appendChild(el);
const stat = createStat({ container: el, title: 'Answer', compute: () => 42 });
return `kpi ${kpi.value('answer')} | stat ${stat.value()}`;

bands and goodWhen judge different things. goodWhen says whether a rise is good news, and colours the change indicator. bands judge the value itself. They are separate because a Cpk of 0.9 is bad news whether it rose or fell to get there.

The tile follows the grid by default. Filter the grid and the figure updates. scope chooses filtered, all or selected rows; live: false detaches it and leaves refresh() to you.

value also takes show, which reports a field from the row holding an extreme rather than the extreme itself: { of: 'sales', fn: 'max', show: 'rep' } is the name of the best rep. It needs min or max, because no single row holds an average.