{"spec_id":"line-growth-percentile","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-growth-percentile: Pediatric Growth Chart with Percentile Curves\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-20\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 112, bottom: 72, left: 82 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// WHO weight-for-age reference data for boys (0–36 months, approximate)\nconst refAges = [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36];\nconst pctValues = {\n  p3:  [2.5, 4.4, 6.1, 7.1,  7.8,  8.2,  8.6,  9.0,  9.4,  9.7, 10.1, 10.4, 10.8],\n  p10: [2.9, 5.0, 6.8, 7.8,  8.6,  9.1,  9.5,  9.9, 10.3, 10.7, 11.1, 11.5, 11.9],\n  p25: [3.2, 5.5, 7.4, 8.5,  9.3,  9.9, 10.3, 10.8, 11.2, 11.6, 12.0, 12.4, 12.9],\n  p50: [3.5, 6.0, 8.0, 9.2, 10.0, 10.6, 11.1, 11.6, 12.1, 12.6, 13.1, 13.5, 14.0],\n  p75: [3.8, 6.5, 8.6, 9.9, 10.8, 11.5, 12.0, 12.5, 13.1, 13.6, 14.1, 14.6, 15.1],\n  p90: [4.1, 6.9, 9.1, 10.5, 11.5, 12.2, 12.8, 13.3, 14.0, 14.5, 15.0, 15.5, 16.1],\n  p97: [4.5, 7.4, 9.7, 11.2, 12.2, 13.0, 13.6, 14.2, 14.9, 15.5, 16.1, 16.6, 17.2],\n};\nconst refData = refAges.map((age, i) => ({\n  age,\n  p3: pctValues.p3[i], p10: pctValues.p10[i], p25: pctValues.p25[i],\n  p50: pctValues.p50[i], p75: pctValues.p75[i], p90: pctValues.p90[i],\n  p97: pctValues.p97[i],\n}));\n\n// Individual patient: boy tracking ~65th percentile with slight dip at 18 months\nconst patientData = [\n  { age: 0,  w: 3.6 },\n  { age: 6,  w: 8.2 },\n  { age: 12, w: 10.3 },\n  { age: 18, w: 11.4 },\n  { age: 24, w: 12.4 },\n  { age: 30, w: 13.3 },\n  { age: 36, w: 14.2 },\n];\n\n// Scales\nconst x = d3.scaleLinear().domain([0, 36]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 20]).range([ih, 0]);\n\n// SVG\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Imprint blue (#4467A3, position 3) for WHO reference bands\nconst BAND_COL = \"#4467A3\";\n\n// Filled bands between percentiles — graduated opacity (darker at extremes, lighter near median)\nconst bands = [\n  { lo: \"p3\",  hi: \"p10\", opacity: 0.28 },\n  { lo: \"p10\", hi: \"p25\", opacity: 0.18 },\n  { lo: \"p25\", hi: \"p50\", opacity: 0.10 },\n  { lo: \"p50\", hi: \"p75\", opacity: 0.10 },\n  { lo: \"p75\", hi: \"p90\", opacity: 0.18 },\n  { lo: \"p90\", hi: \"p97\", opacity: 0.28 },\n];\n\nfor (const { lo, hi, opacity } of bands) {\n  const areaFn = d3.area()\n    .x(d => x(d.age))\n    .y0(d => y(d[lo]))\n    .y1(d => y(d[hi]))\n    .curve(d3.curveCatmullRom.alpha(0.5));\n  g.append(\"path\").datum(refData)\n    .attr(\"d\", areaFn)\n    .attr(\"fill\", BAND_COL)\n    .attr(\"opacity\", opacity);\n}\n\n// Y gridlines\ng.selectAll(\".ygrid\").data(y.ticks(10)).join(\"line\")\n  .attr(\"class\", \"ygrid\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", d => y(d)).attr(\"y2\", d => y(d))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\n// Percentile boundary lines (all except P50)\nfor (const key of [\"p3\", \"p10\", \"p25\", \"p75\", \"p90\", \"p97\"]) {\n  const lineFn = d3.line()\n    .x(d => x(d.age))\n    .y(d => y(d[key]))\n    .curve(d3.curveCatmullRom.alpha(0.5));\n  g.append(\"path\").datum(refData)\n    .attr(\"d\", lineFn)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", BAND_COL)\n    .attr(\"stroke-width\", 1)\n    .attr(\"opacity\", 0.55);\n}\n\n// P50 median line — emphasized with thicker stroke\nconst p50Fn = d3.line()\n  .x(d => x(d.age))\n  .y(d => y(d.p50))\n  .curve(d3.curveCatmullRom.alpha(0.5));\ng.append(\"path\").datum(refData)\n  .attr(\"d\", p50Fn)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", BAND_COL)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"opacity\", 0.9);\n\n// Percentile labels on right margin (anchored at age 36)\nconst lastRef = refData[refData.length - 1];\nconst pctLabels = [\n  { key: \"p3\",  label: \"P3\"  }, { key: \"p10\", label: \"P10\" },\n  { key: \"p25\", label: \"P25\" }, { key: \"p50\", label: \"P50\" },\n  { key: \"p75\", label: \"P75\" }, { key: \"p90\", label: \"P90\" },\n  { key: \"p97\", label: \"P97\" },\n];\nfor (const { key, label } of pctLabels) {\n  const isMed = key === \"p50\";\n  svg.append(\"text\")\n    .attr(\"x\", margin.left + iw + 8)\n    .attr(\"y\", margin.top + y(lastRef[key]) + 4)\n    .attr(\"fill\", BAND_COL)\n    .attr(\"opacity\", isMed ? 0.95 : 0.65)\n    .style(\"font-size\", isMed ? \"14px\" : \"12px\")\n    .style(\"font-weight\", isMed ? \"700\" : \"400\")\n    .text(label);\n}\n\n// Patient data: line + dots (Imprint green #009E73 — position 1, contrasts with blue bands)\nconst patLineFn = d3.line()\n  .x(d => x(d.age))\n  .y(d => y(d.w))\n  .curve(d3.curveCatmullRom.alpha(0.5));\ng.append(\"path\").datum(patientData)\n  .attr(\"d\", patLineFn)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5);\n\ng.selectAll(\".pdot\").data(patientData).join(\"circle\")\n  .attr(\"class\", \"pdot\")\n  .attr(\"cx\", d => x(d.age))\n  .attr(\"cy\", d => y(d.w))\n  .attr(\"r\", 5.5)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// X axis\nconst xAxisG = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x)\n    .tickValues([0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36])\n    .tickFormat(d3.format(\"d\")));\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Y axis\nconst yAxisG = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(10));\nyAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nyAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nyAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Axis labels\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Age (months)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2).attr(\"y\", -64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Weight (kg)\");\n\n// Legend\nconst lx = iw - 244;\nconst ly = 12;\n\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"y1\", ly + 7)\n  .attr(\"x2\", lx + 26).attr(\"y2\", ly + 7)\n  .attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 2.5);\ng.append(\"circle\")\n  .attr(\"cx\", lx + 13).attr(\"cy\", ly + 7).attr(\"r\", 4.5)\n  .attr(\"fill\", t.palette[0]).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\ng.append(\"text\")\n  .attr(\"x\", lx + 34).attr(\"y\", ly + 12)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"13px\")\n  .text(\"Patient trajectory\");\n\ng.append(\"rect\")\n  .attr(\"x\", lx).attr(\"y\", ly + 26).attr(\"width\", 26).attr(\"height\", 14)\n  .attr(\"fill\", BAND_COL).attr(\"opacity\", 0.22);\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"y1\", ly + 33)\n  .attr(\"x2\", lx + 26).attr(\"y2\", ly + 33)\n  .attr(\"stroke\", BAND_COL).attr(\"stroke-width\", 2.0).attr(\"opacity\", 0.85);\ng.append(\"text\")\n  .attr(\"x\", lx + 34).attr(\"y\", ly + 38)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"13px\")\n  .text(\"WHO reference bands\");\n\n// Title (scaled for length)\nconst titleStr = \"Boys Weight-for-Age · line-growth-percentile · javascript · d3 · anyplot.ai\";\nconst titlePx = Math.max(16, Math.round(22 * 67 / titleStr.length));\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titlePx}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleStr);\n\n// Subtitle\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"WHO Growth Standards · Boys 0–36 months\");\n"}