{"spec_id":"histogram-epidemic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nhistogram-epidemic: Epidemic Curve (Epi Curve)\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file (bokeh.py) from shadowing the installed bokeh package when\n# Python prepends the script's directory to sys.path at startup.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import (\n    ColumnDataSource,\n    HoverTool,\n    Label,\n    Legend,\n    LegendItem,\n    LinearAxis,\n    NumeralTickFormatter,\n    Range1d,\n    Span,\n)\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette — default-style-guide.md \"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\"\n\n# Imprint categorical palette — position 1 is ALWAYS first series\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — simulated foodborne illness outbreak over 90 days\nnp.random.seed(42)\n\nstart_date = pd.Timestamp(\"2024-01-15\")\ndates = pd.date_range(start_date, periods=90, freq=\"D\")\ndays = np.arange(90)\n\n# Primary wave: sharp peak ~day 12 (point-source contaminated event)\nconfirmed_wave1 = np.random.poisson(lam=np.clip(45 * np.exp(-0.5 * ((days - 12) / 3.5) ** 2), 0.5, None))\n# Secondary propagated wave ~day 35\nconfirmed_wave2 = np.random.poisson(lam=np.clip(20 * np.exp(-0.5 * ((days - 35) / 6) ** 2), 0.2, None))\n# Low endemic tail\nconfirmed_tail = np.random.poisson(lam=np.clip(1.5 * np.exp(-0.03 * days), 0.1, None))\nconfirmed = confirmed_wave1 + confirmed_wave2 + confirmed_tail\n\nprobable = np.random.poisson(\n    lam=np.clip(12 * np.exp(-0.5 * ((days - 14) / 4) ** 2) + 7 * np.exp(-0.5 * ((days - 37) / 7) ** 2), 0.1, None)\n)\nsuspect = np.random.poisson(\n    lam=np.clip(5 * np.exp(-0.5 * ((days - 13) / 5) ** 2) + 3 * np.exp(-0.5 * ((days - 36) / 8) ** 2), 0.05, None)\n)\n\ndf = pd.DataFrame({\"date\": dates, \"confirmed\": confirmed, \"probable\": probable, \"suspect\": suspect})\ndf[\"total\"] = df[\"confirmed\"] + df[\"probable\"] + df[\"suspect\"]\ndf[\"cumulative\"] = df[\"total\"].cumsum()\ndf[\"date_str\"] = df[\"date\"].dt.strftime(\"%b %d\")\nbar_width = 0.8 * 24 * 60 * 60 * 1000  # 0.8 day in milliseconds\n\nsource = ColumnDataSource(\n    data={\n        \"date\": df[\"date\"],\n        \"date_str\": df[\"date_str\"],\n        \"confirmed\": df[\"confirmed\"],\n        \"probable\": df[\"probable\"],\n        \"suspect\": df[\"suspect\"],\n        \"total\": df[\"total\"],\n        \"cumulative\": df[\"cumulative\"],\n    }\n)\n\nstack_labels = [\"confirmed\", \"probable\", \"suspect\"]\ndisplay_labels = [\"Confirmed\", \"Probable\", \"Suspect\"]\ncolors = IMPRINT_PALETTE[:3]  # #009E73, #C475FD, #4467A3\n\ntitle = \"histogram-epidemic · python · bokeh · anyplot.ai\"\n# 49 chars < 67 baseline — no fontsize scaling needed\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Date of Symptom Onset\",\n    y_axis_label=\"New Cases (per day)\",\n    x_axis_type=\"datetime\",\n    toolbar_location=None,  # prevent ~30-50px toolbar bloat above canvas\n    min_border_bottom=160,  # room for 34pt tick + 42pt axis label\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=200,  # extra room for right secondary-axis label\n)\n\n# Stacked bars (idiomatic Bokeh)\nrenderers = p.vbar_stack(\n    stack_labels, x=\"date\", width=bar_width, color=colors, source=source, line_color=PAGE_BG, line_width=0.5, alpha=0.9\n)\n\nhover = HoverTool(\n    renderers=list(renderers),\n    tooltips=[\n        (\"Date\", \"@date_str\"),\n        (\"Confirmed\", \"@confirmed\"),\n        (\"Probable\", \"@probable\"),\n        (\"Suspect\", \"@suspect\"),\n        (\"Total\", \"@total\"),\n        (\"Cumulative\", \"@cumulative{0,0}\"),\n    ],\n    mode=\"vline\",\n)\np.add_tools(hover)\n\n# Intervention lines — matte red (#AE3030) for source; INK_SOFT for response\ncontamination_date = pd.Timestamp(\"2024-01-27\")\nintervention_date = pd.Timestamp(\"2024-02-05\")\n\np.add_layout(\n    Span(\n        location=contamination_date,\n        dimension=\"height\",\n        line_color=IMPRINT_PALETTE[4],  # #AE3030 — source / error semantic\n        line_width=3,\n        line_dash=\"dashed\",\n        line_alpha=0.8,\n    )\n)\np.add_layout(\n    Span(\n        location=intervention_date,\n        dimension=\"height\",\n        line_color=INK_SOFT,\n        line_width=3,\n        line_dash=\"dashed\",\n        line_alpha=0.8,\n    )\n)\n\nmax_cases = int(df[\"total\"].max())\n\np.add_layout(\n    Label(\n        x=contamination_date,\n        y=max_cases * 0.95,\n        text=\"Source Identified\",\n        text_font_size=\"28pt\",\n        text_color=IMPRINT_PALETTE[4],\n        text_font_style=\"bold\",\n        x_offset=10,\n    )\n)\np.add_layout(\n    Label(\n        x=intervention_date,\n        y=max_cases * 0.82,\n        text=\"Intervention Began\",\n        text_font_size=\"28pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"bold\",\n        x_offset=10,\n    )\n)\n\n# Secondary y-axis — cumulative burden line\ncumulative_max = int(df[\"cumulative\"].max())\np.extra_y_ranges = {\"cumulative\": Range1d(start=0, end=cumulative_max * 1.1)}\n\ncumulative_axis = LinearAxis(\n    y_range_name=\"cumulative\",\n    axis_label=\"Cumulative Cases\",\n    axis_label_text_font_size=\"42pt\",\n    axis_label_text_color=INK,\n    major_label_text_font_size=\"34pt\",\n    major_label_text_color=INK_SOFT,\n    axis_line_color=INK_SOFT,\n    minor_tick_line_color=None,\n    major_tick_line_color=INK_SOFT,\n    formatter=NumeralTickFormatter(format=\"0,0\"),\n)\np.add_layout(cumulative_axis, \"right\")\n\nsource_cumulative = ColumnDataSource(data={\"date\": df[\"date\"], \"cumulative\": df[\"cumulative\"]})\nr_cumulative = p.line(\n    x=\"date\",\n    y=\"cumulative\",\n    source=source_cumulative,\n    line_color=INK,\n    line_width=3,\n    line_alpha=0.55,\n    y_range_name=\"cumulative\",\n)\n\n# Legend\nlegend_items = [LegendItem(label=lbl, renderers=[r]) for lbl, r in zip(display_labels, renderers, strict=False)]\nlegend_items.append(LegendItem(label=f\"Cumulative (total: {cumulative_max:,})\", renderers=[r_cumulative]))\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=50,\n    glyph_height=30,\n    spacing=14,\n    padding=20,\n    background_fill_alpha=0.9,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    border_line_alpha=0.5,\n)\np.add_layout(legend, \"center\")\n\n# Typography (canonical bokeh.md sizing: title 50pt, labels 42pt, ticks 34pt)\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis[0].axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis[0].axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis[0].major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis[0].major_label_text_color = INK_SOFT\np.yaxis[0].formatter = NumeralTickFormatter(format=\"0,0\")\n\n# Grid\np.xgrid.visible = False\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_width = 1\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis[0].axis_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis[0].minor_tick_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis[0].major_tick_line_color = INK_SOFT\n\np.y_range.start = 0\np.y_range.end = max_cases * 1.15\n\n# Save HTML (interactive catalog artifact)\noutput_file(f\"plot-{THEME}.html\", title=title)\nsave(p)\n\n# Screenshot via headless Chrome — Selenium 4 auto-resolves the driver\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# CDP override ensures exact viewport — window-size alone is eaten by browser chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# Belt-and-braces: pin saved PNG to exact target dims so the post-render gate passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}