{"spec_id":"line-growth-percentile","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-growth-percentile: Pediatric Growth Chart with Percentile Curves\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-20\n\nconst t = window.ANYPLOT_TOKENS;\n\n// WHO-approximate weight-for-age reference curves for boys, 0–36 months\n// Piecewise-linear interpolation of WHO Child Growth Standards medians + SDs\nconst medianKnots = [\n  [0,3.3],[3,5.8],[6,7.9],[9,9.2],[12,9.6],\n  [15,10.2],[18,10.9],[21,11.5],[24,12.2],\n  [27,12.7],[30,13.3],[33,13.8],[36,14.3],\n];\nconst sdKnots = [[0,0.42],[6,0.95],[12,1.10],[24,1.20],[36,1.30]];\n\nconst lerp    = (x, x0, y0, x1, y1) => y0 + (y1 - y0) * (x - x0) / (x1 - x0);\nconst interp  = (month, knots) => {\n  for (let i = 0; i < knots.length - 1; i++) {\n    if (month >= knots[i][0] && month <= knots[i + 1][0]) {\n      return lerp(month, knots[i][0], knots[i][1], knots[i + 1][0], knots[i + 1][1]);\n    }\n  }\n  return knots[knots.length - 1][1];\n};\nconst refCurve = (month, z) =>\n  +(interp(month, medianKnots) + z * interp(month, sdKnots)).toFixed(2);\n\nconst ages = Array.from({ length: 37 }, (_, i) => i);   // 0..36 months\n\nconst Z   = { p3: -1.88, p10: -1.28, p25: -0.67, p50: 0, p75: 0.67, p90: 1.28, p97: 1.88 };\nconst p3  = ages.map(m => refCurve(m, Z.p3));\nconst p10 = ages.map(m => refCurve(m, Z.p10));\nconst p25 = ages.map(m => refCurve(m, Z.p25));\nconst p50 = ages.map(m => refCurve(m, Z.p50));\nconst p75 = ages.map(m => refCurve(m, Z.p75));\nconst p90 = ages.map(m => refCurve(m, Z.p90));\nconst p97 = ages.map(m => refCurve(m, Z.p97));\n\n// Well-child visit weights: starts near P50, trends toward P75–P90 by 36 m\nconst patientVisits = [\n  { x: 0,  y: 3.5  }, { x: 1,  y: 4.3  }, { x: 2,  y: 5.5  }, { x: 4,  y: 7.0  },\n  { x: 6,  y: 8.5  }, { x: 9,  y: 10.0 }, { x: 12, y: 10.6 }, { x: 15, y: 11.4 },\n  { x: 18, y: 12.1 }, { x: 24, y: 13.4 }, { x: 30, y: 14.7 }, { x: 36, y: 15.8 },\n];\n\nconst toXY = arr => ages.map((m, i) => ({ x: m, y: arr[i] }));\n\n// Imprint blue (#4467A3 = rgb 68,103,163) for boys' WHO reference bands\nconst B = '68,103,163';\n\n// Inline plugin: page background fill + right-margin percentile labels\nconst growthExtras = {\n  id: 'growthExtras',\n  beforeDraw({ ctx, width, height }) {\n    ctx.save();\n    ctx.fillStyle = t.pageBg;\n    ctx.fillRect(0, 0, width, height);\n    ctx.restore();\n  },\n  afterDraw(chart) {\n    const { ctx, scales: { x: xSc, y: ySc } } = chart;\n    const rx = xSc.right + 10;\n    const pctLines = [\n      { name: 'P97', vals: p97 }, { name: 'P90', vals: p90 },\n      { name: 'P75', vals: p75 }, { name: 'P50', vals: p50 },\n      { name: 'P25', vals: p25 }, { name: 'P10', vals: p10 },\n      { name: 'P3',  vals: p3  },\n    ];\n    ctx.save();\n    ctx.textAlign = 'left';\n    ctx.textBaseline = 'middle';\n    pctLines.forEach(({ name, vals }) => {\n      const yPx = ySc.getPixelForValue(vals[vals.length - 1]);\n      ctx.font      = name === 'P50' ? 'bold 13px system-ui,sans-serif' : '13px system-ui,sans-serif';\n      ctx.fillStyle = name === 'P50' ? t.ink : t.inkSoft;\n      ctx.fillText(name, rx, yPx);\n    });\n    ctx.restore();\n  },\n};\n\n// Mount\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\nconst TITLE    = 'line-growth-percentile · javascript · chartjs · anyplot.ai';\nconst titleSize = Math.max(16, Math.round(22 * Math.min(1.0, 67 / TITLE.length)));\n\nnew Chart(canvas, {\n  type: 'line',\n  plugins: [growthExtras],\n  data: {\n    datasets: [\n      // Percentile reference curves — ordered so fill:'-1' creates adjacent bands\n      {\n        label: 'P3',\n        data: toXY(p3),\n        borderColor: `rgba(${B},0.45)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: false,\n        tension: 0.4,\n      },\n      {\n        label: 'P10',\n        data: toXY(p10),\n        borderColor: `rgba(${B},0.45)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.22)`,\n        tension: 0.4,\n      },\n      {\n        label: 'P25',\n        data: toXY(p25),\n        borderColor: `rgba(${B},0.35)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.14)`,\n        tension: 0.4,\n      },\n      {\n        label: 'P50 (Median)',\n        data: toXY(p50),\n        borderColor: '#4467A3',\n        borderWidth: 2.5,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.08)`,\n        tension: 0.4,\n      },\n      {\n        label: 'P75',\n        data: toXY(p75),\n        borderColor: `rgba(${B},0.35)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.08)`,\n        tension: 0.4,\n      },\n      {\n        label: 'P90',\n        data: toXY(p90),\n        borderColor: `rgba(${B},0.45)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.14)`,\n        tension: 0.4,\n      },\n      {\n        label: 'P97',\n        data: toXY(p97),\n        borderColor: `rgba(${B},0.45)`,\n        borderWidth: 1,\n        pointRadius: 0,\n        fill: '-1',\n        backgroundColor: `rgba(${B},0.22)`,\n        tension: 0.4,\n      },\n      // Individual patient trajectory (Imprint green — first categorical series)\n      {\n        label: 'Patient weight',\n        data: patientVisits,\n        borderColor: t.palette[0],\n        backgroundColor: t.palette[0],\n        borderWidth: 2.5,\n        pointRadius: 6,\n        pointHoverRadius: 8,\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        fill: false,\n        tension: 0.3,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 52, top: 10, left: 10, bottom: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: titleSize, weight: '600' },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        position: 'top',\n        align: 'end',\n        labels: {\n          filter: item => item.text === 'P50 (Median)' || item.text === 'Patient weight',\n          color: t.ink,\n          font: { size: 14 },\n          usePointStyle: true,\n          padding: 20,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: 0,\n        max: 36,\n        title: {\n          display: true,\n          text: 'Age (months)',\n          color: t.ink,\n          font: { size: 15 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          stepSize: 3,\n          callback: v => `${v}m`,\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n      y: {\n        min: 0,\n        max: 20,\n        title: {\n          display: true,\n          text: 'Weight (kg)',\n          color: t.ink,\n          font: { size: 15 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: v => `${v} kg`,\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}