demo D246
Seasonality and smoothing in React
A daily series split into trend, weekly season and residual as shadow columns, a smoothed level, a correlogram and a stationarity verdict
shadow: tsSeasonal, tsTrend · statistics.adf, acf
This grid splits a daily series into trend, weekly season and residual as shadow columns, with a smoothed level, a correlogram and a stationarity verdict. It pulls apart the slow trend, the repeating pattern and the noise, so each can be read on its own.
This is the React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.
The configuration
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/lattice-grid.min.css">
<div id="app"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/lattice-grid.esm.min.js';
import { createLatticeReact } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/react.esm.min.js';
import { createChart } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/modules/charts.esm.min.js';
const { LatticeGrid, LatticeChart } = createLatticeReact({ React, createGrid, createChart });
const columns = [
{ field: 'day', title: 'Day', type: 'dateString', layout: { width: 130, pin: 'start' } },
{ field: 'sales', title: 'Sales', type: 'number', total: 'avg' },
// One ordered pass splits the series into trend, the repeating weekly
// index and the residual, all as shadow columns. period 7 is the rhythm;
// the leading and trailing weeks report null with coverage 0.
{ id: 'trend', title: 'Trend', type: 'number', format: { decimals: 1 }, shadow: { kind: 'tsTrend', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'season', title: 'Weekly', type: 'number', format: { decimals: 1, signed: true }, shadow: { kind: 'tsSeasonal', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'resid', title: 'Residual', type: 'number', format: { decimals: 1, signed: true }, shadow: { kind: 'tsResidual', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'cover', title: 'Coverage', type: 'number', shadow: { kind: 'tsCoverage', of: 'sales', orderBy: 'day', period: 7 } },
{ id: 'level', title: 'Smoothed', type: 'number', format: { decimals: 1 }, shadow: { kind: 'tsSmoothed', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
];
const rows = [/* one row a day, in order: day, sales */];
function App() {
const [grid, setGrid] = React.useState(null);
const [acfSpec, setAcfSpec] = React.useState(null);
const onGridReady = (g) => {
setGrid(g);
// How far back does the series lean on itself? acf returns the
// autocorrelation by lag with the white-noise band to read it against.
const res = g.statistics.acf({ of: 'sales', orderBy: 'day', maxlag: 21 });
setAcfSpec({
points: res.acf.map((v, lag) => ({ x: lag, y: v })),
reference: [{ value: res.bounds.upper }, { value: res.bounds.lower }, { value: 0 }],
});
// Does the series revert to a level or wander? adf returns a verdict at
// the 5% level with its statistic and the lag it used.
g.statistics.adf({ of: 'sales', orderBy: 'day' });
};
return (
<>
{grid && acfSpec && (
<LatticeChart grid={grid} type="bar" points={acfSpec.points} reference={acfSpec.reference} title="Autocorrelation by lag, with the white-noise band" style={{ height: '220px' }} />
)}
<LatticeGrid
rowKey="id"
toolPanel={{ side: 'left', panels: ['columns', 'statistics'] }}
columns={columns}
rows={rows}
onGridReady={onGridReady}
style={{ height: '380px', marginTop: '14px' }}
/>
</>
);
}
createRoot(document.getElementById('app')).render(<App />);
</script>
Splitting a series into trend, season and residual
A daily series with a weekly rhythm is three things at once: a slow trend, a repeating weekly pattern, and the noise those two leave behind. This demo pulls them apart into shadow columns over one ordered pass: a centred moving average for the trend, the repeating weekly index for the season, and the residual that remains. The period is declared, not guessed, so seven says the cycle is weekly in daily data. The additive model is the default; a multiplicative split is a declared option for series whose swings grow with the level. The leading and trailing days have no centred window, so they read null and report a coverage of zero rather than being emitted as though they were full, which keeps the partial edges honest in a JavaScript data grid an analyst is reading figures from.
Alongside the decomposition, a smoothed level is fitted with Holt’s method, pulling the signal out of the noise without claiming to forecast the future, and the smoothing factors it chose are reported rather than hidden. Beside the grid, the correlogram shows how far back the series leans on itself, each lag with its white-noise band, and the stationarity readout runs an Augmented Dickey-Fuller test and gives a plain verdict on whether the series reverts to a level or wanders. The p-value it reports is stamped approximate, because it is interpolated from published tables.
How do you decompose a time series in a data grid?
Add shadow columns for tsTrend, tsSeasonal and tsResidual, each naming the value column, the column to order by, and the period of the cycle. Lattice Grid computes all three over one ordered pass and marks the partial edges at the start and end with a null value and a coverage of zero. Add a tsSmoothed column for an exponentially smoothed level, and read grid.statistics.acf and grid.statistics.adf for the correlogram and the stationarity verdict.