{"spec_id":"line-training-load-pmc","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// line-training-load-pmc: Training Load Performance Management Chart\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-13\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === \"dark\";\n\n// Seeded LCG for deterministic data (no seeded RNG in the browser)\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return function () {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 0x100000000;\n  };\n}\nconst rng = makeLcg(42);\n\n// 182-day (26-week) training block — build1 → build2 → peak → taper\nconst DAYS = 182;\nconst START_MS = Date.UTC(2025, 0, 1);\nconst MS_PER_DAY = 86400000;\n\nfunction phaseBase(weekIdx) {\n  if (weekIdx < 8)  return 65;\n  if (weekIdx < 16) return 90;\n  if (weekIdx < 22) return 115;\n  return 45; // taper\n}\n\nconst rawTss = [];\nfor (let i = 0; i < DAYS; i++) {\n  const week = Math.floor(i / 7);\n  const dow  = i % 7;\n  const base = phaseBase(week);\n  const r    = rng();\n\n  let tss;\n  if (dow === 0) {\n    tss = 0;                                          // rest day\n  } else if (dow === 3) {\n    tss = Math.max(0, base * 0.45 + (r - 0.5) * 20); // easy\n  } else if (dow === 6) {\n    tss = Math.max(0, base * 1.9  + (r - 0.5) * 35); // long/hard\n  } else {\n    tss = Math.max(0, base        + (r - 0.5) * 30); // normal\n  }\n  rawTss.push(Math.round(tss));\n}\n\n// EWMA: ATL 7-day, CTL 42-day; TSB = previous-day CTL − ATL\nconst tssPoints = [], atlPoints = [], ctlPoints = [], tsbPoints = [];\nlet prevAtl = 30, prevCtl = 30;\n\nfor (let i = 0; i < DAYS; i++) {\n  const ts  = START_MS + i * MS_PER_DAY;\n  const tss = rawTss[i];\n\n  tsbPoints.push([ts, Math.round((prevCtl - prevAtl) * 10) / 10]);\n  tssPoints.push([ts, tss]);\n\n  const newAtl = prevAtl + (tss - prevAtl) / 7;\n  const newCtl = prevCtl + (tss - prevCtl) / 42;\n\n  atlPoints.push([ts, Math.round(newAtl * 10) / 10]);\n  ctlPoints.push([ts, Math.round(newCtl * 10) / 10]);\n\n  prevAtl = newAtl;\n  prevCtl = newCtl;\n}\n\n// Title font scaled for length > 67 chars\nconst TITLE = \"Training Load PMC · line-training-load-pmc · javascript · highcharts · anyplot.ai\";\nconst titleSize = Math.max(14, Math.round(22 * 67 / TITLE.length));\n\n// Dark bg absorbs semi-transparent color more; boost negative fill alpha on dark\nconst negFillAlpha = isDark ? 0.35 : 0.22;\n\n// Phase date boundaries for x-axis plotBands\nconst phase1End = START_MS + 8  * 7 * MS_PER_DAY;\nconst phase2End = START_MS + 16 * 7 * MS_PER_DAY;\nconst phase3End = START_MS + 22 * 7 * MS_PER_DAY;\nconst END_MS    = START_MS + DAYS * MS_PER_DAY;\n\nconst bandAlpha = isDark ? 0.07 : 0.05;\nconst phaseLabel = { style: { color: t.inkSoft, fontSize: \"11px\", fontStyle: \"italic\" }, y: 18, align: \"center\" };\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginRight: 90\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: titleSize + \"px\", fontWeight: \"600\" }\n  },\n  xAxis: {\n    type: \"datetime\",\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    // Phase bands guide unfamiliar viewers through the training periodization narrative\n    plotBands: [\n      { from: START_MS, to: phase1End, color: `rgba(68,103,163,${bandAlpha})`,  label: { ...phaseLabel, text: \"Build 1\" } },\n      { from: phase1End, to: phase2End, color: `rgba(68,103,163,${bandAlpha * 1.5})`, label: { ...phaseLabel, text: \"Build 2\" } },\n      { from: phase2End, to: phase3End, color: `rgba(174,48,48,${bandAlpha})`,  label: { ...phaseLabel, text: \"Peak\" } },\n      { from: phase3End, to: END_MS,    color: `rgba(0,158,115,${bandAlpha})`,  label: { ...phaseLabel, text: \"Taper\" } }\n    ]\n  },\n  yAxis: [\n    {\n      // Left — CTL, ATL, and TSS share TSS-unit scale\n      title: {\n        text: \"Training Load (TSS units)\",\n        style: { color: t.inkSoft, fontSize: \"16px\" }\n      },\n      min: 0,\n      gridLineColor: t.grid,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } }\n    },\n    {\n      // Right — TSB form oscillates around zero\n      title: {\n        text: \"Form / TSB\",\n        style: { color: t.inkSoft, fontSize: \"16px\" }\n      },\n      opposite: true,\n      gridLineColor: \"transparent\",\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n      plotLines: [\n        {\n          value: 0,\n          color: t.inkSoft,\n          width: 1,\n          dashStyle: \"ShortDash\",\n          zIndex: 3\n        },\n        // CVD-redundant positional labels: provide text cues in addition to color\n        {\n          value: 20,\n          color: \"transparent\",\n          width: 0,\n          label: {\n            text: \"Fresh ↑\",\n            style: { color: t.palette[0], fontSize: \"11px\", fontStyle: \"italic\" },\n            align: \"right\",\n            x: -4\n          }\n        },\n        {\n          value: -22,\n          color: \"transparent\",\n          width: 0,\n          label: {\n            text: \"Fatigued ↓\",\n            style: { color: t.palette[4], fontSize: \"11px\", fontStyle: \"italic\" },\n            align: \"right\",\n            x: -4\n          }\n        }\n      ]\n    }\n  ],\n  legend: {\n    enabled: true,\n    align: \"center\",\n    verticalAlign: \"bottom\",\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"normal\" },\n    itemHoverStyle: { color: t.ink }\n  },\n  plotOptions: {\n    series: { animation: false },\n    column: {\n      borderWidth: 0,\n      pointPadding: 0,\n      groupPadding: 0,\n      maxPointWidth: 4\n    },\n    area: { marker: { enabled: false } },\n    spline: { marker: { enabled: false } }\n  },\n  series: [\n    {\n      // Raw daily training stress — subdued ochre bars, read as background input\n      name: \"Daily TSS\",\n      type: \"column\",\n      data: tssPoints,\n      yAxis: 0,\n      color: t.palette[3],  // ochre #BD8233\n      opacity: 0.45,\n      zIndex: 1\n    },\n    {\n      // TSB form area — green above zero (fresh), red below (fatigued)\n      name: \"Form (TSB)\",\n      type: \"area\",\n      data: tsbPoints,\n      yAxis: 1,\n      threshold: 0,\n      color: t.palette[0],                                        // green #009E73 — positive/fresh\n      fillColor: \"rgba(0,158,115,0.22)\",\n      negativeColor: t.palette[4],                               // red #AE3030 — negative/fatigued\n      negativeFillColor: `rgba(174,48,48,${negFillAlpha})`,      // boosted alpha on dark bg\n      lineWidth: 1.5,\n      zIndex: 2\n    },\n    {\n      // CTL fitness — smoothest, slowest-reacting line\n      name: \"Fitness (CTL)\",\n      type: \"spline\",\n      data: ctlPoints,\n      yAxis: 0,\n      color: t.palette[2],  // blue #4467A3\n      lineWidth: 3,\n      zIndex: 4\n    },\n    {\n      // ATL fatigue — spikier, faster-reacting line\n      name: \"Fatigue (ATL)\",\n      type: \"spline\",\n      data: atlPoints,\n      yAxis: 0,\n      color: t.palette[1],  // lavender #C475FD\n      lineWidth: 2.5,\n      zIndex: 3\n    }\n  ]\n});\n"}