Lattice Grid Buy a licence

developer guide

Combine Several Grids Into One List

Several sources can be read as one list, each row remembering where it came from, so two feeds that share no key can still be ranked together. The combined grid filters, groups and totals like any other.

Developer guideSources and pushdown › Combine Several Grids Into One List

Combining several grids into one

A join matches two grids on a shared key. Some questions have no key to share at all - "the worst performers across two regional datasets" when the two regions use unrelated ids - and for those, from takes an array of sources instead of one grid: every source is read, concatenated in the order you declared them, and only then does the rest of the pipeline run, once, over the combined set.

Worst performers, two unrelated datasets

createGrid(right, {
  columns: [{ id: 'title' }, { id: 'severity', type: 'number' }, { id: '__source' }],
  source: {
    mode: 'derived',
    from: [
      { grid: eastIncidents, label: 'east' },
      { grid: westIncidents, label: 'west', map: (row) => ({ title: row.name, severity: row.rating }) },
    ],
    sort: [{ col: 'severity', dir: 'desc' }],
    limit: 10,
  },
});

label defaults to the source's position in the array ('0', '1', …), and follow is independent per source - filtering one narrows only its own contribution, exactly as a lone from follows its grid today. A source with differently-named fields uses map to project them into a common shape before it joins the rest.

__source is not optional. Every combined row carries it - the entry's label, or its index when unlabelled - and it is an ordinary field to where, groupBy and select. Leaving it out would mean a combined list that cannot say where any row came from, which defeats most of the reason to combine several sources in the first place.

The union of fields, never a merge. A field only one source has is undefined on the others' rows, not fabricated and not type-coerced - two sources disagreeing about what a field means is yours to resolve with map, not something the union guesses at. And there is no dedup: two sources reporting the same fact both appear as separate rows. There is no UNION-vs-UNION-ALL distinction to draw; reach for join when rows should be matched on a key rather than stacked.

The key is namespaced, only where it needs to be. Two sources can easily share row identifiers, so the derived __key is qualified by the source tag when nothing is grouped. Grouped, __key is the group value exactly as it always has been, and rows from different sources landing in the same group is what grouping a union is for, not a collision.

An empty source is fine; a broken one is loud. A source with no rows contributes nothing. A source that throws while being read is named in a console warning and skipped for that pass - a silently missing source would make "worst across both" quietly wrong, so it is reported rather than swallowed.

A cycle is refused when the source is built. If a union's sources include the grid being derived, directly or through a chain of other derived grids, it is refused up front, naming the offending source, rather than being recursed into.

A union never patches - know the cost before you reach for one at scale. A lone from maintains its grouping incrementally: an edit that names the rows it touched re-reduces only the groups those rows belong to. A union does not do this for any of its sources - every change on any parent re-reads and re-derives the whole combined set from scratch. On a synthetic 200,000-row union across four sources, one row changed on one parent cost on the order of 800 ms-3 s (machine-dependent; run node bench/union-parents.mjs against your own shape), against a few milliseconds for the equivalent patched change on a lone from over the same row count. And because a union is watched by as many independent change streams as it has sources, that full-rescan cost is paid once per source that moves, not once per union - four active parents each firing their own updates pay it four times over. Below a few tens of thousands of combined rows this is unlikely to matter; above that, or with several sources each updating on their own live feed, budget for it, keep refresh away from every tick (idle, or a debounce), and prefer fewer, larger sources over many small ones where the shape of your data allows it.

Not supported alongside a union. crossFilter has no single target once there is more than one parent; profile and statistics reduce one grid's own columns. All three are refused with a warning rather than guessed at, and the top-level follow is ignored in favour of each source's own.