Lattice Grid Buy a licence

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

Building…
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);