Lattice Grid Buy a licence

blog

Units and Revenue by SKU: Unnest Order Lines

Building…
Loading a live grid…

Whoever has to answer “what’s actually selling” does not find it on the orders grid at all. The orders table has one row per order; the SKU sits one level down, inside each order’s own line items, three or four to an order. So the answer gets built the slow way: export the orders, explode every order’s lines into their own rows in a spreadsheet, and pivot by SKU. It is correct for exactly as long as nobody touches the filter. Narrow the orders to one sales channel and the spreadsheet pivot still shows every channel, because it was built from an export, not from the grid, and nothing told it to run again. Flattening a nested array into a JavaScript table that stays live is the step the export was standing in for.

A derived grid skips the export. Point a second grid at the orders grid, unnest the lines array so each line becomes its own row, and group those lines by SKU: units sold and revenue, summed per SKU, straight from the orders grid’s own rows. Filter the orders grid to a channel and the SKU breakdown re-derives on the next frame, because it reads the orders grid’s live rows rather than a copy taken at export time.

That removes the pivot step entirely, not just the risk of it going stale. The SKU breakdown is a few lines of configuration next to the orders grid, not a recurring export somebody owns and reruns by hand.

Flatten the nested array in plain JavaScript

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

const orders = [
  { id: 1,  channel: 'Online',    lines: [{ sku: 'SKU-100', qty: 3,  value: 210 }, { sku: 'SKU-200', qty: 1, value: 85 }] },
  { id: 2,  channel: 'Wholesale', lines: [{ sku: 'SKU-300', qty: 10, value: 640 }] },
  { id: 3,  channel: 'Retail',    lines: [{ sku: 'SKU-100', qty: 2,  value: 140 }, { sku: 'SKU-400', qty: 4, value: 360 }] },
  { id: 4,  channel: 'Online',    lines: [{ sku: 'SKU-200', qty: 5,  value: 425 }] },
  { id: 5,  channel: 'Wholesale', lines: [{ sku: 'SKU-100', qty: 8,  value: 560 }, { sku: 'SKU-300', qty: 6, value: 384 }] },
  { id: 6,  channel: 'Retail',    lines: [{ sku: 'SKU-400', qty: 2,  value: 180 }] },
  { id: 7,  channel: 'Online',    lines: [{ sku: 'SKU-300', qty: 4,  value: 256 }, { sku: 'SKU-200', qty: 2, value: 170 }] },
  { id: 8,  channel: 'Wholesale', lines: [{ sku: 'SKU-100', qty: 12, value: 840 }] },
  { id: 9,  channel: 'Retail',    lines: [{ sku: 'SKU-200', qty: 3,  value: 255 }, { sku: 'SKU-400', qty: 1, value: 90 }] },
  { id: 10, channel: 'Online',    lines: [{ sku: 'SKU-100', qty: 1,  value: 70 }] },
  { id: 11, channel: 'Wholesale', lines: [{ sku: 'SKU-400', qty: 9,  value: 810 }, { sku: 'SKU-300', qty: 3, value: 192 }] },
  { id: 12, channel: 'Retail',    lines: [{ sku: 'SKU-100', qty: 5,  value: 350 }] },
  { id: 13, channel: 'Online',    lines: [{ sku: 'SKU-200', qty: 4,  value: 340 }] },
  { id: 14, channel: 'Wholesale', lines: [{ sku: 'SKU-300', qty: 15, value: 960 }] },
  { id: 15, channel: 'Retail',    lines: [{ sku: 'SKU-400', qty: 3,  value: 270 }, { sku: 'SKU-100', qty: 2, value: 140 }] },
  { id: 16, channel: 'Online',    lines: [{ sku: 'SKU-300', qty: 2,  value: 128 }] },
  { id: 17, channel: 'Wholesale', lines: [{ sku: 'SKU-200', qty: 7,  value: 595 }, { sku: 'SKU-100', qty: 4, value: 280 }] },
  { id: 18, channel: 'Retail',    lines: [{ sku: 'SKU-400', qty: 5,  value: 450 }] },
  { id: 19, channel: 'Online',    lines: [{ sku: 'SKU-100', qty: 6,  value: 420 }, { sku: 'SKU-300', qty: 1, value: 64 }] },
  { id: 20, channel: 'Wholesale', lines: [{ sku: 'SKU-200', qty: 2,  value: 170 }, { sku: 'SKU-400', qty: 4, value: 360 }] },
];

const book = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  columns: [
    { field: 'id',      title: 'Order' },
    { field: 'channel', title: 'Channel', filter: { type: 'set' } },
    { field: 'lines',   title: 'Lines' },
  ],
  rows: orders,
});

// A second grid whose rows are the orders' own line items, unnested and grouped.
const bySku = createGrid(document.querySelector('#by-sku'), {
  source: {
    mode: 'derived',
    from: book,          // the grid to read
    unnest: 'lines',     // one row per line item, keeping the parent's channel
    groupBy: 'lines.sku',
    select: {
      units:   { of: 'lines.qty',   fn: 'sum' },
      revenue: { of: 'lines.value', fn: 'sum' },
    },
    sort: [{ col: 'revenue', dir: 'desc' }],
  },
  columns: [
    { field: 'sku', title: 'SKU' },
    { field: 'units',   title: 'Units',   type: 'number' },
    { field: 'revenue', title: 'Revenue', type: 'number' },
  ],
});

Filter the orders grid’s Channel column to Online and Wholesale, dropping Retail, and bySku re-totals every SKU down to just those two channels’ lines, with no second pass over the orders array anywhere in the example above.

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

  const orderColumns = [
    { field: 'id',      title: 'Order' },
    { field: 'channel', title: 'Channel', filter: { type: 'set' } },
    { field: 'lines',   title: 'Lines' },
  ];

  const skuColumns = [
    { field: 'sku', title: 'SKU' },
    { field: 'units',   title: 'Units',   type: 'number' },
    { field: 'revenue', title: 'Revenue', type: 'number' },
  ];

  return (
    <>
      <LatticeGrid
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        onGridReady={setBook}
        style={{ height: 320 }}
      />
      {book && (
        <LatticeGrid
          columns={skuColumns}
          source={{
            mode: 'derived',
            from: book,
            unnest: 'lines',
            groupBy: 'lines.sku',
            select: {
              units:   { of: 'lines.qty',   fn: 'sum' },
              revenue: { of: 'lines.value', fn: 'sum' },
            },
            sort: [{ col: 'revenue', dir: 'desc' }],
          }}
          style={{ height: 160 }}
        />
      )}
    </>
  );
}

onGridReady hands back the live orders grid as soon as it mounts, which is what the SKU panel’s from needs; the panel itself only renders once that instance exists. Filtering the orders grid’s Channel column moves the SKU 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.
unnest Expand an array property, one row per element, keeping the parent’s own fields. Address the element with a dotted path afterwards: lines.sku is the element, channel is still the parent.
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: sum, count, avg and more.
sort How to order the derived rows before any limit is applied.

What the example actually produced

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

SKU Units Revenue
SKU-100 43 3,010
SKU-300 41 2,624
SKU-400 28 2,520
SKU-200 24 2,040

Filtering the orders grid’s Channel column to Online and Wholesale, dropping every Retail order, and re-reading bySku gives:

SKU Units Revenue
SKU-300 41 2,624
SKU-100 34 2,380
SKU-200 21 1,785
SKU-400 13 1,170

Every SKU’s units and revenue drop to match, in the same row set, because the totals were never anywhere but in the derived grid to begin with.

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 top sellers and a company-wide tile alongside the per-rep totals, see the sales dashboard demo.

Read next

All posts RSS