{"spec_id":"line-training-load-pmc","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-training-load-pmc: Training Load Performance Management Chart\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-13\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { AreaPlot, LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic LCG for reproducible synthetic data ---\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(1664525, s) + 1013904223) >>> 0;\n    return s / 4294967295;\n  };\n}\nconst rand = lcg(42);\n\n// --- Generate 180-day PMC training data (6-month endurance build to race) ---\nconst DAYS = 180;\nconst start = new Date(2025, 1, 3); // 3 Feb 2025\n// Taper begins at week 22 (day 154) — where positive form zone opens\nconst taperDate = new Date(start.getTime() + 154 * 24 * 60 * 60 * 1000);\n\nconst dates = [];\nconst tssData = [];\nconst ctlData = [];\nconst atlData = [];\nconst tsbPos = []; // TSB when form is positive (fresh)\nconst tsbNeg = []; // TSB when form is negative (fatigued)\n\nlet ctl = 36; // starting chronic training load\nlet atl = 34; // starting acute training load\n\nfor (let i = 0; i < DAYS; i++) {\n  const d = new Date(start);\n  d.setDate(start.getDate() + i);\n  dates.push(d);\n\n  const week = Math.floor(i / 7);\n  const dow = i % 7; // 0 = Mon … 6 = Sun\n\n  // Phase-structured TSS with realistic training block\n  let tss;\n  const noise = (rand() - 0.5) * 18;\n\n  if (dow === 6) {\n    // Easy Sunday recovery\n    tss = Math.round(Math.max(0, 22 + noise * 0.4));\n  } else if (week < 6) {\n    // Base phase: low-moderate volume, progressive\n    tss = Math.round(Math.max(0, 50 + week * 4 + (dow % 2 === 0 ? 28 : 12) + noise));\n  } else if (week < 15) {\n    // Build phase: progressive overload with recovery every 4th week\n    const buildProg = (week - 6) / 9;\n    const isRec = (week - 6) % 4 === 3;\n    const base = 80 + buildProg * 55;\n    const variation = dow % 3 === 0 ? 50 : dow % 2 === 0 ? 28 : 12;\n    tss = Math.round(Math.max(0, (base + variation + noise) * (isRec ? 0.5 : 1)));\n  } else if (week < 22) {\n    // Peak phase: high load with mini-recovery cycles\n    const isRec = (week - 15) % 4 === 3;\n    tss = Math.round(Math.max(0, (120 + (dow % 2 === 0 ? 48 : 22) + noise) * (isRec ? 0.48 : 1)));\n  } else {\n    // Taper: 4 weeks of declining load before race\n    const tapProg = Math.min(1, (week - 22) / 3.5);\n    tss = Math.round(Math.max(10, 95 * (1 - tapProg * 0.72) + noise * 0.5));\n  }\n\n  // TSB = previous-day CTL minus previous-day ATL (form available today)\n  const tsb = Math.round((ctl - atl) * 10) / 10;\n\n  // Update EWMA with today's TSS (42-day fitness constant, 7-day fatigue constant)\n  ctl += (tss - ctl) / 42;\n  atl += (tss - atl) / 7;\n\n  tssData.push(tss);\n  ctlData.push(Math.round(ctl * 10) / 10);\n  atlData.push(Math.round(atl * 10) / 10);\n  // Split TSB into positive (fresh) and negative (fatigued) for two-toned area\n  tsbPos.push(tsb >= 0 ? tsb : null);\n  tsbNeg.push(tsb < 0 ? tsb : null);\n}\n\n// TSS as scatter points: {x: Date, y: tss, id} format for ScatterPlot\nconst tssScatter = dates.map((d, i) => ({ x: d, y: tssData[i], id: i }));\n\nconst TITLE = \"line-training-load-pmc · javascript · muix · anyplot.ai\";\nconst TITLE_H = 58;\nconst MONTHS = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      {/* Title */}\n      <Box\n        sx={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          px: \"40px\",\n          pt: \"10px\",\n        }}\n      >\n        <Typography\n          sx={{ color: t.ink, fontSize: \"24px\", fontWeight: 500, lineHeight: 1 }}\n        >\n          {TITLE}\n        </Typography>\n      </Box>\n\n      {/* PMC chart */}\n      <ChartContainer\n        skipAnimation\n        width={W}\n        height={H - TITLE_H}\n        sx={{\n          \"& .MuiAreaElement-series-tsbPos\": { fillOpacity: 0.3 },\n          \"& .MuiAreaElement-series-tsbNeg\": { fillOpacity: 0.3 },\n        }}\n        xAxis={[{\n          id: \"date\",\n          data: dates,\n          scaleType: \"time\",\n          valueFormatter: (v) => {\n            const d = new Date(v);\n            return `${MONTHS[d.getMonth()]} ${d.getFullYear()}`;\n          },\n          tickNumber: 6,\n        }]}\n        yAxis={[\n          // Primary left axis: CTL and ATL training load values\n          { id: \"load\", min: 0, max: 190 },\n          // Secondary right axis: TSB form (oscillates around 0)\n          { id: \"tsb\",  min: -90, max: 90 },\n          // Hidden axis for TSS scatter (scaled so points cluster at bottom quarter)\n          { id: \"tss\",  min: 0, max: 760 },\n        ]}\n        series={[\n          // CTL is first series → receives brand green (Imprint palette position 1)\n          {\n            id: \"ctl\",\n            type: \"line\",\n            data: ctlData,\n            yAxisId: \"load\",\n            label: \"CTL – Fitness\",\n            color: t.palette[0], // #009E73 brand green\n            showMark: false,\n            curve: \"monotoneX\",\n          },\n          {\n            id: \"atl\",\n            type: \"line\",\n            data: atlData,\n            yAxisId: \"load\",\n            label: \"ATL – Fatigue\",\n            color: t.palette[1], // #C475FD lavender\n            showMark: false,\n            curve: \"monotoneX\",\n          },\n          // TSB split into two-toned areas: blue for fresh, red for fatigued\n          {\n            id: \"tsbPos\",\n            type: \"line\",\n            data: tsbPos,\n            yAxisId: \"tsb\",\n            label: \"Form (Fresh)\",\n            color: t.palette[2], // #4467A3 blue\n            area: true,\n            showMark: false,\n            curve: \"monotoneX\",\n          },\n          {\n            id: \"tsbNeg\",\n            type: \"line\",\n            data: tsbNeg,\n            yAxisId: \"tsb\",\n            label: \"Form (Fatigued)\",\n            color: t.palette[4], // #AE3030 matte red (semantic: bad/fatigue)\n            area: true,\n            showMark: false,\n            curve: \"monotoneX\",\n          },\n          // TSS as scatter points — daily raw load shown as light dots near the bottom\n          {\n            id: \"tss\",\n            type: \"scatter\",\n            data: tssScatter,\n            xAxisId: \"date\",\n            yAxisId: \"tss\",\n            label: \"Daily TSS\",\n            color: t.palette[3], // #BD8233 ochre\n            markerSize: 3,\n          },\n        ]}\n        margin={{ top: 20, bottom: 100, left: 90, right: 90 }}\n      >\n        <ChartsGrid horizontal />\n        <AreaPlot />\n        <LinePlot />\n        <ScatterPlot />\n        <ChartsXAxis\n          axisId=\"date\"\n          tickLabelStyle={{ fill: t.inkSoft, fontSize: 14 }}\n        />\n        <ChartsYAxis\n          axisId=\"load\"\n          label=\"Training Load (AU)\"\n          tickLabelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 15 }}\n        />\n        <ChartsYAxis\n          axisId=\"tsb\"\n          position=\"right\"\n          label=\"Form / TSB (AU)\"\n          tickLabelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 15 }}\n        />\n        {/* TSB = 0 horizontal reference — separates fresh from fatigued zones */}\n        <ChartsReferenceLine\n          y={0}\n          yAxisId=\"tsb\"\n          lineStyle={{\n            stroke: t.ink,\n            strokeOpacity: 0.28,\n            strokeDasharray: \"6 3\",\n            strokeWidth: 1.5,\n          }}\n        />\n        {/* Taper start — marks the positive-form window before race day */}\n        <ChartsReferenceLine\n          x={taperDate}\n          xAxisId=\"date\"\n          label=\"↑ Taper\"\n          labelAlign=\"start\"\n          labelStyle={{ fill: t.inkSoft, fontSize: 12, fontStyle: \"italic\" }}\n          lineStyle={{\n            stroke: t.inkSoft,\n            strokeOpacity: 0.4,\n            strokeDasharray: \"4 4\",\n            strokeWidth: 1,\n          }}\n        />\n        <ChartsLegend\n          position={{ vertical: \"bottom\", horizontal: \"middle\" }}\n          slotProps={{\n            legend: {\n              labelStyle: { fill: t.inkSoft, fontSize: 14 },\n              itemMarkWidth: 20,\n              itemMarkHeight: 4,\n            },\n          }}\n        />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}