angular data grid · server-side with duckdb
Angular Grid: Server-Side DuckDB
createPushdownSource plus duckdbAdapter push the grid's filter, group and total work down into a real analytical engine instead of computing over rows already sitting on screen, and fullDataset says the subtotal is over the whole matching set, not one page of it.
Also in: React
Install and import
npm install @toclocoinc/lattice-grid
# DuckDB itself loads from a CDN at runtime, not from this package
# free on localhost; a production domain needs a licence key (see /pricing/)
The file
readings.component.ts
// readings.component.ts
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: unknown[]) => {
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-readings',
standalone: true,
imports: [CommonModule, LatticeGridComponent],
template:
'<div style="font-size:13px;color:var(--ink-2)">{{ status }}</div>' +
'<lattice-grid *ngIf="gridConfig" [config]="gridConfig" style="display:block;height:520px;margin-top:12px"></lattice-grid>',
})
export class ReadingsComponent implements OnInit, OnDestroy {
status = 'Starting DuckDB in the browser...';
gridConfig: any = null;
private disposed = false;
private connection: any;
private worker: any;
// DuckDB is your own 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 package 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@1.29.0/+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.
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 every matching row, not
// one page of them.
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: COLUMNS,
source,
};
this.status = 'Rows pulled into the tab and grouped by plant.';
} catch {
this.status = 'DuckDB could not start here: it needs WebAssembly and a CDN connection.';
}
}
ngOnDestroy() {
this.disposed = true;
try { this.connection?.close?.(); } catch {}
try { this.worker?.terminate?.(); } catch {}
}
}
bootstrapApplication(ReadingsComponent, {
providers: [provideLattice({ createGrid })],
});
See it running
Open this demo's Angular tab →
Working in Angular
The grid is not in the template until the source is ready. Building the DuckDB
connection is asynchronous, so *ngIf="gridConfig" keeps
<lattice-grid> out of the DOM until ngOnInit has finished setting it.
Clean up in ngOnDestroy. Closing the DuckDB connection and terminating its worker
there means navigating away mid-query does not leave a worker running in a tab nobody is looking
at, and a late resolution checks disposed before touching the destroyed component.
compute: Lattice. The pushdown source still needs the grid's own compute helpers for anything DuckDB itself is not asked to do, which is why the whole package namespace is passed in alongside the adapter.
Related
Read the docs Angular data grid overview Free on localhost. See pricing.