demo D231
A hundred thousand rows in DuckDB, no server in Angular
DuckDB reads a Parquet file in the browser and the whole set is pulled in, so the plant subtotals and the grand total are computed over all 100,000 rows
createPushdownSource · duckdbAdapter · fullDataset
This grid reads a Parquet file with DuckDB in the browser and pulls the whole hundred-thousand-row set through it, so the subtotals and grand total are computed over every row. It shows analytical figures that cover the full dataset, with no server involved.
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, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import * as Lattice from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { LatticeGridComponent, provideLattice } from '@toclocoinc/lattice-grid/angular';
const { createGrid, createPushdownSource, duckdbAdapter } = Lattice;
// The share of a group's rows inside tolerance. Under fullDataset the scope is
// the whole group, so a subtotal is a whole-group rate, not a page's.
const inSpecRate = (values: any[]) => {
let seen = 0, ok = 0;
for (const v of values) { if (typeof v === 'boolean') { seen++; if (v) ok++; } }
return seen ? ok / seen : null;
};
const columns = [
{ field: 'plant', title: 'Plant', filter: { type: 'set' }, group: { enabled: true, index: 0 }, total: 'count' },
{ field: 'line', title: 'Line', filter: { type: 'set' } },
{ field: 'product', title: 'Product' },
{ field: 'bore_mm', title: 'Bore', type: 'millimetres', spec: { lower: 9.95, upper: 10.05, target: 10 }, total: 'avg' },
{ field: 'cycle_seconds', title: 'Cycle', type: 'seconds', total: 'avg' },
{ id: 'in_spec_rate', field: 'in_spec', title: 'In-spec rate', type: 'number', format: { style: 'percent', decimals: 2 }, total: inSpecRate },
{ field: 'taken_at', title: 'Taken', type: 'dateString' },
];
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, LatticeGridComponent],
template:
'<div style="font-size:13px;color:var(--ink-2);line-height:1.5">{{ status }}</div>' +
'<lattice-grid *ngIf="gridConfig" [config]="gridConfig" style="display:block;height:600px;margin-top:12px"></lattice-grid>',
})
export class AppComponent implements OnInit, OnDestroy {
status = 'Starting DuckDB in the browser...';
gridConfig: any = null;
private disposed = false;
private connection: any;
private worker: any;
// DuckDB is the caller's engine, loaded from a CDN; the grid imports none of
// it. duckdbAdapter takes the connection you made, so the source drives a full
// analytical engine while the grid stays dependency-free.
async ngOnInit() {
try {
this.status = 'Loading DuckDB from a CDN...';
const duckdb = await import('https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/+esm');
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
this.worker = await duckdb.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(duckdb.LogLevel.ERROR), this.worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
if (this.disposed) return;
this.connection = await db.connect();
// The engine resolves the URL, so it must be absolute. The file is a
// hundred thousand rows in a Parquet file, on this origin.
const file = new URL('/demo-data/readings.parquet', location.origin).href;
const adapter = duckdbAdapter({ connection: this.connection, from: `read_parquet('${file}')` });
// fullDataset pulls the whole matching set in once and holds it, so the
// subtotals and the grand total are over all the rows, not a page.
const source = createPushdownSource({
adapter, compute: Lattice, pageSize: 200,
fullDataset: { enabled: true, maxRows: 250000 },
});
this.gridConfig = {
rowKey: 'reading_id',
toolPanel: { side: 'left', panels: ['filters', 'columns'] },
grandTotalRow: 'bottom',
groupFooter: true,
showTotalInHeader: true,
columns,
source,
};
this.status = 'Rows pulled into the tab and grouped by plant. Expand a plant, or filter a line, and every figure recomputes over the whole matching set.';
} catch (err) {
this.status = 'DuckDB could not start here. It loads from a CDN and needs a browser with WebAssembly and cross-origin isolation.';
}
}
ngOnDestroy() {
this.disposed = true;
try { this.connection?.close?.(); } catch (e) {}
try { this.worker?.terminate?.(); } catch (e) {}
}
}
bootstrapApplication(AppComponent, {
providers: [provideLattice({ createGrid })],
});