{"spec_id":"timeseries-forecast-uncertainty","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Updated: 2026-05-19\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT   <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                 \"#AE3030\", \"#2ABCCD\", \"#954477\")\n# On dark backgrounds, orange at low alpha reads as muddy brown; use higher\n# alpha so the hue stays clearly orange rather than blending to near-black.\nALPHA_95    <- if (THEME == \"light\") 0.10 else 0.30\nALPHA_80    <- if (THEME == \"light\") 0.18 else 0.45\n\n# --- Data generation (monthly sales forecast) --------------------------------\n# Historical: 36 months of actual sales data\ndates_hist <- seq(as.Date(\"2022-01-01\"), by = \"month\", length.out = 36)\nactual_sales <- 50000 + cumsum(rnorm(36, 500, 1000)) +\n                5000 * sin(seq(0, 4 * pi, length.out = 36))\n\n# Forecast: 6 months ahead\ndates_fcst <- seq(dates_hist[length(dates_hist)] + 31, by = \"month\", length.out = 6)\nforecast_values <- tail(actual_sales, 1) + cumsum(rnorm(6, 400, 800))\n\n# Uncertainty widens with forecast horizon (realistic forecast behavior)\nforecast_std <- 2000 * sqrt(seq_len(6))\nforecast_lower_80 <- forecast_values - qnorm(0.9)   * forecast_std\nforecast_upper_80 <- forecast_values + qnorm(0.9)   * forecast_std\nforecast_lower_95 <- forecast_values - qnorm(0.975) * forecast_std\nforecast_upper_95 <- forecast_values + qnorm(0.975) * forecast_std\n\n# Combine into single dataframe\ndf <- tibble::tibble(\n  date     = c(dates_hist, dates_fcst),\n  actual   = c(actual_sales, rep(NA, 6)),\n  forecast = c(rep(NA, 36), forecast_values),\n  lower_80 = c(rep(NA, 36), forecast_lower_80),\n  upper_80 = c(rep(NA, 36), forecast_upper_80),\n  lower_95 = c(rep(NA, 36), forecast_lower_95),\n  upper_95 = c(rep(NA, 36), forecast_upper_95)\n)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(df, aes(x = date)) +\n  # 95% confidence band (lighter shade)\n  geom_ribbon(aes(ymin = lower_95, ymax = upper_95, fill = \"95% CI\"),\n              alpha = ALPHA_95, color = NA) +\n  # 80% confidence band (darker shade)\n  geom_ribbon(aes(ymin = lower_80, ymax = upper_80, fill = \"80% CI\"),\n              alpha = ALPHA_80, color = NA) +\n  # Forecast line (dashed per spec)\n  geom_line(aes(y = forecast, color = \"Forecast\"),\n            linewidth = 1.2, linetype = \"dashed\") +\n  # Historical data line\n  geom_line(aes(y = actual, color = \"Historical\"), linewidth = 1.2) +\n  # Forecast start marker\n  geom_vline(xintercept = dates_hist[36] + 15.5, linetype = \"dashed\",\n             color = INK_SOFT, linewidth = 0.8, alpha = 0.6) +\n  scale_color_manual(\n    values = c(\"Historical\" = IMPRINT[1], \"Forecast\" = IMPRINT[2]),\n    breaks = c(\"Historical\", \"Forecast\")\n  ) +\n  scale_fill_manual(\n    values = c(\"80% CI\" = IMPRINT[2], \"95% CI\" = IMPRINT[2]),\n    breaks = c(\"95% CI\", \"80% CI\")\n  ) +\n  scale_x_date(expand = expansion(mult = c(0.02, 0.05))) +\n  labs(\n    title = \"timeseries-forecast-uncertainty · r · ggplot2 · anyplot.ai\",\n    x     = \"Date\",\n    y     = \"Monthly Sales ($)\",\n    color = NULL,\n    fill  = NULL\n  ) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major.y = element_line(color = INK_SOFT, linewidth = 0.2),\n    panel.grid.major.x = element_blank(),\n    panel.grid.minor   = element_blank(),\n    axis.title        = element_text(color = INK,      size = 12),\n    axis.text         = element_text(color = INK_SOFT, size = 10),\n    plot.title        = element_text(color = INK,      size = 14),\n    legend.position   = \"top\",\n    legend.text       = element_text(color = INK_SOFT, size = 10),\n    legend.background = element_rect(fill = PAGE_BG,   color = NA),\n    legend.spacing.x  = unit(1, \"cm\")\n  )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}