Skip to content

Commit 328b3f4

Browse files
feat(altair): implement forest-basic (#2428)
## Implementation: `forest-basic` - altair Implements the **altair** version of `forest-basic`. **File:** `plots/forest-basic/implementations/altair.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20543640932)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 40f4222 commit 328b3f4

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
""" pyplots.ai
2+
forest-basic: Meta-Analysis Forest Plot
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-27
5+
"""
6+
7+
import altair as alt
8+
import pandas as pd
9+
10+
11+
# Data: Meta-analysis of RCTs comparing treatment efficacy (standardized mean difference)
12+
studies = [
13+
"Johnson 2018",
14+
"Smith 2019",
15+
"Garcia 2020",
16+
"Williams 2020",
17+
"Brown 2021",
18+
"Davis 2021",
19+
"Miller 2022",
20+
"Wilson 2022",
21+
"Anderson 2023",
22+
"Taylor 2023",
23+
]
24+
25+
effect_sizes = [-0.45, -0.32, -0.58, -0.21, -0.67, -0.38, -0.52, -0.29, -0.41, -0.55]
26+
ci_lower = [-0.78, -0.61, -0.95, -0.48, -1.02, -0.69, -0.88, -0.56, -0.72, -0.91]
27+
ci_upper = [-0.12, -0.03, -0.21, 0.06, -0.32, -0.07, -0.16, -0.02, -0.10, -0.19]
28+
weights = [8.5, 10.2, 7.8, 11.5, 6.9, 9.3, 8.1, 10.8, 9.7, 7.2]
29+
30+
# Pooled estimate
31+
pooled_effect = -0.42
32+
pooled_ci_lower = -0.53
33+
pooled_ci_upper = -0.31
34+
35+
# Order labels for y-axis (studies at top, pooled estimate at bottom)
36+
y_labels = list(reversed(studies)) + ["Pooled Estimate"]
37+
38+
# Create DataFrame for studies
39+
df_studies = pd.DataFrame(
40+
{"study": studies, "effect_size": effect_sizes, "ci_lower": ci_lower, "ci_upper": ci_upper, "weight": weights}
41+
)
42+
43+
# Normalize weights for marker sizing (scale between 150 and 500 for visibility)
44+
weight_min = min(weights)
45+
weight_max = max(weights)
46+
df_studies["marker_size"] = 150 + (df_studies["weight"] - weight_min) / (weight_max - weight_min) * 350
47+
48+
# Create DataFrame for pooled estimate (CI line and diamond point)
49+
df_pooled = pd.DataFrame(
50+
{
51+
"study": ["Pooled Estimate"],
52+
"effect_size": [pooled_effect],
53+
"ci_lower": [pooled_ci_lower],
54+
"ci_upper": [pooled_ci_upper],
55+
}
56+
)
57+
58+
# Vertical reference line at null effect (0)
59+
reference_line = (
60+
alt.Chart(pd.DataFrame({"x": [0]}))
61+
.mark_rule(color="#888888", strokeDash=[8, 4], strokeWidth=2, opacity=0.7)
62+
.encode(x="x:Q")
63+
)
64+
65+
# Confidence interval lines for studies
66+
ci_lines = (
67+
alt.Chart(df_studies)
68+
.mark_rule(color="#306998", strokeWidth=4)
69+
.encode(
70+
x=alt.X("ci_lower:Q", scale=alt.Scale(domain=[-1.15, 0.15])), x2="ci_upper:Q", y=alt.Y("study:N", sort=y_labels)
71+
)
72+
)
73+
74+
# Effect size points for studies
75+
points = (
76+
alt.Chart(df_studies)
77+
.mark_point(filled=True, color="#306998", stroke="white", strokeWidth=2)
78+
.encode(
79+
x=alt.X("effect_size:Q", scale=alt.Scale(domain=[-1.15, 0.15])),
80+
y=alt.Y("study:N", sort=y_labels),
81+
size=alt.Size("marker_size:Q", legend=None, scale=alt.Scale(range=[150, 500])),
82+
tooltip=[
83+
alt.Tooltip("study:N", title="Study"),
84+
alt.Tooltip("effect_size:Q", title="Effect Size", format=".2f"),
85+
alt.Tooltip("ci_lower:Q", title="CI Lower", format=".2f"),
86+
alt.Tooltip("ci_upper:Q", title="CI Upper", format=".2f"),
87+
],
88+
)
89+
)
90+
91+
# Pooled estimate confidence interval line
92+
pooled_ci = (
93+
alt.Chart(df_pooled)
94+
.mark_rule(color="#306998", strokeWidth=4)
95+
.encode(
96+
x=alt.X("ci_lower:Q", scale=alt.Scale(domain=[-1.15, 0.15])), x2="ci_upper:Q", y=alt.Y("study:N", sort=y_labels)
97+
)
98+
)
99+
100+
# Diamond marker for pooled estimate using mark_point with shape
101+
pooled_diamond = (
102+
alt.Chart(df_pooled)
103+
.mark_point(shape="diamond", filled=True, size=1500, color="#FFD43B", stroke="#306998", strokeWidth=3)
104+
.encode(
105+
x=alt.X("effect_size:Q", scale=alt.Scale(domain=[-1.15, 0.15])),
106+
y=alt.Y("study:N", sort=y_labels),
107+
tooltip=[
108+
alt.Tooltip("study:N", title="Estimate"),
109+
alt.Tooltip("effect_size:Q", title="Effect Size", format=".2f"),
110+
alt.Tooltip("ci_lower:Q", title="CI Lower", format=".2f"),
111+
alt.Tooltip("ci_upper:Q", title="CI Upper", format=".2f"),
112+
],
113+
)
114+
)
115+
116+
# Text annotations for "Favors Treatment" and "Favors Control"
117+
annotation_left = (
118+
alt.Chart(pd.DataFrame({"x": [-1.05], "study": ["Pooled Estimate"], "text": ["← Favors Treatment"]}))
119+
.mark_text(fontSize=16, color="#555555", align="left", dy=35)
120+
.encode(x=alt.X("x:Q", scale=alt.Scale(domain=[-1.15, 0.15])), y=alt.Y("study:N", sort=y_labels), text="text:N")
121+
)
122+
123+
annotation_right = (
124+
alt.Chart(pd.DataFrame({"x": [0.05], "study": ["Pooled Estimate"], "text": ["Favors Control →"]}))
125+
.mark_text(fontSize=16, color="#555555", align="right", dy=35)
126+
.encode(x=alt.X("x:Q", scale=alt.Scale(domain=[-1.15, 0.15])), y=alt.Y("study:N", sort=y_labels), text="text:N")
127+
)
128+
129+
# Combine all layers
130+
chart = (
131+
alt.layer(reference_line, ci_lines, points, pooled_ci, pooled_diamond, annotation_left, annotation_right)
132+
.properties(
133+
width=1600, height=900, title=alt.Title("forest-basic · altair · pyplots.ai", fontSize=28, anchor="middle")
134+
)
135+
.configure_axis(labelFontSize=18, titleFontSize=22)
136+
.configure_axisX(title="Standardized Mean Difference (95% CI)", grid=True, gridOpacity=0.3, gridDash=[4, 4])
137+
.configure_axisY(title=None, grid=False, ticks=False, domain=False)
138+
.configure_view(strokeWidth=0)
139+
)
140+
141+
# Save as PNG and HTML
142+
chart.save("plot.png", scale_factor=3.0)
143+
chart.save("plot.html")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: altair
2+
specification_id: forest-basic
3+
created: '2025-12-27T19:59:34Z'
4+
updated: '2025-12-27T20:01:48Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20543640932
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/forest-basic/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/forest-basic/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/forest-basic/altair/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent visual clarity with well-sized text and markers
17+
- Diamond shape for pooled estimate is distinctive and correctly implemented
18+
- Good use of Altair layered chart composition with 7 layers cleanly combined
19+
- Weight-proportional marker sizing implemented correctly
20+
- Appropriate color scheme with blue/yellow contrast
21+
- Clean code structure following KISS principles
22+
- Helpful tooltips for interactivity
23+
weaknesses:
24+
- The Favors Treatment and Favors Control annotations appear slightly cut off at
25+
the edges
26+
- All studies show negative effects favoring treatment - having at least one study
27+
crossing the null line would better demonstrate forest plot interpretation
28+
- Could leverage more Altair-specific features like selection for highlighting

0 commit comments

Comments
 (0)