demo D222
Chaining, three levels deep
sales → revenue per rep → top three and a profile, with tiles reading the middle level; one filter moves all of it
source: { mode: 'derived', from, groupBy, profile } · 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 WHOLE = { maximumFractionDigits: 0 };
const bookColumns = [
{ field: 'rep', title: 'Rep' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Deal', type: 'number', format: USD, total: 'sum' },
];
const byRepColumns = [
{ field: 'rep', title: 'Rep' },
{ field: 'revenue', title: 'Revenue', type: 'number', format: USD },
{ field: 'deals', title: 'Deals', type: 'number' },
];
const top3Columns = [
{ field: 'rep', title: 'Rep' },
{ field: 'revenue', title: 'Revenue', type: 'number', format: USD },
];
const profileColumns = [
{ field: 'column', title: 'Measure' },
{ field: 'mean', title: 'Mean', type: 'number', format: WHOLE },
{ field: 'stddev', title: 'Spread', type: 'number', format: WHOLE },
{ field: 'min', title: 'Weakest', type: 'number', format: WHOLE },
{ field: 'max', title: 'Strongest', type: 'number', format: WHOLE },
];
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, LatticeGridComponent],
template:
'<div #tiles style="display:grid;grid-template-columns:repeat(5, 1fr);gap:12px;margin-bottom:16px"></div>' +
'<lattice-grid #book [config]="bookGrid" style="display:block;height:320px"></lattice-grid>' +
'<div style="display:flex;gap:16px;margin-top:16px">' +
' <lattice-grid #byRepGrid *ngIf="byRep" [config]="byRep" (ready)="onByRep()" style="flex:1"></lattice-grid>' +
' <lattice-grid *ngIf="top3" [config]="top3" style="flex:1"></lattice-grid>' +
' <lattice-grid *ngIf="profile" [config]="profile" style="flex:1"></lattice-grid>' +
'</div>',
})
export class AppComponent implements AfterViewInit {
@ViewChild('book') bookRef!: LatticeGridComponent;
@ViewChild('byRepGrid') byRepRef!: LatticeGridComponent;
@ViewChild('tiles') tilesEl!: ng.ElementRef<HTMLElement>;
byRep: any = null;
top3: any = null;
profile: any = null;
bookGrid = { rowKey: 'id', toolPanel: { side: 'left', panels: ['filters', 'columns'] }, columns: bookColumns, rows: [/* e.g. { id: 'S0', rep: 'Ana Bright', region: 'EMEA', amount: 2140 } */] };
// Level 1: revenue per rep, derived from the book and following its filter.
ngAfterViewInit() {
const book = this.bookRef.grid;
this.byRep = { autoHeight: true, columns: byRepColumns, source: { mode: 'derived', from: book, follow: 'filtered', refresh: 'live', groupBy: 'rep',
select: { revenue: { of: 'amount', fn: 'sum' }, deals: { fn: 'count' } },
sort: [{ col: 'revenue', dir: 'desc' }] } };
}
// Level 2: derived from byRep, not from the book, so the chain composes. Runs
// once byRep is a live instance, which its ready event announces.
onByRep() {
const rep = this.byRepRef.grid;
this.top3 = { autoHeight: true, columns: top3Columns, source: { mode: 'derived', from: rep, refresh: 'live', sort: [{ col: 'revenue', dir: 'desc' }], limit: 3 } };
this.profile = { autoHeight: true, columns: profileColumns, source: { mode: 'derived', from: rep, profile: ['revenue'], refresh: 'live' } };
// Each tile is one reduced figure over byRep. show: 'rep' turns max into an
// argmax, naming the rep behind the figure.
const tile = () => this.tilesEl.nativeElement.appendChild(document.createElement('div'));
createStat({ grid: rep, container: tile(), title: 'Revenue concentration', of: 'revenue', fn: 'gini', goodWhen: 'down', footer: '0 = even, 1 = one rep has it all' });
createStat({ grid: rep, container: tile(), title: 'Best rep', of: 'revenue', fn: 'max', show: 'rep' });
createStat({ grid: rep, container: tile(), title: 'Weakest rep', of: 'revenue', fn: 'min', show: 'rep' });
createStat({ grid: rep, container: tile(), title: 'Spread across reps', of: 'revenue', fn: 'stddev' });
createStat({ grid: rep, container: tile(), title: 'Reps selling', fn: 'count' });
}
}
bootstrapApplication(AppComponent);