Pediatric Growth Chart with Percentile Curves in MUI X Charts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Pediatric Growth Chart with Percentile Curves in Python, R, Julia and JavaScript.

A WHO/CDC-style growth chart displaying smooth percentile curves (3rd, 10th, 25th, 50th, 75th, 90th, 97th) as colored bands, with individual patient data points overlaid and connected by a line. This chart is a standard clinical tool for monitoring child development metrics such as height, weight, or BMI across age. It enables quick visual assessment of whether a child's growth trajectory falls within expected population ranges.

Pediatric Growth Chart with Percentile Curves rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// line-growth-percentile: Pediatric Growth Chart with Percentile Curves
// Library: muix 9.16.0 | JavaScript 22.23.3
// Quality: 87/100 | Updated: 2026-10-24

import { ChartsContainer } from "@mui/x-charts/ChartsContainer";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { LinePlot, MarkPlot } from "@mui/x-charts/LineChart";
import { useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// Constants
const FONT = '"Plus Jakarta Sans", "Inter", -apple-system, BlinkMacSystemFont, sans-serif';
const MONO = '"JetBrains Mono", "SFMono-Regular", Menlo, monospace';
const BANDS: Array<{ loIdx: number; hiIdx: number; opacity: number }> = [
  { loIdx: 0, hiIdx: 1, opacity: 0.25 },
  { loIdx: 1, hiIdx: 2, opacity: 0.17 },
  { loIdx: 2, hiIdx: 4, opacity: 0.09 },
  { loIdx: 4, hiIdx: 5, opacity: 0.17 },
  { loIdx: 5, hiIdx: 6, opacity: 0.25 },
];
const X_PAD_SHARE = 0.02;
const Y_TOP_PAD_SHARE = 0.08;
const X_TICK_COUNT = 7;
const Y_TICK_COUNT = 6;

// Columns: set these to your data's column names
const AGE = "Age (months)"; // role: age_months
const P3 = "P3"; // role: percentile_3
const P10 = "P10"; // role: percentile_10
const P25 = "P25"; // role: percentile_25
const P50 = "P50"; // role: percentile_50
const P75 = "P75"; // role: percentile_75
const P90 = "P90"; // role: percentile_90
const P97 = "P97"; // role: percentile_97
const PATIENT = "Patient weight (kg)"; // role: measurement
const PERCENTILES = [P3, P10, P25, P50, P75, P90, P97] as const;

// Data: replace this block with your own records
const Z_SCORES = [-1.88, -1.28, -0.67, 0, 0.67, 1.28, 1.88];
const PATIENT_VISITS: Record<number, number> = {
  0: 3.35,
  2: 5.1,
  4: 6.45,
  6: 7.65,
  9: 9.05,
  12: 10.2,
  15: 10.95,
  18: 11.65,
  21: 12.3,
  24: 13.15,
  30: 14.5,
  36: 15.9,
};

const data: Array<{
  [AGE]: number;
  [P3]: number;
  [P10]: number;
  [P25]: number;
  [P50]: number;
  [P75]: number;
  [P90]: number;
  [P97]: number;
  [PATIENT]: number | null;
}> = [];

for (let ageM = 0; ageM <= 36; ageM += 1) {
  const med = 16 - 12.7 * Math.exp(-0.057 * ageM);
  const cv = 0.128 - (0.013 * ageM) / 36;
  data.push({
    [AGE]: ageM,
    [P3]: Number((med * (1 + Z_SCORES[0] * cv)).toFixed(2)),
    [P10]: Number((med * (1 + Z_SCORES[1] * cv)).toFixed(2)),
    [P25]: Number((med * (1 + Z_SCORES[2] * cv)).toFixed(2)),
    [P50]: Number((med * (1 + Z_SCORES[3] * cv)).toFixed(2)),
    [P75]: Number((med * (1 + Z_SCORES[4] * cv)).toFixed(2)),
    [P90]: Number((med * (1 + Z_SCORES[5] * cv)).toFixed(2)),
    [P97]: Number((med * (1 + Z_SCORES[6] * cv)).toFixed(2)),
    [PATIENT]: ageM in PATIENT_VISITS ? PATIENT_VISITS[ageM] : null,
  });
}

// Plot: reads only data and the constants above
const ageUnitMatch = AGE.match(/\(([^)]+)\)/);
const ageUnitRaw = ageUnitMatch ? ageUnitMatch[1] : "";
const ageUnit = ageUnitRaw === "months" ? "mo" : ageUnitRaw;

const measUnitMatch = PATIENT.match(/\(([^)]+)\)/);
const measUnit = measUnitMatch ? measUnitMatch[1] : "";

const sortedData = [...data].sort((a, b) => a[AGE] - b[AGE]);
const ages = sortedData.map((d) => d[AGE]);
const rawXMin = Math.min(...ages);
const rawXMax = Math.max(...ages);
const xSpan = rawXMax - rawXMin;
const X_MIN = rawXMin - xSpan * X_PAD_SHARE;
const X_MAX = rawXMax + xSpan * X_PAD_SHARE;

const allRefValues = sortedData.flatMap((d) =>
  PERCENTILES.map((col) => d[col]),
);
const patientPoints = sortedData
  .filter((d) => d[PATIENT] !== null)
  .map((d) => ({ age: d[AGE], weight: d[PATIENT]! }));

const maxRefVal = Math.max(
  ...allRefValues,
  ...patientPoints.map((p) => p.weight),
);
const Y_MIN = 0;
const Y_MAX = maxRefVal * (1 + Y_TOP_PAD_SHARE);

const BAND_COLOR = t.palette[2];
const PATIENT_COLOR = t.palette[0];

const PATIENT_ID = "patient";

const series = [
  ...PERCENTILES.map((col) => ({
    type: "line" as const,
    id: col,
    label: col,
    xAxisId: "age",
    yAxisId: "weight",
    data: sortedData.map((d) => d[col]),
    color: BAND_COLOR,
    curve: "linear" as const,
    showMark: false,
  })),
  {
    type: "line" as const,
    id: PATIENT_ID,
    label: PATIENT,
    xAxisId: "age",
    yAxisId: "weight",
    data: sortedData.map((d) => d[PATIENT]),
    color: PATIENT_COLOR,
    curve: "linear" as const,
    connectNulls: true,
    showMark: true,
    shape: "circle" as const,
  },
];

// Filled bands and right-margin labels: the parts line series can't draw
function BandLayer() {
  const xScale = useXScale("age") as (v: number) => number;
  const yScale = useYScale("weight") as (v: number) => number;

  return (
    <g>
      {/* Filled percentile bands (P3→P97) */}
      {BANDS.map((band, idx) => {
        const loCol = PERCENTILES[band.loIdx];
        const hiCol = PERCENTILES[band.hiIdx];
        let d = `M ${xScale(sortedData[0][AGE]).toFixed(1)} ${yScale(
          sortedData[0][hiCol],
        ).toFixed(1)}`;
        for (let i = 1; i < sortedData.length; i += 1) {
          d += ` L ${xScale(sortedData[i][AGE]).toFixed(1)} ${yScale(
            sortedData[i][hiCol],
          ).toFixed(1)}`;
        }
        for (let i = sortedData.length - 1; i >= 0; i -= 1) {
          d += ` L ${xScale(sortedData[i][AGE]).toFixed(1)} ${yScale(
            sortedData[i][loCol],
          ).toFixed(1)}`;
        }
        d += " Z";

        return (
          <path
            key={`band-${idx}`}
            d={d}
            fill={BAND_COLOR}
            fillOpacity={band.opacity}
            stroke="none"
          />
        );
      })}
    </g>
  );
}

function PercentileLabels() {
  const xScale = useXScale("age") as (v: number) => number;
  const yScale = useYScale("weight") as (v: number) => number;
  const rightEdgeX = xScale(rawXMax) + 12;
  const lastRow = sortedData[sortedData.length - 1];

  return (
    <g>
      {PERCENTILES.map((col) => {
        const isMedian = col === P50;
        return (
          <text
            key={`lbl-${col}`}
            x={rightEdgeX}
            y={yScale(lastRow[col])}
            fontSize={isMedian ? 16 : 14}
            fontWeight={isMedian ? 700 : 600}
            fontFamily={MONO}
            fill={isMedian ? t.ink : BAND_COLOR}
            dominantBaseline="middle"
          >
            {col}
          </text>
        );
      })}
    </g>
  );
}

export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const headerHeight = 84;
  const chartHeight = height - headerHeight;
  const firstVisit = patientPoints[0];
  const lastVisit = patientPoints[patientPoints.length - 1];

  return (
    <div
      style={{
        width,
        height,
        background: t.pageBg,
        color: t.ink,
        fontFamily: FONT,
        display: "flex",
        flexDirection: "column",
        boxSizing: "border-box",
      }}
    >
      <div
        style={{
          height: headerHeight,
          padding: "14px 72px 4px 104px",
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          boxSizing: "border-box",
        }}
      >
        <div>
          <div
            style={{
              fontSize: 22,
              fontWeight: 700,
              color: t.ink,
              lineHeight: 1.2,
            }}
          >
            line-growth-percentile · javascript · muix · anyplot.ai
          </div>
          <div
            style={{
              fontSize: 14,
              color: t.inkSoft,
              marginTop: 4,
              fontFamily: MONO,
            }}
          >
            WHO-like reference (synthetic, {P3}–{P97}, {rawXMin}–{rawXMax}{" "}
            {ageUnit}) · {patientPoints.length} visits (
            {firstVisit.weight.toFixed(2)} {measUnit} at {firstVisit.age}{" "}
            {ageUnit} → {lastVisit.weight.toFixed(2)} {measUnit} at{" "}
            {lastVisit.age} {ageUnit})
          </div>
        </div>

        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 24,
            fontSize: 15,
            color: t.ink,
          }}
        >
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span
              style={{
                width: 12,
                height: 12,
                borderRadius: "50%",
                background: PATIENT_COLOR,
                display: "inline-block",
              }}
            />
            <span>{PATIENT}</span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span
              style={{
                width: 24,
                height: 3,
                borderRadius: 2,
                background: BAND_COLOR,
                display: "inline-block",
              }}
            />
            <span>Median ({P50})</span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span
              style={{
                width: 20,
                height: 14,
                borderRadius: 3,
                background: `${BAND_COLOR}42`,
                border: `1.5px solid ${BAND_COLOR}`,
                display: "inline-block",
                boxSizing: "border-box",
              }}
            />
            <span>
              Reference bands ({P3}–{P97})
            </span>
          </div>
        </div>
      </div>

      <ChartsContainer
        width={width}
        height={chartHeight}
        margin={{ left: 20, right: 72, top: 20, bottom: 20 }}
        skipAnimation
        series={series}
        xAxis={[
          {
            id: "age",
            data: ages,
            scaleType: "linear",
            min: X_MIN,
            max: X_MAX,
            height: 60,
            tickNumber: X_TICK_COUNT,
            label: AGE,
            labelStyle: { fontSize: 16, fontWeight: 600, fill: t.ink },
            tickLabelStyle: {
              fontSize: 14,
              fill: t.inkSoft,
              fontFamily: MONO,
            },
            valueFormatter: (v: number) => String(v),
          },
        ]}
        yAxis={[
          {
            id: "weight",
            scaleType: "linear",
            min: Y_MIN,
            max: Y_MAX,
            width: 84,
            tickNumber: Y_TICK_COUNT,
            label: PATIENT,
            labelStyle: { fontSize: 16, fontWeight: 600, fill: t.ink },
            tickLabelStyle: {
              fontSize: 14,
              fill: t.inkSoft,
              fontFamily: MONO,
            },
            valueFormatter: (v: number) => String(v),
          },
        ]}
        sx={{
          "& .MuiChartsGrid-line": { stroke: t.grid },
          "& .MuiLineChart-line": {
            strokeWidth: 1.3,
            strokeOpacity: 0.58,
            strokeLinecap: "round",
            strokeLinejoin: "round",
          },
          [`& .MuiLineChart-line[data-series="${P50}"]`]: {
            strokeWidth: 2.8,
            strokeOpacity: 0.94,
          },
          [`& .MuiLineChart-line[data-series="${PATIENT_ID}"]`]: {
            strokeWidth: 3.2,
            strokeOpacity: 1,
          },
          [`& .MuiLineChart-mark[data-series="${PATIENT_ID}"]`]: {
            r: 6.2,
            fill: PATIENT_COLOR,
            stroke: t.pageBg,
            strokeWidth: 2,
          },
        }}
      >
        <ChartsGrid horizontal />
        <BandLayer />
        <LinePlot />
        <MarkPlot />
        <PercentileLabels />
        <ChartsXAxis axisId="age" />
        <ChartsYAxis axisId="weight" />
      </ChartsContainer>
    </div>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/line-growth-percentile/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "line-growth-percentile",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/line-growth-percentile/javascript/muix",
  "hub": "https://anyplot.ai/line-growth-percentile",
  "code_json": "https://api.anyplot.ai/specs/line-growth-percentile/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/line-growth-percentile",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/line-growth-percentile/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/line-growth-percentile/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/line-growth-percentile/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/line-growth-percentile/javascript/muix/plot-dark.html",
  "quality_score": 87.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Pediatric Growth Chart with Percentile Curves on anyplot.ai.

Other implementations