Time Series Decomposition Plot — Chart.js

A time series decomposition plot displays a time series broken down into its constituent components: the original series, trend, seasonal pattern, and residual noise. Each component is shown as a separate subplot stacked vertically, sharing a common time axis. This visualization is essential for understanding the underlying structure of time series data and identifying patterns that may not be visible in the raw series.

Time Series Decomposition Plot rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// timeseries-decomposition: Time Series Decomposition Plot
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-09

const t = window.ANYPLOT_TOKENS;

// --- Data: monthly retail sales, 2018-2025 (in-memory, deterministic) ------
const N_MONTHS = 96;
const START_YEAR = 2018;
const labels = Array.from({ length: N_MONTHS }, (_, i) => {
  const year = START_YEAR + Math.floor(i / 12);
  const month = (i % 12) + 1;
  return `${year}-${String(month).padStart(2, "0")}`;
});

// Fixed-seed LCG -> Box-Muller gaussian (browser has no seeded RNG)
let lcgSeed = 42;
function uniformRandom() {
  lcgSeed = (lcgSeed * 1103515245 + 12345) % 2147483648;
  return lcgSeed / 2147483648;
}
function gaussianNoise(std) {
  const u1 = Math.max(uniformRandom(), 1e-9);
  const u2 = uniformRandom();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2) * std;
}

function hexToRgba(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

// Underlying growth (steady) + holiday-season seasonality (Jan=0 .. Dec=11)
const GROWTH_BASE = 120;
const GROWTH_SLOPE = 1.15;
const SEASONAL_PATTERN = [-9, -6, -2, 1, 3, 5, 6, 4, 2, 4, 11, 18];
const NOISE_STD = 3.5;

const salesKUsd = Array.from({ length: N_MONTHS }, (_, i) => {
  const growth = GROWTH_BASE + GROWTH_SLOPE * i;
  const seasonal = SEASONAL_PATTERN[i % 12];
  return growth + seasonal + gaussianNoise(NOISE_STD);
});

// --- Classical additive decomposition (centered 2x12 moving average) -------
const HALF_PERIOD = 6;
const trend = new Array(N_MONTHS).fill(null);
for (let i = HALF_PERIOD; i < N_MONTHS - HALF_PERIOD; i++) {
  let sum = 0.5 * salesKUsd[i - HALF_PERIOD] + 0.5 * salesKUsd[i + HALF_PERIOD];
  for (let k = -(HALF_PERIOD - 1); k <= HALF_PERIOD - 1; k++) sum += salesKUsd[i + k];
  trend[i] = sum / 12;
}

const detrended = salesKUsd.map((v, i) => (trend[i] === null ? null : v - trend[i]));
const seasonalIndex = Array.from({ length: 12 }, (_, m) => {
  const vals = detrended.filter((v, i) => i % 12 === m && v !== null);
  return vals.reduce((a, b) => a + b, 0) / vals.length;
});
const seasonalMean = seasonalIndex.reduce((a, b) => a + b, 0) / 12;
const centeredSeasonalIndex = seasonalIndex.map((v) => v - seasonalMean);
const seasonal = Array.from({ length: N_MONTHS }, (_, i) => centeredSeasonalIndex[i % 12]);

const residual = salesKUsd.map((v, i) => (trend[i] === null ? null : v - trend[i] - seasonal[i]));
const zeroLine = new Array(N_MONTHS).fill(0);

// --- Layout: four stacked panels sharing one time axis ----------------------
const container = document.getElementById("container");
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.boxSizing = "border-box";
container.style.padding = "10px 22px 4px";
container.style.backgroundColor = t.pageBg;

const PANELS = [
  { key: "original", title: "Original", axisLabel: "Sales ($k)", data: salesKUsd, kind: "line", showMainTitle: true, flex: 1.2 },
  { key: "trend", title: "Trend", axisLabel: "Sales ($k)", data: trend, kind: "line", showMainTitle: false, flex: 1 },
  { key: "seasonal", title: "Seasonal", axisLabel: "Effect ($k)", data: seasonal, kind: "line", showMainTitle: false, flex: 1 },
  { key: "residual", title: "Residual", axisLabel: "Sales ($k)", data: residual, kind: "points", showMainTitle: false, flex: 1 },
];

PANELS.forEach((panel, idx) => {
  const row = document.createElement("div");
  row.style.flex = `${panel.flex} 1 0`;
  row.style.minHeight = "0";
  row.style.position = "relative";
  row.style.borderBottom = idx < PANELS.length - 1 ? `1px solid ${t.grid}` : "none";
  row.style.paddingBottom = idx < PANELS.length - 1 ? "4px" : "0";
  container.appendChild(row);

  const canvas = document.createElement("canvas");
  row.appendChild(canvas);

  const isBottom = idx === PANELS.length - 1;
  const isOriginal = panel.key === "original";
  const datasets = [
    {
      label: panel.title,
      data: panel.data,
      borderColor: t.palette[0],
      backgroundColor: isOriginal
        ? (context) => {
            const { chartArea, ctx } = context.chart;
            if (!chartArea) return hexToRgba(t.palette[0], 0.2);
            const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
            gradient.addColorStop(0, hexToRgba(t.palette[0], 0.3));
            gradient.addColorStop(1, hexToRgba(t.palette[0], 0.02));
            return gradient;
          }
        : t.palette[0],
      borderWidth: panel.kind === "line" ? 3 : 0,
      showLine: panel.kind === "line",
      pointRadius: panel.kind === "line" ? 0 : 4,
      pointHoverRadius: 0,
      spanGaps: false,
      tension: 0.15,
      fill: isOriginal,
    },
  ];
  if (panel.key === "residual") {
    datasets.push({
      label: "Zero reference",
      data: zeroLine,
      borderColor: t.ink,
      borderWidth: 1.5,
      borderDash: [6, 5],
      pointRadius: 0,
      showLine: true,
    });
  }

  new Chart(canvas, {
    type: "line",
    data: { labels, datasets },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      animation: false,
      plugins: {
        title: {
          display: panel.showMainTitle,
          text: "timeseries-decomposition · javascript · chartjs · anyplot.ai",
          color: t.ink,
          font: { size: 22, weight: "500" },
          padding: { bottom: 8 },
        },
        subtitle: {
          display: true,
          text: panel.title,
          color: t.ink,
          align: "start",
          font: { size: 19, weight: "600" },
          padding: { bottom: 6 },
        },
        legend: { display: false },
      },
      scales: {
        x: {
          ticks: {
            display: isBottom,
            color: t.inkSoft,
            font: { size: 15 },
            maxRotation: 0,
            autoSkip: true,
            maxTicksLimit: 12,
          },
          grid: { color: t.grid, drawTicks: false },
          title: { display: isBottom, text: "Month", color: t.ink, font: { size: 17 } },
        },
        y: {
          ticks: { color: t.inkSoft, font: { size: 15 } },
          grid: { display: false },
          title: { display: true, text: panel.axisLabel, color: t.ink, font: { size: 17 } },
        },
      },
    },
  });
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/timeseries-decomposition/chartjs/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "timeseries-decomposition",
  "language": "javascript",
  "library": "chartjs",
  "page": "https://anyplot.ai/timeseries-decomposition/javascript/chartjs",
  "hub": "https://anyplot.ai/timeseries-decomposition",
  "code_json": "https://api.anyplot.ai/specs/timeseries-decomposition/chartjs/code",
  "spec_json": "https://api.anyplot.ai/specs/timeseries-decomposition",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/chartjs/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/chartjs/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/chartjs/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/chartjs/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Time Series Decomposition Plot on anyplot.ai.

Other implementations