demo D234
This month against last
Two grids compared column by column, ranked by effect size rather than a p-value
grid.statistics.datasetVsDataset(otherGrid)
The configuration
'dataset-vs-dataset': () => {
const cols = [
{ field: 'account', title: 'Account', layout: { pin: 'start', width: 150 } },
{ field: 'region', title: 'Region', filter: { type: 'set' }, layout: { width: 110 } },
{ field: 'plan', title: 'Plan', filter: { type: 'set' }, layout: { width: 130 } },
{ field: 'mrr', title: 'MRR', type: 'number', format: { style: 'currency', currency: 'USD', decimals: 0 }, total: 'sum', layout: { width: 130 } },
{ field: 'seats', title: 'Seats', type: 'number', total: 'sum', layout: { width: 100 } },
{ field: 'supportTickets', title: 'Tickets', type: 'number', total: 'sum', layout: { width: 110 } },
];
const bookFor = (seed: number, monthIndex: number) => {
const rand = seeded(seed);
const REGIONS = ['EU', 'US', 'APAC'];
return Array.from({ length: 500 }, (unused, i) => {
const promoted = monthIndex === 1 && i % 3 === 0;
const plan = promoted ? 'Growth' : (i % 3 === 0 ? 'Starter' : i % 3 === 1 ? 'Growth' : 'Enterprise');
const base = plan === 'Enterprise' ? 4500 : plan === 'Growth' ? 600 : 90;
return {
id: `A-${String(i + 1).padStart(4, '0')}`,
account: `Account ${i + 1}`,
region: REGIONS[i % 3],
plan,
mrr: Math.round((base + rand() * base * 0.4) * 100) / 100,
seats: plan === 'Enterprise' ? 40 + Math.floor(rand() * 200) : 1 + Math.floor(rand() * 15),
supportTickets: Math.floor(rand() * (promoted ? 5 : 3)),
};
});
};
return {
rows: [], config: {},
mount: (el: HTMLElement, LG: any) => {
el.textContent = '';
el.style.cssText = 'display:flex;flex-direction:column;gap:14px;height:700px';
const gridsRow = document.createElement('div');
gridsRow.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:14px;flex:1;min-height:0';
const aWrap = document.createElement('div'); aWrap.style.cssText = 'display:flex;flex-direction:column;gap:6px;min-width:0';
const bWrap = document.createElement('div'); bWrap.style.cssText = 'display:flex;flex-direction:column;gap:6px;min-width:0';
const aLabel = document.createElement('p'); aLabel.textContent = 'Last month'; aLabel.style.cssText = 'margin:0;font:600 12px system-ui;color:var(--ink-2)';
const bLabel = document.createElement('p'); bLabel.textContent = 'This month'; bLabel.style.cssText = 'margin:0;font:600 12px system-ui;color:var(--ink-2)';
const aEl = document.createElement('div'); aEl.style.cssText = 'flex:1;min-height:0';
const bEl = document.createElement('div'); bEl.style.cssText = 'flex:1;min-height:0';
aWrap.append(aLabel, aEl); bWrap.append(bLabel, bEl);
gridsRow.append(aWrap, bWrap);
const panel = document.createElement('div');
panel.style.cssText = 'border:1px solid var(--rule);border-radius:11px;padding:14px 16px;background:var(--paper)';
el.append(gridsRow, panel);
const gridA = LG.createGrid(aEl, { rowKey: 'id', theme: 'light', columns: cols, rows: bookFor(20261010, 0) });
const gridB = LG.createGrid(bEl, { rowKey: 'id', theme: 'light', columns: cols, rows: bookFor(20261011, 1) });
const titleOf = (colId: string) => cols.find((c) => c.field === colId)?.title ?? colId;
const bar = (frac: number) =>
`<div style="flex:1;height:8px;border-radius:4px;background:var(--rule);overflow:hidden">` +
`<div style="height:100%;width:${Math.round(Math.max(0, Math.min(1, frac)) * 100)}%;background:#2f6bff"></div></div>`;
const render = () => {
const r = gridA.statistics?.datasetVsDataset?.(gridB);
if (!r) { panel.textContent = 'datasetVsDataset unavailable'; return; }
const rows2 = r.ranked.map((c: any) => {
const label = titleOf(c.column);
const pct = (c.distance * 100).toFixed(0);
const measureName = c.measure === 'categoricalTotalVariation' ? 'category mix' : "Cohen's d";
return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--rule)">` +
`<div style="width:110px;font:600 13px system-ui">${label}</div>` +
bar(c.distance) +
`<div style="width:170px;font:12px system-ui;color:var(--ink-2);text-align:right">${pct}% of scale · ${measureName}</div>` +
`</div>`;
}).join('');
panel.innerHTML =
`<p style="margin:0 0 4px;font:13px system-ui"><strong>${count(r.nA)}</strong> vs <strong>${count(r.nB)}</strong> accounts</p>` +
`<p style="margin:0 0 8px;font:12px system-ui;color:var(--ink-3)">ranked by effect size on the same 0–1 scale subsetVsPopulation uses, so a numeric shift and a mix shift compare honestly</p>` +
rows2;
};
const offA = gridA.on?.('filter:changed', render);
const offB = gridB.on?.('filter:changed', render);
render();
return () => { offA?.(); offB?.(); gridA?.destroy?.(); gridB?.destroy?.(); panel.remove(); };
},
foot: ['two independent grids, one filter each, compared live', 'grid.statistics.datasetVsDataset(otherGrid) ranks the columns they share', "Cohen's d is symmetric: A-vs-B ranks the same as B-vs-A"],
};
},
Comparing this month against last, column by column
subsetVsPopulation answers “what makes this filtered slice different from the rest of the same dataset.” grid.statistics.datasetVsDataset(otherGrid) answers the sibling question: what changed between two independent datasets, such as this month’s book and last month’s, two regions’ rosters, or a test group against a control. Call it on one grid, pass the other, and it returns the same shape of ranked result: every shared column scored on a single bounded scale and ordered by how much it moved, with columns that exist in only one dataset named separately as unmatched rather than silently skipped. The numeric measure is Cohen’s d against a pooled deviation rather than the asymmetric measure a subset-vs-population comparison uses, because with two independent datasets neither one is the baseline the other is judged against; comparing A to B and B to A gives the same ranking, just with the sign flipped, which is the property that makes the comparison trustworthy in either direction. Each grid keeps its own filter, so narrowing dataset A to a segment before comparing answers a narrower question without touching dataset B at all.
How do I compare two datasets and find what changed between them?
Build two grids, one over each dataset, then call gridA.statistics.datasetVsDataset(gridB). The result’s ranked array orders every column both grids share by effect size, nA and nB report how many rows each side compared, and unmatched.onlyA / unmatched.onlyB name any columns that exist in one dataset but not the other. Filtering either grid before calling the method compares whatever rows are currently in scope, not the full underlying set.
Why not just diff a summary table between the two periods?
A summary table shows the numbers moved; it does not say which of thirty columns moved the most, and a manual scan treats a categorical shift and a numeric one as impossible to compare on the same axis. datasetVsDataset puts a number’s shift and a category’s shift on one scale and sorts by it, so the column actually worth investigating is first rather than found by scrolling.