A time series plot that displays historical observed data followed by a forecast projection with confidence intervals or uncertainty bands. The plot clearly distinguishes between the historical period and the forecast period using a vertical line marker, with shaded bands representing different confidence levels (typically 80% and 95%). This visualization is essential for communicating prediction uncertainty in forecasting applications, helping stakeholders understand both the expected values and the range of possible outcomes.

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