developer guide
In-Cell Charts and Sparklines in a Data Grid
A column can hold a picture of its own numbers rather than one of them: line, area and column sparklines, bullet and gauge, win-loss, pie and donut, range and delta. They are drawn in the cell, so a thousand rows of trend scroll like any other column.
Developer guide › Columns and cell rendering › In-Cell Charts and Sparklines in a Data Grid
In-cell charts
Eleven chart types for a cell: line, area, column,
winloss, pie, donut, bullet,
stacked, range, gauge and delta. Most read an array;
bullet and gauge read a number.
A trend, a mix and a target
columns: [
{ id: 'trend', field: 'readings', cell: 'line' },
{ id: 'mix', field: 'split', cell: { render: 'donut', props: { hole: 0.55 } } },
{ id: 'sla', field: 'uptime',
cell: { render: 'bullet', props: { target: 80, bands: [60, 85], max: 120 } } },
// Two views of one field: give each an explicit id.
{ id: 'shape', field: 'readings', cell: 'line' },
{ id: 'detail', field: 'readings', cell: 'column' },
]
A bullet whose target and bands come from the row
const { createTestDom, resetTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
resetTestDom();
const { root } = createTestDom({ width: 800, height: 300 });
const { createGrid } = await import('../packages/dom/src/index.js');
createGrid(root, {
rowKey: 'id',
rows: [
{ id: 'a', progress: 40, expected: 25, edges: [20, 50] },
{ id: 'b', progress: 40, expected: 75, edges: [30, 60, 100] },
],
columns: [{
field: 'progress',
cell: { render: 'bullet', props: {
min: 0, max: 100,
target: 'expected', // a row field; or (p) => p.data.expected
bands: (p) => p.data.edges, // a function; or a field name: bands: 'edges'
} },
}],
});
flushFrames();
const cells = [...root.querySelectorAll('[data-key]')]
.sort((x, y) => (x.getAttribute('data-key') < y.getAttribute('data-key') ? -1 : 1));
const x = (c) => c.querySelector('.lat-chart__target').getAttribute('d').match(/^M([\d.]+)/)[1];
const n = (c) => (c.querySelector('.lat-chart__band').getAttribute('d').match(/M/g) || []).length;
return `targets ${cells.map((c) => Number(x(c))).join(',')} | bands ${cells.map(n).join(',')}`;
Bullet target and bands follow the row.
Each accepts a number (or an array of band edges) as before, the name of a row field, or a
function of the cell params, resolved per cell the way min and max
are and re-resolved when the row changes. A target that is missing, empty or not a number
draws no marker and logs nothing; bands that are not an array draw none. The marker takes
the theme's foreground token, so it reads in light and dark.
A cell is a few hundred pixels seen for a second, so there are no axes, no
gridlines and no legend. The chart carries one idea (a shape, a share, a comparison) and the
number beside it carries the precision. Hide the number with label: false when the
column next to it already says the same thing.
Pin min and max when columns are meant to compare.
A sparkline scaled to its own data fills its cell whatever the magnitude, so two rows differing
by an order of magnitude draw identically. Pinning the scale is what makes the column readable
down its length rather than only across it.
A gap is not a zero. Entries that are not numbers break the line and omit the bar, rather than being drawn at the baseline: joining across a missing reading would draw a trend nobody measured, and drawing it at zero invents a dip.
Cost. Each chart is one SVG built once, with the paths' d
attributes the only thing a repaint writes, and a bar chart is two paths rather than one
element per bar. Rows recycle as you scroll, so this is what keeps a chart column the same
price as a text one. Nothing measures the DOM, the drawing happens in a fixed coordinate
space that CSS scales.
Accessibility. The chart is aria-hidden and the cell carries a
summary: "12 points, 9 to 20, ending 18". A path cannot be read aloud, and a series announced
value by value tells a listener less than the sentence does.