{"spec_id":"line-confidence","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-confidence: Line Plot with Confidence Interval\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(tibble)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens ------------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND <- IMPRINT_PALETTE[1]\n\n# --- Data ---------------------------------------------------------------------\n# 90-day daily-active-user forecast: growth trend plus weekly seasonality,\n# with a 95% prediction interval that widens with the forecast horizon\n# (sqrt-of-horizon growth, the standard random-walk-forecast uncertainty shape).\nhorizon_days <- 90\nhorizon <- seq_len(horizon_days)\nforecast_dates <- as.Date(\"2026-09-05\") + horizon\n\ntrend <- 48000 + 180 * horizon\nseasonality <- 1400 * sin(2 * pi * horizon / 7)\nnoise <- rnorm(horizon_days, mean = 0, sd = 250)\ndau_forecast <- trend + seasonality + noise\n\nstandard_error <- 300 + 55 * sqrt(horizon)\ndau_lower <- dau_forecast - 1.96 * standard_error\ndau_upper <- dau_forecast + 1.96 * standard_error\n\ndf <- tibble(\n  date  = forecast_dates,\n  dau   = dau_forecast,\n  lower = dau_lower,\n  upper = dau_upper\n)\n\n# --- Narrative anchors ------------------------------------------------------\n# Weekly seasonality peak (first cycle) - gives viewers a concrete landmark\n# for the sawtooth pattern instead of leaving it purely implicit. The label\n# sits just above the peak's own ribbon (not the chart's global max) so it\n# stays visually anchored to the point it describes.\npeak_row  <- df[which.max(df$dau[1:14]), ]\ny_span    <- diff(range(c(df$lower, df$upper)))\npeak_label_y <- peak_row$upper + y_span * 0.035\n\n# Final-horizon interval width - turns \"the band widens\" into a concrete\n# number, anchoring the growing-uncertainty story at the point it matters most.\nlast_row   <- df[horizon_days, ]\nhalf_width <- (last_row$upper - last_row$lower) / 2\n\n# --- Plot -----------------------------------------------------------------------\np <- ggplot(df, aes(x = date)) +\n  geom_ribbon(aes(ymin = lower, ymax = upper, fill = \"95% prediction interval\"),\n              alpha = 0.25) +\n  geom_line(aes(y = dau, color = \"Forecast mean\"), linewidth = 1.1) +\n  geom_vline(xintercept = peak_row$date, linetype = \"dashed\",\n             color = INK_SOFT, linewidth = 0.4) +\n  annotate(\"text\", x = peak_row$date, y = peak_label_y, label = \"Weekly peak\",\n           hjust = -0.1, vjust = 0, size = 2.6, color = INK_SOFT) +\n  geom_segment(data = last_row,\n               aes(x = date, xend = date, y = lower, yend = upper),\n               inherit.aes = FALSE, color = INK, linewidth = 0.5,\n               arrow = grid::arrow(ends = \"both\", length = grid::unit(0.05, \"in\"))) +\n  annotate(\"text\", x = last_row$date, y = (last_row$lower + last_row$upper) / 2,\n           label = sprintf(\"95%% CI: ±%s\", comma(round(half_width))),\n           hjust = 1.1, vjust = 0.5, size = 2.6, color = INK, fontface = \"italic\") +\n  scale_fill_manual(name = NULL, values = c(\"95% prediction interval\" = BRAND)) +\n  scale_color_manual(name = NULL, values = c(\"Forecast mean\" = BRAND)) +\n  scale_x_date(expand = expansion(mult = c(0.01, 0.05))) +\n  scale_y_continuous(labels = label_comma()) +\n  coord_cartesian(clip = \"off\") +\n  labs(\n    title = \"line-confidence · r · ggplot2 · anyplot.ai\",\n    x = \"Forecast Date\",\n    y = \"Daily Active Users\"\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.x = element_blank(),\n    panel.grid.major.y = element_line(color = INK, linewidth = 0.3),\n    panel.grid.minor   = element_blank(),\n    panel.border       = element_blank(),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.ticks         = element_blank(),\n    axis.line          = element_line(color = INK_SOFT),\n    plot.title         = element_text(color = INK, size = 12),\n    legend.position    = \"top\",\n    legend.background  = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.key         = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.text        = element_text(color = INK_SOFT, size = 8)\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"}