{"spec_id":"line-training-load-pmc","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nline-training-load-pmc: Training Load Performance Management Chart\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 90/100 | Created: 2026-06-13\n\"\"\"\n\nimport os as _os\nimport sys as _sys\n\n\n# Prevent this file (seaborn.py) from shadowing the installed seaborn package\n_here = _os.path.dirname(_os.path.abspath(__file__))\nwhile _here in _sys.path:\n    _sys.path.remove(_here)\ndel _here\n\nimport os\n\nimport matplotlib.dates as mdates\nimport matplotlib.lines as mlines\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\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 positions for PMC series (first series always #009E73)\nCTL_COLOR = \"#009E73\"  # brand green (pos 1) — Fitness/CTL: growth, health\nATL_COLOR = \"#C475FD\"  # lavender   (pos 2) — Fatigue/ATL\nTSB_POS = \"#4467A3\"  # blue       (pos 3) — positive form (fresh)\nTSB_NEG = \"#AE3030\"  # matte red  (pos 5) — negative form (semantic: bad/fatigued)\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data — 180-day endurance training block (Sep 2025 – Feb 2026)\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(\"2025-09-01\", periods=n_days, freq=\"D\")\n\n# Periodised schedule: base → build → peak → taper\ntss_values = []\nfor i in range(n_days):\n    week = i // 7\n    dow = i % 7\n\n    if week % 4 == 3:  # recovery week\n        base = 45\n    elif week < 6:  # base phase\n        base = 55 + week * 5\n    elif week < 14:  # build phase\n        base = 80 + (week - 6) * 7\n    elif week < 20:  # peak phase\n        base = 128 - (week - 14) * 2\n    else:  # taper\n        base = max(30, 116 - (week - 20) * 22)\n\n    if dow == 0:  # Monday: complete rest\n        load = 0.0\n    elif dow == 6:  # Sunday: long session\n        load = base * 1.7 + np.random.normal(0, 12)\n    elif dow in (2, 4):  # Tue/Thu: quality sessions\n        load = base * 1.1 + np.random.normal(0, 8)\n    else:  # easy/moderate\n        load = base * 0.7 + np.random.normal(0, 8)\n\n    tss_values.append(max(0.0, round(load, 1)))\n\ntss = np.array(tss_values)\n\n# TrainingPeaks EWMA: CTL (42-day time constant) and ATL (7-day)\nalpha_ctl = 1 - np.exp(-1 / 42)\nalpha_atl = 1 - np.exp(-1 / 7)\n\nctl = np.zeros(n_days)\natl = np.zeros(n_days)\ntsb = np.zeros(n_days)\nctl[0] = tss[0]\natl[0] = tss[0]\n\nfor i in range(1, n_days):\n    tsb[i] = ctl[i - 1] - atl[i - 1]\n    ctl[i] = ctl[i - 1] + (tss[i] - ctl[i - 1]) * alpha_ctl\n    atl[i] = atl[i - 1] + (tss[i] - atl[i - 1]) * alpha_atl\n\n# Long-form dataframe for seaborn lineplot\ndf_lines = pd.concat(\n    [\n        pd.DataFrame({\"date\": dates, \"value\": ctl, \"metric\": \"Fitness (CTL)\"}),\n        pd.DataFrame({\"date\": dates, \"value\": atl, \"metric\": \"Fatigue (ATL)\"}),\n    ],\n    ignore_index=True,\n)\n\n# Plot\nfig, ax1 = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax1.set_facecolor(PAGE_BG)\n\nax2 = ax1.twinx()\n\n# Layer 1 — daily TSS impulses (background)\nax1.bar(dates, tss, color=INK_MUTED, alpha=0.22, width=pd.Timedelta(days=1), zorder=1)\n\n# Layer 2 — TSB filled zones on secondary axis (fresh vs fatigued)\nax2.fill_between(dates, 0, np.where(tsb >= 0, tsb, 0), color=TSB_POS, alpha=0.38, zorder=2)\nax2.fill_between(dates, np.where(tsb < 0, tsb, 0), 0, color=TSB_NEG, alpha=0.38, zorder=2)\nax2.axhline(0, color=INK_SOFT, linewidth=0.8, alpha=0.55, zorder=3)\n\n# Layer 3 — CTL and ATL smoothed lines via seaborn\nsns.lineplot(\n    data=df_lines,\n    x=\"date\",\n    y=\"value\",\n    hue=\"metric\",\n    palette={\"Fitness (CTL)\": CTL_COLOR, \"Fatigue (ATL)\": ATL_COLOR},\n    linewidth=2.5,\n    ax=ax1,\n    zorder=5,\n)\nif ax1.get_legend() is not None:\n    ax1.get_legend().remove()\n\n# Explicit ylim with headroom so phase labels have a clear slot above bars\ny_data_max = max(float(tss.max()), float(atl.max()))\nax1.set_ylim(0, y_data_max * 1.22)\n\n# Training phase boundary lines (subtle dashes at phase transitions)\nphase_transition_dates = [dates[42], dates[98], dates[140]]\nfor d in phase_transition_dates:\n    ax1.axvline(d, color=INK_SOFT, alpha=0.22, linewidth=0.7, linestyle=\"--\", zorder=1)\n\n# Phase labels — annotated just above the top axis spine (axes-fraction y)\nphase_segments = [\n    (\"Base\", dates[0], dates[41]),\n    (\"Build\", dates[42], dates[97]),\n    (\"Peak\", dates[98], dates[139]),\n    (\"Taper\", dates[140], dates[179]),\n]\nxaxis_transform = ax1.get_xaxis_transform()\nfor phase_name, start, end in phase_segments:\n    mid = start + (end - start) / 2\n    ax1.annotate(\n        phase_name,\n        xy=(mid, 1.01),\n        xycoords=xaxis_transform,\n        ha=\"center\",\n        va=\"bottom\",\n        fontsize=6.5,\n        color=INK_MUTED,\n        style=\"italic\",\n        annotation_clip=False,\n    )\n\n# Style — primary axis: seaborn-idiomatic spine removal via sns.despine\ntitle = \"line-training-load-pmc · python · seaborn · anyplot.ai\"\nax1.set_title(title, fontsize=12, fontweight=\"medium\", color=INK, pad=10)\nax1.set_xlabel(\"\", color=INK)\nax1.set_ylabel(\"Training Load (TSS units)\", fontsize=10, color=INK)\nax1.tick_params(axis=\"y\", labelsize=8, colors=INK_SOFT)\nsns.despine(ax=ax1, top=True, right=True)\nax1.yaxis.grid(True, alpha=0.12, linewidth=0.6, color=INK, zorder=0)\n\nax1.xaxis.set_major_locator(mdates.MonthLocator())\nax1.xaxis.set_major_formatter(mdates.DateFormatter(\"%b '%y\"))\nax1.tick_params(axis=\"x\", labelsize=8, colors=INK_SOFT, rotation=30)\n\n# Style — secondary axis (TSB)\nax2.set_ylabel(\"Form (TSB)\", fontsize=10, color=INK)\nax2.tick_params(axis=\"y\", labelsize=8, colors=INK_SOFT)\nax2.yaxis.label.set_color(INK)\nax2.spines[\"top\"].set_visible(False)\nax2.spines[\"left\"].set_visible(False)\nax2.spines[\"bottom\"].set_visible(False)\nax2.spines[\"right\"].set_color(INK_SOFT)\n\n# Combined legend with manual handles\nlegend_handles = [\n    mlines.Line2D([], [], color=CTL_COLOR, linewidth=2.5, label=\"Fitness (CTL)\"),\n    mlines.Line2D([], [], color=ATL_COLOR, linewidth=2.5, label=\"Fatigue (ATL)\"),\n    mpatches.Patch(facecolor=TSB_POS, alpha=0.5, label=\"Form+ (TSB ≥ 0)\"),\n    mpatches.Patch(facecolor=TSB_NEG, alpha=0.5, label=\"Form− (TSB < 0)\"),\n    mpatches.Patch(facecolor=INK_MUTED, alpha=0.45, label=\"Daily TSS\"),\n]\nax1.legend(\n    handles=legend_handles, loc=\"upper left\", fontsize=8, framealpha=0.9, facecolor=ELEVATED_BG, edgecolor=INK_SOFT\n)\n\nfig.subplots_adjust(left=0.09, right=0.89, top=0.91, bottom=0.13)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}