{"spec_id":"line-timeseries-rolling","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-timeseries-rolling: Time Series with Rolling Average Overlay\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\nGRID     <- if (THEME == \"light\") \"#CBC9BE\" else \"#454540\"  # INK blended ~25% into PAGE_BG\n\nIMPRINT_PALETTE <- c(\n  \"#009E73\", # 1 - brand green, always first series (raw data)\n  \"#C475FD\"  # 2 - lavender (rolling average)\n)\n\n# --- Data ----------------------------------------------------------------\nwindow_size <- 14\nn_days <- 180\n\ndates <- seq(as.Date(\"2025-01-01\"), by = \"day\", length.out = n_days)\nseasonal <- 8 * sin(seq(0, 4 * pi, length.out = n_days))\ntrend <- seq(0, 12, length.out = n_days)\nnoise <- rnorm(n_days, mean = 0, sd = 4)\nengagement_score <- 62 + trend + seasonal + noise\n\ndf <- tibble::tibble(date = dates, value = engagement_score) %>%\n  mutate(rolling_avg = as.numeric(stats::filter(value, rep(1 / window_size, window_size), sides = 1)))\n\npeak_row <- df %>% filter(!is.na(rolling_avg)) %>% slice_max(rolling_avg, n = 1)\n\n# --- Plot ------------------------------------------------------------------\ntitle_text <- sprintf(\n  \"%d-Day Rolling Average · line-timeseries-rolling · r · ggplot2 · anyplot.ai\",\n  window_size\n)\ntitle_fontsize <- round(12 * min(1.0, 67 / nchar(title_text)))\n\np <- ggplot(df, aes(x = date)) +\n  geom_line(aes(y = value, color = \"Raw Data\"), linewidth = 0.5, alpha = 0.4) +\n  geom_line(\n    data = df %>% filter(!is.na(rolling_avg)),\n    aes(y = rolling_avg, color = sprintf(\"Rolling Average (%d-day)\", window_size)),\n    linewidth = 1.4\n  ) +\n  geom_point(data = peak_row, aes(y = rolling_avg), color = IMPRINT_PALETTE[2], size = 2.2) +\n  annotate(\n    \"text\",\n    x = peak_row$date, y = peak_row$rolling_avg,\n    label = sprintf(\"Peak: %.1f\", peak_row$rolling_avg),\n    color = INK, size = 3, vjust = -1.2, fontface = \"bold\"\n  ) +\n  scale_color_manual(\n    values = setNames(\n      IMPRINT_PALETTE,\n      c(\"Raw Data\", sprintf(\"Rolling Average (%d-day)\", window_size))\n    ),\n    name = NULL\n  ) +\n  scale_x_date(date_labels = \"%b %Y\", date_breaks = \"1 month\") +\n  labs(\n    title = title_text,\n    x = \"Date\",\n    y = \"Engagement Score\"\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  = element_line(color = GRID, linewidth = 0.4),\n    panel.grid.minor  = element_blank(),\n    axis.title        = element_text(color = INK, size = 10),\n    axis.text         = element_text(color = INK_SOFT, size = 8),\n    axis.text.x       = element_text(angle = 30, hjust = 1),\n    axis.line         = element_blank(),\n    axis.ticks        = element_line(color = INK_SOFT),\n    plot.title        = element_text(color = INK, size = title_fontsize),\n    legend.position   = \"top\",\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.background = element_rect(fill = PAGE_BG, color = NA),\n    legend.key        = element_rect(fill = PAGE_BG, color = NA)\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"}