{"spec_id":"line-training-load-pmc","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-training-load-pmc: Training Load Performance Management Chart\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 86/100 | Created: 2026-06-13\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nRULE = \"#D5D4C8\" if THEME == \"light\" else \"#2E2E2A\"\n\n# Imprint categorical palette — first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nCOLOR_CTL = IMPRINT_PALETTE[0]  # green — Fitness (first series, always)\nCOLOR_ATL = IMPRINT_PALETTE[1]  # lavender — Fatigue\nCOLOR_TSB_POS = IMPRINT_PALETTE[2]  # blue — positive form / fresh\nCOLOR_TSB_NEG = IMPRINT_PALETTE[4]  # matte red — negative form / fatigued (semantic)\n\n# Data — 180-day periodized training block (Jan–Jun 2024)\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(\"2024-01-01\", periods=n_days)\n\n# Four-week blocks (3 build + 1 recovery), load ramps through month 4, tapers month 5\ntss_list = []\nfor i in range(n_days):\n    week_in_block = (i // 7) % 4\n    day_of_week = i % 7\n    month = min(i // 30, 5)\n    base = [55, 65, 75, 85, 90, 60][month]\n    if week_in_block == 3:\n        base *= 0.55\n    if day_of_week == 0:\n        tss_i = max(0.0, np.random.normal(15, 8))\n    elif day_of_week == 3:\n        tss_i = max(0.0, np.random.normal(base * 0.50, 10))\n    elif day_of_week in [1, 4, 6]:\n        tss_i = max(0.0, np.random.normal(base * 1.35, 18))\n    else:\n        tss_i = max(0.0, np.random.normal(base * 0.75, 12))\n    tss_list.append(round(tss_i, 1))\n\ntss = np.array(tss_list)\n\n# CTL: 42-day EWMA (chronic fitness), ATL: 7-day EWMA (acute fatigue)\nk_ctl = 2 / (42 + 1)\nk_atl = 2 / (7 + 1)\nctl = np.zeros(n_days)\natl = np.zeros(n_days)\nctl[0] = atl[0] = tss[0]\nfor i in range(1, n_days):\n    ctl[i] = tss[i] * k_ctl + ctl[i - 1] * (1 - k_ctl)\n    atl[i] = tss[i] * k_atl + atl[i - 1] * (1 - k_atl)\n\n# TSB (form) = previous-day CTL minus previous-day ATL\ntsb = np.zeros(n_days)\nfor i in range(1, n_days):\n    tsb[i] = ctl[i - 1] - atl[i - 1]\n\ndf = pd.DataFrame(\n    {\n        \"date\": dates,\n        \"tss\": tss,\n        \"ctl\": ctl,\n        \"atl\": atl,\n        \"tsb\": tsb,\n        \"tsb_pos\": np.maximum(tsb, 0.0),\n        \"tsb_neg\": np.minimum(tsb, 0.0),\n        \"zero\": np.zeros(n_days),\n        \"tsb_pos_label\": \"Positive Form (TSB)\",\n        \"tsb_neg_label\": \"Negative Form (TSB)\",\n        \"tss_label\": \"Daily TSS\",\n    }\n)\n\n# Long format for CTL/ATL — drives the color legend via aes(color='label')\ndf_lines = pd.melt(\n    df[[\"date\", \"ctl\", \"atl\"]], id_vars=[\"date\"], value_vars=[\"ctl\", \"atl\"], var_name=\"metric\", value_name=\"value\"\n)\ndf_lines[\"label\"] = df_lines[\"metric\"].map({\"ctl\": \"Fitness (CTL)\", \"atl\": \"Fatigue (ATL)\"})\n\n# Title — 55 chars < 67 baseline, no font scaling needed\ntitle_str = \"line-training-load-pmc · python · letsplot · anyplot.ai\"\n\n# Shared theme (applied to both panels)\nbase_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=RULE, size=0.3),\n    panel_grid_minor=element_line(color=RULE, size=0.2),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    axis_ticks=element_blank(),\n    panel_border=element_blank(),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_blank(),\n)\n\n# Main panel: TSB ribbon (two-toned) + CTL/ATL lines + zero reference\nplot_main = (\n    ggplot()\n    + geom_ribbon(data=df, mapping=aes(x=\"date\", ymin=\"zero\", ymax=\"tsb_pos\", fill=\"tsb_pos_label\"), alpha=0.32)\n    + geom_ribbon(data=df, mapping=aes(x=\"date\", ymin=\"tsb_neg\", ymax=\"zero\", fill=\"tsb_neg_label\"), alpha=0.32)\n    + geom_hline(yintercept=0, color=INK_SOFT, size=0.5, linetype=\"dashed\")\n    + geom_line(data=df_lines, mapping=aes(x=\"date\", y=\"value\", color=\"label\"), size=1.5)\n    + scale_color_manual(values={\"Fitness (CTL)\": COLOR_CTL, \"Fatigue (ATL)\": COLOR_ATL})\n    + scale_fill_manual(values={\"Positive Form (TSB)\": COLOR_TSB_POS, \"Negative Form (TSB)\": COLOR_TSB_NEG})\n    + scale_x_datetime(format=\"%b\")\n    + labs(title=title_str, x=\"\", y=\"Training Load (CTL / ATL / TSB)\")\n    + base_theme\n    + theme(\n        plot_title=element_text(color=INK, size=16),\n        axis_text_x=element_blank(),\n        axis_ticks_x=element_blank(),\n        legend_position=[0.82, 0.85],\n    )\n)\n\n# TSS panel: daily training stress scores as bars\nplot_tss = (\n    ggplot(df, aes(x=\"date\", y=\"tss\"))\n    + geom_bar(stat=\"identity\", mapping=aes(fill=\"tss_label\"), alpha=0.65)\n    + scale_fill_manual(values={\"Daily TSS\": INK_MUTED})\n    + scale_x_datetime(format=\"%b\")\n    + labs(x=\"Month (2024)\", y=\"TSS\")\n    + base_theme\n    + theme(plot_title=element_blank(), legend_position=[0.88, 0.82])\n)\n\n# Combine panels — main 75%, TSS 25%\ncombined = gggrid([plot_main, plot_tss], ncol=1, heights=[3, 1]) + ggsize(800, 450)\n\n# Save PNG and HTML for both themes\nggsave(combined, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(combined, f\"plot-{THEME}.html\", path=\".\")\n"}