Time Series Decomposition Plot — MUI X Charts

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// timeseries-decomposition: Time Series Decomposition Plot
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-09
import { LineChart } from "@mui/x-charts/LineChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { Box, Typography } from "@mui/material";

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// Monthly retail sales revenue ($ thousands), 10 years — additive decomposition:
// original = trend + seasonal + residual
const MONTHS = 120;

// Fixed-seed LCG — the browser has no seeded Math.random
let seed = 42;
function nextRandom() {
  seed = (seed * 1103515245 + 12345) % 2147483648;
  return seed / 2147483648;
}

// Jan..Dec seasonal index: winter dip, Nov/Dec holiday spike, sums to 0
const SEASONAL_INDEX = [-22, -18, -10, -4, 2, 6, 8, 4, -2, -6, 14, 28];

const dates = [];
const trendSeries = [];
const seasonalSeries = [];
const residualSeries = [];
const originalSeries = [];

for (let i = 0; i < MONTHS; i += 1) {
  const trendValue = 240 + 1.9 * i + 0.006 * i * i;
  const seasonalValue = SEASONAL_INDEX[i % 12];
  const residualValue = (nextRandom() - 0.5) * 24;

  dates.push(new Date(2015, i, 1));
  trendSeries.push(trendValue);
  seasonalSeries.push(seasonalValue);
  residualSeries.push(residualValue);
  originalSeries.push(trendValue + seasonalValue + residualValue);
}

const formatMonth = (date) => date.toLocaleDateString("en-US", { month: "short", year: "numeric" });

// Canonical Imprint 1->N order, top to bottom: Original, Trend, Seasonal,
// Residual each take the next palette slot. This also keeps every series a
// fixed hex that stays identical across themes (no theme-adaptive tokens on
// data colors).
const PANELS = [
  { label: "Original", data: originalSeries, color: t.palette[0], zeroLine: false },
  { label: "Trend", data: trendSeries, color: t.palette[1], zeroLine: false },
  { label: "Seasonal", data: seasonalSeries, color: t.palette[2], zeroLine: true },
  { label: "Residual", data: residualSeries, color: t.palette[3], zeroLine: true },
];

const TITLE = "timeseries-decomposition · javascript · muix · anyplot.ai";
const TITLE_HEIGHT = 46;

// --- Chart (default-exported component — the harness mounts it) ------------
export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const panelHeight = (height - TITLE_HEIGHT) / PANELS.length;

  return (
    <Box sx={{ width, height, display: "flex", flexDirection: "column" }}>
      <Typography
        sx={{
          height: TITLE_HEIGHT,
          lineHeight: `${TITLE_HEIGHT}px`,
          pl: 1,
          color: t.ink,
          fontSize: 22,
          fontWeight: 500,
        }}
      >
        {TITLE}
      </Typography>
      {PANELS.map((panel, index) => {
        const isLast = index === PANELS.length - 1;
        return (
          <Box key={panel.label} sx={{ position: "relative" }}>
            <Typography
              sx={{
                position: "absolute",
                top: 6,
                left: 96,
                fontSize: 15,
                fontWeight: 600,
                color: t.ink,
                pointerEvents: "none",
              }}
            >
              {panel.label}
            </Typography>
            {/* Native yAxis.label collides with tick numbers in these short
                panels regardless of margin, so the units label is drawn by
                hand instead, spanning the plot area (excludes the x-axis
                band at the bottom). */}
            <Box
              sx={{
                position: "absolute",
                left: 2,
                top: 0,
                bottom: isLast ? 56 : 8,
                width: 16,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                pointerEvents: "none",
              }}
            >
              <Typography
                sx={{
                  writingMode: "vertical-rl",
                  transform: "rotate(180deg)",
                  fontSize: 13,
                  color: t.inkSoft,
                  whiteSpace: "nowrap",
                }}
              >
                $k
              </Typography>
            </Box>
            <LineChart
              width={width}
              height={panelHeight}
              skipAnimation
              series={[
                {
                  data: panel.data,
                  label: panel.label,
                  color: panel.color,
                  showMark: false,
                  curve: "monotoneX",
                },
              ]}
              xAxis={[
                {
                  data: dates,
                  scaleType: "time",
                  valueFormatter: isLast ? formatMonth : () => "",
                },
              ]}
              margin={{ left: 88, right: 30, top: 14, bottom: isLast ? 56 : 8 }}
              grid={{ horizontal: true }}
              slotProps={{ legend: { hidden: true } }}
            >
              {panel.zeroLine ? (
                <ChartsReferenceLine
                  y={0}
                  lineStyle={{ stroke: t.inkSoft, strokeDasharray: "4 4", strokeOpacity: 0.6 }}
                />
              ) : null}
            </LineChart>
          </Box>
        );
      })}
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/timeseries-decomposition/muix/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": "muix",
  "page": "https://anyplot.ai/timeseries-decomposition/javascript/muix",
  "hub": "https://anyplot.ai/timeseries-decomposition",
  "code_json": "https://api.anyplot.ai/specs/timeseries-decomposition/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/timeseries-decomposition",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/javascript/muix/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