Lattice Grid Buy a licence

blog

Trend Figures for Any Series, as Grid Rows

Building…
Loading a live grid…

Whoever reports on trend ends up building the same six numbers from scratch, every time someone asks for them: how far the figure moved since the start of the run, how choppy that run has been, the worst dip along the way and when it happened, how many periods went up against how many went down. Each one is its own formula in a spreadsheet column, copied down next to a date column, and recalculated by hand the moment someone asks to see just one channel instead of the whole book. Time series statistics in JavaScript, computed inside the grid that already holds the rows, removes that spreadsheet from the loop.

A derived grid turns the formula column into a second grid. Point it at the orders grid, name the column to measure and the column that orders it, and statistics: { fn: 'series', of: 'amount', by: 'date' } hands back the same figures as rows: the change since the first point, the volatility, the worst drawdown and where it fell, the up periods against the down ones. Filter the orders grid to one channel and every one of those rows recomputes from just the rows that are left, on the next frame.

That is the spreadsheet rebuild gone, not hidden somewhere else: the trend figures live on a grid that already holds the orders, so a narrower filter produces a narrower trend without anyone reopening a sheet to redo the formulas. Six figures, one definition each, correct for whatever the filter currently shows.

Time series statistics in JavaScript

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

const orders = [
  { id: 1,  date: '2026-09-01', channel: 'Online',    amount: 1180 },
  { id: 2,  date: '2026-09-02', channel: 'Wholesale', amount: 1240 },
  { id: 3,  date: '2026-09-03', channel: 'Online',    amount: 1050 },
  { id: 4,  date: '2026-09-04', channel: 'Wholesale', amount: 1310 },
  { id: 5,  date: '2026-09-05', channel: 'Online',    amount: 1395 },
  { id: 6,  date: '2026-09-08', channel: 'Wholesale', amount: 1288 },
  { id: 7,  date: '2026-09-09', channel: 'Online',    amount: 1460 },
  { id: 8,  date: '2026-09-10', channel: 'Wholesale', amount: 1120 },
  { id: 9,  date: '2026-09-11', channel: 'Online',    amount: 980 },
  { id: 10, date: '2026-09-12', channel: 'Wholesale', amount: 1035 },
  { id: 11, date: '2026-09-15', channel: 'Online',    amount: 1210 },
  { id: 12, date: '2026-09-16', channel: 'Wholesale', amount: 1375 },
  { id: 13, date: '2026-09-17', channel: 'Online',    amount: 1520 },
  { id: 14, date: '2026-09-18', channel: 'Wholesale', amount: 1448 },
  { id: 15, date: '2026-09-19', channel: 'Online',    amount: 1605 },
  { id: 16, date: '2026-09-22', channel: 'Wholesale', amount: 1690 },
  { id: 17, date: '2026-09-23', channel: 'Online',    amount: 1572 },
  { id: 18, date: '2026-09-24', channel: 'Wholesale', amount: 1715 },
  { id: 19, date: '2026-09-25', channel: 'Online',    amount: 1840 },
  { id: 20, date: '2026-09-26', channel: 'Wholesale', amount: 1920 },
];

const book = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  columns: [
    { field: 'id',      title: 'Order' },
    { field: 'date',    title: 'Date', type: 'dateString' },
    { field: 'channel', title: 'Channel', filter: { type: 'set' } },
    { field: 'amount',  title: 'Amount', type: 'number' },
  ],
  rows: orders,
});

// A second grid whose rows are the trend figures for the amount series,
// ordered by date: one row per metric, not one row per order.
const trend = createGrid(document.querySelector('#trend'), {
  source: {
    mode: 'derived',
    from: book,              // the grid to read
    statistics: { fn: 'series', of: 'amount', by: 'date' },
  },
  columns: [
    { field: 'metric', title: 'Metric' },
    { field: 'value',  title: 'Value', type: 'number' },
    { field: 'n',      title: 'N', type: 'number' },
  ],
});

Filter the orders grid’s Channel column to Online, dropping every Wholesale order, and trend recomputes every figure from just those ten rows, 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 AmountTrend({ orders }) {
  const [book, setBook] = React.useState(null);

  const orderColumns = [
    { field: 'id',      title: 'Order' },
    { field: 'date',    title: 'Date', type: 'dateString' },
    { field: 'channel', title: 'Channel', filter: { type: 'set' } },
    { field: 'amount',  title: 'Amount', type: 'number' },
  ];

  const trendColumns = [
    { field: 'metric', title: 'Metric' },
    { field: 'value',  title: 'Value', type: 'number' },
    { field: 'n',      title: 'N', type: 'number' },
  ];

  return (
    <>
      <LatticeGrid
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        onGridReady={setBook}
        style={{ height: 320 }}
      />
      {book && (
        <LatticeGrid
          columns={trendColumns}
          source={{
            mode: 'derived',
            from: book,
            statistics: { fn: 'series', of: 'amount', by: 'date' },
          }}
          style={{ height: 320 }}
        />
      )}
    </>
  );
}

onGridReady hands back the live orders grid as soon as it mounts, which is what the trend panel’s from needs; the panel itself only renders once that instance exists. Filtering the orders grid’s Channel column moves the trend 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, and re-derived when that one changes.
from The grid to read.
statistics Projects a figure that needs more than one column, or a second grid, into rows. It replaces the rest of the pipeline rather than joining it.
statistics.fn: 'series' Selects the series-summary producer: volatility, growth, drawdown, autocorrelation, one row per metric.
statistics.of The column to summarise.
statistics.by The column that orders the series. Required, and never guessed, because a derived grid’s own sort is not the order a trend is computed in.

What the example actually produced

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

Metric Value N
n 20 20
first 1,180 20
last 1,920 20
change 740 20
changePercent 62.7 20
volatility 0.121 20
annualisedVolatility null 20
growth 0.026 20
maxDrawdown 0.329 20
maxDrawdownFrom 6 20
maxDrawdownTo 8 20
autocorrelation 0.715 20
upDays 13 20
downDays 6 20

annualisedVolatility is null because no periodsPerYear was given; pass one and volatility and growth are annualised against it. maxDrawdownFrom and maxDrawdownTo are positions in the ordered series, not dates: row 6 is 9 September, the peak at 1,460, and row 8 is 11 September, the trough at 980.

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

Metric Value N
n 10 10
first 1,180 10
last 1,840 10
change 660 10
changePercent 55.9 10
volatility 0.206 10
annualisedVolatility null 10
growth 0.051 10
maxDrawdown 0.329 10
maxDrawdownFrom 3 10
maxDrawdownTo 4 10
autocorrelation 0.359 10
upDays 6 10
downDays 3 10

n drops to the ten Online rows, and most figures move with it, the volatility rises because the smaller, noisier series has fewer points smoothing it out. maxDrawdown lands on the same ratio, because the same peak and trough both happen to fall on Online days, but the positions that name them shift to 3 and 4, their place in the shorter, filtered series.

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 another grid that reads a time column, see the monthly revenue trend post, which feeds the same kind of derived grid into a line chart instead of a statistics row.

Read next

All posts RSS