demo D223
A sales dashboard
Top reps by grouping, most-sold products by unnesting the order lines, and five tiles, one ignoring the filter
groupBy · unnest · scope: 'all' · createStat
Loading a live grid…
The configuration
import * as ng from '@angular/core';
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid, createStat } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import createLatticeGrid from '@toclocoinc/lattice-grid/modules/angular';
const { LatticeGridComponent } = createLatticeGrid({ ng, createGrid });
const USD = { style: 'currency', currency: 'USD', maximumFractionDigits: 0 };
const bookColumns = [
{ field: 'rep', title: 'Sales rep' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Deal value', type: 'number', format: USD, total: 'sum' },
];
const repColumns = [
{ field: 'rep', title: 'Rep' },
{ field: 'revenue', title: 'Revenue', type: 'number', format: USD },
];
const skuColumns = [
{ field: 'sku', title: 'SKU' },
{ field: 'product', title: 'Product' },
{ field: 'units', title: 'Units', type: 'number' },
];
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, LatticeGridComponent],
template:
'<lattice-grid #book [config]="bookGrid" style="display:block;height:320px"></lattice-grid>' +
'<div #tiles style="margin:16px 0"></div>' +
'<lattice-grid *ngIf="reps" [config]="reps" style="display:block;margin-top:16px"></lattice-grid>' +
'<lattice-grid *ngIf="skus" [config]="skus" style="display:block;margin-top:16px"></lattice-grid>',
})
export class AppComponent implements AfterViewInit {
@ViewChild('book') bookRef!: LatticeGridComponent;
@ViewChild('tiles') tilesEl!: ng.ElementRef<HTMLElement>;
reps: any = null;
skus: any = null;
bookGrid = { rowKey: 'id', toolPanel: { side: 'left', panels: ['filters', 'columns'] }, columns: bookColumns, rows: [/* each sale carries a lines: [{ sku, product, qty, value }] array */] };
// Both roll-ups derive from the live book and follow its filter, so a region
// narrows the ranking and the SKU league at once.
ngAfterViewInit() {
const book = this.bookRef.grid;
// Group and rank: revenue per rep, top five.
this.reps = { autoHeight: true, columns: repColumns, source: { mode: 'derived', from: book, follow: 'filtered', refresh: 'live', groupBy: 'rep',
select: { revenue: { of: 'amount', fn: 'sum' }, deals: { fn: 'count' }, biggest: { of: 'amount', fn: 'max' } },
sort: [{ col: 'revenue', dir: 'desc' }], limit: 5 } };
// Unnest: the SKUs live in a lines array, so one sale becomes several rows
// before grouping. groupBy reads the dotted path into the unnested element.
this.skus = { autoHeight: true, columns: skuColumns, source: { mode: 'derived', from: book, follow: 'filtered', refresh: 'live', unnest: 'lines', groupBy: 'lines.sku',
select: { product: { of: 'lines.product', fn: 'first' }, units: { of: 'lines.qty', fn: 'sum' } },
sort: [{ col: 'units', dir: 'desc' }], limit: 5 } };
// A tile over the source. scope: 'all' holds the company total while the grid
// and every roll-up follow the filter.
const tile = this.tilesEl.nativeElement.appendChild(document.createElement('div'));
createStat({ grid: book, container: tile, title: 'Company total', of: 'amount', fn: 'sum', scope: 'all' });
}
}
bootstrapApplication(AppComponent);