{"spec_id":"line-training-load-pmc","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-training-load-pmc: Training Load Performance Management Chart\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 87/100 | Created: 2026-06-13\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid importing local altair.py\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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 assignment for PMC metrics\nCOLOR_CTL = \"#4467A3\"  # blue — fitness/chronic (smooth, rising)\nCOLOR_ATL = \"#C475FD\"  # lavender — fatigue/acute (volatile)\nCOLOR_TSB_POS = \"#009E73\"  # brand green — positive form (fresh)\nCOLOR_TSB_NEG = \"#AE3030\"  # matte red — negative form (fatigued)\nCOLOR_TSS = INK_MUTED  # muted neutral — raw daily load bars\n\n# Data — 180-day training block with realistic PMC values\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(\"2025-01-06\", periods=n_days, freq=\"D\")\n\n# Simulate TSS: weekly structure, 3-week build + 1-week recovery mesocycle\ntss_raw = np.zeros(n_days)\nfor i in range(n_days):\n    week = i // 7\n    day_of_week = i % 7\n    recovery_week = week % 4 == 3\n    base = 40 if recovery_week else 70 + min(week, 12) * 2.0\n    if day_of_week == 5:  # Saturday long workout\n        base *= 1.9\n    elif day_of_week == 6:  # Sunday easy/rest\n        base *= 0.2\n    elif day_of_week == 2:  # Wednesday quality session\n        base *= 1.3\n    tss_raw[i] = max(0.0, np.random.normal(base, base * 0.18))\n\n# PMC EWMA — seed CTL/ATL realistically (trained athlete starting value)\nctl = np.zeros(n_days)\natl = np.zeros(n_days)\ntsb = np.zeros(n_days)\nalpha_ctl = 1 - np.exp(-1 / 42)\nalpha_atl = 1 - np.exp(-1 / 7)\n\nctl[0] = 52.0  # realistic fitness base at start of block\natl[0] = 58.0  # slightly elevated fatigue at block start\ntsb[0] = ctl[0] - atl[0]\n\nfor i in range(1, n_days):\n    ctl[i] = ctl[i - 1] + alpha_ctl * (tss_raw[i] - ctl[i - 1])\n    atl[i] = atl[i - 1] + alpha_atl * (tss_raw[i] - atl[i - 1])\n    tsb[i] = ctl[i - 1] - atl[i - 1]  # previous-day values per PMC convention\n\ndf_main = pd.DataFrame({\"date\": dates, \"ctl\": ctl, \"atl\": atl, \"tsb\": tsb})\ndf_tss = pd.DataFrame({\"date\": dates, \"tss\": tss_raw})\ndf_tsb_pos = df_main[[\"date\"]].copy()\ndf_tsb_pos[\"tsb_pos\"] = df_main[\"tsb\"].clip(lower=0)\ndf_tsb_neg = df_main[[\"date\"]].copy()\ndf_tsb_neg[\"tsb_neg\"] = df_main[\"tsb\"].clip(upper=0)\n\n# Title sizing\ntitle_str = \"line-training-load-pmc · python · altair · anyplot.ai\"\nn_chars = len(title_str)\ntitle_fontsize = max(round(16 * 67 / n_chars), 11)\n\n# ── Top panel: CTL / ATL lines + TSB filled areas (shared y-axis) ────────────\n# Shared y spans CTL/ATL range (~30-100) and TSB range (~-40 to +30) together.\n# TSB fills use y/y2 anchored at 0; all layers share one axis → no label clash.\n\ntsb_pos_area = (\n    alt.Chart(df_tsb_pos)\n    .mark_area(color=COLOR_TSB_POS, opacity=0.42, line=False)\n    .encode(x=alt.X(\"date:T\", axis=None), y=alt.Y(\"tsb_pos:Q\", title=\"Load / Form\"), y2=alt.Y2(datum=0))\n)\ntsb_neg_area = (\n    alt.Chart(df_tsb_neg)\n    .mark_area(color=COLOR_TSB_NEG, opacity=0.42, line=False)\n    .encode(x=alt.X(\"date:T\", axis=None), y=alt.Y(\"tsb_neg:Q\"), y2=alt.Y2(datum=0))\n)\ntsb_zero = (\n    alt.Chart(pd.DataFrame({\"y\": [0]}))\n    .mark_rule(color=INK_SOFT, strokeWidth=1, strokeDash=[5, 3], opacity=0.6)\n    .encode(y=alt.Y(\"y:Q\"))\n)\n\n# Melt CTL/ATL into long form for a clean colour-encoded legend\ndf_lines = df_main[[\"date\", \"ctl\", \"atl\"]].melt(id_vars=\"date\", var_name=\"metric\", value_name=\"value\")\ndf_lines[\"metric\"] = df_lines[\"metric\"].map({\"ctl\": \"Fitness (CTL)\", \"atl\": \"Fatigue (ATL)\"})\n\n# Add dummy entries so TSB and TSS appear in the shared legend (NaN = no visible line rendered)\ndf_legend_extras = pd.DataFrame(\n    {\"date\": [dates[0], dates[0]], \"metric\": [\"Form (TSB)\", \"Daily TSS\"], \"value\": [np.nan, np.nan]}\n)\ndf_lines = pd.concat([df_lines, df_legend_extras], ignore_index=True)\n\n_LEGEND_DOMAIN = [\"Fitness (CTL)\", \"Fatigue (ATL)\", \"Form (TSB)\", \"Daily TSS\"]\n_LEGEND_COLORS = [COLOR_CTL, COLOR_ATL, COLOR_TSB_POS, COLOR_TSS]\n\nmetric_lines = (\n    alt.Chart(df_lines)\n    .mark_line(strokeWidth=2.8)\n    .encode(\n        x=alt.X(\"date:T\", axis=None),\n        y=alt.Y(\n            \"value:Q\",\n            title=\"Load / Form\",\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12, titleColor=INK, labelColor=INK_SOFT),\n        ),\n        color=alt.Color(\n            \"metric:N\",\n            scale=alt.Scale(domain=_LEGEND_DOMAIN, range=_LEGEND_COLORS),\n            legend=alt.Legend(\n                title=\"PMC Components\", orient=\"right\", symbolStrokeWidth=3, labelFontSize=10, titleFontSize=10\n            ),\n        ),\n        strokeDash=alt.condition(alt.datum.metric == \"Fatigue (ATL)\", alt.value([6, 3]), alt.value([1, 0])),\n        tooltip=[\n            alt.Tooltip(\"date:T\", format=\"%b %d\"),\n            alt.Tooltip(\"metric:N\", title=\"Series\"),\n            alt.Tooltip(\"value:Q\", format=\".1f\"),\n        ],\n    )\n)\n\ntop_panel = alt.layer(tsb_pos_area, tsb_neg_area, tsb_zero, metric_lines).properties(width=580, height=220)\n\n# ── Bottom panel: daily TSS bars ─────────────────────────────────────────────\ntss_bars = (\n    alt.Chart(df_tss)\n    .mark_bar(color=COLOR_TSS, opacity=0.55, width=2)\n    .encode(\n        x=alt.X(\n            \"date:T\",\n            title=\"Date\",\n            axis=alt.Axis(\n                format=\"%b %Y\", labelAngle=-30, labelFontSize=10, titleFontSize=12, titleColor=INK, labelColor=INK_SOFT\n            ),\n        ),\n        y=alt.Y(\n            \"tss:Q\", title=\"TSS\", axis=alt.Axis(labelFontSize=10, titleFontSize=12, titleColor=INK, labelColor=INK_SOFT)\n        ),\n        tooltip=[alt.Tooltip(\"date:T\", format=\"%b %d\"), alt.Tooltip(\"tss:Q\", title=\"TSS\", format=\".0f\")],\n    )\n    .properties(width=580, height=90)\n)\n\n# ── Compose full chart ────────────────────────────────────────────────────────\nchart = (\n    alt.vconcat(top_panel, tss_bars, spacing=4)\n    .properties(\n        background=PAGE_BG, title=alt.TitleParams(text=title_str, fontSize=title_fontsize, color=INK, anchor=\"start\")\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save PNG then pad to exact 3200×1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}