demo D247
One feed, many views
Split a single live feed across two grids and a chart, with a pick in one narrowing another
createDataRouter
The configuration
'data-router': () => ({
rows: [], config: {},
foot: [
'one feed, partitioned into two grids and a chart by createDataRouter',
'select orders on the left and Returns narrows to their regions: cross-grid selection',
'the feed keeps arriving and each view updates on its own',
],
mount: (el: HTMLElement, LG: any) => {
let a = 20260931 >>> 0;
const rnd = () => { a = (a + 0x6d2b79f5) >>> 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
const pick = <T,>(list: T[]) => list[Math.floor(rnd() * list.length)];
const REGIONS = ['EMEA', 'AMER', 'APAC'];
const SERVICES = ['Compute', 'Storage', 'Network', 'Support'];
let nextId = 1;
let nextTs = 0;
const money = { style: 'currency', currency: 'USD', decimals: 0 } as const;
const order = () => ({ id: 'E-' + String(1000 + nextId++), type: 'order', region: pick(REGIONS), service: pick(SERVICES), value: 200 + Math.round(rnd() * 9800) });
const refund = () => ({ id: 'E-' + String(1000 + nextId++), type: 'return', region: pick(REGIONS), service: pick(SERVICES), value: 50 + Math.round(rnd() * 1200) });
const metric = () => ({ id: 'M-' + nextTs, type: 'metric', ts: nextTs++, value: 40 + Math.round(rnd() * 60) });
const gridsEl = document.createElement('div');
gridsEl.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:12px';
const ordersEl = document.createElement('div');
ordersEl.style.cssText = 'height:300px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
const returnsEl = document.createElement('div');
returnsEl.style.cssText = 'height:300px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
gridsEl.append(ordersEl, returnsEl);
const chartEl = document.createElement('div');
chartEl.id = 'dr-chart';
chartEl.style.cssText = 'height:200px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0;margin-top:12px';
el.append(gridsEl, chartEl);
const orderCols = [
{ field: 'id', title: 'Ref', layout: { width: 110, pin: 'start' } },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'service', title: 'Service', filter: { type: 'set' } },
{ field: 'value', title: 'Value', type: 'number', format: money, total: 'sum' },
];
const orders = LG.createGrid(ordersEl, { rowKey: 'id', theme: 'light', selection: 'multiple', columns: orderCols, rows: [] });
const returns = LG.createGrid(returnsEl, { rowKey: 'id', theme: 'light', columns: orderCols.map((c) => (c.field === 'value' ? { ...c, title: 'Refund' } : c)), rows: [] });
const metrics = LG.createHeadlessGrid({ rowKey: 'ts', columns: [{ field: 'ts', title: 'Tick', type: 'number' }, { field: 'value', title: 'Throughput', type: 'number' }], rows: [] });
let chart: any;
let timer: any;
Promise.all([loadDataRouter(), loadCharts()])
.then(([dr, ch]: any[]) => {
const router = dr.createDataRouter({ key: 'type', rowKey: 'id' });
router.attach(orders, 'order');
router.attach(returns, 'return');
router.attach(metrics, 'metric', { rowKey: 'ts' });
router.link(orders, returns, { from: 'region', to: 'region' });
const seed: any[] = [];
for (let i = 0; i < 60; i++) seed.push(order());
for (let i = 0; i < 30; i++) seed.push(refund());
for (let i = 0; i < 24; i++) seed.push(metric());
router.load(seed);
chart = ch.createChart({ grid: metrics, container: chartEl, type: 'line', x: 'ts', y: 'value', title: 'Throughput per tick, routed to a headless grid' });
timer = setInterval(() => {
if (document.hidden) return;
const batch: any[] = [{ op: 'upsert', row: metric() }];
const n = 1 + Math.floor(rnd() * 3);
for (let i = 0; i < n; i++) batch.push({ op: 'upsert', row: rnd() < 0.7 ? order() : refund() });
router.apply(batch);
}, 1400);
})
.catch((err) => console.error('[data-router]', err));
return () => {
clearInterval(timer);
chart?.destroy?.();
orders?.destroy?.();
returns?.destroy?.();
metrics?.destroy?.();
};
},
})
Splitting one live feed across several grids and a chart at once
Most dashboards carry a single source of truth that has to appear in several places at once: orders in one table, returns in another, throughput on a chart, all of it moving as the same events arrive. Lattice Grid does this without you fanning the feed out by hand. You hand createDataRouter one key to partition on, attach each grid or chart to the value it cares about, and from then on every event that arrives is delivered only to the views it belongs in, each through that view’s own incremental update path. One order lands in the orders table, one return in the returns table, one reading on the chart, and none of the three knows the others exist. Load an opening snapshot in a single call and it is split across every attached view for you; push a mixed batch of changes and each view takes only its share.
The views stay decoupled but need not stay unaware of one another. Cross-grid selection ties two grids together on a shared field: link the orders grid to the returns grid on region, pick an order, and the returns grid narrows to the same region on its own. A reader drills from one view into a related one with a single click, and the feed keeps arriving underneath the whole time. This is the shape behind an operations wall, a trading book split by desk, or any screen where one stream has to drive many honest, live views side by side.
How do I feed one data stream into multiple grids and a chart?
Create a router with createDataRouter({ key, rowKey }), then call router.attach(grid, value) for each grid or headless-grid-backed chart, naming the partition value it should receive. Load an initial snapshot with router.load(rows) and stream changes with router.apply([{ op: 'upsert', row }]); the router routes each record to the matching views only. Add router.link(source, target, { from, to }) to make a selection in one grid narrow another on a shared field.