This Quarter vs Last Quarter: a Grid That Diffs the Two
Whoever builds the quarterly business review pulls this quarter’s number from the live orders grid easily enough, then goes looking for last quarter’s, which by now only exists as a spreadsheet exported the week that quarter closed. The two sit side by side on the slide until someone in the room asks whether the swing holds for EMEA alone, or for one rep’s patch, and the exported half cannot answer, because it was flattened to a single number before the question existed. A period over period comparison in JavaScript can run the same way the two grids either side of it do: off live data, not off a number pasted in before anyone asked a follow-up.
A derived grid turns the comparison into configuration instead of an export. Split the orders grid into two more derived grids, one reading this quarter’s rows and one reading last quarter’s, then point a third grid at both: it ranks every shared column by how much it moved between them, using an effect size rather than eyeballing two totals. Filter the orders grid to EMEA, or to one rep, and all three follow, because each reads the one underlying grid rather than a copy frozen at quarter end.
That removes the export step from the review entirely. Nobody re-pulls last quarter’s numbers to check the comparison still holds, because last quarter’s grid was never a snapshot, it is still reading live off the same orders. Twenty orders split into two quarters here, and the swing on amount comes out the same whether the room asks about EMEA or not.
Period over period comparison in JavaScript, from two grids
import { createGrid } from '@toclocoinc/lattice-grid';
const orders = [
{ id: 1, date: '2026-04-06', region: 'EMEA', amount: 420 },
{ id: 2, date: '2026-04-14', region: 'APAC', amount: 180 },
{ id: 3, date: '2026-04-21', region: 'Americas', amount: 560 },
{ id: 4, date: '2026-05-02', region: 'EMEA', amount: 310 },
{ id: 5, date: '2026-05-09', region: 'Americas', amount: 340 },
{ id: 6, date: '2026-05-18', region: 'APAC', amount: 275 },
{ id: 7, date: '2026-05-27', region: 'EMEA', amount: 490 },
{ id: 8, date: '2026-06-04', region: 'Americas', amount: 250 },
{ id: 9, date: '2026-06-15', region: 'APAC', amount: 410 },
{ id: 10, date: '2026-06-28', region: 'EMEA', amount: 220 },
{ id: 11, date: '2026-07-03', region: 'Americas', amount: 730 },
{ id: 12, date: '2026-07-12', region: 'APAC', amount: 395 },
{ id: 13, date: '2026-07-24', region: 'EMEA', amount: 605 },
{ id: 14, date: '2026-08-02', region: 'Americas', amount: 540 },
{ id: 15, date: '2026-08-11', region: 'APAC', amount: 515 },
{ id: 16, date: '2026-08-20', region: 'EMEA', amount: 675 },
{ id: 17, date: '2026-09-05', region: 'Americas', amount: 820 },
{ id: 18, date: '2026-09-15', region: 'APAC', amount: 460 },
{ id: 19, date: '2026-09-22', region: 'EMEA', amount: 450 },
{ id: 20, date: '2026-09-29', region: 'Americas', amount: 590 },
];
const book = createGrid(document.querySelector('#orders'), {
rowKey: 'id',
columns: [
{ field: 'id', title: 'Order' },
{ field: 'date', title: 'Date', type: 'date' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
],
rows: orders,
});
// A fresh columns array for each quarter grid, not a reference borrowed
// from book's own config.
const quarterColumns = [
{ field: 'id', title: 'Order' },
{ field: 'date', title: 'Date', type: 'date' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
];
const thisQuarter = createGrid(document.querySelector('#this-quarter'), {
rowKey: 'id',
columns: quarterColumns,
source: {
mode: 'derived',
from: book,
follow: 'filtered',
where: (r) => r.date >= '2026-07-01' && r.date < '2026-10-01',
},
});
const lastQuarter = createGrid(document.querySelector('#last-quarter'), {
rowKey: 'id',
columns: quarterColumns,
source: {
mode: 'derived',
from: book,
follow: 'filtered',
where: (r) => r.date >= '2026-04-01' && r.date < '2026-07-01',
},
});
// A terminal producer, not a group by: one row per compared column.
const compare = createGrid(document.querySelector('#compare'), {
rowKey: 'column',
columns: [
{ field: 'column', title: 'Column' },
{ field: 'magnitude', title: "Effect size", type: 'number' },
{ field: 'direction', title: 'Direction' },
{ field: 'nA', title: 'This quarter', type: 'number' },
{ field: 'nB', title: 'Last quarter', type: 'number' },
],
source: {
mode: 'derived',
from: thisQuarter,
statistics: { fn: 'datasetVsDataset', with: lastQuarter, columns: ['amount'] },
},
});
Filter the orders grid to EMEA and thisQuarter, lastQuarter and compare
all narrow to EMEA’s rows, because none of the three holds a copy of
anything, they read the orders grid and each other. where on the two
quarter grids runs before anything else; statistics on the comparison
grid is a terminal producer with no groupBy or select beside it, because it
replaces that pipeline rather than running alongside it.
React
import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
// Build the component once, at module scope, not on every render.
const LatticeGrid = createLatticeGrid({ React, createGrid });
const quarterColumns = [
{ field: 'id', title: 'Order' },
{ field: 'date', title: 'Date', type: 'date' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
];
const compareColumns = [
{ field: 'column', title: 'Column' },
{ field: 'magnitude', title: 'Effect size', type: 'number' },
{ field: 'direction', title: 'Direction' },
{ field: 'nA', title: 'This quarter', type: 'number' },
{ field: 'nB', title: 'Last quarter', type: 'number' },
];
function QuarterOverQuarter({ orders }) {
const [book, setBook] = React.useState(null);
const [thisQuarter, setThisQuarter] = React.useState(null);
const [lastQuarter, setLastQuarter] = React.useState(null);
return (
<>
<LatticeGrid
rowKey="id"
columns={quarterColumns}
rows={orders}
onGridReady={setBook}
style={{ height: 320 }}
/>
{book && (
<LatticeGrid
rowKey="id"
columns={quarterColumns}
source={{
mode: 'derived',
from: book,
follow: 'filtered',
where: (r) => r.date >= '2026-07-01' && r.date < '2026-10-01',
}}
onGridReady={setThisQuarter}
style={{ height: 220 }}
/>
)}
{book && (
<LatticeGrid
rowKey="id"
columns={quarterColumns}
source={{
mode: 'derived',
from: book,
follow: 'filtered',
where: (r) => r.date >= '2026-04-01' && r.date < '2026-07-01',
}}
onGridReady={setLastQuarter}
style={{ height: 220 }}
/>
)}
{thisQuarter && lastQuarter && (
<LatticeGrid
rowKey="column"
columns={compareColumns}
source={{
mode: 'derived',
from: thisQuarter,
statistics: { fn: 'datasetVsDataset', with: lastQuarter, columns: ['amount'] },
}}
style={{ height: 140 }}
/>
)}
</>
);
}
The comparison panel waits for both quarter instances, the same way the
quarter panels wait for book, because statistics.with needs a real grid
to read, the same as from does. Filter the orders grid from its own
header and every panel downstream narrows with it, through the same public
API the plain example reads.
The options used
| Option | What it does |
|---|---|
mode: 'derived' |
This grid’s rows are computed from another grid’s, not loaded from an array or a server. |
from |
The grid to read. |
follow: 'filtered' |
Read the rows the filters leave. The default. |
where |
A row predicate, applied before the rest of the pipeline runs. |
statistics.fn: 'datasetVsDataset' |
Selects the dataset-comparison producer: one row per compared column, against a second grid. A terminal producer, so groupBy, select, sort and limit have no place alongside it. |
statistics.with |
The second grid to compare this one against. |
statistics.columns |
Restrict the comparison to these columns. All shared columns by default. |
What the example actually produced
Running the plain JavaScript example above with createHeadlessGrid from
the published package, and printing compare’s rows, gives:
| Column | Effect size | Direction | This quarter | Last quarter |
|---|---|---|---|---|
| amount | 1.91 | this quarter higher | 10 | 10 |
Ten orders a side, an effect size of 1.91 on amount, and the direction
says the swing runs upward: this quarter’s total of 5,780 against last
quarter’s 3,455. Filter either quarter grid to EMEA and the comparison
re-reads only EMEA’s rows on both sides, so the same question the room asks
after the slide goes up has an answer already live on the screen.
The derived and chained grids guide covers statistics
alongside correlation, column profiles and chaining a derived grid from a
derived grid in full. For a derived grid that buckets by month instead of
splitting by quarter, see the monthly revenue trend grid.