{"spec_id":"line-training-load-pmc","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-training-load-pmc: Training Load Performance Management Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Created: 2026-06-13\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\"\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\"\nGRID_COLOR  <- adjustcolor(INK, alpha.f = 0.12)\n\n# Imprint palette — hybrid-v3 canonical order\nIMPRINT_PALETTE <- c(\n  \"#009E73\",  # 1 — brand green (CTL / Fitness)\n  \"#C475FD\",  # 2 — lavender (ATL / Fatigue)\n  \"#4467A3\",  # 3 — blue (positive TSB / Fresh)\n  \"#BD8233\",  # 4 — ochre\n  \"#AE3030\"   # 5 — matte red (negative TSB / Fatigued)\n)\n\n# Data — 180-day cyclist build season (Jan–Jul 2024)\nn_days <- 180\ndates  <- seq(as.Date(\"2024-01-15\"), by = \"day\", length.out = n_days)\n\n# Daily TSS with phased training structure\ntss_raw <- numeric(n_days)\nfor (i in seq_len(n_days)) {\n  week <- ceiling(i / 7)\n  if (week %% 4 == 0) {\n    # Recovery week — lower load, more rest days\n    tss_raw[i] <- if (runif(1) < 0.45) 0 else runif(1, 15, 65)\n  } else if (week > 22) {\n    # Taper — reduced volume before target event\n    tss_raw[i] <- if (runif(1) < 0.35) 0 else runif(1, 20, 55)\n  } else {\n    # Build / intensity block — progressive overload\n    max_load    <- min(50 + week * 5, 155)\n    tss_raw[i]  <- if (runif(1) < 0.18) 0 else runif(1, 40, max_load)\n  }\n}\n\n# Compute EWMA: CTL (42-day fitness) and ATL (7-day fatigue)\nctl_k  <- 2 / (42 + 1)\natl_k  <- 2 / (7 + 1)\nctl    <- numeric(n_days)\natl    <- numeric(n_days)\ntsb    <- numeric(n_days)\nctl[1] <- 30\natl[1] <- 30\ntsb[1] <- 0\n\nfor (i in 2:n_days) {\n  ctl[i] <- ctl[i - 1] + ctl_k * (tss_raw[i] - ctl[i - 1])\n  atl[i] <- atl[i - 1] + atl_k * (tss_raw[i] - atl[i - 1])\n  tsb[i] <- ctl[i - 1] - atl[i - 1]\n}\n\n# Scale TSS for display at bottom of chart (0–22 range)\ntss_display <- tss_raw * 22 / max(tss_raw, na.rm = TRUE)\n\n# TSB secondary axis: TSB=0 maps to primary y=60, scale factor=0.6\n# Primary break 30 → TSB -50; break 60 → TSB 0; break 90 → TSB 50\ntsb_center <- 60\ntsb_factor <- 0.6\n\ndf <- data.frame(\n  date         = dates,\n  tss_display  = tss_display,\n  ctl          = ctl,\n  atl          = atl,\n  tsb          = tsb,\n  tsb_pos_ymin = tsb_center,\n  tsb_pos_ymax = tsb_center + pmax(tsb * tsb_factor, 0),\n  tsb_neg_ymin = tsb_center + pmin(tsb * tsb_factor, 0),\n  tsb_neg_ymax = tsb_center\n)\n\ntitle_str <- \"line-training-load-pmc · r · ggplot2 · anyplot.ai\"\n\n# Plot\np <- ggplot(df, aes(x = date)) +\n  # TSB ribbons — two-toned; fill hardcoded so only color drives the single legend\n  geom_ribbon(\n    aes(ymin = tsb_neg_ymin, ymax = tsb_neg_ymax, color = \"Fatigued (TSB < 0)\"),\n    fill = IMPRINT_PALETTE[5], alpha = 0.35, linewidth = 0\n  ) +\n  geom_ribbon(\n    aes(ymin = tsb_pos_ymin, ymax = tsb_pos_ymax, color = \"Fresh (TSB > 0)\"),\n    fill = IMPRINT_PALETTE[3], alpha = 0.35, linewidth = 0\n  ) +\n  # Daily TSS segments — raw load at bottom, mapped to color for legend entry\n  geom_segment(\n    aes(xend = date, y = 0, yend = tss_display, color = \"Daily TSS\"),\n    linewidth = 0.2, alpha = 0.55\n  ) +\n  # ATL line — fatigue, faster-reacting to recent training\n  geom_line(aes(y = atl, color = \"Fatigue (ATL)\"), linewidth = 1.0) +\n  # CTL line — fitness, slowly built over ~42 days\n  geom_line(aes(y = ctl, color = \"Fitness (CTL)\"), linewidth = 1.3) +\n  # TSB = 0 reference: above = fresh, below = fatigued\n  geom_hline(\n    yintercept = tsb_center,\n    color = INK_SOFT, linewidth = 0.55, linetype = \"dashed\"\n  ) +\n  scale_color_manual(\n    name   = \"PMC Metrics\",\n    values = c(\n      \"Fitness (CTL)\"      = IMPRINT_PALETTE[1],\n      \"Fatigue (ATL)\"      = IMPRINT_PALETTE[2],\n      \"Daily TSS\"          = INK_MUTED,\n      \"Fresh (TSB > 0)\"    = IMPRINT_PALETTE[3],\n      \"Fatigued (TSB < 0)\" = IMPRINT_PALETTE[5]\n    ),\n    breaks = c(\"Fitness (CTL)\", \"Fatigue (ATL)\", \"Daily TSS\",\n               \"Fresh (TSB > 0)\", \"Fatigued (TSB < 0)\"),\n    guide = guide_legend(\n      override.aes = list(\n        fill      = c(NA, NA, NA,\n                      adjustcolor(IMPRINT_PALETTE[3], alpha.f = 0.35),\n                      adjustcolor(IMPRINT_PALETTE[5], alpha.f = 0.35)),\n        linewidth = c(1.3, 1.0, 1.0, 0, 0),\n        alpha     = c(1, 1, 0.55, 1, 1)\n      )\n    )\n  ) +\n  scale_x_date(date_breaks = \"1 month\", date_labels = \"%b %Y\") +\n  scale_y_continuous(\n    name   = \"Fitness / Fatigue (TSS points)\",\n    limits = c(0, 125),\n    breaks = c(0, 30, 60, 90, 120),\n    sec.axis = sec_axis(\n      ~ (. - tsb_center) / tsb_factor,\n      name   = \"Form / TSB\",\n      labels = function(x) round(x)\n    )\n  ) +\n  labs(title = title_str, x = NULL) +\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 = GRID_COLOR, linewidth = 0.3),\n    panel.grid.major.x = element_blank(),\n    panel.grid.minor   = element_blank(),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.title.y.right = element_text(color = INK_SOFT, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.text.x        = element_text(angle = 30, hjust = 1),\n    axis.text.y.right  = element_text(color = INK_SOFT, size = 8),\n    axis.line.x        = element_line(color = INK_SOFT, linewidth = 0.5),\n    plot.title         = element_text(color = INK, size = 12, face = \"bold\"),\n    legend.background  = element_rect(fill = ELEVATED_BG, color = INK_SOFT,\n                                      linewidth = 0.3),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 9),\n    legend.position        = \"inside\",\n    legend.position.inside = c(0.08, 0.82),\n    legend.justification   = c(0, 1),\n    legend.key.size    = unit(0.9, \"lines\"),\n    legend.box         = \"vertical\",\n    axis.ticks         = element_blank(),\n    plot.margin        = margin(15, 20, 10, 12)\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"}