{"spec_id":"forest-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nforest-basic: Meta-Analysis Forest Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\n# Remove local directory from sys.path to avoid importing local bokeh.py\nsys.path = [p for p in sys.path if not p.startswith(str(Path(__file__).parent))]\n\nimport time\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\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive colors\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette\nBRAND = \"#009E73\"\nSECONDARY = \"#C475FD\"\n\n# Data - Meta-analysis of blood pressure reduction trials\nnp.random.seed(42)\n\nstudies = [\n    \"Smith et al. 2018\",\n    \"Johnson et al. 2019\",\n    \"Williams et al. 2019\",\n    \"Brown et al. 2020\",\n    \"Davis et al. 2020\",\n    \"Miller et al. 2021\",\n    \"Wilson et al. 2021\",\n    \"Moore et al. 2022\",\n    \"Taylor et al. 2022\",\n    \"Anderson et al. 2023\",\n    \"Thomas et al. 2023\",\n    \"Chen et al. 2023\",\n    \"Pooled Estimate\",\n]\n\n# Effect sizes (mean difference in mmHg) with confidence intervals\n# Added study crossing null line for better forest plot demonstration\neffect_sizes = np.array([-3.2, -5.1, -2.8, -4.5, -6.2, -3.9, -4.1, -5.8, -3.5, 0.3, -2.9, -4.7, -4.1])\nci_lower = np.array([-5.8, -8.2, -5.1, -7.3, -9.1, -6.5, -6.8, -8.9, -6.2, -2.5, -5.6, -7.1, -5.0])\nci_upper = np.array([-0.6, -2.0, -0.5, -1.7, -3.3, -1.3, -1.4, -2.7, -0.8, 3.1, -0.2, -2.3, -3.2])\n\n# Weights based on sample size\nweights = np.array([8, 12, 6, 15, 10, 9, 11, 14, 7, 9, 5, 11, 22])\n\n# Y positions (reversed so first study is at top)\ny_positions = list(range(len(studies) - 1, -1, -1))\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"forest-basic · bokeh · anyplot.ai\",\n    x_axis_label=\"Mean Difference in Blood Pressure (mmHg)\",\n    y_range=(-1.5, len(studies) - 0.5),\n    x_range=(-10, 4),\n    tools=\"pan,wheel_zoom,reset\",\n    toolbar_location=\"below\",\n)\n\n# Add vertical reference line at null effect (0)\nnull_line = Span(location=0, dimension=\"height\", line_color=INK_SOFT, line_width=3, line_dash=\"dashed\")\np.add_layout(null_line)\n\n# Prepare data for individual studies (excluding pooled estimate)\nstudy_source = ColumnDataSource(\n    data={\n        \"study\": studies[:-1],\n        \"effect\": effect_sizes[:-1],\n        \"ci_lower\": ci_lower[:-1],\n        \"ci_upper\": ci_upper[:-1],\n        \"y\": y_positions[:-1],\n        \"size\": (weights[:-1] / weights[:-1].max() * 25 + 10).tolist(),\n    }\n)\n\n# Draw confidence interval lines (whiskers)\nfor i in range(len(studies) - 1):\n    p.line(x=[ci_lower[i], ci_upper[i]], y=[y_positions[i], y_positions[i]], line_width=4, line_color=BRAND)\n    # Add CI end caps\n    p.line(\n        x=[ci_lower[i], ci_lower[i]], y=[y_positions[i] - 0.15, y_positions[i] + 0.15], line_width=3, line_color=BRAND\n    )\n    p.line(\n        x=[ci_upper[i], ci_upper[i]], y=[y_positions[i] - 0.15, y_positions[i] + 0.15], line_width=3, line_color=BRAND\n    )\n\n# Plot effect size points (size proportional to weight)\np.scatter(x=\"effect\", y=\"y\", source=study_source, size=\"size\", color=BRAND, alpha=0.85)\n\n# Add HoverTool for interactivity\nhover = HoverTool(\n    tooltips=[(\"Study\", \"@study\"), (\"Effect Size\", \"@effect{0.0}\"), (\"95% CI\", \"[@ci_lower{0.0}, @ci_upper{0.0}]\")]\n)\np.add_tools(hover)\n\n# Add study labels on the left\nfor i, study in enumerate(studies[:-1]):\n    label = Label(\n        x=-9.5,\n        y=y_positions[i],\n        text=study,\n        text_font_size=\"18pt\",\n        text_align=\"left\",\n        text_baseline=\"middle\",\n        text_color=INK,\n    )\n    p.add_layout(label)\n\n# Draw pooled estimate as a diamond\npooled_y = y_positions[-1]\npooled_effect = effect_sizes[-1]\npooled_lower = ci_lower[-1]\npooled_upper = ci_upper[-1]\n\n# Diamond vertices\ndiamond_x = [pooled_lower, pooled_effect, pooled_upper, pooled_effect, pooled_lower]\ndiamond_y = [pooled_y, pooled_y + 0.25, pooled_y, pooled_y - 0.25, pooled_y]\n\np.patch(x=diamond_x, y=diamond_y, fill_color=SECONDARY, line_color=BRAND, line_width=3, alpha=0.85)\n\n# Add pooled estimate label\npooled_label = Label(\n    x=-9.5,\n    y=pooled_y,\n    text=\"Pooled Estimate\",\n    text_font_size=\"18pt\",\n    text_font_style=\"bold\",\n    text_align=\"left\",\n    text_baseline=\"middle\",\n    text_color=INK,\n)\np.add_layout(pooled_label)\n\n# Add \"Favors Treatment\" and \"Favors Control\" labels\nfavors_treatment = Label(\n    x=-5.5,\n    y=-0.8,\n    text=\"← Favors Treatment\",\n    text_font_size=\"16pt\",\n    text_align=\"center\",\n    text_baseline=\"top\",\n    text_color=INK_SOFT,\n)\np.add_layout(favors_treatment)\n\nfavors_control = Label(\n    x=1.5,\n    y=-0.8,\n    text=\"Favors Control →\",\n    text_font_size=\"16pt\",\n    text_align=\"center\",\n    text_baseline=\"top\",\n    text_color=INK_SOFT,\n)\np.add_layout(favors_control)\n\n# Styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\n\n# Color styling\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\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\n\n# Hide y-axis ticks and labels (studies are labeled manually)\np.yaxis.visible = False\n\n# Grid styling\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_color = None\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium\nW, H = 4800, 2700\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)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}