{"spec_id":"histogram-epidemic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nhistogram-epidemic: Epidemic Curve (Epi Curve)\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-02\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 default-style-guide.md)\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\"\n\n# Imprint palette — case classification + cumulative line\nCONFIRMED_COLOR = \"#009E73\"  # Imprint position 1 — brand green\nPROBABLE_COLOR = \"#C475FD\"  # Imprint position 2 — lavender\nSUSPECT_COLOR = \"#4467A3\"  # Imprint position 3 — blue\nCUMULATIVE_COLOR = \"#BD8233\"  # Imprint position 4 — ochre (cumulative line)\n\n# Data\nnp.random.seed(42)\n\ndates = pd.date_range(\"2024-01-15\", periods=120, freq=\"D\")\n\ndays = np.arange(120)\nwave1 = 80 * np.exp(-0.5 * ((days - 25) / 6) ** 2)\nwave2 = 45 * np.exp(-0.5 * ((days - 55) / 8) ** 2)\nwave3 = 25 * np.exp(-0.5 * ((days - 85) / 10) ** 2)\nbase_rate = wave1 + wave2 + wave3 + 2\n\nconfirmed_frac = np.clip(0.6 + 0.2 * np.sin(days / 15), 0.4, 0.85)\nprobable_frac = np.clip(0.25 - 0.05 * np.sin(days / 15), 0.1, 0.35)\n\ntotal_cases = np.round(base_rate + np.random.poisson(2, 120)).astype(int)\nconfirmed = np.round(total_cases * confirmed_frac).astype(int)\nprobable = np.round(total_cases * probable_frac).astype(int)\nsuspect = np.clip(total_cases - confirmed - probable, 0, None).astype(int)\n\ndf = pd.DataFrame(\n    {\n        \"onset_date\": np.tile(dates, 3),\n        \"case_count\": np.concatenate([confirmed, probable, suspect]),\n        \"case_type\": [\"Confirmed\"] * 120 + [\"Probable\"] * 120 + [\"Suspect\"] * 120,\n    }\n)\n\n# Cumulative totals\ndaily_total = pd.DataFrame({\"onset_date\": dates, \"daily_total\": total_cases})\ndaily_total[\"cumulative\"] = daily_total[\"daily_total\"].cumsum()\nmax_daily = int(total_cases.max()) + 15\n\n# Intervention events — staggered y positions to avoid bar interference\nevents = pd.DataFrame(\n    {\n        \"date\": pd.to_datetime([\"2024-02-10\", \"2024-03-01\", \"2024-03-20\"]),\n        \"event\": [\"Source identified\", \"Containment measures\", \"Vaccination campaign\"],\n        \"y_pos\": [max_daily * 0.90, max_daily * 0.73, max_daily * 0.56],\n    }\n)\n\n# Stacked bars with Imprint palette\ntype_order = [\"Confirmed\", \"Probable\", \"Suspect\"]\ncolor_scale = alt.Scale(domain=type_order, range=[CONFIRMED_COLOR, PROBABLE_COLOR, SUSPECT_COLOR])\n\nbars = (\n    alt.Chart(df)\n    .mark_bar(stroke=PAGE_BG, strokeWidth=0.5)\n    .encode(\n        x=alt.X(\n            \"onset_date:T\",\n            title=\"Date of Symptom Onset\",\n            axis=alt.Axis(format=\"%b %d\", labelAngle=-45, tickCount=\"week\"),\n        ),\n        y=alt.Y(\"case_count:Q\", title=\"New Cases\", scale=alt.Scale(domain=[0, max_daily])),\n        color=alt.Color(\"case_type:N\", scale=color_scale, sort=type_order, title=\"Classification\"),\n        order=alt.Order(\"order:Q\"),\n        tooltip=[\n            alt.Tooltip(\"onset_date:T\", title=\"Date\", format=\"%b %d, %Y\"),\n            alt.Tooltip(\"case_type:N\", title=\"Type\"),\n            alt.Tooltip(\"case_count:Q\", title=\"Cases\"),\n        ],\n    )\n    .transform_calculate(order=\"{'Confirmed': 0, 'Probable': 1, 'Suspect': 2}[datum.case_type]\")\n)\n\n# Cumulative line with independent right y-axis\ncumulative_line = (\n    alt.Chart(daily_total)\n    .mark_line(strokeWidth=2.5, interpolate=\"monotone\", color=CUMULATIVE_COLOR)\n    .encode(\n        x=\"onset_date:T\",\n        y=alt.Y(\n            \"cumulative:Q\",\n            title=\"Cumulative Cases\",\n            axis=alt.Axis(titleColor=CUMULATIVE_COLOR, labelColor=CUMULATIVE_COLOR, format=\",.0f\"),\n        ),\n        tooltip=[\n            alt.Tooltip(\"onset_date:T\", title=\"Date\", format=\"%b %d, %Y\"),\n            alt.Tooltip(\"cumulative:Q\", title=\"Cumulative Cases\", format=\",\"),\n        ],\n    )\n)\n\n# Vertical intervention rules\nrules = alt.Chart(events).mark_rule(strokeDash=[6, 4], strokeWidth=1.5, color=INK_SOFT).encode(x=\"date:T\")\n\n# Event labels — horizontal at staggered heights, avoiding bar interference\nrule_labels = (\n    alt.Chart(events)\n    .mark_text(align=\"left\", dx=4, fontSize=11, fontStyle=\"italic\", color=INK_MUTED)\n    .encode(x=\"date:T\", y=\"y_pos:Q\", text=\"event:N\")\n)\n\n# Peak annotation\npeak_day = int(np.argmax(total_cases))\npeak_data = pd.DataFrame(\n    {\n        \"onset_date\": [dates[peak_day]],\n        \"peak_val\": [int(total_cases[peak_day])],\n        \"label\": [f\"Peak: {int(total_cases[peak_day])} cases\"],\n    }\n)\n\npeak_label = (\n    alt.Chart(peak_data)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=CUMULATIVE_COLOR, dy=-10, dx=30)\n    .encode(x=\"onset_date:T\", y=\"peak_val:Q\", text=\"label:N\")\n)\n\n# Title scaling (67-char baseline)\ntitle_str = \"histogram-epidemic · python · altair · anyplot.ai\"\ntitle_fontsize = round(16 * min(1.0, 67 / len(title_str)))\n\n# Layer and compose\nbar_layer = alt.layer(bars, rules, rule_labels, peak_label)\n\nchart = (\n    alt.layer(bar_layer, cumulative_line)\n    .resolve_scale(y=\"independent\")\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_str,\n            fontSize=title_fontsize,\n            anchor=\"start\",\n            color=INK,\n            subtitle=\"Daily new cases by classification with cumulative total\",\n            subtitleFontSize=10,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridOpacity=0.15,\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        domainWidth=0,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_legend(\n        titleFontSize=10,\n        labelFontSize=10,\n        symbolSize=150,\n        orient=\"top-right\",\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        padding=6,\n        cornerRadius=4,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_title(color=INK, anchor=\"start\")\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad PNG to exact target 3200 × 1800 (altair.md canvas contract)\nTW, TH = 3200, 1800\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\nchart.save(f\"plot-{THEME}.html\")\n"}