{"spec_id":"funnel-meta-analysis","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nfunnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\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 palette — first categorical series always #009E73\nIMPRINT_GREEN = \"#009E73\"  # position 1 — high-precision studies\nIMPRINT_LAVENDER = \"#C475FD\"  # position 2 — low-precision studies\n\n# Data: 15 RCTs comparing drug vs placebo (log odds ratios)\nnp.random.seed(42)\nstudy_names = [\n    \"Adams 2018\",\n    \"Baker 2019\",\n    \"Chen 2019\",\n    \"Davis 2020\",\n    \"Evans 2020\",\n    \"Foster 2021\",\n    \"Garcia 2021\",\n    \"Harris 2022\",\n    \"Ibrahim 2022\",\n    \"Jones 2022\",\n    \"Kim 2023\",\n    \"Lee 2023\",\n    \"Martinez 2023\",\n    \"Nelson 2024\",\n    \"O'Brien 2024\",\n]\n\n# Effect sizes (log odds ratios) and standard errors\n# High-precision studies cluster near pooled effect; low-precision studies\n# show rightward asymmetry (missing negative small studies = publication bias)\neffect_sizes = np.array(\n    [-0.50, -0.42, -0.68, -0.46, -0.25, -0.55, -0.48, -0.44, -0.38, -0.20, -0.72, -0.43, -0.08, -0.51, -0.58]\n)\nstd_errors = np.array([0.08, 0.11, 0.17, 0.09, 0.16, 0.21, 0.13, 0.07, 0.22, 0.18, 0.26, 0.10, 0.23, 0.12, 0.24])\n\npooled_effect = -0.47\n\nhigh_precision_mask = std_errors < 0.15\nlow_precision_mask = ~high_precision_mask\n\n# Style — theme-adaptive chrome + Imprint palette\n# Structural reference lines (CI, pooled effect, null) use INK tokens;\n# data series (study groups) use Imprint categorical positions 1 and 3.\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\n        INK_MUTED,  # 0: CI left boundary (structural line, not a data category)\n        INK_MUTED,  # 1: CI right boundary (hidden from legend)\n        INK,  # 2: Pooled effect line (structural reference)\n        INK_SOFT,  # 3: Null effect line (structural reference)\n        IMPRINT_GREEN,  # 4: High-precision studies — Imprint position 1\n        IMPRINT_LAVENDER,  # 5: Low-precision studies — Imprint position 2\n    ),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n    font_family=\"Helvetica, Arial, sans-serif\",\n    opacity=\".9\",\n    opacity_hover=\"1\",\n)\n\n# Chart — 3200×1800 landscape canvas\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    explicit_size=True,\n    title=\"funnel-meta-analysis · python · pygal · anyplot.ai\",\n    x_title=\"Log Odds Ratio (Effect Size)\",\n    y_title=\"Standard Error (precision ↑)\",\n    style=custom_style,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    legend_box_size=30,\n    dots_size=14,\n    stroke=False,\n    show_y_guides=True,\n    show_x_guides=False,\n    margin=120,\n    inverse_y_axis=True,\n    truncate_legend=-1,\n    x_value_formatter=lambda x: f\"{x:.2f}\",\n    y_value_formatter=lambda y: f\"{y:.2f}\",\n    y_labels=[0.00, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30],\n    print_values=False,\n    print_zeroes=False,\n    range=(0, 0.30),\n    xrange=(-1.05, 0.15),\n    spacing=30,\n    tooltip_border_radius=8,\n)\n\n# Funnel boundaries — pseudo 95% CI diagonal lines\nse_values = np.linspace(0, 0.30, 60)\nfunnel_left = [(float(pooled_effect - 1.96 * se), float(se)) for se in se_values]\nchart.add(\n    \"95% Pseudo CI\",\n    funnel_left,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 4, \"dasharray\": \"14, 6\"},\n    formatter=lambda x: \"\",\n)\n\nfunnel_right = [(float(pooled_effect + 1.96 * se), float(se)) for se in se_values]\nchart.add(\n    None,\n    funnel_right,\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 4, \"dasharray\": \"14, 6\"},\n    formatter=lambda x: \"\",\n)\n\n# Vertical line at pooled effect (solid, prominent)\nchart.add(\n    f\"Pooled Effect (LOR = {pooled_effect:.2f})\",\n    [(float(pooled_effect), 0.0), (float(pooled_effect), 0.30)],\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 6},\n    formatter=lambda x: \"\",\n)\n\n# Vertical dashed line at null effect\nchart.add(\n    \"Null Effect (LOR = 0)\",\n    [(0.0, 0.0), (0.0, 0.30)],\n    stroke=True,\n    show_dots=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"10, 8\"},\n    formatter=lambda x: \"\",\n)\n\n# High-precision studies — larger markers, tightly clustered near pooled effect\nhp_points = [\n    {\"value\": (float(es), float(se)), \"label\": f\"{name}: LOR={es:.2f}, SE={se:.2f}\"}\n    for name, es, se in zip(\n        np.array(study_names)[high_precision_mask],\n        effect_sizes[high_precision_mask],\n        std_errors[high_precision_mask],\n        strict=True,\n    )\n]\nchart.add(\"High-precision studies\", hp_points, stroke=False, dots_size=24, formatter=lambda x: \"\")\n\n# Low-precision studies — smaller markers, rightward asymmetry signals publication bias\nlp_points = [\n    {\"value\": (float(es), float(se)), \"label\": f\"{name}: LOR={es:.2f}, SE={se:.2f}\"}\n    for name, es, se in zip(\n        np.array(study_names)[low_precision_mask],\n        effect_sizes[low_precision_mask],\n        std_errors[low_precision_mask],\n        strict=True,\n    )\n]\nchart.add(\"Low-precision studies (bias region)\", lp_points, stroke=False, dots_size=18, formatter=lambda x: \"\")\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}