{"spec_id":"line-growth-percentile","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-growth-percentile: Pediatric Growth Chart with Percentile Curves\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-20\n\"\"\"\n# ruff: noqa: F405\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme-adaptive chrome tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nGRID = \"#E0DDD6\" if THEME == \"light\" else \"#2E2E2A\"\n\n# Imprint categorical palette — positions used by name, not ordinal\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n# Patient trajectory: Imprint position 5 (matte red — semantic: clinical concern / growth faltering)\nPATIENT_COLOR = IMPRINT_PALETTE[4]  # \"#AE3030\"\n\n# ---------------------------------------------------------------------------\n# Data — WHO-approximated weight-for-age for boys 0-36 months\n# ---------------------------------------------------------------------------\nnp.random.seed(42)\nage_months = np.arange(0, 37, 1)\n\n# Smooth log-growth approximating WHO median for boys\npercentile_50 = 3.3 + 3.8 * np.log1p(age_months * 0.6)\n\n# SD widens with age (heteroscedastic spread)\nspread = 0.25 + 0.03 * age_months\npercentile_3 = percentile_50 - 1.88 * spread\npercentile_10 = percentile_50 - 1.28 * spread\npercentile_25 = percentile_50 - 0.67 * spread\npercentile_75 = percentile_50 + 0.67 * spread\npercentile_90 = percentile_50 + 1.28 * spread\npercentile_97 = percentile_50 + 1.88 * spread\n\n# Individual patient — growth faltering episode (months 9–18) then catch-up\npatient_ages = np.array([0, 1, 2, 4, 6, 9, 12, 15, 18, 24, 30, 36])\npatient_weights = np.array([3.5, 4.6, 5.8, 7.2, 8.1, 9.0, 9.6, 10.1, 10.8, 12.4, 14.0, 15.2])\n\ndf_bands = pd.DataFrame(\n    {\n        \"age\": age_months,\n        \"p3\": percentile_3,\n        \"p10\": percentile_10,\n        \"p25\": percentile_25,\n        \"p50\": percentile_50,\n        \"p75\": percentile_75,\n        \"p90\": percentile_90,\n        \"p97\": percentile_97,\n    }\n)\n\np50_at_patient = np.interp(patient_ages, age_months, percentile_50)\np25_at_patient = np.interp(patient_ages, age_months, percentile_25)\np75_at_patient = np.interp(patient_ages, age_months, percentile_75)\ndf_patient = pd.DataFrame(\n    {\n        \"age\": patient_ages,\n        \"weight\": patient_weights,\n        \"p50_ref\": np.round(p50_at_patient, 1),\n        \"p25_ref\": np.round(p25_at_patient, 1),\n        \"p75_ref\": np.round(p75_at_patient, 1),\n    }\n)\n\n# Right-margin percentile labels\nlabel_x = 36.5\ndf_labels = pd.DataFrame(\n    {\n        \"age\": [label_x] * 7,\n        \"weight\": [\n            percentile_3[-1],\n            percentile_10[-1],\n            percentile_25[-1],\n            percentile_50[-1],\n            percentile_75[-1],\n            percentile_90[-1],\n            percentile_97[-1],\n        ],\n        \"label\": [\"P3\", \"P10\", \"P25\", \"P50\", \"P75\", \"P90\", \"P97\"],\n    }\n)\n\n# ---------------------------------------------------------------------------\n# Color scheme — Imprint palette members for boys convention, graduated by proximity to median\n# Outer extremes: Imprint blue (#4467A3, slot 3); inner/mid: Imprint cyan (#2ABCCD, slot 6)\n# ---------------------------------------------------------------------------\nblue_outer = IMPRINT_PALETTE[2]  # \"#4467A3\" — extreme percentiles (P3, P97)\nblue_mid = IMPRINT_PALETTE[5]  # \"#2ABCCD\" — mid bands (P10, P90)\nblue_inner = IMPRINT_PALETTE[5]  # \"#2ABCCD\" — inner band (P25, P75), lower alpha → appears lighter\nblue_median = IMPRINT_PALETTE[2]  # \"#4467A3\" — median P50 line\n\n# Rich tooltips — lets-plot-native structured interactivity\npatient_tooltips = (\n    layer_tooltips()\n    .title(\"Patient Visit\")\n    .line(\"Age|@age months\")\n    .line(\"Weight|@weight kg\")\n    .line(\"P50 ref|@p50_ref kg\")\n    .line(\"P25–P75|@p25_ref – @p75_ref kg\")\n)\n\n# ---------------------------------------------------------------------------\n# Plot\n# ---------------------------------------------------------------------------\nplot = (\n    ggplot()\n    # Percentile bands — graduated opacity (darker at extremes, lighter near median)\n    + geom_ribbon(aes(x=\"age\", ymin=\"p3\", ymax=\"p10\"), data=df_bands, fill=blue_outer, alpha=0.35, tooltips=\"none\")\n    + geom_ribbon(aes(x=\"age\", ymin=\"p90\", ymax=\"p97\"), data=df_bands, fill=blue_outer, alpha=0.35, tooltips=\"none\")\n    + geom_ribbon(aes(x=\"age\", ymin=\"p10\", ymax=\"p25\"), data=df_bands, fill=blue_mid, alpha=0.30, tooltips=\"none\")\n    + geom_ribbon(aes(x=\"age\", ymin=\"p75\", ymax=\"p90\"), data=df_bands, fill=blue_mid, alpha=0.30, tooltips=\"none\")\n    + geom_ribbon(aes(x=\"age\", ymin=\"p25\", ymax=\"p75\"), data=df_bands, fill=blue_inner, alpha=0.28, tooltips=\"none\")\n    # Percentile boundary lines — improved visibility\n    + geom_line(aes(x=\"age\", y=\"p3\"), data=df_bands, color=blue_outer, size=0.9, alpha=0.75, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p10\"), data=df_bands, color=blue_mid, size=0.8, alpha=0.70, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p25\"), data=df_bands, color=blue_inner, size=0.7, alpha=0.70, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p50\"), data=df_bands, color=blue_median, size=1.8, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p75\"), data=df_bands, color=blue_inner, size=0.7, alpha=0.70, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p90\"), data=df_bands, color=blue_mid, size=0.8, alpha=0.70, tooltips=\"none\")\n    + geom_line(aes(x=\"age\", y=\"p97\"), data=df_bands, color=blue_outer, size=0.9, alpha=0.75, tooltips=\"none\")\n    # Patient trajectory — Imprint #AE3030 (semantic: clinical growth concern)\n    + geom_line(aes(x=\"age\", y=\"weight\"), data=df_patient, color=PATIENT_COLOR, size=1.4, tooltips=\"none\")\n    + geom_point(\n        aes(x=\"age\", y=\"weight\"),\n        data=df_patient,\n        color=PATIENT_COLOR,\n        fill=\"white\",\n        shape=21,\n        size=3.0,\n        stroke=1.5,\n        tooltips=patient_tooltips,\n    )\n    # Right-margin percentile labels\n    + geom_text(\n        aes(x=\"age\", y=\"weight\", label=\"label\"), data=df_labels, size=5, color=INK_MUTED, hjust=0, tooltips=\"none\"\n    )\n    # Axis formatting\n    + scale_x_continuous(breaks=list(range(0, 37, 6)), limits=[0, 40], format=\"{d}\")\n    + scale_y_continuous(format=\".1f\")\n    + coord_cartesian(ylim=[1, 18])\n    + labs(\n        title=\"Boys Weight-for-Age · line-growth-percentile · python · letsplot · anyplot.ai\",\n        x=\"Age (months)\",\n        y=\"Weight (kg)\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.4),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=GRID, size=0.4),\n        legend_position=\"none\",\n        plot_margin=[20, 70, 15, 15],\n    )\n    + ggsize(800, 450)\n)\n\n# Save both PNG and HTML (interactive) — theme-suffixed\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}