In-cell sparklines
Problem: a column that shows the shape of a series — twelve months of revenue, a rolling error rate — as a small chart inside the cell rather than a number.
What you need
Section titled “What you need”Nothing, for a plain trend line. Charting is a domain of its own — scales, axes, tooltips, animation, accessibility — so any built-in version would be either too limited to use or too large to justify. The grid’s renderer returns a DOM node, which is all a chart needs, and an inline <svg> is a few lines that cost nothing.
If you outgrow the SVG below, uPlot (~50 kB min+gz) is the usual pick because it’s small and fast enough to instantiate per cell.
A renderer returning an inline <svg> turns an array-valued field into a trend line, with zero extra dependencies.
<tbw-grid style="height: 280px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const SVG_NS = 'http://www.w3.org/2000/svg';
interface Product { name: string; revenue: number; trend: number[]; }
/** Build a fixed-size polyline sparkline scaled to the value range. */ function sparkline(values: number[], width = 96, height = 22): SVGSVGElement { const min = Math.min(...values); const span = Math.max(...values) - min || 1; const step = width / Math.max(values.length - 1, 1); const points = values .map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / span) * height).toFixed(1)}`) .join(' ');
const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.setAttribute('width', String(width)); svg.setAttribute('height', String(height)); svg.setAttribute('aria-hidden', 'true');
const line = document.createElementNS(SVG_NS, 'polyline'); line.setAttribute('points', points); line.setAttribute('fill', 'none'); line.setAttribute('stroke', values[values.length - 1] >= values[0] ? '#16a34a' : '#dc2626'); line.setAttribute('stroke-width', '1.5'); line.setAttribute('stroke-linejoin', 'round'); svg.appendChild(line); return svg; }
const series = (seed: number) => Array.from({ length: 12 }, (_, i) => Math.round(50 + Math.sin(i / 1.7 + seed) * 20 + i * seed));
const grid = queryGrid<Product>('tbw-grid'); if (grid) { grid.columns = [ { field: 'name', header: 'Product', width: 160 }, { field: 'revenue', header: 'Revenue', width: 120, type: 'number', format: (v) => `$${(v).toLocaleString()}` }, { field: 'trend', header: 'Trend (12 mo)', width: 140, // Sparkline is presentation only — sorting/filtering an array column is meaningless. sortable: false, renderer: ({ value }) => sparkline(value), }, ]; grid.rows = [ { name: 'Aurora Keyboard', revenue: 412_500, trend: series(1.2) }, { name: 'Nimbus Mouse', revenue: 288_100, trend: series(-0.8) }, { name: 'Vector Monitor', revenue: 954_300, trend: series(0.4) }, { name: 'Helix Dock', revenue: 176_900, trend: series(-1.5) }, { name: 'Orbit Webcam', revenue: 233_400, trend: series(0.9) }, ]; }const SVG_NS = 'http://www.w3.org/2000/svg';
function sparkline(values: number[], width = 96, height = 22): SVGSVGElement { const min = Math.min(...values); const span = Math.max(...values) - min || 1; const step = width / Math.max(values.length - 1, 1); const points = values .map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / span) * height).toFixed(1)}`) .join(' ');
const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.setAttribute('width', String(width)); svg.setAttribute('height', String(height)); svg.setAttribute('aria-hidden', 'true');
const line = document.createElementNS(SVG_NS, 'polyline'); line.setAttribute('points', points); line.setAttribute('fill', 'none'); line.setAttribute('stroke', values[values.length - 1] >= values[0] ? '#16a34a' : '#dc2626'); line.setAttribute('stroke-width', '1.5'); svg.appendChild(line); return svg;}
grid.columns = [ { field: 'name', header: 'Product' }, { field: 'trend', header: 'Trend (12 mo)', width: 140, sortable: false, renderer: ({ value }) => sparkline(value as number[]), },];For axes, tooltips or interaction, create the chart instance inside the renderer against the node you return.
Caveats
Section titled “Caveats”- Renderers run for every visible cell on every render pass. Inline SVG is cheap enough not to matter; a chart library that costs milliseconds per instance will show up as scroll jank. Measure before reaching for one.
- Chart libraries need teardown. uPlot and friends attach listeners and observers. The grid discards cell content on re-render, so instances created in a renderer leak unless you destroy them — which the renderer API gives you no hook for. Prefer stateless output (SVG, canvas you draw yourself) for virtualized columns.
- An array-valued column is presentation only. Set
sortable: false— sorting or filtering on an array has no meaningful ordering. If you want to sort by trend direction, add a separate numeric column with avalueAccessor. - Mark it
aria-hidden. A polyline conveys nothing to a screen reader. Put the meaning in an adjacent column, or setaria-labelon the cell content with a text summary. - Don’t hardcode colours if you support light and dark themes — use
currentColoror a theme custom property.
See also
Section titled “See also”- Renderers — the seam this recipe plugs into
- Variable row heights — if a chart needs more vertical space