{"spec_id":"funnel-meta-analysis","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nfunnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-10\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 ColumnDataSource, HoverTool, Label, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette 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 positions\nBRAND = \"#009E73\"  # position 1 — inside funnel (first series)\nBLUE = \"#4467A3\"  # position 3 — summary effect line\nRED = \"#AE3030\"  # semantic anchor — outside funnel (bad/error)\n\n# Data — Meta-analysis of 15 RCTs comparing drug vs placebo (log odds ratios)\nnp.random.seed(42)\n\nn_studies = 15\ntrue_effect = 0.3\n\nstd_errors = np.concatenate(\n    [np.random.uniform(0.05, 0.15, 5), np.random.uniform(0.15, 0.30, 6), np.random.uniform(0.30, 0.50, 4)]\n)\n\neffect_sizes = true_effect + np.random.normal(0, 1, n_studies) * std_errors\n# Slight positive bias for small studies (simulating publication bias)\nsmall_study_mask = std_errors > 0.30\neffect_sizes[small_study_mask] += np.random.uniform(0.05, 0.20, small_study_mask.sum())\n\n# Summary effect (inverse-variance weighted)\nweights = 1 / std_errors**2\nsummary_effect = np.sum(weights * effect_sizes) / np.sum(weights)\n\n# Marker sizes proportional to study weight (inverse variance)\nnormalized_weights = weights / weights.max()\nmarker_sizes = 14 + normalized_weights * 22  # range 14–36 px (tighter to reduce crowding)\n\n# Inside/outside funnel classification (semantic coloring + shape redundancy for CVD)\nexpected_lower = summary_effect - 1.96 * std_errors\nexpected_upper = summary_effect + 1.96 * std_errors\noutside_funnel = (effect_sizes < expected_lower) | (effect_sizes > expected_upper)\nmarker_colors = np.where(outside_funnel, RED, BRAND)\nmarker_types = np.where(outside_funnel, \"diamond\", \"circle\")\n\n# Funnel pseudo-95% confidence limits\nse_range = np.linspace(0, 0.55, 100)\nupper_limit = summary_effect + 1.96 * se_range\nlower_limit = summary_effect - 1.96 * se_range\n\nstudies = [f\"Study {i + 1}\" for i in range(n_studies)]\n\n# Plot\nsource = ColumnDataSource(\n    data={\n        \"effect_size\": effect_sizes,\n        \"std_error\": std_errors,\n        \"study\": studies,\n        \"weight\": np.round(weights, 1),\n        \"marker_size\": marker_sizes,\n        \"marker_color\": marker_colors.tolist(),\n        \"status\": [\"Outside funnel\" if o else \"Inside funnel\" for o in outside_funnel],\n        \"marker_type\": marker_types.tolist(),\n    }\n)\n\ntitle = \"funnel-meta-analysis · python · bokeh · anyplot.ai\"\n\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Log Odds Ratio\",\n    y_axis_label=\"Standard Error\",\n    y_range=(0.60, -0.02),\n    x_range=(-0.85, 1.15),\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# Funnel confidence region (pseudo 95% CI shaded area)\nfunnel_xs = np.concatenate([lower_limit, upper_limit[::-1]]).tolist()\nfunnel_ys = np.concatenate([se_range, se_range[::-1]]).tolist()\nFUNNEL_ALPHA = 0.15 if THEME == \"light\" else 0.05  # lower alpha in dark avoids green-on-green contrast\np.patch(\n    funnel_xs,\n    funnel_ys,\n    fill_color=BRAND,\n    fill_alpha=FUNNEL_ALPHA,\n    line_color=BRAND,\n    line_alpha=0.45,\n    line_width=2.5,\n    line_dash=\"dashed\",\n)\n\n# Summary effect vertical line\np.add_layout(Span(location=summary_effect, dimension=\"height\", line_color=BLUE, line_width=3.5, line_alpha=0.85))\n\n# Null effect line (log-OR = 0 → no effect)\np.add_layout(\n    Span(location=0, dimension=\"height\", line_color=INK_SOFT, line_width=2.5, line_dash=\"dashed\", line_alpha=0.70)\n)\n\n# Study scatter — sized by weight, shaped by funnel status (circle=inside, diamond=outside)\nscatter = p.scatter(\n    x=\"effect_size\",\n    y=\"std_error\",\n    source=source,\n    marker=\"marker_type\",\n    size=\"marker_size\",\n    fill_alpha=0.80,\n    fill_color=\"marker_color\",\n    line_color=PAGE_BG,\n    line_width=2.0,\n)\n\n# HoverTool — Bokeh's interactive feature; works in HTML artifact\nhover = HoverTool(\n    renderers=[scatter],\n    tooltips=[\n        (\"Study\", \"@study\"),\n        (\"Effect Size\", \"@effect_size{0.3f}\"),\n        (\"Std Error\", \"@std_error{0.3f}\"),\n        (\"Weight\", \"@weight{0.1f}\"),\n        (\"Status\", \"@status\"),\n    ],\n)\np.add_tools(hover)\n\n# Summary effect label (top of chart, just below summary line)\np.add_layout(\n    Label(\n        x=summary_effect + 0.03,\n        y=0.03,\n        text=f\"Summary: {summary_effect:.2f}\",\n        text_font_size=\"28pt\",\n        text_color=BLUE,\n        text_font_style=\"bold\",\n        text_align=\"left\",\n        text_baseline=\"top\",\n    )\n)\n\n# Null label\np.add_layout(\n    Label(\n        x=0.03,\n        y=0.03,\n        text=\"Null (0)\",\n        text_font_size=\"28pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"normal\",\n        text_align=\"left\",\n        text_baseline=\"top\",\n    )\n)\n\n# Color-code legend annotations (lower-left, outside funnel region)\nn_outside = int(outside_funnel.sum())\np.add_layout(\n    Label(\n        x=-0.80,\n        y=0.47,\n        text=f\"● Inside funnel ({n_studies - n_outside} studies)\",\n        text_font_size=\"30pt\",\n        text_color=BRAND,\n        text_align=\"left\",\n        text_baseline=\"middle\",\n    )\n)\np.add_layout(\n    Label(\n        x=-0.80,\n        y=0.52,\n        text=f\"◆ Outside funnel ({n_outside} studies)\",\n        text_font_size=\"30pt\",\n        text_color=RED,\n        text_align=\"left\",\n        text_baseline=\"middle\",\n    )\n)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_color = INK\np.title.text_font_size = \"50pt\"\np.title.align = \"center\"\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\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.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_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.ygrid.grid_line_alpha = 0.12\n\n# Save — HTML first (interactive artifact), then PNG via headless Chrome\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Use CDP setDeviceMetricsOverride so the inner viewport is authoritative:\n# --window-size alone is eaten by Chrome chrome in headless mode (gives 1661 instead of 1800).\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)\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"}