# In-cell sparklines

> Render trend lines inside grid cells with inline SVG, escalating to uPlot when you need axes and tooltips.

**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

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`](https://toolboxjs.com/grid/core.md#renderers) 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](https://github.com/leeoniya/uPlot) (~50 kB min+gz) is the usual pick because it's small and fast enough to instantiate per cell.

## Code

```ts
// SparklineRendererDemo.astro
  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>('#demo-sparkline');
  if (grid) {
    grid.columns = [
      { field: 'name', header: 'Product', width: 160 },
      { field: 'revenue', header: 'Revenue', width: 120, type: 'number', format: (v) => `$${(v as number).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 as number[]),
      },
    ];
    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) },
    ];
  }
```

```typescript
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

- **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 a [`valueAccessor`](https://toolboxjs.com/grid/core.md#value-accessors).
- **Mark it `aria-hidden`.** A polyline conveys nothing to a screen reader. Put the meaning in an adjacent column, or set `aria-label` on the cell content with a text summary.
- **Don't hardcode colours** if you support light and dark themes — use `currentColor` or a [theme custom property](https://toolboxjs.com/grid/guides/theming.md).

## See also

- [Renderers](https://toolboxjs.com/grid/core.md#renderers) — the seam this recipe plugs into
- [Variable row heights](https://toolboxjs.com/grid/core.md#variable-row-heights) — if a chart needs more vertical space
