Lattice Grid Buy a licence

blog

Region by Category Revenue Matrix From One Order Grid

Building…
Loading a live grid…

You have built this panel before: a region by category breakdown sitting beside the orders table, fed by its own query and kept roughly in step by an event hook and good intentions. It works the day you ship it. Then someone filters the orders grid to this quarter, or to one region, and the breakdown keeps showing the old totals, because nothing told it to run again. Now there are two numbers on one screen and only one of them is right. A pivot table in JavaScript can live inside the grid instead.

A derived grid closes that gap by making the breakdown computed rather than queried. Build the orders grid the way you already do, then build a second grid whose source reads the first: group by region and category, sum the revenue, and the result is a live grid, not a snapshot of one.

source: {
  mode: 'derived',
  from: orders,
  groupBy: ['region', 'category'],
  select: { revenue: { of: 'amount', fn: 'sum' } },
}

Filter the orders grid once, by region, by a date range, by typing into the quick filter, and the matrix recomputes on the next frame, because it was never a second query to begin with. It is the same table, read a different way.

For you that means the breakdown panel stops being a second thing to maintain. There is no second query to write, no event hook to wire up, and no meeting where two numbers on one screen do not agree, because the matrix is never able to see a row the table has filtered out. One filter moves both grids, every time, with no code of yours in between.

Plain JavaScript

Twenty orders, a grid that holds them, and a second grid grouped by region and category:

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

const orders = [
  { id: 'O1',  region: 'EMEA', category: 'Hardware', amount: 2400 },
  { id: 'O2',  region: 'EMEA', category: 'Software', amount: 1800 },
  { id: 'O3',  region: 'EMEA', category: 'Hardware', amount: 1200 },
  { id: 'O4',  region: 'EMEA', category: 'Services',  amount: 900 },
  { id: 'O5',  region: 'EMEA', category: 'Software', amount: 2600 },
  { id: 'O6',  region: 'AMER', category: 'Hardware', amount: 3100 },
  { id: 'O7',  region: 'AMER', category: 'Software', amount: 1500 },
  { id: 'O8',  region: 'AMER', category: 'Services',  amount: 2200 },
  { id: 'O9',  region: 'AMER', category: 'Hardware', amount: 1700 },
  { id: 'O10', region: 'AMER', category: 'Support',   amount: 600 },
  { id: 'O11', region: 'AMER', category: 'Software', amount: 2000 },
  { id: 'O12', region: 'APAC', category: 'Hardware', amount: 2900 },
  { id: 'O13', region: 'APAC', category: 'Software', amount: 1100 },
  { id: 'O14', region: 'APAC', category: 'Services',  amount: 1600 },
  { id: 'O15', region: 'APAC', category: 'Hardware', amount: 800 },
  { id: 'O16', region: 'APAC', category: 'Support',   amount: 400 },
  { id: 'O17', region: 'EMEA', category: 'Support',   amount: 700 },
  { id: 'O18', region: 'AMER', category: 'Services',  amount: 1300 },
  { id: 'O19', region: 'APAC', category: 'Software', amount: 1900 },
  { id: 'O20', region: 'EMEA', category: 'Services',  amount: 1100 },
];

const orderColumns = [
  { field: 'id', title: 'Order' },
  { field: 'region', title: 'Region', filter: { type: 'set' } },
  { field: 'category', title: 'Category', filter: { type: 'set' } },
  { field: 'amount', title: 'Amount', type: 'number', total: 'sum' },
];

// The table a reader already knows how to build.
const ordersGrid = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  columns: orderColumns,
  rows: orders,
});

// The matrix beside it, grouped two levels deep and summed.
const matrix = createGrid(document.querySelector('#matrix'), {
  columns: [
    { field: 'region', title: 'Region' },
    { field: 'category', title: 'Category' },
    { field: 'revenue', title: 'Revenue', type: 'number' },
  ],
  source: {
    mode: 'derived',
    from: ordersGrid,
    groupBy: ['region', 'category'],
    select: { revenue: { of: 'amount', fn: 'sum' } },
    sort: [{ col: 'revenue', dir: 'desc' }],
  },
});

Filter ordersGrid to one region and matrix narrows to the same rows, re-summed, on the next frame.

React

The same two grids through the React adapter. The orders grid forwards a ref; once it is mounted, its live instance becomes the from of the second grid’s derived source:

import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';

const LatticeGrid = createLatticeGrid({ React, createGrid });

const orderColumns = [
  { field: 'id', title: 'Order' },
  { field: 'region', title: 'Region', filter: { type: 'set' } },
  { field: 'category', title: 'Category', filter: { type: 'set' } },
  { field: 'amount', title: 'Amount', type: 'number', total: 'sum' },
];

const matrixColumns = [
  { field: 'region', title: 'Region' },
  { field: 'category', title: 'Category' },
  { field: 'revenue', title: 'Revenue', type: 'number' },
];

function RevenueMatrix({ orders }) {
  const ordersRef = React.useRef(null);
  const [matrix, setMatrix] = React.useState(null);

  // Runs once the orders grid exists, reached through its ref.
  React.useEffect(() => {
    const ordersGrid = ordersRef.current && ordersRef.current.grid;
    if (!ordersGrid) return;
    setMatrix({
      mode: 'derived',
      from: ordersGrid,
      groupBy: ['region', 'category'],
      select: { revenue: { of: 'amount', fn: 'sum' } },
      sort: [{ col: 'revenue', dir: 'desc' }],
    });
  }, []);

  return (
    <>
      <LatticeGrid
        ref={ordersRef}
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        style={{ height: 320 }}
      />
      {matrix && (
        <LatticeGrid
          autoHeight
          columns={matrixColumns}
          source={matrix}
          style={{ marginTop: 16 }}
        />
      )}
    </>
  );
}

source takes the derived config directly, the same shape as the plain JavaScript example; only the way the grid is mounted changes.

The options used

Option Type What it does
from Grid The grid to read rows from.
groupBy string | string[] The dimension, or dimensions, to group by. Omit to pass rows through.
select Record<string, { of, fn }> The reduced columns, by output id. fn is any key of the totals row kernels, so sum, avg, median and p95 are as available as the plain sum used here.
sort { col, dir }[] Order the derived rows before limiting them.

The pivot table it produces

Running the plain JavaScript example above against those twenty orders produces twelve rows, one for every region and category combination that has at least one order in it:

Region Category Revenue
AMER Hardware 4800
EMEA Software 4400
APAC Hardware 3700
EMEA Hardware 3600
AMER Software 3500
AMER Services 3500
APAC Software 3000
EMEA Services 2000
APAC Services 1600
EMEA Support 700
AMER Support 600
APAC Support 400

Filter the orders grid to AMER and the matrix drops to its four AMER rows, still summed, still sorted, with no second request behind it.

Read the derived and chained grids guide for grouping, joins and profiles in full, or see derived sources as a grid source option for how a derived grid fits beside every other way of feeding one.

Read next

All posts RSS