{"spec_id":"curve-dose-response","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncurve-dose-response: Pharmacological Dose-Response Curve\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Band, ColumnDataSource, Label, Legend, LegendItem, Span, Whisker\nfrom bokeh.plotting import figure\nfrom scipy.optimize import curve_fit\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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 categorical palette — first series always #009E73\nCOLOR_A = \"#009E73\"  # brand green — Compound A\nCOLOR_B = \"#C475FD\"  # lavender — Compound B\n\n# Data\nnp.random.seed(42)\n\nconcentrations = np.array([1e-9, 3e-9, 1e-8, 3e-8, 1e-7, 3e-7, 1e-6, 3e-6, 1e-5, 3e-5, 1e-4])\n\n\ndef logistic_4pl(x, bottom, top, ec50, hill):\n    return bottom + (top - bottom) / (1 + (ec50 / x) ** hill)\n\n\n# Compound A — potent agonist with EC50 ~100 nM\ntrue_params_a = [5, 95, 1e-7, 1.2]\nresponse_a_mean = logistic_4pl(concentrations, *true_params_a)\nresponse_a_sem = np.random.uniform(2, 5, len(concentrations))\nresponse_a_raw = response_a_mean + np.random.normal(0, 3, len(concentrations))\n\n# Compound B — less potent agonist with EC50 ~1 µM\ntrue_params_b = [8, 85, 1e-6, 1.5]\nresponse_b_mean = logistic_4pl(concentrations, *true_params_b)\nresponse_b_sem = np.random.uniform(2, 6, len(concentrations))\nresponse_b_raw = response_b_mean + np.random.normal(0, 3, len(concentrations))\n\n# Fit 4PL to noisy data\npopt_a, pcov_a = curve_fit(logistic_4pl, concentrations, response_a_raw, p0=[0, 100, 1e-7, 1], maxfev=10000)\npopt_b, pcov_b = curve_fit(logistic_4pl, concentrations, response_b_raw, p0=[0, 100, 1e-6, 1], maxfev=10000)\n\n# Smooth fitted curves\nconc_smooth = np.logspace(-9.5, -3.5, 300)\nfit_a = logistic_4pl(conc_smooth, *popt_a)\nfit_b = logistic_4pl(conc_smooth, *popt_b)\n\n# 95% CI for Compound A via parameter covariance sampling\nn_samples = 200\nci_samples = np.zeros((n_samples, len(conc_smooth)))\nfor i in range(n_samples):\n    sampled_params = np.random.multivariate_normal(popt_a, pcov_a)\n    ci_samples[i] = logistic_4pl(conc_smooth, *sampled_params)\nci_lower_a = np.percentile(ci_samples, 2.5, axis=0)\nci_upper_a = np.percentile(ci_samples, 97.5, axis=0)\n\n# EC50 and half-response values from fitted parameters\nec50_a = popt_a[2]\nec50_b = popt_b[2]\nhalf_response_a = popt_a[0] + (popt_a[1] - popt_a[0]) / 2\nhalf_response_b = popt_b[0] + (popt_b[1] - popt_b[0]) / 2\nfold_diff = ec50_b / ec50_a\n\n# Plot — 3200×1800 canonical landscape canvas\ntitle = \"curve-dose-response · python · bokeh · anyplot.ai\"\n\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Concentration (M)\",\n    y_axis_label=\"Response (%)\",\n    x_axis_type=\"log\",\n    x_range=(3e-10, 3e-4),\n    y_range=(-5, 108),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Confidence band — Compound A 95% CI\nband_source = ColumnDataSource(data={\"conc\": conc_smooth, \"lower\": ci_lower_a, \"upper\": ci_upper_a})\nband = Band(\n    base=\"conc\",\n    lower=\"lower\",\n    upper=\"upper\",\n    source=band_source,\n    fill_alpha=0.15,\n    fill_color=COLOR_A,\n    line_color=COLOR_A,\n    line_alpha=0.0,\n)\np.add_layout(band)\n\n# Fitted curves\nsource_a = ColumnDataSource(data={\"conc\": conc_smooth, \"response\": fit_a})\nsource_b = ColumnDataSource(data={\"conc\": conc_smooth, \"response\": fit_b})\nline_a = p.line(\"conc\", \"response\", source=source_a, line_width=5, line_color=COLOR_A)\nline_b = p.line(\"conc\", \"response\", source=source_b, line_width=5, line_color=COLOR_B)\n\n# Data points with SEM error bars\npts_source_a = ColumnDataSource(\n    data={\n        \"conc\": concentrations,\n        \"response\": response_a_raw,\n        \"upper\": response_a_raw + response_a_sem,\n        \"lower\": response_a_raw - response_a_sem,\n    }\n)\npts_source_b = ColumnDataSource(\n    data={\n        \"conc\": concentrations,\n        \"response\": response_b_raw,\n        \"upper\": response_b_raw + response_b_sem,\n        \"lower\": response_b_raw - response_b_sem,\n    }\n)\n\nwhisker_a = Whisker(\n    base=\"conc\", upper=\"upper\", lower=\"lower\", source=pts_source_a, line_color=COLOR_A, line_width=3, line_alpha=0.7\n)\nwhisker_a.upper_head.line_color = COLOR_A\nwhisker_a.upper_head.size = 14\nwhisker_a.lower_head.line_color = COLOR_A\nwhisker_a.lower_head.size = 14\np.add_layout(whisker_a)\n\nwhisker_b = Whisker(\n    base=\"conc\", upper=\"upper\", lower=\"lower\", source=pts_source_b, line_color=COLOR_B, line_width=3, line_alpha=0.7\n)\nwhisker_b.upper_head.line_color = COLOR_B\nwhisker_b.upper_head.size = 14\nwhisker_b.lower_head.line_color = COLOR_B\nwhisker_b.lower_head.size = 14\np.add_layout(whisker_b)\n\nscatter_a = p.scatter(\"conc\", \"response\", source=pts_source_a, size=18, color=COLOR_A, line_color=PAGE_BG, line_width=2)\nscatter_b = p.scatter(\"conc\", \"response\", source=pts_source_b, size=18, color=COLOR_B, line_color=PAGE_BG, line_width=2)\n\n# EC50 reference lines — Compound A\nec50_vline_a = Span(\n    location=ec50_a, dimension=\"height\", line_color=COLOR_A, line_width=2, line_dash=\"dashed\", line_alpha=0.5\n)\np.add_layout(ec50_vline_a)\nec50_hline_src_a = ColumnDataSource(data={\"x\": [3e-10, ec50_a], \"y\": [half_response_a, half_response_a]})\np.line(\"x\", \"y\", source=ec50_hline_src_a, line_color=COLOR_A, line_width=2, line_dash=\"dashed\", line_alpha=0.5)\n\n# EC50 reference lines — Compound B\nec50_vline_b = Span(\n    location=ec50_b, dimension=\"height\", line_color=COLOR_B, line_width=2, line_dash=\"dashed\", line_alpha=0.5\n)\np.add_layout(ec50_vline_b)\nec50_hline_src_b = ColumnDataSource(data={\"x\": [3e-10, ec50_b], \"y\": [half_response_b, half_response_b]})\np.line(\"x\", \"y\", source=ec50_hline_src_b, line_color=COLOR_B, line_width=2, line_dash=\"dashed\", line_alpha=0.5)\n\n# Asymptote reference lines (Compound A)\ntop_asymptote = Span(\n    location=popt_a[1], dimension=\"width\", line_color=INK_MUTED, line_width=2, line_dash=\"dotted\", line_alpha=0.5\n)\nbottom_asymptote = Span(\n    location=popt_a[0], dimension=\"width\", line_color=INK_MUTED, line_width=2, line_dash=\"dotted\", line_alpha=0.5\n)\np.add_layout(top_asymptote)\np.add_layout(bottom_asymptote)\n\n# Asymptote labels\ntop_asym_label = Label(\n    x=1e-4,\n    y=popt_a[1] + 2,\n    text=f\"Top asymptote ({popt_a[1]:.0f}%)\",\n    text_font_size=\"22pt\",\n    text_color=INK_MUTED,\n    text_font_style=\"italic\",\n    text_align=\"right\",\n)\nbottom_asym_label = Label(\n    x=5e-10,\n    y=popt_a[0] - 7,\n    text=f\"Bottom asymptote ({popt_a[0]:.0f}%)\",\n    text_font_size=\"22pt\",\n    text_color=INK_MUTED,\n    text_font_style=\"italic\",\n)\np.add_layout(top_asym_label)\np.add_layout(bottom_asym_label)\n\n# EC50 annotations\nec50_a_label = Label(\n    x=ec50_a * 2.5,\n    y=half_response_a + 6,\n    text=f\"EC₅₀ = {ec50_a:.1e} M\",\n    text_font_size=\"28pt\",\n    text_color=COLOR_A,\n    text_font_style=\"bold\",\n)\nec50_b_label = Label(\n    x=ec50_b * 2.5,\n    y=half_response_b + 6,\n    text=f\"EC₅₀ = {ec50_b:.1e} M\",\n    text_font_size=\"28pt\",\n    text_color=COLOR_B,\n    text_font_style=\"bold\",\n)\np.add_layout(ec50_a_label)\np.add_layout(ec50_b_label)\n\n# Potency comparison annotation\npotency_label = Label(\n    x=2e-5,\n    y=20,\n    text=f\"Compound A is {fold_diff:.0f}× more potent\",\n    text_font_size=\"24pt\",\n    text_color=INK_MUTED,\n    text_font_style=\"italic\",\n)\np.add_layout(potency_label)\n\n# Legend — top-left avoids crowding near the plateau (top-right)\nlegend = Legend(\n    items=[\n        LegendItem(label=\"Compound A (Hill = {:.1f})\".format(popt_a[3]), renderers=[line_a, scatter_a]),\n        LegendItem(label=\"Compound B (Hill = {:.1f})\".format(popt_b[3]), renderers=[line_b, scatter_b]),\n    ],\n    location=\"top_left\",\n)\nlegend.label_text_font_size = \"34pt\"\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 0.95\nlegend.border_line_color = INK_SOFT\nlegend.border_line_width = 1\nlegend.spacing = 14\nlegend.padding = 24\nlegend.glyph_width = 50\nlegend.glyph_height = 36\np.add_layout(legend)\n\n# Style — theme-adaptive chrome (canonical bokeh sizing: 50pt / 42pt / 34pt)\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_style = \"normal\"\np.yaxis.axis_label_text_font_style = \"normal\"\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\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.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\n\n# Save — HTML interactive artifact + Selenium PNG screenshot\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\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# Override viewport to exact canvas dimensions (window size includes browser chrome)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"mobile\": False, \"width\": W, \"height\": H, \"deviceScaleFactor\": 1}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}