{"spec_id":"line-training-load-pmc","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nline-training-load-pmc: Training Load Performance Management Chart\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 90/100 | Created: 2026-06-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the installed matplotlib package\n_here = os.path.dirname(os.path.abspath(__file__))\nif sys.path and sys.path[0] == _here:\n    sys.path.pop(0)\n\nimport matplotlib.dates as mdates\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom matplotlib.patches import Patch\n\n\n# Theme tokens\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\"\n\n# Imprint palette — semantic assignments for endurance-training domain convention\nCOLOR_CTL = \"#4467A3\"  # blue     → Fitness / Chronic Training Load\nCOLOR_ATL = \"#C475FD\"  # lavender → Fatigue / Acute Training Load\nCOLOR_TSB_POS = \"#009E73\"  # green    → positive form (fresh, rested)\nCOLOR_TSB_NEG = \"#AE3030\"  # matte red → negative form (fatigued, overloaded)\n\n# Data — 180-day cycling training block (1 Jan – 29 Jun 2026)\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(\"2026-01-01\", periods=n_days, freq=\"D\")\n\n# Periodized TSS: 3 build weeks + 1 recovery, progressive loading per mesocycle\ntss = np.zeros(n_days)\nfor i in range(n_days):\n    week = i // 7\n    cycle_week = week % 4  # 0,1,2 = build; 3 = recovery\n    dow = i % 7  # 6 = Sunday rest day\n    mesocycle = week // 4  # progressive overload tier (0–5)\n\n    if dow == 6:\n        daily = 0.0\n    elif cycle_week == 3:\n        daily = np.random.uniform(25, 55)\n    else:\n        overload = min(1.0 + mesocycle * 0.10, 1.5)\n        daily = np.random.uniform(60, 130) * overload\n    tss[i] = max(0.0, daily + np.random.normal(0, 6))\n\n# Two 3-day stage races with high-TSS peaks\ntss[40:43] = [145.0, 165.0, 95.0]\ntss[118:121] = [135.0, 155.0, 85.0]\n\n# Taper: last 21 days — progressive TSS reduction for race-day freshness\nfor i in range(n_days - 21, n_days):\n    factor = (i - (n_days - 21)) / 21.0\n    tss[i] = max(0.0, tss[i] * (1.0 - 0.65 * factor))\n\n# EWMA: CTL (42-day), ATL (7-day); TSB = previous-day CTL − ATL\nctl = np.zeros(n_days)\natl = np.zeros(n_days)\ntsb = np.zeros(n_days)\nctl[0] = tss[0] / 42.0\natl[0] = tss[0] / 7.0\n\nfor i in range(1, n_days):\n    tsb[i] = ctl[i - 1] - atl[i - 1]\n    ctl[i] = tss[i] / 42.0 + ctl[i - 1] * (1.0 - 1.0 / 42.0)\n    atl[i] = tss[i] / 7.0 + atl[i - 1] * (1.0 - 1.0 / 7.0)\n\n# Plot — landscape 3200×1800 px\nfig, ax1 = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax1.set_facecolor(PAGE_BG)\n\n# Secondary axis for TSB — rendered behind primary via z-order trick\nax2 = ax1.twinx()\nax2.set_facecolor(PAGE_BG)\nax2.set_zorder(ax1.get_zorder() - 1)\nax1.patch.set_visible(False)\n\n# TSB filled areas on secondary axis\nax2.axhline(0, color=INK_SOFT, linewidth=0.8, alpha=0.5)\nax2.fill_between(dates, tsb, 0, where=(tsb >= 0), color=COLOR_TSB_POS, alpha=0.28, interpolate=True)\nax2.fill_between(dates, tsb, 0, where=(tsb < 0), color=COLOR_TSB_NEG, alpha=0.28, interpolate=True)\n\n# Daily TSS bars on primary axis (subordinate layer — raw input)\nax1.bar(dates, tss, width=0.85, color=INK_MUTED, alpha=0.18, label=\"Daily TSS\", zorder=2)\n\n# CTL and ATL smooth lines on primary axis\nax1.plot(dates, ctl, color=COLOR_CTL, linewidth=2.5, label=\"Fitness (CTL)\", zorder=4)\nax1.plot(dates, atl, color=COLOR_ATL, linewidth=2.0, linestyle=\"--\", label=\"Fatigue (ATL)\", zorder=4)\n\n# Event annotations — race blocks and taper onset\ntaper_start_date = dates[n_days - 21]\nax1.axvspan(taper_start_date, dates[-1], alpha=0.07, color=COLOR_TSB_POS, zorder=1)\n\nfor event_date, label, ls in [\n    (dates[41], \"Race 1\", \":\"),\n    (dates[119], \"Race 2\", \":\"),\n    (taper_start_date, \"Taper\", \"--\"),\n]:\n    ax1.axvline(event_date, color=INK_MUTED, linewidth=0.7, linestyle=ls, alpha=0.7, zorder=5)\n    ax1.text(\n        event_date,\n        0.02,\n        label,\n        fontsize=7,\n        color=INK_MUTED,\n        ha=\"center\",\n        va=\"bottom\",\n        transform=ax1.get_xaxis_transform(),\n    )\n\n# Style — primary axis\ntitle = \"line-training-load-pmc · python · matplotlib · anyplot.ai\"\nn = len(title)\ntitle_fs = max(8, round(12 * 67 / n)) if n > 67 else 12\n\nax1.set_title(title, fontsize=title_fs, fontweight=\"medium\", color=INK, pad=10)\nax1.set_xlabel(\"Date\", fontsize=10, color=INK)\nax1.set_ylabel(\"Training Load (TSS / CTL / ATL)\", fontsize=10, color=INK)\nax1.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax1.xaxis.set_major_formatter(mdates.DateFormatter(\"%b %Y\"))\nax1.xaxis.set_major_locator(mdates.MonthLocator())\nax1.set_ylim(bottom=0)\n\nax1.spines[\"top\"].set_visible(False)\nfor sp in (\"left\", \"bottom\"):\n    ax1.spines[sp].set_color(INK_SOFT)\nax1.yaxis.grid(True, alpha=0.12, linewidth=0.7, color=INK, zorder=0)\n\n# Style — secondary axis\nax2.set_ylabel(\"Form / TSB\", fontsize=10, color=INK)\nax2.tick_params(axis=\"y\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nfor sp in (\"top\", \"left\", \"bottom\"):\n    ax2.spines[sp].set_visible(False)\nax2.spines[\"right\"].set_color(INK_SOFT)\n\n# Combined legend with patches for TSB fills\ntsb_pos_patch = Patch(color=COLOR_TSB_POS, alpha=0.5, label=\"Form > 0 (fresh)\")\ntsb_neg_patch = Patch(color=COLOR_TSB_NEG, alpha=0.5, label=\"Form < 0 (fatigued)\")\nh1, l1 = ax1.get_legend_handles_labels()\nall_handles = h1 + [tsb_pos_patch, tsb_neg_patch]\nall_labels = l1 + [\"Form > 0 (fresh)\", \"Form < 0 (fatigued)\"]\n\nleg = ax1.legend(all_handles, all_labels, fontsize=8, loc=\"upper left\", ncol=2, framealpha=0.9, borderpad=0.7)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\n# Layout — space for twin y-axis labels on both sides\nfig.subplots_adjust(left=0.09, right=0.91, top=0.91, bottom=0.13)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}