Lattice Grid Buy a licence

demo D30

Computed columns in Angular

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

value: { deps, compute }

This grid derives a column from other columns on the same row and recomputes it only when one of its declared inputs changes. It keeps calculated fields correct and cheap, so a total or a margin updates the moment its inputs move and stays idle otherwise.

Building…
Loading a live grid…

This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.

The configuration

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { LatticeGridComponent, provideLattice } from '@toclocoinc/lattice-grid/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent],
  template: '<lattice-grid [config]="grid" style="display:block;height:540px"></lattice-grid>',
})
export class AppComponent {
  grid = {
    rowKey: 'id',
    toolPanel: { side: 'left', panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'], exportName: 'lattice-demo' },
    state: { sort: [{ col: 'rps', dir: 'desc' }] },
    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 names the fields this value reads, so the graph knows which
        // columns invalidate it and the value is cached rather than recomputed
        // on every scroll. A dep read but not declared is warned about, loudly.
        value: {
          deps: ['records', 'elapsed'], pure: true,
          compute: (d: any) => (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: any) => (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: any) => (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: any) => {
            const rps = d.elapsed ? (d.records * 1000) / d.elapsed : 0;
            if (rps > 200000) return 'fast';
            if (rps > 20000) 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: any) => `${d.pipeline}/${d.stage} ${d.outcome} in ${Math.round(d.elapsed / 1000)}s`,
        },
      },
    ],
    rows: [/* pipeline run records */],
  };
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid })],
});

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.