Top Customers by Revenue: the 80% Cutoff
Every account review opens the same way: export the orders, sort by customer, and run a finger down the revenue column until the running total crosses eighty percent. Whoever does it copies that list of names into a slide or a tab of its own, because the one thing nobody keeps live is the cutoff itself, where the eighty percent line actually falls this quarter. Filter the orders to one region, or drop last quarter’s trial accounts, and the exercise starts over from the top, by hand, because the running total was a point-in-time read, not a rule attached to the data. It is Pareto analysis, and in JavaScript it can be a rule the grid keeps.
A derived grid turns the cutoff into a rule instead of a read. Build a second grid from the orders grid: group by customer, sum the revenue, sort it highest first, and tell the derived source to keep rows only until the running share of that sorted total reaches 0.8. Filter the orders book to a quarter, a region or a product line, and the derived grid regroups, re-sorts and re-applies the cutoff on the next pass, so the top-customers list is always this quarter’s eighty percent, not the one someone worked out last Tuesday.
That is the finger-down-the-column exercise gone, and the account list a renewal or success team works from updates itself every time the book changes, with no second query to keep in step. Across the whole order book below, eighty percent of revenue sits with three customers; filtered to one quarter, it sits with two.
Plain JavaScript
import { createGrid } from '@toclocoinc/lattice-grid';
const orders = [
{ id: 1, customer: 'Northwind', quarter: 'Q1', amount: 1200 },
{ id: 2, customer: 'Northwind', quarter: 'Q1', amount: 980 },
{ id: 3, customer: 'Northwind', quarter: 'Q2', amount: 860 },
{ id: 4, customer: 'Northwind', quarter: 'Q2', amount: 740 },
{ id: 5, customer: 'Northwind', quarter: 'Q1', amount: 630 },
{ id: 6, customer: 'Acme', quarter: 'Q1', amount: 710 },
{ id: 7, customer: 'Acme', quarter: 'Q2', amount: 690 },
{ id: 8, customer: 'Acme', quarter: 'Q1', amount: 540 },
{ id: 9, customer: 'Acme', quarter: 'Q2', amount: 410 },
{ id: 10, customer: 'Globex', quarter: 'Q2', amount: 480 },
{ id: 11, customer: 'Globex', quarter: 'Q1', amount: 350 },
{ id: 12, customer: 'Globex', quarter: 'Q2', amount: 300 },
{ id: 13, customer: 'Initech', quarter: 'Q1', amount: 260 },
{ id: 14, customer: 'Initech', quarter: 'Q2', amount: 220 },
{ id: 15, customer: 'Initech', quarter: 'Q1', amount: 180 },
{ id: 16, customer: 'Umbrella', quarter: 'Q2', amount: 150 },
{ id: 17, customer: 'Umbrella', quarter: 'Q1', amount: 120 },
{ id: 18, customer: 'Soylent', quarter: 'Q1', amount: 95 },
{ id: 19, customer: 'Soylent', quarter: 'Q2', amount: 80 },
{ id: 20, customer: 'Hooli', quarter: 'Q2', amount: 60 },
];
const book = createGrid(document.querySelector('#orders'), {
rowKey: 'id',
columns: [
{ field: 'id', title: 'Order' },
{ field: 'customer', title: 'Customer' },
{ field: 'quarter', title: 'Quarter', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
],
rows: orders,
});
// A second grid: customers ranked by revenue, kept until they cross 80%.
const topCustomers = createGrid(document.querySelector('#top-customers'), {
source: {
mode: 'derived',
from: book, // the grid to read
follow: 'filtered', // read whatever the filters leave
groupBy: 'customer',
select: {
revenue: { of: 'amount', fn: 'sum' },
orders: { fn: 'count' },
},
sort: [{ col: 'revenue', dir: 'desc' }],
cumulative: { of: 'revenue', upTo: 0.8 },
},
columns: [
{ field: 'customer', title: 'Customer' },
{ field: 'revenue', title: 'Revenue', type: 'number' },
{ field: 'orders', title: 'Orders', type: 'number' },
],
});
Filter the orders grid to Q1 only, and topCustomers drops from three
customers to two: Northwind and Acme between them cross eighty percent of
that quarter’s revenue, and Globex, which was needed to reach the cutoff
across the whole book, is not needed within a single quarter. No second call
anywhere in the example above drives that change; it is the same cumulative
cutoff, re-applied to whatever the orders grid now shows.
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 });
function TopCustomers({ orders }) {
const [book, setBook] = React.useState(null);
const orderColumns = [
{ field: 'id', title: 'Order' },
{ field: 'customer', title: 'Customer' },
{ field: 'quarter', title: 'Quarter', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
];
const customerColumns = [
{ field: 'customer', title: 'Customer' },
{ field: 'revenue', title: 'Revenue', type: 'number' },
{ field: 'orders', title: 'Orders', type: 'number' },
];
return (
<>
<LatticeGrid
rowKey="id"
columns={orderColumns}
rows={orders}
onGridReady={setBook}
style={{ height: 320 }}
/>
{book && (
<LatticeGrid
columns={customerColumns}
source={{
mode: 'derived',
from: book,
follow: 'filtered',
groupBy: 'customer',
select: {
revenue: { of: 'amount', fn: 'sum' },
orders: { fn: 'count' },
},
sort: [{ col: 'revenue', dir: 'desc' }],
cumulative: { of: 'revenue', upTo: 0.8 },
}}
style={{ height: 160 }}
/>
)}
</>
);
}
onGridReady hands back the live orders grid as soon as it mounts, which is
what the customer panel’s from needs; the panel itself only renders once
that instance exists. Filtering the orders grid narrows book, and
topCustomers re-derives its cutoff from what is left, the same as the plain
example, because both read the grid’s public API rather than a copy of its
rows.
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. |
groupBy |
The dimension to group by, here the customer. |
select |
The reduced columns, by output id: of names the field to reduce, fn is a totals function such as sum or count. |
sort |
Orders the derived rows before cumulative runs over them. |
cumulative: { of, upTo } |
Keeps rows until their running share of the total reaches upTo, 0 to 1. The row that crosses the cutoff is kept, because the set has to reach the share. |
The Pareto analysis it produced
Running the plain JavaScript example above with createHeadlessGrid from the
published package, and reading topCustomers’s rows, gives:
| Customer | Revenue | Orders |
|---|---|---|
| Northwind | 4,410 | 5 |
| Acme | 2,350 | 4 |
| Globex | 1,130 | 3 |
Northwind and Acme together hold 75% of the book’s $9,055, short of eighty, so Globex is kept as the customer that crosses the line: the three hold $7,890, 87% of the book.
Filtering the orders to Q1 and reading topCustomers again:
| Customer | Revenue | Orders |
|---|---|---|
| Northwind | 2,810 | 3 |
| Acme | 1,250 | 2 |
$4,060 of Q1’s $5,065, 80.2%, and one customer fewer than the whole-book answer, because Q1 alone does not need Globex to cross the line.
The derived and chained grids guide covers joins, column profiles, chaining a derived grid from a derived grid, and cross-filtering back into the source in full. For a worked dashboard built the same way, with a top-sellers ranking alongside a company-wide total, see the sales dashboard demo.