Lattice Grid Buy a licence

blog

Median, p95 and Spread Per Rep, Not Just the Totals

Building…
Loading a live grid…

Whoever runs the quarterly comp conversation starts with one number per rep: revenue, summed from an export or a saved query, to decide who gets the bonus. That number hides the shape of the quarter completely. A rep who closed one $940 deal and three small ones ranks next to a rep who closed four steady $420 deals, because a sum cannot tell a lucky outlier from a dependable quarter, and the panel built to tell them apart is usually its own query against the same orders, with its own groupBy and its own average. Filter that query to one region and it stops matching the table in front of it, because nothing told it to run again. A median and percentile JavaScript grid, reading the same orders, is the fix for a total that cannot tell the two reps apart.

Point a second grid at the orders grid, group by rep, and reduce each group with three kernels instead of one: median for the typical deal, p95 for the outlier end, stddev for how far the deals sit from each other. All three read the orders grid’s own rows, so narrowing the orders to a region re-derives the same three figures per rep on the next frame, not a stale sum.

That turns the comp conversation from a debate about whose export is current into a read of one screen. Filter the orders to a region and the median, p95 and spread per rep recompute together, with no second export to rerun before the meeting starts. A rep whose p95 sits close to their median sold about the same deal every time; a rep whose p95 is three or four times their median sold one big deal and the rest were filler, and the spread says so in one column without anyone opening the raw orders.

Median and percentile JavaScript, grouped by rep

import { createGrid } from '@toclocoinc/lattice-grid';

const orders = [
  { id: 1,  rep: 'Priya Shah',   region: 'EMEA',     amount: 940 },
  { id: 2,  rep: 'Priya Shah',   region: 'EMEA',     amount: 150 },
  { id: 3,  rep: 'Priya Shah',   region: 'Americas', amount: 160 },
  { id: 4,  rep: 'Priya Shah',   region: 'Americas', amount: 175 },
  { id: 5,  rep: 'Mateo Ruiz',   region: 'EMEA',     amount: 420 },
  { id: 6,  rep: 'Mateo Ruiz',   region: 'EMEA',     amount: 410 },
  { id: 7,  rep: 'Mateo Ruiz',   region: 'Americas', amount: 440 },
  { id: 8,  rep: 'Mateo Ruiz',   region: 'Americas', amount: 460 },
  { id: 9,  rep: 'Jin Park',     region: 'EMEA',     amount: 295 },
  { id: 10, rep: 'Jin Park',     region: 'EMEA',     amount: 305 },
  { id: 11, rep: 'Jin Park',     region: 'Americas', amount: 300 },
  { id: 12, rep: 'Jin Park',     region: 'Americas', amount: 310 },
  { id: 13, rep: 'Owen Clarke',  region: 'EMEA',     amount: 95 },
  { id: 14, rep: 'Owen Clarke',  region: 'EMEA',     amount: 150 },
  { id: 15, rep: 'Owen Clarke',  region: 'Americas', amount: 820 },
  { id: 16, rep: 'Owen Clarke',  region: 'Americas', amount: 275 },
  { id: 17, rep: 'Lena Fischer', region: 'EMEA',     amount: 220 },
  { id: 18, rep: 'Lena Fischer', region: 'EMEA',     amount: 230 },
  { id: 19, rep: 'Lena Fischer', region: 'Americas', amount: 260 },
  { id: 20, rep: 'Lena Fischer', region: 'Americas', amount: 340 },
];

const book = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  columns: [
    { field: 'id',     title: 'Order' },
    { field: 'rep',    title: 'Rep', filter: { type: 'set' } },
    { field: 'region', title: 'Region', filter: { type: 'set' } },
    { field: 'amount', title: 'Amount', type: 'number' },
  ],
  rows: orders,
});

// A second grid whose rows come from the first.
const byRep = createGrid(document.querySelector('#by-rep'), {
  source: {
    mode: 'derived',
    from: book,            // the grid to read
    follow: 'filtered',    // read whatever the filters leave
    groupBy: 'rep',
    select: {
      median: { of: 'amount', fn: 'median' },
      p95:    { of: 'amount', fn: 'p95' },
      spread: { of: 'amount', fn: 'stddev' },
    },
    sort: [{ col: 'median', dir: 'desc' }],
  },
  columns: [
    { field: 'rep',    title: 'Rep' },
    { field: 'median', title: 'Median', type: 'number' },
    { field: 'p95',    title: 'p95', type: 'number' },
    { field: 'spread', title: 'Spread', type: 'number' },
  ],
});

Filter the orders grid’s Region column to EMEA and byRep re-derives all three figures from only the EMEA rows, because median, p95 and stddev all read the orders grid’s current, filtered rows rather than a copy taken when the page loaded.

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 RepSpread({ orders }) {
  const [book, setBook] = React.useState(null);

  const orderColumns = [
    { field: 'id',     title: 'Order' },
    { field: 'rep',    title: 'Rep', filter: { type: 'set' } },
    { field: 'region', title: 'Region', filter: { type: 'set' } },
    { field: 'amount', title: 'Amount', type: 'number' },
  ];

  const repColumns = [
    { field: 'rep',    title: 'Rep' },
    { field: 'median', title: 'Median', type: 'number' },
    { field: 'p95',    title: 'p95', type: 'number' },
    { field: 'spread', title: 'Spread', type: 'number' },
  ];

  return (
    <>
      <LatticeGrid
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        onGridReady={setBook}
        style={{ height: 320 }}
      />
      {book && (
        <LatticeGrid
          columns={repColumns}
          source={{
            mode: 'derived',
            from: book,
            follow: 'filtered',
            groupBy: 'rep',
            select: {
              median: { of: 'amount', fn: 'median' },
              p95:    { of: 'amount', fn: 'p95' },
              spread: { of: 'amount', fn: 'stddev' },
            },
            sort: [{ col: 'median', dir: 'desc' }],
          }}
          style={{ height: 160 }}
        />
      )}
    </>
  );
}

onGridReady hands back the live orders grid as soon as it mounts, which is what the rep panel’s from needs; the panel itself only renders once that instance exists. Filtering the orders grid’s Region column moves the rep panel the same way it does in the plain example, because both read the grid’s public API, not a copy of the rows taken at render time.

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, or dimensions, to group by.
select The reduced columns, by output id: of names the field to reduce, fn is any totals function, including median, p95 and stddev.
sort How to order the derived rows.

What the example actually produced

Running the plain JavaScript example above with createHeadlessGrid from the published package, and printing byRep’s rows, gives:

Rep Median p95 Spread
Mateo Ruiz 430 457 22.2
Jin Park 302.5 309.3 6.5
Lena Fischer 245 328 54.4
Owen Clarke 212.5 738.2 332
Priya Shah 167.5 825.2 389.3

Priya Shah and Owen Clarke have the lowest medians of the five, yet the highest p95 and spread by a wide margin: each has one deal several times the size of their others, and the spread names it instead of burying it in a sum. Mateo Ruiz and Jin Park sit at the opposite end, their p95 within a few percent of their median, which is what a rep selling about the same deal every time looks like in these columns.

Filtering the orders grid’s Region column to EMEA and re-reading byRep gives:

Rep Median p95 Spread
Priya Shah 545 900.5 558.6
Mateo Ruiz 415 419.5 7.1
Jin Park 300 304.5 7.1
Lena Fischer 225 229.5 7.1
Owen Clarke 122.5 147.3 38.9

Every figure moved to match just the EMEA rows, Priya Shah’s spread most of all, because her one outlier deal happened to land in EMEA. Nothing in byRep’s own configuration changed; the region filter sat on the orders grid the whole time.

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 per-rep statistical profile alongside a revenue leaderboard, see the sales dashboard demo.

Read next

All posts RSS