{"spec_id":"line-training-load-pmc","library":"echarts","language":"javascript","code":"// anyplot.ai\n// line-training-load-pmc: Training Load Performance Management Chart\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-13\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === \"dark\";\n\n// Deterministic data (xorshift RNG — no Math.random())\nlet seed = 12345;\nconst rand = () => {\n  seed ^= seed << 13;\n  seed ^= seed >> 17;\n  seed ^= seed << 5;\n  return (seed >>> 0) / 0x100000000;\n};\n\n// Generate 180 days starting Dec 16 2025, ending ~Jun 13 2026\nconst N = 180;\nconst dates = [];\nconst tssArr = [];\nconst ctlArr = [];\nconst atlArr = [];\nconst tsbArr = [];\n\nlet ctl = 38;\nlet atl = 42;\n\nfor (let i = 0; i < N; i++) {\n  const d = new Date(2025, 11, 16 + i); // Dec 16 2025 + i days (JS handles overflow)\n  dates.push(d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" }));\n\n  // TSB = yesterday's CTL - yesterday's ATL (form going into the day)\n  tsbArr.push(+(ctl - atl).toFixed(1));\n\n  // Training phase determines TSS distribution\n  const phase = i / N;\n  const dow = d.getDay(); // 0 = Sunday\n\n  let tss = 0;\n  if (dow !== 0) {\n    const r = rand();\n    const restChance = dow === 6 ? 0.3 : 0.15;\n    if (r >= restChance) {\n      let baseTSS;\n      if (phase < 0.35) {\n        baseTSS = 55 + phase * 70;           // base build: 55 → 79\n      } else if (phase < 0.65) {\n        baseTSS = 79 + (phase - 0.35) * 130; // intensity: 79 → 118\n      } else if (phase < 0.82) {\n        baseTSS = 118 + (phase - 0.65) * 30; // peak: 118 → 123\n      } else {\n        baseTSS = 123 * Math.max(0.22, 1 - (phase - 0.82) * 4.3); // taper\n      }\n      tss = Math.round(baseTSS * (0.65 + rand() * 0.70));\n    } else {\n      rand(); // consume RNG slot to keep sequence consistent\n    }\n  }\n\n  tssArr.push(tss);\n\n  // EWMA: CTL ≈ 42-day constant, ATL ≈ 7-day constant\n  ctl = ctl + (tss - ctl) / 42;\n  atl = atl + (tss - atl) / 7;\n  ctlArr.push(+ctl.toFixed(1));\n  atlArr.push(+atl.toFixed(1));\n}\n\n// Two-tone TSB: positive clipped to [0, ∞) and negative clipped to (-∞, 0]\nconst tsbPos = tsbArr.map(v => Math.max(v, 0));\nconst tsbNeg = tsbArr.map(v => Math.min(v, 0));\n\n// Phase boundary indices (matching data generation thresholds)\nconst baseEnd = Math.floor(N * 0.35) - 1;      // day 62\nconst intensityEnd = Math.floor(N * 0.65) - 1; // day 116\nconst peakEnd = Math.floor(N * 0.82) - 1;      // day 147\n\n// Peak CTL date for annotation\nlet peakCtlIdx = 0;\nfor (let i = 1; i < ctlArr.length; i++) {\n  if (ctlArr[i] > ctlArr[peakCtlIdx]) peakCtlIdx = i;\n}\n\n// Very subtle phase background shading, theme-adaptive\nconst phaseShade = isDark ? \"rgba(255,255,255,0.04)\" : \"rgba(0,0,0,0.04)\";\n\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nconst titleText =\n  \"Training Load PMC · line-training-load-pmc · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.max(14, Math.round(22 * 67 / titleText.length));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: titleText,\n    left: \"center\",\n    top: 14,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: \"bold\" },\n  },\n\n  legend: {\n    data: [\"Fitness (CTL)\", \"Fatigue (ATL)\", \"Form (TSB)\", \"Daily TSS\"],\n    bottom: 14,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    itemWidth: 24,\n    itemHeight: 12,\n  },\n\n  grid: { left: 80, right: 90, top: 80, bottom: 62 },\n\n  xAxis: {\n    type: \"category\",\n    data: dates,\n    boundaryGap: false,\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n\n  yAxis: [\n    {\n      // Primary — CTL, ATL, raw TSS bars\n      name: \"Load (TSS units)\",\n      nameTextStyle: { color: t.inkSoft, fontSize: 13, padding: [0, 0, 6, 44] },\n      type: \"value\",\n      min: 0,\n      max: 185,\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { show: true, lineStyle: { color: t.inkSoft } },\n      axisTick: { show: false },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      // Secondary — TSB oscillates around zero\n      name: \"Form (TSB)\",\n      nameTextStyle: { color: t.inkSoft, fontSize: 13, padding: [0, 36, 6, 0] },\n      type: \"value\",\n      position: \"right\",\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { show: true, lineStyle: { color: t.inkSoft } },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n  ],\n\n  series: [\n    // --- Primary axis series ---\n    {\n      name: \"Fitness (CTL)\",\n      type: \"line\",\n      data: ctlArr,\n      smooth: 0.4,\n      symbol: \"none\",\n      // palette[0] = brand green: first categorical series (hard rule) + fitness/progress reads as green\n      lineStyle: { width: 3.5, color: t.palette[0] },\n      itemStyle: { color: t.palette[0] },\n      yAxisIndex: 0,\n      z: 5,\n      // Annotate peak fitness moment for data storytelling\n      markLine: {\n        silent: true,\n        symbol: [\"none\", \"none\"],\n        lineStyle: { color: t.palette[0], opacity: 0.5, width: 1.5, type: \"dashed\" },\n        label: {\n          show: true,\n          formatter: \"Peak\",\n          position: \"insideStart\",\n          fontSize: 10,\n          color: t.inkSoft,\n        },\n        data: [{ xAxis: dates[peakCtlIdx] }],\n      },\n    },\n    {\n      name: \"Fatigue (ATL)\",\n      type: \"line\",\n      data: atlArr,\n      smooth: 0.3,\n      symbol: \"none\",\n      lineStyle: { width: 2.5, color: t.palette[1] }, // lavender — acute fatigue\n      itemStyle: { color: t.palette[1] },\n      yAxisIndex: 0,\n      z: 4,\n    },\n    {\n      // Raw daily training load — muted vertical bars; carries training phase markArea (z:1, below data)\n      name: \"Daily TSS\",\n      type: \"bar\",\n      data: tssArr,\n      yAxisIndex: 0,\n      barMaxWidth: 4,\n      itemStyle: { color: t.inkSoft, opacity: 0.35 },\n      z: 1,\n      // Subtle phase shading with italic labels for data storytelling\n      markArea: {\n        silent: true,\n        itemStyle: { color: phaseShade },\n        label: {\n          show: true,\n          position: \"insideTop\",\n          fontSize: 10,\n          color: t.inkSoft,\n          fontStyle: \"italic\",\n        },\n        data: [\n          [{ name: \"Base\", xAxis: dates[0] }, { xAxis: dates[baseEnd] }],\n          [{ name: \"Intensity\", xAxis: dates[baseEnd + 1] }, { xAxis: dates[intensityEnd] }],\n          [{ name: \"Peak\", xAxis: dates[intensityEnd + 1] }, { xAxis: dates[peakEnd] }],\n          [{ name: \"Taper\", xAxis: dates[peakEnd + 1] }, { xAxis: dates[N - 1] }],\n        ],\n      },\n    },\n\n    // --- Secondary axis: TSB two-tone area ---\n    {\n      // Positive TSB (fresh / peak form) — blue fill (spec: \"positive form = fresh/blue\")\n      name: \"Form (TSB)\",\n      type: \"line\",\n      data: tsbPos,\n      smooth: 0.4,\n      symbol: \"none\",\n      lineStyle: { width: 0 },\n      areaStyle: { color: t.palette[2], opacity: 0.45 }, // blue — fresh/positive form\n      itemStyle: { color: t.palette[2] },\n      yAxisIndex: 1,\n      z: 3,\n      // Idiomatic ECharts zero reference via markLine (avoids phantom series)\n      markLine: {\n        silent: true,\n        symbol: [\"none\", \"none\"],\n        lineStyle: { color: t.inkSoft, opacity: 0.55, width: 1 },\n        data: [{ yAxis: 0 }],\n      },\n    },\n    {\n      // Negative TSB (fatigued) — matte red fill\n      name: \"_tsb_neg\",\n      type: \"line\",\n      data: tsbNeg,\n      smooth: 0.4,\n      symbol: \"none\",\n      lineStyle: { width: 0 },\n      areaStyle: { color: \"#AE3030\", opacity: 0.40 }, // matte red — fatigued\n      itemStyle: { color: \"#AE3030\" },\n      yAxisIndex: 1,\n      z: 3,\n      legendHoverLink: false,\n      tooltip: { show: false },\n    },\n  ],\n});\n"}