{"spec_id":"curve-power-duration","library":"muix","language":"javascript","code":"// anyplot.ai\n// curve-power-duration: Mean-Maximal Power Duration Curve\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 84/100 | Created: 2026-06-13\n\nimport { LineChart, ChartsReferenceLine } from \"@mui/x-charts\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst W = window.ANYPLOT_SIZE.width;\nconst H = window.ANYPLOT_SIZE.height;\nconst TITLE_H = 54;\n\n// --- Data (deterministic, in-memory) ----------------------------------------\n\nconst CP = 280;        // Critical Power in watts — aerobic asymptote\nconst W_PRIME = 20000; // Anaerobic work capacity in joules\nconst PEAK = 1100;     // Neuromuscular peak (1-second best), watts\nconst BLEND = 30;      // Seconds below which CP model overestimates\n\n// 55 log-spaced durations: 1 s → 18 000 s (5 h)\nconst N = 55;\nconst durations = Array.from({ length: N }, (_, i) => {\n  const lo = Math.log10(1);\n  const hi = Math.log10(18000);\n  return Math.pow(10, lo + (i / (N - 1)) * (hi - lo));\n});\n\n// Empirical curve: neuromuscular plateau (< 30 s) blends into CP model (≥ 30 s)\nconst cpAtBlend = CP + W_PRIME / BLEND; // ≈ 947 W\nconst empiricalBase = durations.map((d) => {\n  if (d >= BLEND) return CP + W_PRIME / d;\n  const frac = Math.log10(d) / Math.log10(BLEND); // 0 at 1 s, 1 at BLEND s\n  return PEAK + (cpAtBlend - PEAK) * frac;\n});\n\n// LCG for reproducible noise\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1664525 + 1013904223) & 0xffffffff;\n  return (seed >>> 0) / 0xffffffff;\n};\n\n// Add small noise, then enforce monotone non-increase\nconst empiricalNoisy = empiricalBase.map((b) => b * (1 + (lcg() - 0.5) * 0.03));\nconst empiricalPower = [];\nlet prevW = Infinity;\nfor (const v of empiricalNoisy) {\n  const val = Math.min(v, prevW);\n  empiricalPower.push(Math.round(val));\n  prevW = val;\n}\n\n// Fitted CP model (hyperbolic): null below BLEND so the line starts at 30 s\nconst modelPower = durations.map((d) =>\n  d >= BLEND ? Math.round(CP + W_PRIME / d) : null\n);\n\n// Human-readable duration labels for axis ticks\nconst fmtDur = (s) => {\n  s = Math.round(s);\n  if (s < 60) return `${s}s`;\n  if (s < 3600) return `${Math.round(s / 60)}min`;\n  return `${Math.round(s / 3600)}h`;\n};\n\n// --- Component (default-exported; harness mounts it) ------------------------\n\nexport default function Chart() {\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        display: \"flex\",\n        flexDirection: \"column\",\n        bgcolor: t.pageBg,\n      }}\n    >\n      <Typography\n        sx={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: t.ink,\n          fontSize: 22,\n          fontWeight: 500,\n          flexShrink: 0,\n        }}\n      >\n        curve-power-duration · javascript · muix · anyplot.ai\n      </Typography>\n      <LineChart\n        width={W}\n        height={H - TITLE_H}\n        skipAnimation\n        colors={[t.palette[0], t.palette[1]]}\n        xAxis={[\n          {\n            id: \"duration\",\n            data: durations,\n            scaleType: \"log\",\n            label: \"Effort Duration\",\n            valueFormatter: fmtDur,\n            tickInterval: [1, 5, 30, 60, 300, 1200, 3600, 7200, 18000],\n            min: 1,\n            max: 20000,\n            tickLabelStyle: { fontSize: 13 },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"power\",\n            label: \"Mean-Maximal Power (W)\",\n            min: 0,\n            max: 1200,\n            tickLabelStyle: { fontSize: 13 },\n          },\n        ]}\n        series={[\n          {\n            id: \"empirical\",\n            data: empiricalPower,\n            label: \"Best MMP (empirical)\",\n            showMark: false,\n            curve: \"monotoneX\",\n          },\n          {\n            id: \"model\",\n            data: modelPower,\n            label: `CP Model · CP = ${CP} W, W′ = ${W_PRIME / 1000} kJ`,\n            showMark: false,\n            curve: \"monotoneX\",\n            connectNulls: false,\n          },\n        ]}\n        sx={{\n          \".MuiLineElement-series-model\": {\n            strokeDasharray: \"12 6\",\n            strokeWidth: 2.5,\n          },\n          \".MuiLineElement-series-empirical\": {\n            strokeWidth: 3.5,\n          },\n        }}\n        margin={{ left: 90, right: 40, top: 20, bottom: 90 }}\n      >\n        <ChartsReferenceLine\n          x={5}\n          label=\"5 s\"\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 3\", strokeWidth: 1 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n        <ChartsReferenceLine\n          x={60}\n          label=\"1 min\"\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 3\", strokeWidth: 1 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n        <ChartsReferenceLine\n          x={300}\n          label=\"5 min\"\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 3\", strokeWidth: 1 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n        <ChartsReferenceLine\n          x={1200}\n          label=\"20 min (FTP)\"\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 3\", strokeWidth: 1 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n        <ChartsReferenceLine\n          y={CP}\n          label={`Critical Power = ${CP} W`}\n          labelAlign=\"end\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4 3\", strokeWidth: 1 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n      </LineChart>\n    </Box>\n  );\n}\n"}