Time Series Decomposition Plot — Makie.jl

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 Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# timeseries-decomposition: Time Series Decomposition Plot
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 88/100 | Created: 2026-09-09

using CairoMakie
using Colors
using Random
using Statistics
using Dates

Random.seed!(42)

# --- Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome") --
THEME     = get(ENV, "ANYPLOT_THEME", "light")
PAGE_BG   = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
INK       = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
INK_SOFT  = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
INK_MUTED = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"

# Imprint palette (see prompts/default-style-guide.md "Categorical Palette")
IMPRINT_PALETTE = [
    colorant"#009E73",  # 1 — brand green (Original)
    colorant"#C475FD",  # 2 — lavender (Trend)
    colorant"#4467A3",  # 3 — blue (Seasonal)
]

# --- Data: monthly retail sales over 9 years -----------------------------
n_years  = 9
n_months = n_years * 12
period   = 12
dates    = [Date(2019, 1, 1) + Month(i) for i in 0:(n_months - 1)]
xs       = collect(1:n_months)

trend_true    = 240.0 .+ 3.4 .* xs
seasonal_true = 42.0 .* sin.(2π .* xs ./ period) .+ 14.0 .* sin.(4π .* xs ./ period)
noise         = 11.0 .* randn(n_months)
sales         = trend_true .+ seasonal_true .+ noise

# --- Decomposition: classical additive, centered 2xN moving average -------
half  = period ÷ 2
trend = fill(NaN, n_months)
for i in (half + 1):(n_months - half)
    window   = sales[(i - half):(i + half)]
    trend[i] = (sum(window) - 0.5 * window[1] - 0.5 * window[end]) / period
end

detrended    = sales .- trend
seasonal_avg = zeros(period)
for m in 1:period
    vals = [detrended[i] for i in 1:n_months if !isnan(detrended[i]) && ((i - 1) % period + 1) == m]
    seasonal_avg[m] = mean(vals)
end
seasonal_avg .-= mean(seasonal_avg)
seasonal = [seasonal_avg[(i - 1) % period + 1] for i in 1:n_months]
residual = sales .- trend .- seasonal
valid    = .!isnan.(residual)

# --- Title (fontsize scaled per prompts/plot-generator.md formula) --------
title_text     = "Monthly Retail Sales · timeseries-decomposition · julia · makie · anyplot.ai"
title_fontsize = length(title_text) > 67 ? round(Int, 23 * 67 / length(title_text)) : 23

# --- Figure -----------------------------------------------------------------
fig = Figure(
    resolution      = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

Label(fig[0, 0:1], title_text; fontsize = title_fontsize, color = INK, font = :bold)
Label(fig[1:4, 0], "Sales (thousand USD)"; rotation = pi / 2, color = INK_SOFT,
      fontsize = 13, tellheight = false)

year_ticks  = collect(1:12:n_months)
year_labels = string.(year.(dates[year_ticks]))
grid_light  = RGBAf(INK.r, INK.g, INK.b, 0.12)

ax_original = Axis(
    fig[1, 1];
    title = "Original", titlealign = :left, titlesize = 16, titlecolor = INK,
    backgroundcolor = PAGE_BG,
    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,
    topspinevisible = false, rightspinevisible = false,
    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,
    xgridcolor = grid_light, ygridcolor = grid_light,
    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,
)
ax_trend = Axis(
    fig[2, 1];
    title = "Trend", titlealign = :left, titlesize = 16, titlecolor = INK,
    backgroundcolor = PAGE_BG,
    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,
    topspinevisible = false, rightspinevisible = false,
    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,
    xgridcolor = grid_light, ygridcolor = grid_light,
    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,
)
ax_seasonal = Axis(
    fig[3, 1];
    title = "Seasonal", titlealign = :left, titlesize = 16, titlecolor = INK,
    backgroundcolor = PAGE_BG,
    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,
    topspinevisible = false, rightspinevisible = false,
    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,
    xgridcolor = grid_light, ygridcolor = grid_light,
    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,
)
ax_residual = Axis(
    fig[4, 1];
    title = "Residual", titlealign = :left, titlesize = 16, titlecolor = INK,
    xlabel = "Date", xlabelcolor = INK,
    backgroundcolor = PAGE_BG,
    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,
    topspinevisible = false, rightspinevisible = false,
    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,
    xgridcolor = grid_light, ygridcolor = grid_light,
    xticks = (year_ticks, year_labels), xticklabelsvisible = true, xticksvisible = true,
)

lines!(ax_original, xs, sales; color = IMPRINT_PALETTE[1], linewidth = 2.5)
lines!(ax_trend, xs, trend; color = IMPRINT_PALETTE[2], linewidth = 2.5)
lines!(ax_seasonal, xs, seasonal; color = IMPRINT_PALETTE[3], linewidth = 2.5)

hlines!(ax_residual, [0.0]; color = INK_SOFT, linewidth = 1, linestyle = :dash)
for (xi, yi) in zip(xs[valid], residual[valid])
    lines!(ax_residual, [xi, xi], [0.0, yi]; color = INK_MUTED, linewidth = 1.3)
end
scatter!(ax_residual, xs[valid], residual[valid]; color = INK_MUTED, markersize = 7, strokewidth = 0)

linkxaxes!(ax_original, ax_trend, ax_seasonal, ax_residual)
rowgap!(fig.layout, 8)
colgap!(fig.layout, 10)

# --- Save -------------------------------------------------------------------
save("plot-$(THEME).png", fig; px_per_unit = 2)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/timeseries-decomposition/makie/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": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/timeseries-decomposition/julia/makie",
  "hub": "https://anyplot.ai/timeseries-decomposition",
  "code_json": "https://api.anyplot.ai/specs/timeseries-decomposition/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/timeseries-decomposition",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/julia/makie/plot-dark.png",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Time Series Decomposition Plot on anyplot.ai.

Other implementations