{"spec_id":"forest-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nforest-basic: Meta-Analysis Forest Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-11\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\n\n\n# Theme tokens\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\"\n\n# Data: Meta-analysis of RCTs comparing treatment efficacy\nstudies = [\n    \"Johnson 2018\",\n    \"Smith 2019\",\n    \"Garcia 2020\",\n    \"Williams 2020\",\n    \"Brown 2021\",\n    \"Davis 2021\",\n    \"Miller 2022\",\n    \"Wilson 2022\",\n    \"Anderson 2023\",\n    \"Taylor 2023\",\n]\n\neffect_sizes = [-0.45, -0.32, -0.58, 0.12, -0.67, -0.38, -0.52, -0.29, -0.41, -0.55]\nci_lower = [-0.78, -0.61, -0.95, -0.18, -1.02, -0.69, -0.88, -0.56, -0.72, -0.91]\nci_upper = [-0.12, -0.03, -0.21, 0.42, -0.32, -0.07, -0.16, -0.02, -0.10, -0.19]\nweights = [8.5, 10.2, 7.8, 11.5, 6.9, 9.3, 8.1, 10.8, 9.7, 7.2]\n\n# Pooled estimate\npooled_effect = -0.38\npooled_ci_lower = -0.49\npooled_ci_upper = -0.27\n\n# Order labels for y-axis (studies at top, pooled estimate at bottom)\ny_labels = list(reversed(studies)) + [\"Pooled Estimate\"]\n\n# Create DataFrame for studies\ndf_studies = pd.DataFrame(\n    {\"study\": studies, \"effect_size\": effect_sizes, \"ci_lower\": ci_lower, \"ci_upper\": ci_upper, \"weight\": weights}\n)\n\n# Normalize weights for marker sizing\nweight_min = min(weights)\nweight_max = max(weights)\ndf_studies[\"marker_size\"] = 150 + (df_studies[\"weight\"] - weight_min) / (weight_max - weight_min) * 350\n\n# Create DataFrame for pooled estimate\ndf_pooled = pd.DataFrame(\n    {\n        \"study\": [\"Pooled Estimate\"],\n        \"effect_size\": [pooled_effect],\n        \"ci_lower\": [pooled_ci_lower],\n        \"ci_upper\": [pooled_ci_upper],\n    }\n)\n\n# Vertical reference line at null effect (0)\nreference_line = (\n    alt.Chart(pd.DataFrame({\"x\": [0]}))\n    .mark_rule(color=INK_SOFT, strokeDash=[8, 4], strokeWidth=2, opacity=0.7)\n    .encode(x=\"x:Q\")\n)\n\n# Confidence interval lines for studies\nci_lines = (\n    alt.Chart(df_studies)\n    .mark_rule(color=\"#009E73\", strokeWidth=4)\n    .encode(\n        x=alt.X(\"ci_lower:Q\", scale=alt.Scale(domain=[-1.15, 0.60])), x2=\"ci_upper:Q\", y=alt.Y(\"study:N\", sort=y_labels)\n    )\n)\n\n# Effect size points for studies\npoints = (\n    alt.Chart(df_studies)\n    .mark_point(filled=True, color=\"#009E73\", stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        x=alt.X(\"effect_size:Q\", scale=alt.Scale(domain=[-1.15, 0.60])),\n        y=alt.Y(\"study:N\", sort=y_labels),\n        size=alt.Size(\"marker_size:Q\", legend=None, scale=alt.Scale(range=[150, 500])),\n        tooltip=[\n            alt.Tooltip(\"study:N\", title=\"Study\"),\n            alt.Tooltip(\"effect_size:Q\", title=\"Effect Size\", format=\".2f\"),\n            alt.Tooltip(\"ci_lower:Q\", title=\"CI Lower\", format=\".2f\"),\n            alt.Tooltip(\"ci_upper:Q\", title=\"CI Upper\", format=\".2f\"),\n        ],\n    )\n)\n\n# Pooled estimate confidence interval line\npooled_ci = (\n    alt.Chart(df_pooled)\n    .mark_rule(color=\"#009E73\", strokeWidth=4)\n    .encode(\n        x=alt.X(\"ci_lower:Q\", scale=alt.Scale(domain=[-1.15, 0.60])), x2=\"ci_upper:Q\", y=alt.Y(\"study:N\", sort=y_labels)\n    )\n)\n\n# Diamond marker for pooled estimate\npooled_diamond = (\n    alt.Chart(df_pooled)\n    .mark_point(shape=\"diamond\", filled=True, size=1500, color=\"#AE3030\", stroke=\"#009E73\", strokeWidth=3)\n    .encode(\n        x=alt.X(\"effect_size:Q\", scale=alt.Scale(domain=[-1.15, 0.60])),\n        y=alt.Y(\"study:N\", sort=y_labels),\n        tooltip=[\n            alt.Tooltip(\"study:N\", title=\"Estimate\"),\n            alt.Tooltip(\"effect_size:Q\", title=\"Effect Size\", format=\".2f\"),\n            alt.Tooltip(\"ci_lower:Q\", title=\"CI Lower\", format=\".2f\"),\n            alt.Tooltip(\"ci_upper:Q\", title=\"CI Upper\", format=\".2f\"),\n        ],\n    )\n)\n\n# Combine all layers\nchart = (\n    alt.layer(reference_line, ci_lines, points, pooled_ci, pooled_diamond)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\"forest-basic · altair · anyplot.ai\", fontSize=28, anchor=\"middle\"),\n    )\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_axisX(title=\"Standardized Mean Difference (95% CI)\", grid=True, gridOpacity=0.10)\n    .configure_axisY(title=None, grid=False, ticks=False, domain=False)\n    .configure_title(color=INK)\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n)\n\n# Save as PNG and HTML with theme-suffixed names\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}