Lattice Grid Buy a licence

demo D246

Seasonality and smoothing

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

Building…
Loading a live grid…

The configuration

'seasonality-and-smoothing': () => ({
  rows: [], config: {},
  foot: [
    'trend, weekly season and residual as shadow columns, period 7',
    'the first and last few days are partial edges, reported as null with coverage 0',
    'the correlogram and the stationarity verdict read the same ordered series',
  ],
  mount: (el: HTMLElement, LG: any) => {
    const rnd = seeded(20260907);
    const gauss = () => Math.sqrt(-2 * Math.log(Math.max(rnd(), 1e-9))) * Math.cos(2 * Math.PI * rnd());
    const start = Date.UTC(2026, 0, 1);
    const weekly = [4, 10, 13, 7, -2, -16, -16];
    const rows: any[] = Array.from({ length: 140 }, (_, i) => {
      const date = new Date(start + i * 86400000).toISOString().slice(0, 10);
      const sales = Math.round(120 + i * 0.7 + weekly[i % 7] + gauss() * 4);
      return { id: 'd' + i, day: date, sales };
    });
    const adfEl = document.createElement('div');
    adfEl.style.cssText = 'border:1px solid var(--rule);border-radius:11px;padding:12px 14px;background:var(--paper);margin-bottom:12px';
    const acfEl = document.createElement('div');
    acfEl.id = 'ts-acf';
    acfEl.style.cssText = 'height:220px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0;margin-bottom:12px';
    const gridEl = document.createElement('div');
    gridEl.style.cssText = 'flex:1;min-height:0';
    el.append(adfEl, acfEl, gridEl);
    const grid = LG.createGrid(gridEl, {
      rowKey: 'id', theme: 'light',
      toolPanel: { side: 'left', panels: ['columns', 'statistics'] },
      columns: [
        { field: 'day', title: 'Day', type: 'dateString', layout: { width: 130, pin: 'start' } },
        { field: 'sales', title: 'Sales', type: 'number', total: 'avg' },
        { 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' } },
      ],
      rows,
    });
    const num = (v: any, d = 3) => (typeof v === 'number' ? v.toFixed(d) : String(v));
    try {
      const adf = grid.statistics.adf({ of: 'sales', orderBy: 'day' });
      adfEl.innerHTML =
        '<div style="font:600 13px ui-monospace,monospace">grid.statistics.adf</div>' +
        '<div style="color:var(--ink-2);font-size:12.5px">verdict <strong>' + (adf?.verdict ?? 'n/a') + '</strong>' +
        ' · statistic ' + num(adf?.statistic) + ' · lag ' + (adf?.usedLag ?? 'n/a') + '</div>';
    } catch (err) {
      console.error('[seasonality-and-smoothing] adf', err);
    }
    let chart: any;
    loadCharts()
      .then(({ createChart }: any) => {
        const res = grid.statistics.acf({ of: 'sales', orderBy: 'day', maxlag: 21 });
        const acf = res?.acf ?? [];
        const bounds = res?.bounds ?? { upper: 0, lower: 0 };
        chart = createChart({
          grid, container: acfEl, type: 'bar',
          points: acf.map((v: number, lag: number) => ({ x: lag, y: v })),
          reference: [{ value: bounds.upper }, { value: bounds.lower }, { value: 0 }],
          title: 'Autocorrelation by lag, with the white-noise band',
        });
      })
      .catch((err) => console.error('[seasonality-and-smoothing] acf', err));
    return () => { chart?.destroy?.(); grid?.destroy?.(); };
  },
})

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.