Lattice Grid Buy a licence

blog

Top Customers by Revenue: the 80% Cutoff

Building…
Loading a live grid…

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.

Read next

All posts RSS