demo D231
A hundred thousand rows in DuckDB, no server
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
Loading a live grid…
The configuration
import * as ng from '@angular/core';
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 createLatticeGrid from '@toclocoinc/lattice-grid/modules/angular';
const { createGrid, createPushdownSource, duckdbAdapter } = Lattice;
const { LatticeGridComponent } = createLatticeGrid({ ng, createGrid });
// 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);