{"spec_id":"line-growth-percentile","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-growth-percentile: Pediatric Growth Chart with Percentile Curves\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-20\n//# anyplot-orientation: landscape\n// anyplot.ai\n// line-growth-percentile: Pediatric Growth Chart with Percentile Curves\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-20\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\nconst BLUE = \"#4467A3\";      // Imprint palette pos 3 — WHO reference band color (boys)\nconst GREEN = t.palette[0];  // \"#009E73\" — brand green, first categorical series (patient)\n\n// WHO Boys weight-for-age reference data (0–36 months)\nconst ages = Array.from({ length: 37 }, (_, i) => i);\nconst X_MAX = 36;\nconst Y_MIN = 0;\nconst Y_MAX = 22;\n\n// Median weight growth model approximating WHO Boys standard\nfunction medianWeight(ageM) {\n  return 16 - 12.7 * Math.exp(-0.057 * ageM);\n}\n\n// Coefficient of variation (decreasing slightly with age)\nfunction cvFn(ageM) {\n  return 0.128 - 0.013 * ageM / 36;\n}\n\n// Percentile z-scores and labels (P3 → P97)\nconst Z_SCORES = [-1.88, -1.28, -0.67, 0, 0.67, 1.28, 1.88];\nconst P_LABELS = [\"P3\", \"P10\", \"P25\", \"P50\", \"P75\", \"P90\", \"P97\"];\n\n// Compute all 7 percentile curves (37 age points each)\nconst pctArrays = Z_SCORES.map((z) =>\n  ages.map((a) => medianWeight(a) * (1 + z * cvFn(a)))\n);\n\n// Individual patient well-child visit weights (boy tracking slightly above median)\nconst patientAges    = [0,    2,    4,    6,    9,    12,    15,    18,    21,    24,    30,    36];\nconst patientWeights = [3.35, 5.10, 6.45, 7.65, 9.05, 10.20, 10.95, 11.65, 12.30, 13.15, 14.50, 15.90];\n\n// Band fill config [lower percentile index, upper index, opacity]\n// Outer bands (extreme percentiles) are darker; inner band (P25–P75) is lightest\nconst BANDS = [\n  [0, 1, 0.24],   // P3 → P10  (outer, darkest)\n  [1, 2, 0.16],   // P10 → P25 (middle)\n  [2, 4, 0.09],   // P25 → P75 (inner, lightest)\n  [4, 5, 0.16],   // P75 → P90 (middle)\n  [5, 6, 0.24],   // P90 → P97 (outer, darkest)\n];\n\n// Horizontal grid tick positions\nconst Y_GRID = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];\n\n// Build a closed SVG path for a filled band between two percentile curves\nfunction makeBandPath(lowerArr, upperArr, toX, toY) {\n  let d = `M ${toX(0).toFixed(1)} ${toY(upperArr[0]).toFixed(1)}`;\n  for (let i = 1; i < ages.length; i++) {\n    d += ` L ${toX(i).toFixed(1)} ${toY(upperArr[i]).toFixed(1)}`;\n  }\n  for (let i = ages.length - 1; i >= 0; i--) {\n    d += ` L ${toX(i).toFixed(1)} ${toY(lowerArr[i]).toFixed(1)}`;\n  }\n  return d + \" Z\";\n}\n\n// Build an open SVG path for a single percentile line\nfunction makeLinePath(arr, toX, toY) {\n  return arr\n    .map((v, i) => `${i === 0 ? \"M\" : \"L\"} ${toX(i).toFixed(1)} ${toY(v).toFixed(1)}`)\n    .join(\" \");\n}\n\n// ---- Custom SVG layers (must run inside ChartContainer context) ----\n\nfunction GridLayer() {\n  const { left, top, width, height } = useDrawingArea();\n  const toY = (w) => top + (1 - (w - Y_MIN) / (Y_MAX - Y_MIN)) * height;\n  return (\n    <g>\n      {Y_GRID.map((w) => (\n        <line\n          key={w}\n          x1={left}\n          y1={toY(w)}\n          x2={left + width}\n          y2={toY(w)}\n          stroke={t.grid}\n          strokeWidth={1}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction PercentileBands() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (i) => left + (i / X_MAX) * width;\n  const toY = (w) => top + (1 - (w - Y_MIN) / (Y_MAX - Y_MIN)) * height;\n  return (\n    <g>\n      {BANDS.map(([lo, hi, op], idx) => (\n        <path\n          key={idx}\n          d={makeBandPath(pctArrays[lo], pctArrays[hi], toX, toY)}\n          fill={BLUE}\n          fillOpacity={op}\n          stroke=\"none\"\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction PercentileLines() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (i) => left + (i / X_MAX) * width;\n  const toY = (w) => top + (1 - (w - Y_MIN) / (Y_MAX - Y_MIN)) * height;\n  return (\n    <g fill=\"none\">\n      {pctArrays.map((arr, i) => {\n        const isP50 = i === 3;\n        return (\n          <path\n            key={i}\n            d={makeLinePath(arr, toX, toY)}\n            stroke={BLUE}\n            strokeWidth={isP50 ? 2.8 : 1.2}\n            strokeOpacity={isP50 ? 0.92 : 0.55}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nfunction PatientLayer() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (a) => left + (a / X_MAX) * width;\n  const toY = (w) => top + (1 - (w - Y_MIN) / (Y_MAX - Y_MIN)) * height;\n  const pts = patientAges.map((a, i) => [toX(a), toY(patientWeights[i])]);\n  const d = pts\n    .map(([x, y], i) => `${i === 0 ? \"M\" : \"L\"} ${x.toFixed(1)} ${y.toFixed(1)}`)\n    .join(\" \");\n  return (\n    <g>\n      <path\n        d={d}\n        stroke={GREEN}\n        strokeWidth={3.2}\n        fill=\"none\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      {pts.map(([x, y], i) => (\n        <circle\n          key={i}\n          cx={x}\n          cy={y}\n          r={6.5}\n          fill={GREEN}\n          stroke={t.pageBg}\n          strokeWidth={2}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction PercentileLabels() {\n  const { left, top, width, height } = useDrawingArea();\n  const toX = (a) => left + (a / X_MAX) * width;\n  const toY = (w) => top + (1 - (w - Y_MIN) / (Y_MAX - Y_MIN)) * height;\n  const lx = toX(36) + 14;\n  return (\n    <g fontFamily={FONT}>\n      {P_LABELS.map((label, i) => {\n        const yPx = toY(pctArrays[i][36]);\n        const isP50 = i === 3;\n        return (\n          <text\n            key={i}\n            x={lx}\n            y={yPx + 5}\n            fontSize={isP50 ? 16 : 13}\n            fontWeight={isP50 ? \"700\" : \"500\"}\n            fill={BLUE}\n            fillOpacity={isP50 ? 1 : 0.82}\n          >\n            {label}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction LegendLayer() {\n  const { left, top } = useDrawingArea();\n  const lx = left + 28;\n  const ly = top + 38;\n  return (\n    <g fontFamily={FONT} fontSize={15}>\n      {/* Patient swatch */}\n      <line x1={lx} y1={ly} x2={lx + 30} y2={ly} stroke={GREEN} strokeWidth={3} strokeLinecap=\"round\" />\n      <circle cx={lx + 15} cy={ly} r={5.5} fill={GREEN} stroke={t.pageBg} strokeWidth={1.5} />\n      <text x={lx + 38} y={ly + 5} fill={t.inkSoft}>\n        Patient trajectory\n      </text>\n      {/* Band swatch */}\n      <rect x={lx} y={ly + 22} width={30} height={14} fill={BLUE} fillOpacity={0.20} rx={3} />\n      <line\n        x1={lx}\n        y1={ly + 29}\n        x2={lx + 30}\n        y2={ly + 29}\n        stroke={BLUE}\n        strokeWidth={1.5}\n        strokeOpacity={0.7}\n      />\n      <text x={lx + 38} y={ly + 34} fill={t.inkSoft}>\n        WHO reference bands (P3–P97)\n      </text>\n      {/* P50 swatch */}\n      <line\n        x1={lx}\n        y1={ly + 56}\n        x2={lx + 30}\n        y2={ly + 56}\n        stroke={BLUE}\n        strokeWidth={2.8}\n        strokeOpacity={0.92}\n        strokeLinecap=\"round\"\n      />\n      <text x={lx + 38} y={ly + 61} fill={t.inkSoft}>\n        Median (P50)\n      </text>\n    </g>\n  );\n}\n\n// Dummy series data to anchor axis scales (all values null — renders nothing)\nconst anchorSeries = [\n  {\n    type: \"line\" as const,\n    id: \"_anchor\",\n    data: ages.map(() => null),\n    color: \"transparent\",\n    showMark: false,\n  },\n];\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const TITLE =\n    \"Boys Weight-for-Age · line-growth-percentile · javascript · muix · anyplot.ai\";\n  const titleH = 58;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        pt: \"16px\",\n      }}\n    >\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 19,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          mb: \"6px\",\n          fontFamily: \"inherit\",\n        }}\n      >\n        {TITLE}\n      </Typography>\n\n      <ChartContainer\n        width={width}\n        height={height - titleH}\n        margin={{ left: 90, right: 110, top: 28, bottom: 80 }}\n        series={anchorSeries}\n        xAxis={[\n          {\n            scaleType: \"linear\",\n            min: 0,\n            max: X_MAX,\n            tickMinStep: 6,\n            valueFormatter: (v) => String(v),\n          },\n        ]}\n        yAxis={[\n          {\n            scaleType: \"linear\",\n            min: Y_MIN,\n            max: Y_MAX,\n            tickMinStep: 2,\n            valueFormatter: (v) => String(v),\n          },\n        ]}\n      >\n        <GridLayer />\n        <PercentileBands />\n        <PercentileLines />\n        <PatientLayer />\n        <LegendLayer />\n        <PercentileLabels />\n        <ChartsXAxis\n          label=\"Age (months)\"\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n          labelStyle={{\n            fontSize: 16,\n            fontWeight: \"500\",\n            fill: t.inkSoft,\n            fontFamily: FONT,\n          }}\n        />\n        <ChartsYAxis\n          label=\"Weight (kg)\"\n          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n          labelStyle={{\n            fontSize: 16,\n            fontWeight: \"500\",\n            fill: t.inkSoft,\n            fontFamily: FONT,\n          }}\n        />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}