{"spec_id":"curve-power-duration","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// curve-power-duration: Mean-Maximal Power Duration Curve\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-13\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) -----------------------------------------\n// Well-trained cyclist: CP = 280 W, W' = 20 000 J\n// Empirical MMP uses 3-param hyperbolic model P = CP + a/(t+b) for\n// physiologically realistic sprint power at short durations\nconst CP = 280;\nconst W_PRIME = 20000;\n\n// Log-spaced durations: 1 s to 18 000 s (5 h), 50 points\nconst N = 50;\nconst LOG_MAX = Math.log(18000);\n\n// Deterministic LCG for repeatable noise\nlet seed = 42;\nfunction lcg() {\n  seed = ((seed * 1664525 + 1013904223) >>> 0);\n  return seed / 4294967295;\n}\n\n// Empirical MMP: 3-param model + small noise, enforced monotonically non-increasing\nconst durList = [];\nfor (let i = 0; i < N; i++) {\n  durList.push(Math.exp((i / (N - 1)) * LOG_MAX));\n}\n\nconst rawPowers = durList.map(d => {\n  const mp = CP + 8000 / (d + 8);\n  return Math.max(CP + 2, mp + (lcg() - 0.5) * mp * 0.04);\n});\nfor (let i = 1; i < rawPowers.length; i++) {\n  if (rawPowers[i] > rawPowers[i - 1]) rawPowers[i] = rawPowers[i - 1];\n}\nconst empiricalData = durList.map((d, i) => [d, Math.round(rawPowers[i] * 10) / 10]);\n\n// 2-param CP model: P = CP + W'/t, sampled from 1 s to 18 000 s\n// Highcharts clips points above the y-axis max; the line naturally enters the\n// chart area around ~20 s, matching the empirical curve without a hard cutoff.\nconst modelData = [];\nfor (let i = 0; i < 200; i++) {\n  const d = Math.exp((i / 199) * LOG_MAX);\n  modelData.push([d, CP + W_PRIME / d]);\n}\n\n// --- Chart -------------------------------------------------------------------\n// Title: 59 chars < 67 baseline — no fontSize shrinkage needed\nconst titleText = \"curve-power-duration · javascript · highcharts · anyplot.ai\";\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    plotBorderWidth: 0,\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: titleText,\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n    margin: 20,\n  },\n  xAxis: {\n    type: \"logarithmic\",\n    tickPositions: [1, 5, 15, 30, 60, 300, 600, 1200, 3600, 18000],\n    minorTicks: false,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter: function () {\n        const v = this.value;\n        const allowed = [1, 5, 15, 30, 60, 300, 600, 1200, 3600, 18000];\n        if (!allowed.some(a => Math.abs(v - a) / a < 0.01)) return \"\";\n        if (v < 60) return Math.round(v) + \" s\";\n        if (v < 3600) return Math.round(v / 60) + \" min\";\n        return Math.round(v / 3600) + \" h\";\n      },\n    },\n    title: {\n      text: \"Duration (log scale)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    plotLines: [\n      {\n        value: 5, color: t.grid, dashStyle: \"ShortDash\", width: 1.5, zIndex: 3,\n        label: { text: \"5 s\", rotation: 0, y: 14, style: { color: t.inkSoft, fontSize: \"11px\" } },\n      },\n      {\n        value: 60, color: t.grid, dashStyle: \"ShortDash\", width: 1.5, zIndex: 3,\n        label: { text: \"1 min\", rotation: 0, y: 14, style: { color: t.inkSoft, fontSize: \"11px\" } },\n      },\n      {\n        value: 300, color: t.grid, dashStyle: \"ShortDash\", width: 1.5, zIndex: 3,\n        label: { text: \"5 min\", rotation: 0, y: 14, style: { color: t.inkSoft, fontSize: \"11px\" } },\n      },\n      {\n        value: 1200, color: t.grid, dashStyle: \"ShortDash\", width: 1.5, zIndex: 3,\n        label: { text: \"20 min\", rotation: 0, y: 14, style: { color: t.inkSoft, fontSize: \"11px\" } },\n      },\n    ],\n  },\n  yAxis: {\n    title: {\n      text: \"Power (W)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    min: 210,\n    max: 1250,\n    plotLines: [\n      {\n        value: CP,\n        color: t.inkSoft,\n        dashStyle: \"Dot\",\n        width: 1.5,\n        zIndex: 2,\n        label: {\n          text: \"CP = 280 W\",\n          align: \"right\",\n          x: -6,\n          y: -5,\n          style: { color: t.inkSoft, fontSize: \"12px\" },\n        },\n      },\n    ],\n  },\n  legend: {\n    enabled: true,\n    align: \"right\",\n    verticalAlign: \"top\",\n    layout: \"vertical\",\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"normal\" },\n    itemHoverStyle: { color: t.ink },\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    borderWidth: 1,\n    padding: 10,\n  },\n  tooltip: {\n    formatter: function () {\n      const d = this.x;\n      const label = d < 60 ? d.toFixed(0) + \" s\"\n                  : d < 3600 ? (d / 60).toFixed(1) + \" min\"\n                  : (d / 3600).toFixed(2) + \" h\";\n      return \"<b>\" + this.series.name + \"</b><br/>Duration: \" + label + \"<br/>Power: \" + this.y.toFixed(1) + \" W\";\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    line: { marker: { enabled: false } },\n  },\n  series: [\n    {\n      name: \"Mean-Maximal Power\",\n      data: empiricalData,\n      color: t.palette[0],\n      lineWidth: 2.5,\n      marker: {\n        enabled: true,\n        radius: 3,\n        fillColor: t.palette[0],\n        lineColor: t.pageBg,\n        lineWidth: 1,\n      },\n      zIndex: 4,\n    },\n    {\n      name: \"CP Model (P = CP + W′/t)\",\n      data: modelData,\n      color: t.palette[1],\n      lineWidth: 2,\n      dashStyle: \"Dash\",\n      marker: { enabled: false },\n      zIndex: 3,\n    },\n  ],\n});\n"}