demo D297
A logistics operations platform in Angular
50,000 shipments, 2,000 vehicles and 5,000 exceptions on one feed: a KPI strip, two charts, a shipments grid, a derived carrier panel, and a work queue read as a grid, a board or a timeline
createDataRouter · router.subscribe · createKanban
A whole logistics operations screen from one feed: 50,000 shipments, 2,000 vehicles and 5,000 exceptions routed to a KPI strip, two charts, a shipments grid, a derived carrier panel and a work queue you can read as a grid, a board or a timeline. Click a shipment and the queue narrows to its exceptions; run the disruption and watch every panel move together.
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, ViewChild, AfterViewInit, OnDestroy, signal, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid, type Grid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';
import { createKanban } from '@toclocoinc/lattice-grid/modules/kanban';
import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { MockWebSocket } from '@toclocoinc/lattice-grid/modules/mock-socket';
import {
LatticeGridComponent, LatticeKpiComponent, LatticeKanbanComponent, LatticeGanttComponent,
LatticeRouter, provideLattice, provideLatticeRouter,
} from '@toclocoinc/lattice-grid/angular';
const isOpen = (r: any) => r.kind === 'exception' && r.status !== 'resolved';
// The same feed the vanilla tab writes: one mixed stream of shipments,
// vehicle telemetry and exceptions. See the vanilla tab for the full
// generator; only the shape matters here.
function* logisticsFeed(): Generator<any> { /* yields { kind: 'snapshot', rows } then { kind: 'delta', changes } forever */ }
const SHIPMENT_COLUMNS = [
{ field: 'shipmentId', title: 'Shipment' },
{ field: 'carrier', title: 'Carrier', filter: { type: 'set' } },
{ field: 'sla', title: 'SLA', filter: { type: 'set' } },
{ field: 'etaDrift', title: 'ETA drift', type: 'number' },
{ id: 'drift', title: 'Since you opened this', shadow: { of: 'etaDrift', kind: 'delta' }, type: 'number' },
];
const CARRIER_COLUMNS = [
{ field: 'carrier', title: 'Carrier' },
{ field: 'shipments', title: 'Shipments', type: 'number' },
{ field: 'late', title: 'Late', type: 'number' },
{ field: 'onTime', title: 'On time', type: 'number' },
];
const WORK_COLUMNS = [
{ field: 'exceptionId', title: 'Reference' },
{ field: 'type', title: 'Type' },
{ field: 'severity', title: 'Severity' },
{ field: 'status', title: 'Queue' },
{ field: 'owner', title: 'Owner', editable: true },
];
const BOARD_COLUMNS = [
{ id: 'new', title: 'New' }, { id: 'investigating', title: 'Investigating' },
{ id: 'carrier', title: 'With carrier' }, { id: 'resolved', title: 'Resolved' },
];
@Component({
selector: 'app-root',
standalone: true,
imports: [LatticeGridComponent, LatticeKpiComponent, LatticeKanbanComponent, LatticeGanttComponent],
// Scoped to this component: built once, destroyed with it.
providers: [provideLatticeRouter({ key: 'kind', rowKey: 'id', overlap: true })],
template:
'<lattice-kpi #kpi [config]="kpiConfig" style="display:block;margin-bottom:12px"></lattice-kpi>' +
'<lattice-grid #shipmentsGrid route="shipment" [config]="shipmentConfig" style="display:block;height:400px"></lattice-grid>' +
// A derived grid reads whatever the shipments grid is filtered to and
// re-groups it, so a filter above rewrites this panel with no second query.
'@if (carriersConfig(); as cc) {' +
' <lattice-grid [config]="cc" style="display:block;height:200px;margin-top:14px"></lattice-grid>' +
'}' +
'<lattice-grid #workGrid route="exception" [routeOptions]="{ filter: isOpen }" [config]="workConfig" style="display:block;height:420px;margin-top:14px"></lattice-grid>' +
'<lattice-kanban #board [config]="boardConfig" (cardMove)="onCardMove($event)" style="display:block;height:420px;margin-top:14px"></lattice-kanban>' +
'<lattice-gantt [tasks]="tasks()" [config]="ganttConfig" style="display:block;height:420px;margin-top:14px"></lattice-gantt>',
})
export class AppComponent implements AfterViewInit, OnDestroy {
private routerService = inject(LatticeRouter);
private socket?: MockWebSocket;
private unsubscribe?: () => void;
isOpen = isOpen;
@ViewChild('shipmentsGrid') shipmentsGridRef!: LatticeGridComponent;
@ViewChild('workGrid') workGridRef!: LatticeGridComponent;
@ViewChild('kpi') kpiRef!: LatticeKpiComponent;
@ViewChild('board') boardRef!: LatticeKanbanComponent;
carriersConfig = signal<any>(null);
tasks = signal<any[]>([]);
kpiConfig = {
columns: 3,
tiles: [
{ id: 'late', label: 'Running late', aggregation: 'count', filter: (r: any) => r.kind === 'shipment' && r.sla === 'Late' },
{ id: 'sla', label: 'Delivery SLA', aggregation: 'avg', field: 'onTime', filter: (r: any) => r.kind === 'shipment', format: { type: 'percent', decimals: 1 } },
{ id: 'fleet', label: 'Fleet in use', aggregation: 'avg', field: 'utilisation', filter: (r: any) => r.kind === 'vehicle', format: { type: 'percent', decimals: 1 } },
],
};
shipmentConfig = {
rowKey: 'id', selection: { mode: 'single' },
// Named states a user can return to, stored in this browser.
views: { allowSave: true, local: true, saved: [{ id: 'exposure', name: 'Late and at risk',
state: { filters: { op: 'or', conditions: [{ col: 'sla', op: 'eq', value: 'Late' }, { col: 'sla', op: 'eq', value: 'At risk' }] },
sort: [{ col: 'etaDrift', dir: 'desc' }] } }] },
columns: SHIPMENT_COLUMNS,
};
workConfig = { rowKey: 'id', columns: WORK_COLUMNS };
boardConfig = {
rowKey: 'id', columnProperty: 'status', columns: BOARD_COLUMNS,
card: { title: { field: 'type' }, subtitle: 'lane', badges: 'severity' },
};
ganttConfig = { dateAxis: true, zoom: 'day', groupBy: 'severity', projectStart: '2026-08-26' };
ngAfterViewInit() {
const shipmentsGrid = this.shipmentsGridRef.grid!;
const workGrid = this.workGridRef.grid!;
const kpi = this.kpiRef.instance!;
const board = this.boardRef.instance!;
// A derived grid reads the shipments grid the moment it exists.
this.carriersConfig.set({
source: { mode: 'derived', from: shipmentsGrid, follow: 'filtered', refresh: 400, groupBy: 'carrier',
select: { shipments: { fn: 'count' }, late: { of: 'lateFlag', fn: 'sum' }, onTime: { of: 'onTime', fn: 'avg' } },
sort: [{ col: 'late', dir: 'desc' }], crossFilter: true },
columns: CARRIER_COLUMNS,
});
// A KPI panel and a kanban board are drop-in router targets, the same as
// a grid: both consume the same keyed diff, so router.attach reaches
// them directly once their live instance exists.
this.routerService.attach(kpi, (r: any) => r.kind === 'shipment' || r.kind === 'vehicle' || r.kind === 'exception');
this.routerService.attach(board, 'exception', { filter: isOpen });
// Clicking a shipment narrows the work queue to its own exceptions.
this.routerService.router.link(shipmentsGrid, workGrid, { from: 'shipmentId', to: 'shipmentId' });
// A view that is not a keyed-diff consumer takes the subscribe path
// instead: the same slice and the same diff, handed to whatever you like
// - here, the timeline's task list, through the live tasks signal.
const items = new Map();
this.unsubscribe = this.routerService.router.subscribe('exception', (change: any) => {
for (const row of change.add ?? []) items.set(row.id, row);
for (const row of change.update ?? []) isOpen(row) ? items.set(row.id, row) : items.delete(row.id);
for (const key of change.remove ?? []) items.delete(key);
this.tasks.set([...items.values()].map((x: any) => ({
id: x.id, name: x.type, start: x.openedDay, duration: x.targetDays, percentComplete: x.percentComplete,
})));
}, { filter: isOpen });
// The feed: one line swaps this MockWebSocket for a real one.
this.socket = new MockWebSocket({ feed: logisticsFeed(), rate: 1200, jitter: 400 });
this.socket.onmessage = (event: any) => {
const message = JSON.parse(event.data);
if (message.kind === 'snapshot') this.routerService.router.load(message.rows);
else this.routerService.router.apply(message.changes);
};
}
// A drag is a change to the work item, so put it back into the feed and
// every other view learns about it the same way it learns about anything.
onCardMove(event: any) {
this.routerService.router.apply(event.cards.map((card: any) => ({ op: 'upsert', row: { ...card.row, status: event.to } })));
}
ngOnDestroy() {
this.socket?.close();
this.unsubscribe?.();
}
}
bootstrapApplication(AppComponent, {
providers: [provideLattice({ createGrid, createKPI, createKanban, createGantt, createDataRouter })],
});
The screen a logistics team lives in all day
This is what an operational product looks like when the data layer under it does the work. Fifty thousand shipments, two thousand vehicles reporting position and load, and five thousand exception records arrive as one mixed feed. One Data Router partitions that feed by what each record is, and every panel above it fills with only the slice it is meant to show: the shipments grid, the KPI strip, the delivery performance line, the exceptions chart and the work queue. None of the panels opened a connection of its own, and none of them knows the others exist.
The shipments grid is a working grid, not a display: fifty thousand rows sorted, filtered and searched at full speed, a set filter on every dimension a planner actually uses, a total under the value column, three defined views to jump between and a save form for the reader’s own. Beside it, the carrier panel is a derived grid. It reads whatever the shipments grid is filtered to, groups it by carrier, and re-derives on a short debounce, so filtering the shipments rewrites the panel with no second query and no second copy of the data. Pick a carrier row and the cross-filter sends the choice back the other way, narrowing the shipments to that carrier.
The ETA drift column is an ordinary number: how far the current estimate sits beyond the plan. The column beside it, Since you opened this, is a shadow column, tracking every shipment’s drift against the value it held when the page opened. It sits at zero until something moves, so it reads as a live measure of what has changed on this shift rather than a stale absolute.
The same work, as a grid, a board or a timeline
Under the shipments sits the work queue: the exceptions still open, and the switch that reads them three ways. As a grid, they are a sortable, filterable list whose owner and queue can be changed in place. As a kanban board, the same rows are cards in queue columns, with an age chip on anything sitting too long, dragged between queues by mouse or keyboard. As a timeline, each work item is a bar from the day it was raised to the day it has to be cleared, laid out in severity lanes against a calendar with today marked and anything past its clear-by date outlined.
There is no second copy of the data behind any of that. A kanban board and a KPI panel both consume the same keyed-diff contract a grid does, so each is a single router.attach away from being live. The timeline takes the router’s subscribe path instead, because a schedule is handed a task list rather than a keyed diff, and it receives exactly the same slice and the same diff the two grids do. Drag a card from one queue to another, or change the queue in the grid, and the change goes straight back into the feed, so every other view learns about it the same way it learns about anything else.
Watching a real disruption
The SIMULATE PORT DISRUPTION button holds one origin hub for a few seconds. Every shipment routed through it slips: the ETA drift climbs, the shadow column goes sharply positive, the SLA reading turns from on time to at risk to late, and some divert to customs. Port congestion exceptions open against the worst of them, and because the work queue is fed from that same feed, they appear in the grid, on the board and on the timeline at once. The KPI strip’s late and open-exception counts rise while the delivery SLA falls, the performance line bends downward, and the exceptions chart grows a new bar. Then the hub clears and everything settles back, so the whole thing can be watched again without a reload.
Nothing in that is a separate demo mode. The disruption pushes rows through the identical router.apply path the ambient feed already uses, exactly the way a real port hold is just more of the same feed rather than a different one.
How do I drive a grid, a board, a timeline and a KPI strip from one feed?
Create one router with createDataRouter({ key: 'kind', rowKey: 'id', overlap: true }) and attach each view to the slice it should receive. overlap: true matters the moment more than one view reads the same value: the KPI strip, the exceptions chart, the work grid and the board all read 'exception' records the grids already claim, and without it only the first route on a value would ever see them. A grid, a kanban board and a KPI panel are all drop-in targets, so each is one router.attach(view, value, opts). Anything that is not a keyed-diff consumer, a schedule, a map, a detail pane, takes router.subscribe(value, handler, opts) and receives the same { add, update, remove } diff to apply however it likes. router.link(shipments, workQueue, { from: 'shipmentId', to: 'shipmentId' }) makes a pick in the shipments grid narrow the queue to that shipment’s own exceptions.