Lattice Grid Buy a licence

demo D286

Project a time series forward

Fit a column over the filtered rows and project the months ahead, drawn as its own line carrying on from the last reading, with the fit quality shown beside it

grid.statistics.forecast(col, { by, method, horizon })

Building…
Loading a live grid…

The configuration

'statistics-forecast': () => ({
  rows: [],
  config: {},
  foot: [
    'the grid reads the column over the filtered rows, fits it and projects the months ahead, all in the browser',
    'the projection is drawn as its own line carrying on from the last reading, so where the data ends and the forecast begins is never in doubt',
    'the fit quality is shown beside the chart, so you can see how well the line follows the history before trusting where it points',
    'ask for the forecast the same way you ask for any other figure about a column, off the grid you already have',
  ],
  mount: (el: HTMLElement, LG: any) => {
    const series = Array.from({ length: 24 }, (_, t) => {
      const wave = 14 * Math.sin((t / 6) * Math.PI);
      const jitter = ((t * 37) % 11) - 5;
      return { id: String(t), t, units: Math.round(180 + 7 * t + wave + jitter) };
    });
    const layout = document.createElement('div');
    layout.style.cssText = 'display:grid;grid-template-columns:minmax(0,0.9fr) minmax(0,1.3fr);gap:16px;align-items:start';
    const gridCol = document.createElement('div');
    gridCol.style.minWidth = '0';
    const gridEl = document.createElement('div');
    gridEl.style.cssText = 'height:420px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    gridCol.append(gridTitle('The monthly series'), gridEl);
    const chartCol = document.createElement('div');
    chartCol.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:10px';
    const chartEl = document.createElement('div');
    chartEl.style.cssText = 'height:360px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    const readout = document.createElement('p');
    readout.style.cssText = 'font:12.5px var(--mono,monospace);color:var(--ink-2);margin:0';
    chartCol.append(gridTitle('History, then the projection'), chartEl, readout);
    layout.append(gridCol, chartCol);
    el.append(layout);
    const grid = LG.createGrid(gridEl, {
      rowKey: 'id',
      theme: 'light',
      columns: [
        { field: 't', title: 'Month', type: 'number', layout: { width: 90 } },
        { field: 'units', title: 'Units', type: 'number', total: 'avg' },
      ],
      rows: series,
    });
    let chart: any;
    let chartGrid: any;
    loadCharts()
      .then(({ createChart }) => {
        const horizon = 6;
        const result = grid.statistics.forecast('units', { by: 't', method: 'linear', horizon });
        const last = series[series.length - 1];
        const points: { x: number; y: number; kind: string }[] = series.map((r) => ({ x: r.t, y: r.units, kind: 'Actual' }));
        points.push({ x: last.t, y: last.units, kind: 'Forecast' });
        for (const p of result?.points ?? []) points.push({ x: p.at, y: Math.round(p.mean), kind: 'Forecast' });
        chartGrid = LG.createHeadlessGrid({
          rowKey: (r: any) => `${r.kind}:${r.x}`,
          columns: [
            { field: 'x', title: 'Month', type: 'number' },
            { field: 'y', title: 'Units', type: 'number' },
            { field: 'kind', title: 'Series' },
          ],
          rows: points,
        });
        chart = createChart({
          grid: chartGrid,
          container: chartEl,
          type: 'line',
          x: 'x',
          y: 'y',
          series: 'kind',
          legend: true,
          title: 'Monthly demand, with the months ahead projected',
        });
        const r2 = typeof result?.r2 === 'number' ? result.r2.toFixed(3) : 'n/a';
        const next = result?.points?.[0]?.mean;
        readout.textContent = `linear fit, R² ${r2}` + (next != null ? `; next month about ${Math.round(next)} units` : '');
      })
      .catch((err) => console.error('[statistics-forecast]', err));
    return () => {
      chart?.destroy?.();
      chartGrid?.destroy?.();
      grid?.destroy?.();
    };
  },
})

Where the numbers are heading, from the grid you already have

A column of monthly figures tells you where you have been. The question a room usually asks next is where it is going. This reads the column over the rows currently in the grid, fits it, and projects the months ahead, all in the browser with nothing sent to a server. The projection is drawn as its own line carrying on from the last real reading, so where the history ends and the estimate begins is never in doubt.

You ask for the forecast the same way you ask for any other figure about a column, off the grid you already have. Name the column to project and the column that carries its time axis, choose how far ahead to look, and pick how the line is fitted: a straight least-squares trend for a steady climb, or a seasonal method when the figures rise and fall on a cycle. The fit quality comes back with the result, so you can see how well the line follows the history before you trust where it points.

Because the forecast reads the filtered rows, it follows the grid. Narrow to one region or one product and the projection reshapes to that slice, so a question about part of the data is answered from the part, not the whole. The same grid that holds the numbers is the one that tells you where they are heading.

How do I forecast a column?

Call grid.statistics.forecast with the column to project and an options object: by names the column that orders the time axis, method chooses how the line is fitted, and horizon is how many steps ahead to return. You get back the projected points, each stamped with its position on the time axis and its value, plus the fit quality for a straight-line fit. Draw those points as a second line on a chart, starting from the last real reading so the two meet, and the projection reads as a continuation of the history rather than a separate picture. Because it reads the filtered rows, the forecast updates as you filter the grid.