Lattice Grid Buy a licence

demo D30

Computed columns

A column derived from others, recomputed only when its inputs move

value: { deps, compute }

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.13.0/lattice-grid.min.js"></script>

<div id="grid" style="height: 540px"></div>

<script>
  const grid = LatticeGrid.createGrid(document.getElementById('grid'), {
    rowKey: 'id',
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    columns: [
      { field: 'pipeline', title: 'Pipeline', filter: { type: 'set' }, layout: { width: 120, pin: 'start' } },
      { field: 'stage', title: 'Stage', filter: { type: 'set' }, layout: { width: 120 } },
      { field: 'elapsed', title: 'Elapsed', type: 'duration', layout: { width: 130 }, total: 'sum' },
      { field: 'records', title: 'Records', type: 'number', layout: { width: 130 }, format: { notation: 'compact' }, total: 'sum' },
      { field: 'bytesOut', title: 'Bytes out', type: 'bytes', layout: { width: 130 }, total: 'sum' },
      {
        id: 'rps', title: 'Records / sec', type: 'number', layout: { width: 150 },
        format: { notation: 'compact' }, total: 'avg', filter: { type: 'number' },
        // `deps` is the contract. It names the fields this value reads, so
        // the graph knows which columns invalidate it, and it is what makes
        // the value cacheable rather than something recomputed on every
        // scroll. A dep read but not declared is warned about, loudly.
        value: {
          deps: ['records', 'elapsed'], pure: true,
          compute: (d) => (d.elapsed ? (d.records * 1000) / d.elapsed : null),
        },
      },
      {
        id: 'throughput', title: 'Throughput', type: 'bytes', layout: { width: 140 },
        value: {
          deps: ['bytesOut', 'elapsed'], pure: true,
          compute: (d) => (d.elapsed ? (d.bytesOut * 1000) / d.elapsed : null),
        },
      },
      {
        id: 'perRecord', title: 'Bytes / record', type: 'number', layout: { width: 150 },
        format: { decimals: 0 },
        value: {
          deps: ['bytesOut', 'records'], pure: true,
          compute: (d) => (d.records ? d.bytesOut / d.records : null),
        },
      },
      {
        id: 'grade', title: 'Grade', layout: { width: 120 }, filter: { type: 'set' },
        // A computed column can depend on another computed column. The graph
        // orders them; nothing here says which runs first.
        value: {
          deps: ['records', 'elapsed'], pure: true,
          compute: (d) => {
            const rps = d.elapsed ? (d.records * 1000) / d.elapsed : 0;
            if (rps > 200_000) return 'fast';
            if (rps > 20_000) return 'normal';
            return 'slow';
          },
        },
        cell: { decoration: 'pill', variant: { map: { fast: 'success', normal: 'info', slow: 'warning' } } },
      },
      {
        id: 'label', title: 'Summary', layout: { flex: 1, min: 220 },
        // `deps: '*'` hands the whole record over. Convenient, and the
        // reason to prefer a named list: every column change invalidates it.
        value: {
          deps: '*', pure: true,
          compute: (d) => `${d.pipeline}/${d.stage} ${d.outcome} in ${Math.round(d.elapsed / 1000)}s`,
        },
      },
    ],
    state: { sort: [{ col: 'rps', dir: 'desc' }] },
    rows,  // pipeline runs
  });
</script>

Deriving a column from other columns

A computed column holds no value of its own; it is worked out from other columns on the same row, and Lattice Grid recomputes it only when one of its declared inputs changes. The config option is value: { deps, compute }, where deps names the source columns and compute turns their values into the cell’s output: a margin from a cost and a price column, a full name from a first and last. A developer reaches for this instead of pre-joining the data upstream when the derivation is awkward to maintain as a stored field, particularly when the inputs are themselves edited in the grid and the derived column has to stay in step.

Because deps is explicit, Lattice Grid never guesses which edits matter: a change to a column outside the list leaves the computed cell untouched, and a change to a listed column invalidates only the rows it belongs to. In a JavaScript data grid holding a hundred thousand rows, that is the difference between a redraw confined to a handful of cells and a pass over the whole table. The computed cell still sorts, filters and exports through the same paths as a stored value, so a screen reader announcing a row reads the derived figure exactly as one read straight from the data.

How do you add a calculated column to a JavaScript data grid?

Declare the column with a value object instead of a static field: value: { deps: ['cost', 'price'], compute: (row) => row.price - row.cost }. Lattice Grid re-evaluates compute only for rows where a column listed in deps has changed, so the derived value stays correct without recomputing the whole column on every update.