{"spec_id":"timeseries-forecast-uncertainty","library":"makie","language":"julia","code":"# anyplot.ai\n# timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 85/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Dates\n\nRandom.seed!(42)\n\n# --- Theme tokens -----------------------------------------------------------\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data --------------------------------------------------------------------\n# Monthly sales, 3 years of history plus a 6-month forecast with 80/95% bands.\nn_hist = 36\nn_fcst = 6\nn_total = n_hist + n_fcst\n\nmonths = collect(0:(n_total - 1))\ntrend = 1000.0 .+ 12.0 .* months\nseasonal = 120.0 .* sin.(2π .* months ./ 12)\nnoise = randn(n_total) .* 35.0\nseries = trend .+ seasonal .+ noise\n\nactual = fill(NaN, n_total)\nactual[1:n_hist] .= series[1:n_hist]\n\nforecast = fill(NaN, n_total)\nforecast[n_hist:n_total] .= series[n_hist:n_total]\n\nhorizon = 1:n_fcst\nspread_80 = 40.0 .+ 14.0 .* horizon\nspread_95 = 65.0 .+ 22.0 .* horizon\n\nlower_80 = fill(NaN, n_total)\nupper_80 = fill(NaN, n_total)\nlower_95 = fill(NaN, n_total)\nupper_95 = fill(NaN, n_total)\nlower_80[n_hist:n_total] .= [series[n_hist]; series[(n_hist + 1):n_total] .- spread_80]\nupper_80[n_hist:n_total] .= [series[n_hist]; series[(n_hist + 1):n_total] .+ spread_80]\nlower_95[n_hist:n_total] .= [series[n_hist]; series[(n_hist + 1):n_total] .- spread_95]\nupper_95[n_hist:n_total] .= [series[n_hist]; series[(n_hist + 1):n_total] .+ spread_95]\n\nstart_date = Date(2023, 1, 1)\ndates = [start_date + Month(m) for m in months]\nx = Float64.(months)\ndate_labels = Dict(m => Dates.format(start_date + Month(m), \"yyyy-mm\") for m in months)\ntick_positions = collect(0:6:(n_total - 1))\ntick_labels = [date_labels[m] for m in tick_positions]\n\n# --- Plot ---------------------------------------------------------------------\ntitle_str = \"timeseries-forecast-uncertainty · julia · makie · anyplot.ai\"\n\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = title_str,\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Month\",\n    ylabel             = \"Sales (units)\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n    xticks             = (tick_positions, tick_labels),\n    xticklabelrotation = π / 6,\n)\n\n# Forecast start marker\nvlines!(ax, [Float64(n_hist - 1)]; color = INK_SOFT, linestyle = :dot, linewidth = 1.5)\n\n# 95% band (lighter), then 80% band (darker) nested on top\nband!(ax, x, lower_95, upper_95; color = (IMPRINT_PALETTE[3], 0.20), label = \"95% interval\")\nband!(ax, x, lower_80, upper_80; color = (IMPRINT_PALETTE[3], 0.38), label = \"80% interval\")\n\n# Thin edge strokes distinguish the 80%/95% band boundaries from each other\nlines!(ax, x, lower_95; color = (IMPRINT_PALETTE[3], 0.45), linewidth = 1, linestyle = :dash)\nlines!(ax, x, upper_95; color = (IMPRINT_PALETTE[3], 0.45), linewidth = 1, linestyle = :dash)\nlines!(ax, x, lower_80; color = (IMPRINT_PALETTE[3], 0.75), linewidth = 1)\nlines!(ax, x, upper_80; color = (IMPRINT_PALETTE[3], 0.75), linewidth = 1)\n\nlines!(ax, x, actual; color = IMPRINT_PALETTE[1], linewidth = 3, label = \"Historical\")\nlines!(ax, x, forecast; color = IMPRINT_PALETTE[3], linewidth = 3, linestyle = :dash, label = \"Forecast\")\n\n# Callout labeling the historical/forecast transition point\ny_max = maximum(filter(!isnan, vcat(actual, forecast, upper_95)))\ntext!(\n    ax, Float64(n_hist - 1), y_max;\n    text     = \"Forecast start\",\n    color    = INK_SOFT,\n    fontsize = 11,\n    align    = (:left, :top),\n    offset   = (6, -2),\n)\n\naxislegend(ax; position = :lt, framevisible = false, labelcolor = INK_SOFT, labelsize = 12)\n\n# --- Save ----------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}