{"spec_id":"funnel-meta-analysis","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nfunnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens — Imprint palette + 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 = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette positions used\nBRAND = \"#009E73\"  # position 1 — inside-funnel studies\nBLUE = \"#4467A3\"  # position 3 — pooled effect line\nRED = \"#AE3030\"  # position 5 — semantic anchor for outliers (outside 95% CI)\n\n# Funnel shading derived from BLUE\nFUNNEL_FILL_95 = \"rgba(68,103,163,0.10)\" if THEME == \"light\" else \"rgba(68,103,163,0.16)\"\nFUNNEL_LINE_95 = \"rgba(68,103,163,0.45)\" if THEME == \"light\" else \"rgba(68,103,163,0.65)\"\nFUNNEL_FILL_99 = \"rgba(68,103,163,0.05)\" if THEME == \"light\" else \"rgba(68,103,163,0.08)\"\nFUNNEL_LINE_99 = \"rgba(68,103,163,0.18)\" if THEME == \"light\" else \"rgba(68,103,163,0.30)\"\n\n# Data: 15 RCTs comparing drug vs placebo — log odds ratios and standard errors\nnp.random.seed(42)\n\nstudies = [\n    \"Adams et al. 2016\",\n    \"Baker et al. 2017\",\n    \"Chen et al. 2017\",\n    \"Davis & Park 2018\",\n    \"Evans et al. 2018\",\n    \"Fischer 2019\",\n    \"Gupta et al. 2019\",\n    \"Harris et al. 2020\",\n    \"Ibrahim et al. 2020\",\n    \"Jensen & Liu 2021\",\n    \"Kim et al. 2021\",\n    \"Lambert et al. 2022\",\n    \"Morales et al. 2022\",\n    \"Nielsen 2023\",\n    \"Olsen et al. 2023\",\n]\n\nlog_or = np.array(\n    [-0.52, -0.38, -0.71, -0.15, -0.45, -0.63, -0.29, -0.55, -0.42, -0.33, -0.80, -0.48, -0.36, -0.61, -0.10]\n)\nstd_error = np.array([0.18, 0.25, 0.12, 0.30, 0.20, 0.15, 0.28, 0.17, 0.22, 0.26, 0.11, 0.19, 0.24, 0.14, 0.35])\n\n# Inverse-variance weights and pooled effect\nweights = 1.0 / std_error**2\npooled_effect = np.sum(weights * log_or) / np.sum(weights)\npct_weights = 100 * weights / weights.sum()\n\n# Marker sizes proportional to study weight\nw_norm = weights / weights.max()\nmarker_sizes = 14 + w_norm * 20  # 14–34 px range\n\n# Classify studies inside vs outside the 95% funnel\noutside_funnel = np.abs(log_or - pooled_effect) > 1.96 * std_error\ninside_funnel = ~outside_funnel\n\n# Funnel boundary lines\nse_max = max(std_error) * 1.1\nse_range = np.linspace(0, se_max, 200)\nupper_95 = pooled_effect + 1.96 * se_range\nlower_95 = pooled_effect - 1.96 * se_range\nupper_99 = pooled_effect + 2.576 * se_range\nlower_99 = pooled_effect - 2.576 * se_range\n\n# Build figure\nfig = go.Figure()\n\n# 99% CI shaded region\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([lower_99, upper_99[::-1]]),\n        y=np.concatenate([se_range, se_range[::-1]]),\n        fill=\"toself\",\n        fillcolor=FUNNEL_FILL_99,\n        line={\"color\": FUNNEL_LINE_99, \"width\": 1, \"dash\": \"dot\"},\n        showlegend=True,\n        name=\"99% CI region\",\n        hoverinfo=\"skip\",\n    )\n)\n\n# 95% CI shaded region\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([lower_95, upper_95[::-1]]),\n        y=np.concatenate([se_range, se_range[::-1]]),\n        fill=\"toself\",\n        fillcolor=FUNNEL_FILL_95,\n        line={\"color\": FUNNEL_LINE_95, \"width\": 1.5},\n        showlegend=True,\n        name=\"95% CI region\",\n        hoverinfo=\"skip\",\n    )\n)\n\n# Null effect dashed reference line\nfig.add_trace(\n    go.Scatter(\n        x=[0, 0],\n        y=[0, se_max],\n        mode=\"lines\",\n        line={\"color\": INK_SOFT, \"width\": 1.5, \"dash\": \"dash\"},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    )\n)\n\n# Pooled effect line\nfig.add_trace(\n    go.Scatter(\n        x=[pooled_effect, pooled_effect],\n        y=[0, se_max],\n        mode=\"lines\",\n        line={\"color\": BLUE, \"width\": 2.5},\n        showlegend=True,\n        name=f\"Pooled OR {np.exp(pooled_effect):.2f} (log {pooled_effect:.2f})\",\n        hoverinfo=\"skip\",\n    )\n)\n\n# Studies inside 95% CI funnel\nif inside_funnel.any():\n    hover_inside = [\n        f\"<b>{s}</b><br>Log OR: {e:.2f}  (OR = {np.exp(e):.2f})<br>SE: {se:.3f}<br>Weight: {w:.1f}%\"\n        for s, e, se, w in zip(\n            np.array(studies)[inside_funnel],\n            log_or[inside_funnel],\n            std_error[inside_funnel],\n            pct_weights[inside_funnel],\n            strict=False,\n        )\n    ]\n    fig.add_trace(\n        go.Scatter(\n            x=log_or[inside_funnel],\n            y=std_error[inside_funnel],\n            mode=\"markers\",\n            marker={\n                \"size\": marker_sizes[inside_funnel],\n                \"symbol\": \"circle\",\n                \"color\": BRAND,\n                \"line\": {\"color\": \"#006B4E\", \"width\": 1.5},\n                \"opacity\": 0.88,\n            },\n            text=hover_inside,\n            hovertemplate=\"%{text}<extra></extra>\",\n            hoverlabel={\"bgcolor\": ELEVATED_BG, \"bordercolor\": BRAND, \"font\": {\"size\": 13, \"color\": INK}},\n            name=\"Within 95% CI\",\n            showlegend=True,\n        )\n    )\n\n# Studies outside 95% CI funnel (potential outliers)\nif outside_funnel.any():\n    hover_outside = [\n        f\"<b>{s}</b><br>Log OR: {e:.2f}  (OR = {np.exp(e):.2f})<br>SE: {se:.3f}<br>Weight: {w:.1f}%<br>⚠ Outside 95% CI\"\n        for s, e, se, w in zip(\n            np.array(studies)[outside_funnel],\n            log_or[outside_funnel],\n            std_error[outside_funnel],\n            pct_weights[outside_funnel],\n            strict=False,\n        )\n    ]\n    fig.add_trace(\n        go.Scatter(\n            x=log_or[outside_funnel],\n            y=std_error[outside_funnel],\n            mode=\"markers\",\n            marker={\n                \"size\": marker_sizes[outside_funnel],\n                \"symbol\": \"diamond\",\n                \"color\": RED,\n                \"line\": {\"color\": \"#7A1F1F\", \"width\": 1.5},\n                \"opacity\": 0.88,\n            },\n            text=hover_outside,\n            hovertemplate=\"%{text}<extra></extra>\",\n            hoverlabel={\"bgcolor\": ELEVATED_BG, \"bordercolor\": RED, \"font\": {\"size\": 13, \"color\": INK}},\n            name=\"Outside 95% CI\",\n            showlegend=True,\n        )\n    )\n\n# Layout\ntitle_text = \"funnel-meta-analysis · python · plotly · anyplot.ai\"\nn = len(title_text)\ntitle_size = round(16 * 67 / n) if n > 67 else 16\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK, \"family\": \"Arial, sans-serif\"},\n    title={\n        \"text\": title_text,\n        \"font\": {\"size\": title_size, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.98,\n        \"yanchor\": \"top\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Log Odds Ratio\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 12},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n        \"showline\": True,\n        \"zeroline\": False,\n        \"range\": [-1.15, 0.55],\n    },\n    yaxis={\n        \"title\": {\"text\": \"Standard Error\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 10},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n        \"showline\": True,\n        \"zeroline\": False,\n        \"autorange\": \"reversed\",\n        \"rangemode\": \"tozero\",\n    },\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 0.02,\n        \"y\": 0.98,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n    },\n    hovermode=\"closest\",\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n)\n\n# Direction annotations\nfig.add_annotation(\n    x=0.04,\n    xref=\"paper\",\n    y=se_max * 0.96,\n    text=\"← Favors Treatment\",\n    showarrow=False,\n    font={\"size\": 11, \"color\": BRAND},\n    xanchor=\"left\",\n)\nfig.add_annotation(\n    x=0.96,\n    xref=\"paper\",\n    y=se_max * 0.96,\n    text=\"Favors Control →\",\n    showarrow=False,\n    font={\"size\": 11, \"color\": INK_MUTED},\n    xanchor=\"right\",\n)\n\n# Pooled and null labels near top of chart (where SE ≈ 0)\nfig.add_annotation(\n    x=pooled_effect + 0.03,\n    y=0.005,\n    text=f\"Pooled ({pooled_effect:.2f})\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": BLUE},\n    xanchor=\"left\",\n    yanchor=\"top\",\n)\nfig.add_annotation(\n    x=0.03,\n    y=0.005,\n    text=\"Null (0)\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_MUTED},\n    xanchor=\"left\",\n    yanchor=\"top\",\n)\n\n# CI boundary labels — solid background for readability against funnel fill\nci_label_se = se_max * 0.58\nfig.add_annotation(\n    x=pooled_effect + 1.96 * ci_label_se + 0.03,\n    y=ci_label_se,\n    text=\"95% CI\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_SOFT},\n    bgcolor=PAGE_BG,\n    bordercolor=\"rgba(0,0,0,0)\",\n    xanchor=\"left\",\n)\nfig.add_annotation(\n    x=pooled_effect + 2.576 * ci_label_se + 0.03,\n    y=ci_label_se,\n    text=\"99% CI\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_MUTED},\n    bgcolor=PAGE_BG,\n    bordercolor=\"rgba(0,0,0,0)\",\n    xanchor=\"left\",\n)\n\n# Subtitle\nfig.add_annotation(\n    x=0.5,\n    xref=\"paper\",\n    y=1.0,\n    yref=\"paper\",\n    text=\"Asymmetry at low precision (wide SE) suggests possible publication bias  •  Red ◆ outside 95% CI, green ● inside\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_MUTED},\n    xanchor=\"center\",\n    yanchor=\"bottom\",\n)\n\n# Save — 3200×1800 landscape (800×450 @ scale=4)\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}