{"spec_id":"histogram-epidemic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nhistogram-epidemic: Epidemic Curve (Epi Curve)\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_bar,\n    geom_line,\n    geom_text,\n    geom_vline,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_datetime,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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_COLOR = \"#E0DED8\" if THEME == \"light\" else \"#2A2A27\"\n\n# Imprint categorical palette — first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nCOLOR_LOCKDOWN = IMPRINT_PALETTE[4]  # matte red — restriction / caution\nCOLOR_VACC = IMPRINT_PALETTE[5]  # cyan — health intervention\n\n# Data — simulated foodborne outbreak with point-source shape\nnp.random.seed(42)\n\noutbreak_start = pd.Timestamp(\"2024-03-01\")\nn_days = 45\ndates = pd.date_range(outbreak_start, periods=n_days, freq=\"D\")\n\ndays = np.arange(n_days)\nconfirmed_rate = 120 * np.exp(-0.5 * ((np.log(days + 1) - np.log(12)) / 0.45) ** 2)\nprobable_rate = 35 * np.exp(-0.5 * ((np.log(days + 1) - np.log(14)) / 0.5) ** 2)\nsuspect_rate = 15 * np.exp(-0.5 * ((np.log(days + 1) - np.log(10)) / 0.55) ** 2)\n\nconfirmed_counts = np.random.poisson(np.maximum(confirmed_rate, 0.1)).astype(int)\nprobable_counts = np.random.poisson(np.maximum(probable_rate, 0.1)).astype(int)\nsuspect_counts = np.random.poisson(np.maximum(suspect_rate, 0.1)).astype(int)\n\ndf = pd.DataFrame(\n    {\n        \"onset_date\": np.tile(dates, 3),\n        \"case_count\": np.concatenate([confirmed_counts, probable_counts, suspect_counts]),\n        \"case_type\": [\"Confirmed\"] * n_days + [\"Probable\"] * n_days + [\"Suspect\"] * n_days,\n    }\n)\n\n# Cumulative overlay line (scaled to share primary y-axis)\ndaily_total = confirmed_counts + probable_counts + suspect_counts\ncumulative = np.cumsum(daily_total)\nmax_daily = int(daily_total.max())\nmax_cumulative = int(cumulative[-1])\nscale_factor = max_daily * 0.90 / max_cumulative\n\ndf_cumulative = pd.DataFrame({\"onset_date\": dates, \"scaled_cumulative\": cumulative * scale_factor})\n\n# Intervention markers — ms timestamps required for lets-plot datetime axis\nlockdown_date = pd.Timestamp(\"2024-03-15\")\nvaccination_date = pd.Timestamp(\"2024-04-05\")\nlockdown_ms = lockdown_date.timestamp() * 1000\nvaccination_ms = vaccination_date.timestamp() * 1000\n\ndf_ann_lockdown = pd.DataFrame({\"onset_date\": [lockdown_date], \"y_pos\": [max_daily * 0.94], \"label\": [\"Lockdown\"]})\ndf_ann_vacc = pd.DataFrame({\"onset_date\": [vaccination_date], \"y_pos\": [max_daily * 0.84], \"label\": [\"Vaccination\"]})\n\n# Weekly x-axis breaks\nweekly_dates = pd.date_range(outbreak_start, periods=7, freq=\"7D\")\nweekly_ms = [d.timestamp() * 1000 for d in weekly_dates]\n\n# Title fontsize scaled for length (floor 11px per lets-plot family)\ntitle = \"histogram-epidemic · python · letsplot · anyplot.ai\"\nn_chars = len(title)\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_size = max(11, round(16 * ratio))\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"onset_date\", y=\"case_count\", fill=\"case_type\"))\n    + geom_bar(\n        stat=\"identity\", position=\"stack\", width=0.8, tooltips=layer_tooltips().line(\"@case_type|@case_count cases\")\n    )\n    + geom_line(\n        data=df_cumulative,\n        mapping=aes(x=\"onset_date\", y=\"scaled_cumulative\"),\n        color=INK_SOFT,\n        size=1.2,\n        alpha=0.75,\n        inherit_aes=False,\n    )\n    + geom_vline(xintercept=lockdown_ms, color=COLOR_LOCKDOWN, size=1.0, linetype=\"dashed\")\n    + geom_vline(xintercept=vaccination_ms, color=COLOR_VACC, size=1.0, linetype=\"dashed\")\n    + geom_text(\n        data=df_ann_lockdown,\n        mapping=aes(x=\"onset_date\", y=\"y_pos\", label=\"label\"),\n        color=COLOR_LOCKDOWN,\n        size=4,\n        fontface=\"bold\",\n        hjust=0,\n        nudge_x=80000000,\n        inherit_aes=False,\n    )\n    + geom_text(\n        data=df_ann_vacc,\n        mapping=aes(x=\"onset_date\", y=\"y_pos\", label=\"label\"),\n        color=COLOR_VACC,\n        size=4,\n        fontface=\"bold\",\n        hjust=1,\n        nudge_x=-80000000,\n        inherit_aes=False,\n    )\n    + scale_fill_manual(\n        values={\"Confirmed\": IMPRINT_PALETTE[0], \"Probable\": IMPRINT_PALETTE[1], \"Suspect\": IMPRINT_PALETTE[2]},\n        name=\"Case Classification\",\n    )\n    + scale_x_datetime(name=\"Date of Symptom Onset\", format=\"%b %d\", breaks=weekly_ms)\n    + scale_y_continuous(name=\"Daily New Cases\", format=\"d\")\n    + labs(\n        title=title,\n        subtitle=\"Foodborne outbreak epi curve — daily cases by classification with cumulative trend\",\n        caption=f\"Grey line = cumulative cases (scaled to axis)  ·  {max_cumulative:,} total cases\",\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=title_size, color=INK, face=\"bold\"),\n        plot_subtitle=element_text(size=12, color=INK_SOFT),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=10, color=INK, face=\"bold\"),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        plot_caption=element_text(size=9, color=INK_MUTED, hjust=0.5),\n        legend_position=[0.82, 0.28],\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=GRID_COLOR, size=0.5),\n    )\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}