Pediatric Growth Chart with Percentile Curves — Bokeh

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 Bokeh

Python source (Bokeh)

""" anyplot.ai
line-growth-percentile: Pediatric Growth Chart with Percentile Curves
Library: bokeh 3.9.1 | Python 3.13.14
Quality: 93/100 | Updated: 2026-06-20
"""

import base64
import os
import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource, HoverTool, Label, Legend
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


THEME = os.getenv("ANYPLOT_THEME", "light")

# Theme-adaptive chrome tokens (Imprint)
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette (canonical order)
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Patient line: first categorical series (Imprint green — always first)
PATIENT_COLOR = IMPRINT_PALETTE[0]  # #009E73
# Percentile bands: Imprint blue (boys convention)
BAND_COLOR = IMPRINT_PALETTE[2]  # #4467A3

# --- Data: WHO-style weight-for-age reference for boys, 0–36 months ---
np.random.seed(42)
age_months = np.linspace(0, 36, 73)

# Realistic growth: rapid early gain tapering off
median_weight = 3.3 + 0.7 * age_months - 0.012 * age_months**2 + 0.00012 * age_months**3
sd_factor = 0.35 + 0.03 * age_months

percentile_97 = median_weight + 1.88 * sd_factor
percentile_90 = median_weight + 1.28 * sd_factor
percentile_75 = median_weight + 0.67 * sd_factor
percentile_50 = median_weight
percentile_25 = median_weight - 0.67 * sd_factor
percentile_10 = median_weight - 1.28 * sd_factor
percentile_3 = median_weight - 1.88 * sd_factor

# Individual patient: healthy boy tracked at well-child visits
patient_age = np.array([0, 1, 2, 4, 6, 9, 12, 15, 18, 24, 30, 36])
patient_weight = np.array([3.4, 4.3, 5.4, 6.8, 7.8, 9.0, 10.0, 10.8, 11.5, 12.6, 13.8, 14.8])

# --- Plot ---
p = figure(
    width=3200,
    height=1800,
    title="line-growth-percentile · python · bokeh · anyplot.ai",
    x_axis_label="Age (months)",
    y_axis_label="Weight (kg)",
    toolbar_location=None,  # must be None — toolbar adds ~30–50 px above canvas
    min_border_bottom=160,  # room for 34pt tick labels + 42pt axis label
    min_border_left=180,  # room for 34pt tick labels + 42pt axis label
    min_border_top=110,  # room for 50pt title
    min_border_right=160,  # room for right-margin percentile labels (P3–P97)
)

# Percentile bands — graduated alpha: darker at extremes, lighter near median
band_alphas = [0.55, 0.40, 0.22, 0.22, 0.40, 0.55]
bands = [
    (percentile_3, percentile_10),
    (percentile_10, percentile_25),
    (percentile_25, percentile_50),
    (percentile_50, percentile_75),
    (percentile_75, percentile_90),
    (percentile_90, percentile_97),
]

renderers = []
for i, (lower, upper) in enumerate(bands):
    src = ColumnDataSource(data={"x": age_months, "y1": lower, "y2": upper})
    r = p.varea(x="x", y1="y1", y2="y2", source=src, fill_color=BAND_COLOR, fill_alpha=band_alphas[i])
    renderers.append(r)

# Percentile lines + right-margin labels
percentile_data = [
    (percentile_3, "P3", 2.0, 0.50),
    (percentile_10, "P10", 2.0, 0.50),
    (percentile_25, "P25", 2.0, 0.50),
    (percentile_50, "P50", 5.0, 0.90),  # median emphasised
    (percentile_75, "P75", 2.0, 0.50),
    (percentile_90, "P90", 2.0, 0.50),
    (percentile_97, "P97", 2.0, 0.50),
]

for values, label, lw, la in percentile_data:
    src = ColumnDataSource(data={"x": age_months, "y": values})
    p.line(x="x", y="y", source=src, line_color=BAND_COLOR, line_width=lw, line_alpha=la)
    lbl = Label(
        x=age_months[-1] + 0.6,
        y=values[-1],
        text=label,
        text_font_size="26pt",
        text_color=INK_SOFT,
        text_baseline="middle",
    )
    p.add_layout(lbl)

# Patient trajectory — first categorical series (Imprint green)
patient_src = ColumnDataSource(data={"x": patient_age, "y": patient_weight})
r_line = p.line(x="x", y="y", source=patient_src, line_color=PATIENT_COLOR, line_width=5)
r_pts = p.scatter(x="x", y="y", source=patient_src, size=20, color=PATIENT_COLOR, line_color="white", line_width=2.5)

# HoverTool for interactive HTML artifact
hover = HoverTool(renderers=[r_pts], tooltips=[("Age", "@x months"), ("Weight", "@y kg")], mode="mouse")
p.add_tools(hover)

# Legend
legend = Legend(
    items=[("Patient (Boy A042)", [r_line, r_pts]), ("WHO Reference Bands (P3–P97)", [renderers[2]])],
    location="top_left",
)
p.add_layout(legend)

# --- Typography (canonical bokeh 3200×1800 sizes) ---
p.title.text_font_size = "50pt"
p.title.text_font_style = "bold"
p.title.text_color = INK

p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT

# Legend styling
p.legend.label_text_font_size = "34pt"
p.legend.label_text_color = INK_SOFT
p.legend.background_fill_color = ELEVATED_BG
p.legend.background_fill_alpha = 0.92
p.legend.border_line_color = INK_SOFT
p.legend.glyph_height = 45
p.legend.glyph_width = 45
p.legend.spacing = 15
p.legend.padding = 25

# --- Chrome (theme-adaptive) ---
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = None  # L-shaped frame — no box outline

p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.axis_line_width = 1.5
p.yaxis.axis_line_width = 1.5
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT
p.xaxis.major_tick_line_width = 1.5
p.yaxis.major_tick_line_width = 1.5
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None

# Grid: solid thin lines at 15% opacity
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.15
p.ygrid.grid_line_alpha = 0.15

# Axis ranges
p.y_range.start = 0
p.x_range.end = 38.5  # extra room for right-margin percentile labels

# --- Save HTML (interactive catalog artifact) ---
output_file(f"plot-{THEME}.html", title="line-growth-percentile · bokeh · anyplot.ai")
save(p)

# --- Screenshot with Selenium (required — export_png fails on this system) ---
W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
    "--force-device-scale-factor=1",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
# Force exact viewport via CDP — avoids headless-chrome window-size capping
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
# CDP screenshot at exact viewport dimensions (bypasses Selenium viewport quirks)
result = driver.execute_cdp_cmd(
    "Page.captureScreenshot", {"format": "png", "fromSurface": True, "captureBeyondViewport": False}
)
Path(f"plot-{THEME}.png").write_bytes(base64.b64decode(result["data"]))
driver.quit()

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

Other implementations