JavaScript data grid KPI tiles that filter, 1.111
Look at the screen around any data grid in a real product. Above it a strip of numbers: open items, overdue items, how many were closed this week. Beside it a progress bar. Under it a short list of the same records by owner. Almost none of that comes from the grid. It is hand-built HTML, wired to the grid with a few event listeners, and it goes stale the first time someone adds a filter the listeners did not expect.
The panel above is that screen with nothing hand-built. Press Overdue. The grid filters, the “+ n more” count under it changes, the sprint meter recounts and the list by owner drops the tickets that no longer match, heading counts included. Press it again and everything comes back. The whole panel is about 100 lines of wiring, and none of it is a listener that copies the filter from one place to another. This is the first of several things in 1.111, and they share one idea: the pieces around a grid are the grid’s own, so they follow its filters because they read the same rows.
KPI tiles that filter the grid
The grid’s kpis strip already showed numbers that follow the filters. In 1.111 a tile can also be the filter. Give it filter: { name, where } and a click, Enter or Space turns that filter on. The tile is a button with aria-pressed and a selected style drawn from the theme, so it reads in light and dark.
const grid = createGrid(el, {
rows, kpiSelection: 'single',
kpis: [
{ title: 'Open', fn: 'count', filter: { name: 'open', where: (r) => r.status !== 'Resolved' } },
{ title: 'Overdue', compute: (rows) => rows.filter(isOverdue).length,
filter: { name: 'overdue', where: isOverdue } },
],
});
Each click is one undo step, and the selected state is read from the grid, so clearing the filter from code, from another control or with undo deselects the tile. kpiSelection: 'single' releases the other tiles when one is chosen, and 'multiple' lets several be on at once, combined with AND. onClick(tile, event) fires with or without a filter, and returning false skips the toggle.
Tiles as drawings
A tile can draw its number. visual: 'ring' is a donut arc of the value over a target, with the figure in the middle. visual: 'gauge' is a semicircle from min to max, with bands as coloured segments and a needle at the value. The demo’s last tile is a ring: tickets resolved against a target of 24.
{ title: 'Resolved vs target', visual: 'ring', target: 24,
compute: (rows) => rows.filter(isResolved).length }
Both are inline SVG coloured from theme tokens, so they are crisp at any pixel ratio and follow the theme, and the arc sweeps to its new value when a filter changes it. The tile’s accessible name carries the value and the target.
Meters, with a target
createMeter draws the bar a dashboard otherwise builds by hand: a value in a range, a mark at the target, bands that recolour the fill, or segments for a done, in progress and blocked bar, with a caption such as “18 of 30 closed”. Hand it the grid and its value is an aggregate over the rows the grid shows.
createMeter({
container: el, grid, label: 'Closed', max: 30, target: 30,
compute: (rows) => rows.filter(isClosed).length,
bands: [{ from: 20, tone: 'success' }],
caption: (v) => `${v} of 30 closed`,
});
A tile takes the same bar with bar: true, drawn under its number against its target.
Panels that size to their content
A KPI strip is as tall as its tiles, and that changes when they wrap on a phone. Setting its height by hand means measuring it and measuring it again. In 1.111 a layout or dashboard window takes height: 'content', and so does a shell row (a column takes width: 'content'). The panel gets the height of what it holds, re-flows when that changes, and the other panels share the rest.
windows: [
{ id: 'tickets', xPos: 1, xSize: 8, height: 'content' },
{ id: 'owners', xPos: 1, yPos: 2, xSize: 12, ySize: 6 },
]
minHeight and maxHeight bound it. The sprint and ticket panels in the demo are content-sized, so the ticket panel shrinks to fit as a filter leaves fewer rows, and the list below takes the rest.
Grouped lists and card views
Row grouping in a JavaScript grid has always been the grid’s job. Lists and card views now get it too. createList and createDataView take groupBy, a field name or a function, and draw a heading per group with its label and count. The list at the bottom of the demo is grouped by owner and folds with collapsibleGroups.
createList(el, {
createGrid, data: tickets, groupBy: 'owner', collapsibleGroups: true,
groupHeading: (g) => `${g.label} (${g.count})`,
template: (t) => `${t.id} ${t.title}`,
});
Groups follow the view’s sort, filter and search, and a filter that empties a group removes its heading. groupOrder orders them, stickyGroupHeadings keeps the current heading in view, and list.groups folds them from code. The group rows with subtotals guide covers grouping in the grid itself.
A row cap, and tiles with more to say
A compact panel wants the first few rows and an honest note about the rest. rowCap: 8 paints the first eight rows after sort and filter and a “+ n more” footer under them. The count follows the data: filter 40 rows to 12 under a cap of 5 and it reads “+ 7 more”, filter to 4 and the footer goes away. rowCap: { rows, onMore } calls you when the footer is used, and returning false keeps the cap for a host that opens its own view of the rest. Only what is drawn is cut, so totals, tiles and exports still see every row.
Tiles also take a literal value, a footer for the caption under it (a caption on a KPI panel’s tiles), a tooltip that doubles as the accessible description, and deltaWording, so a tile reads “Up 12 on the previous period (+8%)” instead of an arrow and a percentage:
{ title: 'Revenue', value: 412000, baseline: 381000,
deltaWording: { up: 'Up {change} on the previous period ({percent})' } }
Also in 1.111
- Chart category order. A bar, column, line or area chart takes
categoryOrder:{ by: 'value', dir: 'desc' }for tallest first,{ by: 'label' }, or a list of labels to put first. The order holds when the bound grid is filtered. - Header histograms over DuckDB.
facets: trueover a DuckDB source now draws its header histograms with no provider code. Numbers get equal-width bins, text its commonest values and blanks a last “Empty” bar. See the DuckDB data grid. - Grouping over DuckDB.
groupDefaultExpandedis honoured on a database-backed grid, and changing the grouped columns re-plans the query and loads the child rows. - Column titles stay readable. A heading’s title shortens with an ellipsis beside its icons, hovered or not, sorted or filtered or not, and a sort icon can no longer sit over its last characters.
- Smaller fixes. A shell tab marked
selected: trueactivates on first render, a tile withcomputeand no aggregation uses it, and the chart’s y-axis title clears wide tick labels.
Upgrading from 1.110
There is nothing to change. Everything above is new surface that opts in by use, and a grid with no kpis, rowCap or groupBy renders as it did.
Three things are worth knowing. A KPI tile with a compute function and no aggregation now shows what compute returns, where it used to show the row count. Naming both is a conflict: the aggregation wins and one console warning names the tile. Undo and redo now restore filters added with filters.where, which a tile’s toggle relies on. And a grid with a groupRenderer and stickyGroupHeaders now pins your own heading band at the top instead of an empty row.
The changelog has the full list, and the grid engine page covers the core all of this builds on. Open the panel above, press Overdue, then Due today, and watch the five views move together.