{"spec_id":"timeseries-decomposition","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' timeseries-decomposition: Time Series Decomposition Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n# ggplot2 has no grid-alpha knob, so blend INK toward PAGE_BG by hand for a\n# genuinely faint (~18%) gridline color instead of full-strength INK.\nGRID_COLOR  <- colorRampPalette(c(PAGE_BG, INK))(100)[18]\n\n# Imprint palette (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE <- c(\n  \"#009E73\", # 1 - brand green (Original)\n  \"#C475FD\", # 2 - lavender (Trend)\n  \"#4467A3\", # 3 - blue (Seasonal)\n  \"#BD8233\"  # 4 - ochre (unused)\n)\n\n# --- Data: monthly retail sales over 10 years, additive decomposition --------\nn_years   <- 10\nn_months  <- n_years * 12\nmonth_idx <- 0:(n_months - 1)\n\ntrend_component    <- 520 + 7.5 * month_idx\nseasonal_component <- 65 * sin(2 * pi * month_idx / 12) + 30 * cos(2 * pi * month_idx / 6)\nnoise               <- rnorm(n_months, mean = 0, sd = 22)\nretail_sales        <- trend_component + seasonal_component + noise\n\ndates <- seq(as.Date(\"2015-01-01\"), by = \"month\", length.out = n_months)\nsales_ts <- ts(retail_sales, start = c(2015, 1), frequency = 12)\ndecomp <- decompose(sales_ts, type = \"additive\")\n\ncomponent_levels <- c(\"Original\", \"Trend\", \"Seasonal\", \"Residual\")\n\ndf <- tibble(\n  date     = dates,\n  Original = as.numeric(decomp$x),\n  Trend    = as.numeric(decomp$trend),\n  Seasonal = as.numeric(decomp$seasonal),\n  Residual = as.numeric(decomp$random)\n) %>%\n  pivot_longer(cols = -date, names_to = \"component\", values_to = \"value\") %>%\n  mutate(component = factor(component, levels = component_levels))\n\ncomponent_colors <- c(\n  \"Original\" = IMPRINT_PALETTE[1],\n  \"Trend\"    = IMPRINT_PALETTE[2],\n  \"Seasonal\" = IMPRINT_PALETTE[3],\n  \"Residual\" = INK_MUTED\n)\n\nzero_ref <- tibble(\n  component = factor(\"Residual\", levels = component_levels),\n  yint      = 0\n)\n\n# --- Title (fontsize scaled to length; baseline adjusted for the narrower\n#     6in square canvas vs. the 8in landscape canvas the 67-char/12pt\n#     baseline was calibrated against, with an extra 0.85 trim so the title\n#     sits comfortably inside the panel instead of pressing against it) ----\ntitle_text        <- \"timeseries-decomposition · r · ggplot2 · anyplot.ai\"\ntitle_len         <- nchar(title_text)\nsquare_baseline   <- 67 * (6 / 8) * 0.85\ntitle_fontsize    <- if (title_len > square_baseline) {\n  round(12 * square_baseline / title_len)\n} else {\n  12\n}\ntitle_fontsize <- max(title_fontsize, 8)\n\n# --- Plot -----------------------------------------------------------------------\np <- ggplot(df, aes(x = date, y = value, color = component)) +\n  geom_line(\n    data = filter(df, component %in% c(\"Original\", \"Trend\", \"Seasonal\")),\n    linewidth = 1.0\n  ) +\n  geom_hline(\n    data = zero_ref, aes(yintercept = yint),\n    color = INK_SOFT, linewidth = 0.4, linetype = \"dashed\"\n  ) +\n  geom_point(\n    data = filter(df, component == \"Residual\"),\n    size = 2.1, alpha = 0.75\n  ) +\n  facet_wrap(~component, ncol = 1, scales = \"free_y\") +\n  scale_color_manual(values = component_colors, guide = \"none\") +\n  scale_x_date(date_breaks = \"2 years\", date_labels = \"%Y\") +\n  labs(\n    title = title_text,\n    x     = \"Date\",\n    y     = \"Retail Sales (thousands $)\"\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.minor    = element_blank(),\n    panel.grid.major.y  = element_line(color = GRID_COLOR, linewidth = 0.25),\n    panel.spacing       = unit(1.1, \"lines\"),\n    strip.background    = element_rect(fill = ELEVATED_BG, color = NA),\n    strip.text          = element_text(color = INK, size = 10, face = \"bold\"),\n    axis.title          = element_text(color = INK, size = 10),\n    axis.text           = element_text(color = INK_SOFT, size = 8),\n    axis.line           = element_line(color = INK_SOFT),\n    plot.title          = element_text(color = INK, size = title_fontsize, face = \"bold\"),\n    legend.position      = \"none\"\n  )\n\n# --- Save -----------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}