{"spec_id":"line-growth-percentile","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-growth-percentile: Pediatric Growth Chart with Percentile Curves\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette\nPATIENT_COLOR = \"#009E73\"  # pos 1 — patient trajectory (first active series)\nBAND_COLOR = \"#4467A3\"  # pos 3 (blue) — reference bands, boys by clinical convention\n\n# Data — WHO-style weight-for-age reference for boys, 0–36 months\nnp.random.seed(42)\nage_months = np.arange(0, 37, 1)\n\nmedian = 3.3 + 7.5 * (1 - np.exp(-0.08 * age_months)) + 0.12 * age_months\nsd_base = 0.35 + 0.03 * age_months\n\npercentile_names = [\"P3\", \"P10\", \"P25\", \"P50\", \"P75\", \"P90\", \"P97\"]\nz_scores = [-1.88, -1.28, -0.67, 0.0, 0.67, 1.28, 1.88]\n\nref_df = pd.DataFrame({\"age_months\": age_months})\nfor name, z in zip(percentile_names, z_scores, strict=True):\n    ref_df[name] = median + z * sd_base\n\n# Individual patient: healthy boy tracked at well-child visits\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.7, 7.2, 8.3, 9.5, 10.4, 11.2, 12.0, 13.1, 14.3, 15.6])\npatient_df = pd.DataFrame({\"age_months\": patient_ages, \"weight\": patient_weights})\n\n# Title — 53 chars < 67 baseline, default fontSize is fine\ntitle_str = \"line-growth-percentile · python · altair · anyplot.ai\"\ntitle_fontsize = 16\n\n# Percentile band layers — graduated opacity (darker near extremes, lighter near median)\nband_defs = [\n    (\"P3\", \"P10\", 0.36),\n    (\"P10\", \"P25\", 0.22),\n    (\"P25\", \"P75\", 0.14),\n    (\"P75\", \"P90\", 0.22),\n    (\"P90\", \"P97\", 0.36),\n]\n\nband_layers = []\nfor lower, upper, opacity in band_defs:\n    band = (\n        alt.Chart(ref_df)\n        .mark_area(opacity=opacity, color=BAND_COLOR)\n        .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(f\"{lower}:Q\"), y2=alt.Y2(f\"{upper}:Q\"))\n    )\n    band_layers.append(band)\n\n# Percentile reference lines via idiomatic transform_fold\nline_base = alt.Chart(ref_df).transform_fold(fold=percentile_names, as_=[\"percentile\", \"weight\"])\n\np50_line = (\n    line_base.transform_filter(alt.datum.percentile == \"P50\")\n    .mark_line(strokeWidth=2.5, opacity=1.0)\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"weight:Q\"), color=alt.value(BAND_COLOR))\n)\n\nother_lines = (\n    line_base.transform_filter(alt.datum.percentile != \"P50\")\n    .mark_line(strokeWidth=0.8, opacity=0.55)\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"weight:Q\"), color=alt.value(BAND_COLOR), detail=\"percentile:N\")\n)\n\n# Right-margin percentile labels with nudging to reduce crowding\nlabel_values = {p: ref_df[p].iloc[-1] for p in percentile_names}\nnudge = {\"P3\": -0.1, \"P10\": 0.15, \"P25\": -0.2, \"P50\": 0.05, \"P75\": 0.25, \"P90\": -0.05, \"P97\": 0.05}\nlabel_df = pd.DataFrame(\n    {\n        \"age_months\": [37.3] * 7,\n        \"value\": [label_values[p] + nudge[p] for p in percentile_names],\n        \"label\": percentile_names,\n    }\n)\n\npercentile_text = (\n    alt.Chart(label_df)\n    .mark_text(align=\"left\", dx=2, fontSize=10, fontWeight=\"bold\", font=\"Helvetica Neue, Arial, sans-serif\")\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"value:Q\"), text=\"label:N\", color=alt.value(INK_SOFT))\n)\n\n# Patient trajectory with interactive hover (distinctive Altair feature)\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"age_months\"], empty=False)\n\npatient_line = (\n    alt.Chart(patient_df)\n    .mark_line(strokeWidth=2.5, interpolate=\"monotone\")\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"weight:Q\"), color=alt.value(PATIENT_COLOR))\n)\n\npatient_points = (\n    alt.Chart(patient_df)\n    .mark_circle(stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        x=alt.X(\"age_months:Q\"),\n        y=alt.Y(\"weight:Q\"),\n        color=alt.value(PATIENT_COLOR),\n        size=alt.condition(nearest, alt.value(160), alt.value(80)),\n        tooltip=[\n            alt.Tooltip(\"age_months:Q\", title=\"Age (months)\"),\n            alt.Tooltip(\"weight:Q\", title=\"Weight (kg)\", format=\".1f\"),\n        ],\n    )\n    .add_params(nearest)\n)\n\npatient_label_df = pd.DataFrame({\"age_months\": [26], \"weight\": [13.5], \"label\": [\"Patient A\"]})\npatient_label = (\n    alt.Chart(patient_label_df)\n    .mark_text(align=\"left\", dx=4, dy=-14, fontSize=9, fontWeight=\"bold\", font=\"Helvetica Neue, Arial, sans-serif\")\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"weight:Q\"), text=\"label:N\", color=alt.value(PATIENT_COLOR))\n)\n\n# Storytelling callout: patient's approximate percentile at final visit\ncallout_df = pd.DataFrame({\"age_months\": [36], \"weight\": [15.6], \"label\": [\"≈ P75\"]})\ncallout = (\n    alt.Chart(callout_df)\n    .mark_text(align=\"right\", dx=-6, dy=-12, fontSize=9, fontStyle=\"italic\", font=\"Helvetica Neue, Arial, sans-serif\")\n    .encode(x=alt.X(\"age_months:Q\"), y=alt.Y(\"weight:Q\"), text=\"label:N\", color=alt.value(PATIENT_COLOR))\n)\n\n# Compose all layers\nchart = (\n    alt.layer(\n        *band_layers, other_lines, p50_line, percentile_text, patient_line, patient_points, patient_label, callout\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_str,\n            fontSize=title_fontsize,\n            color=INK,\n            font=\"Helvetica Neue, Arial, sans-serif\",\n            subtitle=[\"Boys Weight-for-Age (0–36 months) · WHO Reference Standard\"],\n            subtitleFontSize=10,\n            subtitleColor=INK_MUTED,\n            subtitleFont=\"Helvetica Neue, Arial, sans-serif\",\n            anchor=\"start\",\n            offset=8,\n        ),\n    )\n    .configure_view(continuousWidth=620, continuousHeight=320, fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.15,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        titlePadding=8,\n        labelFont=\"Helvetica Neue, Arial, sans-serif\",\n        titleFont=\"Helvetica Neue, Arial, sans-serif\",\n    )\n    .configure_axisX(grid=False, tickCount=12)\n    .configure_axisY(grid=True, tickCount=8)\n    .resolve_scale(y=\"shared\")\n    .encode(\n        x=alt.X(\"age_months:Q\", title=\"Age (months)\", scale=alt.Scale(domain=[0, 41])),\n        y=alt.Y(title=\"Weight (kg)\", scale=alt.Scale(domain=[0, 19])),\n    )\n)\n\n# Save PNG and pad to exact canvas target (3200 × 1800 landscape)\nTW, TH = 3200, 1800\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}