{"spec_id":"curve-dose-response","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncurve-dose-response: Pharmacological Dose-Response Curve\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom scipy.optimize import curve_fit\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome (Imprint palette)\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 = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint categorical palette — positions 1 and 2\nCOLORS = [\"#009E73\", \"#C475FD\"]\n\n# --- Data ---\nnp.random.seed(42)\nconcentrations = np.logspace(-9, -4, 8)\n\ncompound_names = [\"Compound A\", \"Compound B\"]\n\nec50_true = [1e-7, 5e-7]\nhill_true = [1.2, 0.9]\ntop_true = [100, 95]\nbottom_true = [5, 10]\n\nraw_data = {}\nfor i, name in enumerate(compound_names):\n    responses = []\n    sems = []\n    for conc in concentrations:\n        true_resp = bottom_true[i] + (top_true[i] - bottom_true[i]) / (1 + (ec50_true[i] / conc) ** hill_true[i])\n        reps = true_resp + np.random.normal(0, 3, 3)\n        responses.append(np.mean(reps))\n        sems.append(np.std(reps, ddof=1) / np.sqrt(3))\n    raw_data[name] = {\"concentrations\": concentrations, \"responses\": np.array(responses), \"sems\": np.array(sems)}\n\n\ndef logistic_4pl(x, bottom, top, ec50, hill):\n    return bottom + (top - bottom) / (1 + (ec50 / x) ** hill)\n\n\nfit_results = {}\nconc_fine = np.logspace(-9.5, -3.8, 300)\n\nfor i, name in enumerate(compound_names):\n    popt, pcov = curve_fit(\n        logistic_4pl,\n        raw_data[name][\"concentrations\"],\n        raw_data[name][\"responses\"],\n        p0=[bottom_true[i], top_true[i], ec50_true[i], hill_true[i]],\n        maxfev=10000,\n    )\n    perr = np.sqrt(np.diag(pcov))\n    fit_results[name] = {\"popt\": popt, \"perr\": perr}\n\n# --- Plot ---\nfig = go.Figure()\n\nfor i, name in enumerate(compound_names):\n    popt = fit_results[name][\"popt\"]\n    bottom, top, ec50, hill = popt\n    color = COLORS[i]\n    fitted_curve = logistic_4pl(conc_fine, *popt)\n\n    # 95% CI band for Compound A\n    if i == 0:\n        perr = fit_results[name][\"perr\"]\n        upper = logistic_4pl(conc_fine, bottom - perr[0], top + perr[1], ec50, hill)\n        lower = logistic_4pl(conc_fine, bottom + perr[0], top - perr[1], ec50, hill)\n        r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)\n        fill_alpha = 0.18 if THEME == \"light\" else 0.25\n\n        fig.add_trace(\n            go.Scatter(x=conc_fine, y=upper, mode=\"lines\", line={\"width\": 0}, showlegend=False, hoverinfo=\"skip\")\n        )\n        fig.add_trace(\n            go.Scatter(\n                x=conc_fine,\n                y=lower,\n                mode=\"lines\",\n                line={\"width\": 0},\n                fill=\"tonexty\",\n                fillcolor=f\"rgba({r},{g},{b},{fill_alpha})\",\n                showlegend=False,\n                hoverinfo=\"skip\",\n            )\n        )\n\n    # Fitted sigmoid curve\n    fig.add_trace(\n        go.Scatter(\n            x=conc_fine,\n            y=fitted_curve,\n            mode=\"lines\",\n            name=f\"{name} (EC₅₀ = {ec50:.2e} M)\",\n            line={\"color\": color, \"width\": 3},\n            hovertemplate=(f\"<b>{name}</b><br>Conc: %{{x:.2e}} M<br>Response: %{{y:.1f}}%<extra></extra>\"),\n        )\n    )\n\n    # Data points with SEM error bars\n    fig.add_trace(\n        go.Scatter(\n            x=raw_data[name][\"concentrations\"],\n            y=raw_data[name][\"responses\"],\n            mode=\"markers\",\n            name=f\"{name} data\",\n            marker={\"size\": 10, \"color\": color, \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n            error_y={\"type\": \"data\", \"array\": raw_data[name][\"sems\"], \"visible\": True, \"color\": color, \"thickness\": 2},\n            showlegend=False,\n            hovertemplate=(\n                f\"<b>{name}</b><br>Conc: %{{x:.2e}} M<br>Response: %{{y:.1f}} ± %{{error_y.array:.1f}}%<extra></extra>\"\n            ),\n        )\n    )\n\n    # EC50 dashed crosshair reference lines\n    half_response = bottom + (top - bottom) / 2\n    fig.add_shape(\n        type=\"line\", x0=ec50, x1=ec50, y0=-5, y1=half_response, line={\"color\": color, \"width\": 1.5, \"dash\": \"dash\"}\n    )\n    fig.add_shape(\n        type=\"line\",\n        x0=1e-10,\n        x1=ec50,\n        y0=half_response,\n        y1=half_response,\n        line={\"color\": color, \"width\": 1.5, \"dash\": \"dash\"},\n    )\n\n    # EC50 annotation with arrow\n    fig.add_annotation(\n        x=np.log10(ec50),\n        y=half_response + 5 + i * 8,\n        text=f\"<b>EC₅₀ = {ec50:.2e} M</b>\",\n        showarrow=True,\n        arrowhead=2,\n        arrowsize=1,\n        arrowwidth=1.5,\n        arrowcolor=color,\n        ax=40 + i * 30,\n        ay=-30 - i * 10,\n        font={\"size\": 10, \"color\": color},\n        bordercolor=color,\n        borderwidth=1.5,\n        borderpad=4,\n        bgcolor=ELEVATED_BG,\n    )\n\n# Top and bottom asymptote dotted reference lines\nfig.add_shape(\n    type=\"line\", x0=1e-10, x1=1e-3, y0=top_true[0], y1=top_true[0], line={\"color\": INK_MUTED, \"width\": 1, \"dash\": \"dot\"}\n)\nfig.add_shape(\n    type=\"line\",\n    x0=1e-10,\n    x1=1e-3,\n    y0=bottom_true[0],\n    y1=bottom_true[0],\n    line={\"color\": INK_MUTED, \"width\": 1, \"dash\": \"dot\"},\n)\n\n# Asymptote labels (placed within axis range)\nfig.add_annotation(\n    x=-4.2, y=top_true[0], text=\"Top asymptote\", showarrow=False, font={\"size\": 10, \"color\": INK_MUTED}, yshift=10\n)\nfig.add_annotation(\n    x=-4.2,\n    y=bottom_true[0],\n    text=\"Bottom asymptote\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_MUTED},\n    yshift=-12,\n)\n\n# --- Layout ---\nfig.update_layout(\n    autosize=False,\n    width=800,\n    height=450,\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    title={\n        \"text\": \"<b>curve-dose-response · python · plotly · anyplot.ai</b>\",\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial, Helvetica, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Concentration (M)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"type\": \"log\",\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"showline\": False,\n        \"range\": [-9.5, -3.8],\n    },\n    yaxis={\n        \"title\": {\"text\": \"Response (%)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 0.5,\n        \"range\": [-5, 115],\n        \"zeroline\": False,\n        \"showline\": False,\n    },\n    template=\"plotly_white\",\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"traceorder\": \"normal\",\n    },\n    updatemenus=[\n        {\n            \"type\": \"dropdown\",\n            \"direction\": \"down\",\n            \"showactive\": True,\n            \"x\": 0.99,\n            \"xanchor\": \"right\",\n            \"y\": 0.01,\n            \"yanchor\": \"bottom\",\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_SOFT,\n            \"font\": {\"color\": INK_SOFT, \"size\": 10},\n            \"buttons\": [\n                {\n                    \"label\": \"Both Compounds\",\n                    \"method\": \"update\",\n                    \"args\": [{\"visible\": [True, True, True, True, True, True]}],\n                },\n                {\n                    \"label\": \"Compound A only\",\n                    \"method\": \"update\",\n                    \"args\": [{\"visible\": [True, True, True, True, False, False]}],\n                },\n                {\n                    \"label\": \"Compound B only\",\n                    \"method\": \"update\",\n                    \"args\": [{\"visible\": [False, False, False, False, True, True]}],\n                },\n            ],\n        }\n    ],\n)\n\n# --- Save ---\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}