{"spec_id":"histogram-epidemic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nhistogram-epidemic: Epidemic Curve (Epi Curve)\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens (Imprint palette reference)\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 categorical palette — first series always #009E73\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data — simulated respiratory outbreak with two waves (propagated transmission)\nnp.random.seed(42)\ndates = pd.date_range(\"2024-01-15\", periods=90, freq=\"D\")\n\ndays = np.arange(90)\nwave1 = 35 * np.exp(-0.5 * ((days - 20) / 7) ** 2)\nwave2 = 50 * np.exp(-0.5 * ((days - 55) / 9) ** 2)\nbaseline = 2 + 3 * np.random.rand(90)\ntotal_signal = wave1 + wave2 + baseline\n\nconfirmed_frac = np.clip(0.6 + 0.15 * np.sin(days / 15), 0.45, 0.75)\nprobable_frac = np.clip(0.25 + 0.05 * np.cos(days / 10), 0.15, 0.35)\nsuspect_frac = 1.0 - confirmed_frac - probable_frac\n\nconfirmed = np.round(total_signal * confirmed_frac).astype(int)\nprobable = np.round(total_signal * probable_frac).astype(int)\nsuspect = np.round(total_signal * suspect_frac).astype(int)\ndaily_total = confirmed + probable + suspect\n\n# Intervention events with spaced dates to avoid label crowding\ninterventions = {\n    10: \"Cluster Identified\",\n    28: \"Contact Tracing\",\n    42: \"Quarantine Order\",\n    62: \"Vaccination Drive\",\n    80: \"Outbreak Contained\",\n}\n\n# X-axis labels — intervention dates get distinct triangle marker + event name\ndate_labels = []\nfor i, d in enumerate(dates):\n    fmt = d.strftime(\"%b %d\")\n    if i in interventions:\n        date_labels.append(f\"▼ {interventions[i]}\")\n    else:\n        date_labels.append(fmt)\n\n# Major labels: monthly anchors + intervention dates, de-crowded\nmonthly_set = {0, 31, 59, 89}\nintervention_set = set(interventions.keys())\nmajor_indices = sorted(monthly_set | intervention_set)\nfiltered_indices = []\nfor idx in major_indices:\n    if idx in intervention_set:\n        filtered_indices.append(idx)\n    elif all(abs(idx - iv) > 5 for iv in intervention_set):\n        filtered_indices.append(idx)\nmajor_labels = [date_labels[i] for i in filtered_indices]\n\n# Build series with rich tooltip dicts for interactive HTML\nconfirmed_series = []\nprobable_series = []\nsuspect_series = []\nfor i in range(90):\n    day_str = dates[i].strftime(\"%b %d, %Y\")\n    total_day = int(daily_total[i])\n    event = interventions.get(i)\n    tip = f\"{day_str} — {total_day} total cases\"\n    if event:\n        tip = f\"⚠ {event}\\n{tip}\"\n    confirmed_series.append({\"value\": int(confirmed[i]), \"label\": tip})\n    probable_series.append({\"value\": int(probable[i]), \"label\": tip})\n    suspect_series.append({\"value\": int(suspect[i]), \"label\": tip})\n\n# Title font size scaled for length (formula: round(66 * 67 / len(title)))\ntitle = \"Epidemic Curve (Respiratory Outbreak) · histogram-epidemic · python · pygal · anyplot.ai\"\ntitle_font_size = round(66 * 67 / len(title))  # prevents overflow at 3200 px\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    tooltip_font_size=36,\n    stroke_width=2.5,\n    opacity=0.92,\n    opacity_hover=1.0,\n)\n\nchart = pygal.StackedBar(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Date of Symptom Onset\",\n    y_title=\"New Cases (Daily)\",\n    show_y_guides=True,\n    show_x_guides=True,\n    legend_at_bottom=True,\n    legend_box_size=28,\n    legend_at_bottom_columns=3,\n    margin=60,\n    margin_bottom=140,\n    margin_right=80,\n    spacing=2,\n    rounded_bars=3,\n    truncate_legend=-1,\n    truncate_label=-1,\n    x_label_rotation=45,\n    show_minor_x_labels=False,\n    print_values=False,\n    range=(0, int(np.max(daily_total) * 1.1)),\n    value_formatter=lambda x: f\"{int(x):,}\" if x else \"\",\n)\n\nchart.x_labels = date_labels\nchart.x_labels_major = major_labels\n# Vertical reference lines at intervention x-positions\nchart.x_guides = list(interventions.keys())\n\nchart.add(\"Confirmed\", confirmed_series)\nchart.add(\"Probable\", probable_series)\nchart.add(\"Suspect\", suspect_series)\n\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}