{"spec_id":"forest-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nforest-basic: Meta-Analysis Forest Plot\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\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\"\nOKABE_ITO_1 = \"#009E73\"\n\n# Data: Meta-analysis of clinical trials comparing treatment vs control\n# Effect sizes are log odds ratios (log OR) - null effect at 0\nstudies = [\n    {\"study\": \"Smith 2018\", \"effect_size\": 0.35, \"ci_lower\": 0.05, \"ci_upper\": 0.65, \"weight\": 12.5},\n    {\"study\": \"Johnson 2019\", \"effect_size\": -0.12, \"ci_lower\": -0.45, \"ci_upper\": 0.21, \"weight\": 10.2},\n    {\"study\": \"Williams 2019\", \"effect_size\": 0.48, \"ci_lower\": 0.18, \"ci_upper\": 0.78, \"weight\": 11.8},\n    {\"study\": \"Brown 2020\", \"effect_size\": 0.22, \"ci_lower\": -0.15, \"ci_upper\": 0.59, \"weight\": 9.5},\n    {\"study\": \"Davis 2020\", \"effect_size\": 0.55, \"ci_lower\": 0.20, \"ci_upper\": 0.90, \"weight\": 8.7},\n    {\"study\": \"Miller 2021\", \"effect_size\": 0.15, \"ci_lower\": -0.18, \"ci_upper\": 0.48, \"weight\": 11.0},\n    {\"study\": \"Wilson 2021\", \"effect_size\": 0.42, \"ci_lower\": 0.12, \"ci_upper\": 0.72, \"weight\": 12.0},\n    {\"study\": \"Moore 2022\", \"effect_size\": 0.28, \"ci_lower\": -0.08, \"ci_upper\": 0.64, \"weight\": 9.8},\n    {\"study\": \"Taylor 2022\", \"effect_size\": 0.65, \"ci_lower\": 0.28, \"ci_upper\": 1.02, \"weight\": 7.5},\n    {\"study\": \"Anderson 2023\", \"effect_size\": 0.18, \"ci_lower\": -0.12, \"ci_upper\": 0.48, \"weight\": 12.8},\n]\n\ndf = pd.DataFrame(studies)\n\n# Calculate pooled estimate (weighted average)\ntotal_weight = df[\"weight\"].sum()\npooled_effect = (df[\"effect_size\"] * df[\"weight\"]).sum() / total_weight\npooled_se = 0.08  # Simplified SE for visualization\npooled_ci_lower = pooled_effect - 1.96 * pooled_se\npooled_ci_upper = pooled_effect + 1.96 * pooled_se\n\n# Order studies by effect size and assign y positions\ndf = df.sort_values(\"effect_size\", ascending=True).reset_index(drop=True)\ndf[\"y_pos\"] = range(len(df), 0, -1)\n\n# Scale weights for marker sizes (proportional to study weight)\ndf[\"marker_size\"] = df[\"weight\"] / df[\"weight\"].max() * 8 + 2\n\n# Create the forest plot\nplot = (\n    ggplot()\n    # Vertical reference line at null effect (0 for log OR)\n    + geom_vline(xintercept=0, color=INK_SOFT, size=1, linetype=\"dashed\")\n    # Confidence interval lines (whiskers)\n    + geom_segment(aes(x=\"ci_lower\", xend=\"ci_upper\", y=\"y_pos\", yend=\"y_pos\"), data=df, color=OKABE_ITO_1, size=1.5)\n    # Point estimates (squares proportional to weight)\n    + geom_point(\n        aes(x=\"effect_size\", y=\"y_pos\", size=\"marker_size\"),\n        data=df,\n        color=OKABE_ITO_1,\n        shape=15,  # Square marker\n    )\n    # Study labels on y-axis\n    + scale_y_continuous(breaks=df[\"y_pos\"].tolist(), labels=df[\"study\"].tolist())\n    # Diamond for pooled estimate\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\"),\n        data=pd.DataFrame(\n            {\"x\": [pooled_ci_lower, pooled_effect, pooled_ci_upper, pooled_effect], \"y\": [-0.5, -1.0, -0.5, 0.0]}\n        ),\n        fill=\"#FFD43B\",\n        color=OKABE_ITO_1,\n        size=1,\n    )\n    # Labels and title\n    + labs(x=\"Log Odds Ratio (95% CI)\", y=\"\", title=\"forest-basic · letsplot · pyplots.ai\")\n    # Theme and sizing\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=24, face=\"bold\", color=INK),\n        axis_title_x=element_text(size=20, color=INK),\n        axis_text_x=element_text(size=16, color=INK_SOFT),\n        axis_text_y=element_text(size=16, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT),\n        legend_position=\"none\",\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n    )\n    + scale_size_identity()\n    + ggsize(1600, 900)\n)\n\n# Add text annotation for pooled estimate using geom_text\npooled_label_df = pd.DataFrame(\n    {\n        \"x\": [pooled_effect],\n        \"y\": [-1.8],\n        \"label\": [f\"Pooled: {pooled_effect:.2f} [{pooled_ci_lower:.2f}, {pooled_ci_upper:.2f}]\"],\n    }\n)\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=pooled_label_df, size=14, color=INK_SOFT)\n\n# Save as PNG (scale 3x for 4800 × 2700 px)\nggsave(plot, f\"plot-{THEME}.png\", scale=3, path=\".\")\n\n# Save as HTML for interactivity\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}