{"spec_id":"timeseries-decomposition","library":"makie","language":"julia","code":"# anyplot.ai\n# timeseries-decomposition: Time Series Decomposition Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-09-09\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Statistics\nusing Dates\n\nRandom.seed!(42)\n\n# --- Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\") --\nTHEME     = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG   = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nINK       = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT  = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nINK_MUTED = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\n# Imprint palette (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green (Original)\n    colorant\"#C475FD\",  # 2 — lavender (Trend)\n    colorant\"#4467A3\",  # 3 — blue (Seasonal)\n]\n\n# --- Data: monthly retail sales over 9 years -----------------------------\nn_years  = 9\nn_months = n_years * 12\nperiod   = 12\ndates    = [Date(2019, 1, 1) + Month(i) for i in 0:(n_months - 1)]\nxs       = collect(1:n_months)\n\ntrend_true    = 240.0 .+ 3.4 .* xs\nseasonal_true = 42.0 .* sin.(2π .* xs ./ period) .+ 14.0 .* sin.(4π .* xs ./ period)\nnoise         = 11.0 .* randn(n_months)\nsales         = trend_true .+ seasonal_true .+ noise\n\n# --- Decomposition: classical additive, centered 2xN moving average -------\nhalf  = period ÷ 2\ntrend = fill(NaN, n_months)\nfor i in (half + 1):(n_months - half)\n    window   = sales[(i - half):(i + half)]\n    trend[i] = (sum(window) - 0.5 * window[1] - 0.5 * window[end]) / period\nend\n\ndetrended    = sales .- trend\nseasonal_avg = zeros(period)\nfor m in 1:period\n    vals = [detrended[i] for i in 1:n_months if !isnan(detrended[i]) && ((i - 1) % period + 1) == m]\n    seasonal_avg[m] = mean(vals)\nend\nseasonal_avg .-= mean(seasonal_avg)\nseasonal = [seasonal_avg[(i - 1) % period + 1] for i in 1:n_months]\nresidual = sales .- trend .- seasonal\nvalid    = .!isnan.(residual)\n\n# --- Title (fontsize scaled per prompts/plot-generator.md formula) --------\ntitle_text     = \"Monthly Retail Sales · timeseries-decomposition · julia · makie · anyplot.ai\"\ntitle_fontsize = length(title_text) > 67 ? round(Int, 23 * 67 / length(title_text)) : 23\n\n# --- Figure -----------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nLabel(fig[0, 0:1], title_text; fontsize = title_fontsize, color = INK, font = :bold)\nLabel(fig[1:4, 0], \"Sales (thousand USD)\"; rotation = pi / 2, color = INK_SOFT,\n      fontsize = 13, tellheight = false)\n\nyear_ticks  = collect(1:12:n_months)\nyear_labels = string.(year.(dates[year_ticks]))\ngrid_light  = RGBAf(INK.r, INK.g, INK.b, 0.12)\n\nax_original = Axis(\n    fig[1, 1];\n    title = \"Original\", titlealign = :left, titlesize = 16, titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,\n    topspinevisible = false, rightspinevisible = false,\n    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,\n    xgridcolor = grid_light, ygridcolor = grid_light,\n    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,\n)\nax_trend = Axis(\n    fig[2, 1];\n    title = \"Trend\", titlealign = :left, titlesize = 16, titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,\n    topspinevisible = false, rightspinevisible = false,\n    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,\n    xgridcolor = grid_light, ygridcolor = grid_light,\n    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,\n)\nax_seasonal = Axis(\n    fig[3, 1];\n    title = \"Seasonal\", titlealign = :left, titlesize = 16, titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,\n    topspinevisible = false, rightspinevisible = false,\n    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,\n    xgridcolor = grid_light, ygridcolor = grid_light,\n    xticks = (year_ticks, year_labels), xticklabelsvisible = false, xticksvisible = false,\n)\nax_residual = Axis(\n    fig[4, 1];\n    title = \"Residual\", titlealign = :left, titlesize = 16, titlecolor = INK,\n    xlabel = \"Date\", xlabelcolor = INK,\n    backgroundcolor = PAGE_BG,\n    yticklabelcolor = INK_SOFT, xticklabelcolor = INK_SOFT,\n    topspinevisible = false, rightspinevisible = false,\n    leftspinecolor = INK_SOFT, bottomspinecolor = INK_SOFT,\n    xgridcolor = grid_light, ygridcolor = grid_light,\n    xticks = (year_ticks, year_labels), xticklabelsvisible = true, xticksvisible = true,\n)\n\nlines!(ax_original, xs, sales; color = IMPRINT_PALETTE[1], linewidth = 2.5)\nlines!(ax_trend, xs, trend; color = IMPRINT_PALETTE[2], linewidth = 2.5)\nlines!(ax_seasonal, xs, seasonal; color = IMPRINT_PALETTE[3], linewidth = 2.5)\n\nhlines!(ax_residual, [0.0]; color = INK_SOFT, linewidth = 1, linestyle = :dash)\nfor (xi, yi) in zip(xs[valid], residual[valid])\n    lines!(ax_residual, [xi, xi], [0.0, yi]; color = INK_MUTED, linewidth = 1.3)\nend\nscatter!(ax_residual, xs[valid], residual[valid]; color = INK_MUTED, markersize = 7, strokewidth = 0)\n\nlinkxaxes!(ax_original, ax_trend, ax_seasonal, ax_residual)\nrowgap!(fig.layout, 8)\ncolgap!(fig.layout, 10)\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}