{"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 9.16.0 | JavaScript 22.23.3\n// Quality: 87/100 | Updated: 2026-10-24\n\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { LinePlot, MarkPlot } from \"@mui/x-charts/LineChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Constants\nconst FONT = '\"Plus Jakarta Sans\", \"Inter\", -apple-system, BlinkMacSystemFont, sans-serif';\nconst MONO = '\"JetBrains Mono\", \"SFMono-Regular\", Menlo, monospace';\nconst BANDS: Array<{ loIdx: number; hiIdx: number; opacity: number }> = [\n  { loIdx: 0, hiIdx: 1, opacity: 0.25 },\n  { loIdx: 1, hiIdx: 2, opacity: 0.17 },\n  { loIdx: 2, hiIdx: 4, opacity: 0.09 },\n  { loIdx: 4, hiIdx: 5, opacity: 0.17 },\n  { loIdx: 5, hiIdx: 6, opacity: 0.25 },\n];\nconst X_PAD_SHARE = 0.02;\nconst Y_TOP_PAD_SHARE = 0.08;\nconst X_TICK_COUNT = 7;\nconst Y_TICK_COUNT = 6;\n\n// Columns: set these to your data's column names\nconst AGE = \"Age (months)\"; // role: age_months\nconst P3 = \"P3\"; // role: percentile_3\nconst P10 = \"P10\"; // role: percentile_10\nconst P25 = \"P25\"; // role: percentile_25\nconst P50 = \"P50\"; // role: percentile_50\nconst P75 = \"P75\"; // role: percentile_75\nconst P90 = \"P90\"; // role: percentile_90\nconst P97 = \"P97\"; // role: percentile_97\nconst PATIENT = \"Patient weight (kg)\"; // role: measurement\nconst PERCENTILES = [P3, P10, P25, P50, P75, P90, P97] as const;\n\n// Data: replace this block with your own records\nconst Z_SCORES = [-1.88, -1.28, -0.67, 0, 0.67, 1.28, 1.88];\nconst PATIENT_VISITS: Record<number, number> = {\n  0: 3.35,\n  2: 5.1,\n  4: 6.45,\n  6: 7.65,\n  9: 9.05,\n  12: 10.2,\n  15: 10.95,\n  18: 11.65,\n  21: 12.3,\n  24: 13.15,\n  30: 14.5,\n  36: 15.9,\n};\n\nconst data: Array<{\n  [AGE]: number;\n  [P3]: number;\n  [P10]: number;\n  [P25]: number;\n  [P50]: number;\n  [P75]: number;\n  [P90]: number;\n  [P97]: number;\n  [PATIENT]: number | null;\n}> = [];\n\nfor (let ageM = 0; ageM <= 36; ageM += 1) {\n  const med = 16 - 12.7 * Math.exp(-0.057 * ageM);\n  const cv = 0.128 - (0.013 * ageM) / 36;\n  data.push({\n    [AGE]: ageM,\n    [P3]: Number((med * (1 + Z_SCORES[0] * cv)).toFixed(2)),\n    [P10]: Number((med * (1 + Z_SCORES[1] * cv)).toFixed(2)),\n    [P25]: Number((med * (1 + Z_SCORES[2] * cv)).toFixed(2)),\n    [P50]: Number((med * (1 + Z_SCORES[3] * cv)).toFixed(2)),\n    [P75]: Number((med * (1 + Z_SCORES[4] * cv)).toFixed(2)),\n    [P90]: Number((med * (1 + Z_SCORES[5] * cv)).toFixed(2)),\n    [P97]: Number((med * (1 + Z_SCORES[6] * cv)).toFixed(2)),\n    [PATIENT]: ageM in PATIENT_VISITS ? PATIENT_VISITS[ageM] : null,\n  });\n}\n\n// Plot: reads only data and the constants above\nconst ageUnitMatch = AGE.match(/\\(([^)]+)\\)/);\nconst ageUnitRaw = ageUnitMatch ? ageUnitMatch[1] : \"\";\nconst ageUnit = ageUnitRaw === \"months\" ? \"mo\" : ageUnitRaw;\n\nconst measUnitMatch = PATIENT.match(/\\(([^)]+)\\)/);\nconst measUnit = measUnitMatch ? measUnitMatch[1] : \"\";\n\nconst sortedData = [...data].sort((a, b) => a[AGE] - b[AGE]);\nconst ages = sortedData.map((d) => d[AGE]);\nconst rawXMin = Math.min(...ages);\nconst rawXMax = Math.max(...ages);\nconst xSpan = rawXMax - rawXMin;\nconst X_MIN = rawXMin - xSpan * X_PAD_SHARE;\nconst X_MAX = rawXMax + xSpan * X_PAD_SHARE;\n\nconst allRefValues = sortedData.flatMap((d) =>\n  PERCENTILES.map((col) => d[col]),\n);\nconst patientPoints = sortedData\n  .filter((d) => d[PATIENT] !== null)\n  .map((d) => ({ age: d[AGE], weight: d[PATIENT]! }));\n\nconst maxRefVal = Math.max(\n  ...allRefValues,\n  ...patientPoints.map((p) => p.weight),\n);\nconst Y_MIN = 0;\nconst Y_MAX = maxRefVal * (1 + Y_TOP_PAD_SHARE);\n\nconst BAND_COLOR = t.palette[2];\nconst PATIENT_COLOR = t.palette[0];\n\nconst PATIENT_ID = \"patient\";\n\nconst series = [\n  ...PERCENTILES.map((col) => ({\n    type: \"line\" as const,\n    id: col,\n    label: col,\n    xAxisId: \"age\",\n    yAxisId: \"weight\",\n    data: sortedData.map((d) => d[col]),\n    color: BAND_COLOR,\n    curve: \"linear\" as const,\n    showMark: false,\n  })),\n  {\n    type: \"line\" as const,\n    id: PATIENT_ID,\n    label: PATIENT,\n    xAxisId: \"age\",\n    yAxisId: \"weight\",\n    data: sortedData.map((d) => d[PATIENT]),\n    color: PATIENT_COLOR,\n    curve: \"linear\" as const,\n    connectNulls: true,\n    showMark: true,\n    shape: \"circle\" as const,\n  },\n];\n\n// Filled bands and right-margin labels: the parts line series can't draw\nfunction BandLayer() {\n  const xScale = useXScale(\"age\") as (v: number) => number;\n  const yScale = useYScale(\"weight\") as (v: number) => number;\n\n  return (\n    <g>\n      {/* Filled percentile bands (P3→P97) */}\n      {BANDS.map((band, idx) => {\n        const loCol = PERCENTILES[band.loIdx];\n        const hiCol = PERCENTILES[band.hiIdx];\n        let d = `M ${xScale(sortedData[0][AGE]).toFixed(1)} ${yScale(\n          sortedData[0][hiCol],\n        ).toFixed(1)}`;\n        for (let i = 1; i < sortedData.length; i += 1) {\n          d += ` L ${xScale(sortedData[i][AGE]).toFixed(1)} ${yScale(\n            sortedData[i][hiCol],\n          ).toFixed(1)}`;\n        }\n        for (let i = sortedData.length - 1; i >= 0; i -= 1) {\n          d += ` L ${xScale(sortedData[i][AGE]).toFixed(1)} ${yScale(\n            sortedData[i][loCol],\n          ).toFixed(1)}`;\n        }\n        d += \" Z\";\n\n        return (\n          <path\n            key={`band-${idx}`}\n            d={d}\n            fill={BAND_COLOR}\n            fillOpacity={band.opacity}\n            stroke=\"none\"\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nfunction PercentileLabels() {\n  const xScale = useXScale(\"age\") as (v: number) => number;\n  const yScale = useYScale(\"weight\") as (v: number) => number;\n  const rightEdgeX = xScale(rawXMax) + 12;\n  const lastRow = sortedData[sortedData.length - 1];\n\n  return (\n    <g>\n      {PERCENTILES.map((col) => {\n        const isMedian = col === P50;\n        return (\n          <text\n            key={`lbl-${col}`}\n            x={rightEdgeX}\n            y={yScale(lastRow[col])}\n            fontSize={isMedian ? 16 : 14}\n            fontWeight={isMedian ? 700 : 600}\n            fontFamily={MONO}\n            fill={isMedian ? t.ink : BAND_COLOR}\n            dominantBaseline=\"middle\"\n          >\n            {col}\n          </text>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const headerHeight = 84;\n  const chartHeight = height - headerHeight;\n  const firstVisit = patientPoints[0];\n  const lastVisit = patientPoints[patientPoints.length - 1];\n\n  return (\n    <div\n      style={{\n        width,\n        height,\n        background: t.pageBg,\n        color: t.ink,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      <div\n        style={{\n          height: headerHeight,\n          padding: \"14px 72px 4px 104px\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          boxSizing: \"border-box\",\n        }}\n      >\n        <div>\n          <div\n            style={{\n              fontSize: 22,\n              fontWeight: 700,\n              color: t.ink,\n              lineHeight: 1.2,\n            }}\n          >\n            line-growth-percentile · javascript · muix · anyplot.ai\n          </div>\n          <div\n            style={{\n              fontSize: 14,\n              color: t.inkSoft,\n              marginTop: 4,\n              fontFamily: MONO,\n            }}\n          >\n            WHO-like reference (synthetic, {P3}–{P97}, {rawXMin}–{rawXMax}{\" \"}\n            {ageUnit}) · {patientPoints.length} visits (\n            {firstVisit.weight.toFixed(2)} {measUnit} at {firstVisit.age}{\" \"}\n            {ageUnit} → {lastVisit.weight.toFixed(2)} {measUnit} at{\" \"}\n            {lastVisit.age} {ageUnit})\n          </div>\n        </div>\n\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 24,\n            fontSize: 15,\n            color: t.ink,\n          }}\n        >\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <span\n              style={{\n                width: 12,\n                height: 12,\n                borderRadius: \"50%\",\n                background: PATIENT_COLOR,\n                display: \"inline-block\",\n              }}\n            />\n            <span>{PATIENT}</span>\n          </div>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <span\n              style={{\n                width: 24,\n                height: 3,\n                borderRadius: 2,\n                background: BAND_COLOR,\n                display: \"inline-block\",\n              }}\n            />\n            <span>Median ({P50})</span>\n          </div>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n            <span\n              style={{\n                width: 20,\n                height: 14,\n                borderRadius: 3,\n                background: `${BAND_COLOR}42`,\n                border: `1.5px solid ${BAND_COLOR}`,\n                display: \"inline-block\",\n                boxSizing: \"border-box\",\n              }}\n            />\n            <span>\n              Reference bands ({P3}–{P97})\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <ChartsContainer\n        width={width}\n        height={chartHeight}\n        margin={{ left: 20, right: 72, top: 20, bottom: 20 }}\n        skipAnimation\n        series={series}\n        xAxis={[\n          {\n            id: \"age\",\n            data: ages,\n            scaleType: \"linear\",\n            min: X_MIN,\n            max: X_MAX,\n            height: 60,\n            tickNumber: X_TICK_COUNT,\n            label: AGE,\n            labelStyle: { fontSize: 16, fontWeight: 600, fill: t.ink },\n            tickLabelStyle: {\n              fontSize: 14,\n              fill: t.inkSoft,\n              fontFamily: MONO,\n            },\n            valueFormatter: (v: number) => String(v),\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"weight\",\n            scaleType: \"linear\",\n            min: Y_MIN,\n            max: Y_MAX,\n            width: 84,\n            tickNumber: Y_TICK_COUNT,\n            label: PATIENT,\n            labelStyle: { fontSize: 16, fontWeight: 600, fill: t.ink },\n            tickLabelStyle: {\n              fontSize: 14,\n              fill: t.inkSoft,\n              fontFamily: MONO,\n            },\n            valueFormatter: (v: number) => String(v),\n          },\n        ]}\n        sx={{\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n          \"& .MuiLineChart-line\": {\n            strokeWidth: 1.3,\n            strokeOpacity: 0.58,\n            strokeLinecap: \"round\",\n            strokeLinejoin: \"round\",\n          },\n          [`& .MuiLineChart-line[data-series=\"${P50}\"]`]: {\n            strokeWidth: 2.8,\n            strokeOpacity: 0.94,\n          },\n          [`& .MuiLineChart-line[data-series=\"${PATIENT_ID}\"]`]: {\n            strokeWidth: 3.2,\n            strokeOpacity: 1,\n          },\n          [`& .MuiLineChart-mark[data-series=\"${PATIENT_ID}\"]`]: {\n            r: 6.2,\n            fill: PATIENT_COLOR,\n            stroke: t.pageBg,\n            strokeWidth: 2,\n          },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <BandLayer />\n        <LinePlot />\n        <MarkPlot />\n        <PercentileLabels />\n        <ChartsXAxis axisId=\"age\" />\n        <ChartsYAxis axisId=\"weight\" />\n      </ChartsContainer>\n    </div>\n  );\n}\n"}