Pediatric Growth Chart with Percentile Curves — plotnine

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 plotnine

Python source (plotnine)

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

import os
import sys


# Remove this file's directory from sys.path to prevent self-import
# (file is named plotnine.py — same name as the library).
_this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _this_dir]

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_line,
    geom_point,
    geom_ribbon,
    geom_text,
    ggplot,
    labs,
    scale_alpha_manual,
    scale_fill_manual,
    scale_x_continuous,
    scale_y_continuous,
    theme,
    theme_minimal,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette — semantic exception: blue for boys (domain convention)
BOYS_BLUE = "#4467A3"  # Imprint position 3
PATIENT_COLOR = "#009E73"  # Imprint position 1 — always first categorical series

# Data — WHO-style weight-for-age reference for boys, 0–36 months
np.random.seed(42)

age_months = np.arange(0, 37, 1)

# Approximate WHO growth standards for boys 0–36 months
median_weight = 3.3 + 0.7 * age_months - 0.008 * age_months**2 + 0.00005 * age_months**3
sd_weight = 0.4 + 0.02 * age_months

percentiles = {
    "P3": median_weight - 1.881 * sd_weight,
    "P10": median_weight - 1.282 * sd_weight,
    "P25": median_weight - 0.674 * sd_weight,
    "P50": median_weight,
    "P75": median_weight + 0.674 * sd_weight,
    "P90": median_weight + 1.282 * sd_weight,
    "P97": median_weight + 1.881 * sd_weight,
}

df_ref = pd.DataFrame({"age": age_months, **{k.lower(): v for k, v in percentiles.items()}})

# Band data — graduated alpha: darker at extremes, lighter near median
band_specs = [
    ("P3–P10", "p3", "p10", 0.40),
    ("P10–P25", "p10", "p25", 0.28),
    ("P25–P75", "p25", "p75", 0.14),
    ("P75–P90", "p75", "p90", 0.28),
    ("P90–P97", "p90", "p97", 0.40),
]

df_bands = pd.concat(
    [
        pd.DataFrame({"age": df_ref["age"], "ymin": df_ref[lo], "ymax": df_ref[hi], "band": label})
        for label, lo, hi, _ in band_specs
    ],
    ignore_index=True,
)
band_order = [s[0] for s in band_specs]
df_bands["band"] = pd.Categorical(df_bands["band"], categories=band_order, ordered=True)

band_fill_map = {s[0]: BOYS_BLUE for s in band_specs}
band_alpha_map = {s[0]: s[3] for s in band_specs}

# Boundary percentile lines (long format, grouped by percentile)
pct_non_median = ["P3", "P10", "P25", "P75", "P90", "P97"]
df_boundary = pd.concat(
    [pd.DataFrame({"age": df_ref["age"], "weight": df_ref[p.lower()], "percentile": p}) for p in pct_non_median],
    ignore_index=True,
)

# Median line — emphasized separately
df_median = pd.DataFrame({"age": df_ref["age"], "weight": df_ref["p50"]})

# Individual patient — a healthy boy tracking around the 65th percentile
patient_ages = np.array([0, 1, 2, 4, 6, 9, 12, 15, 18, 24, 30, 36])
patient_weights = np.array([3.5, 4.6, 5.8, 7.2, 8.3, 9.5, 10.4, 11.2, 11.9, 13.1, 14.5, 15.6])
df_patient = pd.DataFrame({"age": patient_ages, "weight": patient_weights})

# Percentile labels at right margin (age=36)
pct_all = ["P3", "P10", "P25", "P50", "P75", "P90", "P97"]
df_labels = pd.DataFrame({"age": [36] * 7, "weight": [percentiles[p][-1] for p in pct_all], "label": pct_all})

# Plot
title = "line-growth-percentile · python · plotnine · anyplot.ai"
plot = (
    ggplot()
    + geom_ribbon(df_bands, aes(x="age", ymin="ymin", ymax="ymax", fill="band", alpha="band"))
    + scale_fill_manual(values=band_fill_map)
    + scale_alpha_manual(values=band_alpha_map)
    + geom_line(df_boundary, aes(x="age", y="weight", group="percentile"), color=BOYS_BLUE, size=0.5, alpha=0.5)
    + geom_line(df_median, aes(x="age", y="weight"), color=BOYS_BLUE, size=2.5)
    + geom_text(df_labels, aes(x="age", y="weight", label="label"), ha="left", size=3.8, color=INK_SOFT, nudge_x=0.5)
    + geom_line(df_patient, aes(x="age", y="weight"), color=PATIENT_COLOR, size=1.5)
    + geom_point(df_patient, aes(x="age", y="weight"), color=PATIENT_COLOR, fill=PAGE_BG, size=4, stroke=1.0)
    + annotate(
        "text",
        x=patient_ages[-1] + 0.8,
        y=patient_weights[-1],
        label="Patient",
        color=PATIENT_COLOR,
        size=3.5,
        ha="left",
    )
    + labs(x="Age (months)", y="Weight (kg)", title=title)
    + scale_x_continuous(breaks=range(0, 37, 3), limits=(0, 39))
    + scale_y_continuous(breaks=range(2, 20, 2))
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        panel_border=element_blank(),
        panel_grid_major_x=element_blank(),
        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_minor=element_blank(),
        plot_title=element_text(size=12, color=INK),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        legend_position="none",
    )
)

# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

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

Other implementations