Skip to content

Commit 40f4222

Browse files
feat(pygal): implement forest-basic (#2417)
## Implementation: `forest-basic` - pygal Implements the **pygal** version of `forest-basic`. **File:** `plots/forest-basic/implementations/pygal.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20543283240)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c29ebb9 commit 40f4222

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
""" pyplots.ai
2+
forest-basic: Meta-Analysis Forest Plot
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 75/100 | Created: 2025-12-27
5+
"""
6+
7+
import pygal
8+
from pygal.style import Style
9+
10+
11+
# Data: Meta-analysis of treatment effect (mean difference)
12+
# Format: (study_name, effect_size, ci_lower, ci_upper, weight)
13+
studies = [
14+
("Anderson 2023", -0.44, -0.82, -0.06, 12.8),
15+
("Taylor 2022", -0.61, -1.08, -0.14, 7.6),
16+
("Moore 2022", -0.38, -0.71, -0.05, 13.8),
17+
("Wilson 2021", -0.55, -0.98, -0.12, 9.1),
18+
("Miller 2021", -0.29, -0.65, 0.07, 11.7),
19+
("Davis 2020", -0.41, -0.78, -0.04, 14.2),
20+
("Brown 2020", -0.67, -1.15, -0.19, 10.3),
21+
("Williams 2019", -0.18, -0.58, 0.22, 9.8),
22+
("Johnson 2019", -0.52, -0.95, -0.09, 12.5),
23+
("Smith 2018", -0.35, -0.72, 0.02, 8.2),
24+
]
25+
26+
# Pooled estimate (diamond in traditional forest plots)
27+
pooled_effect = -0.43
28+
pooled_ci_lower = -0.58
29+
pooled_ci_upper = -0.28
30+
31+
# Inline weight normalization (min=7.6, max=14.2, range=6.6)
32+
min_weight = 7.6
33+
max_weight = 14.2
34+
weight_range = 6.6
35+
36+
# Custom style for large canvas (4800 x 2700)
37+
custom_style = Style(
38+
background="white",
39+
plot_background="white",
40+
foreground="#333333",
41+
foreground_strong="#333333",
42+
foreground_subtle="#666666",
43+
colors=(
44+
"#306998", # Study markers (Python Blue)
45+
"#FFD43B", # Pooled diamond (Yellow for contrast)
46+
),
47+
title_font_size=56,
48+
label_font_size=32,
49+
major_label_font_size=28,
50+
legend_font_size=28,
51+
value_font_size=24,
52+
tooltip_font_size=28,
53+
stroke_width=6,
54+
font_family="Arial",
55+
)
56+
57+
# Create XY chart for forest plot
58+
chart = pygal.XY(
59+
width=4800,
60+
height=2700,
61+
title="forest-basic · pygal · pyplots.ai",
62+
x_title="Mean Difference (95% CI)",
63+
style=custom_style,
64+
show_legend=False,
65+
dots_size=16,
66+
stroke=False,
67+
show_y_guides=False,
68+
show_x_guides=True,
69+
x_label_rotation=0,
70+
range=(-1.3, 0.4),
71+
margin=100,
72+
)
73+
74+
# Add vertical reference line at x=0 (null effect) first (background layer)
75+
chart.add(
76+
None,
77+
[(0, -0.5), (0, len(studies) + 0.5)],
78+
stroke=True,
79+
show_dots=False,
80+
stroke_style={"width": 3, "dasharray": "12, 6"},
81+
)
82+
83+
# Add CI whiskers (thicker lines for better visibility at this canvas size)
84+
for i, (_study, _effect, ci_low, ci_high, _weight) in enumerate(studies):
85+
y_pos = len(studies) - i
86+
chart.add(None, [(ci_low, y_pos), (ci_high, y_pos)], stroke=True, show_dots=False, stroke_style={"width": 6})
87+
88+
# Add each study point with weight-proportional size (inline calculation)
89+
for i, (_study, effect, _ci_low, _ci_high, weight) in enumerate(studies):
90+
y_pos = len(studies) - i
91+
dot_size = int(12 + ((weight - min_weight) / weight_range) * 16)
92+
chart.add(None, [(effect, y_pos)], dots_size=dot_size, stroke=False)
93+
94+
# Add pooled CI whisker (thicker for emphasis)
95+
chart.add(None, [(pooled_ci_lower, 0), (pooled_ci_upper, 0)], stroke=True, show_dots=False, stroke_style={"width": 7})
96+
97+
# Add pooled estimate as a diamond shape using filled polygon
98+
# Draw diamond with 4 lines forming a closed shape (traditional forest plot diamond)
99+
diamond_half_height = 0.4
100+
# Top-left edge
101+
chart.add(
102+
None,
103+
[(pooled_ci_lower, 0), (pooled_effect, diamond_half_height)],
104+
stroke=True,
105+
show_dots=False,
106+
stroke_style={"width": 4},
107+
)
108+
# Top-right edge
109+
chart.add(
110+
None,
111+
[(pooled_effect, diamond_half_height), (pooled_ci_upper, 0)],
112+
stroke=True,
113+
show_dots=False,
114+
stroke_style={"width": 4},
115+
)
116+
# Bottom-right edge
117+
chart.add(
118+
None,
119+
[(pooled_ci_upper, 0), (pooled_effect, -diamond_half_height)],
120+
stroke=True,
121+
show_dots=False,
122+
stroke_style={"width": 4},
123+
)
124+
# Bottom-left edge
125+
chart.add(
126+
None,
127+
[(pooled_effect, -diamond_half_height), (pooled_ci_lower, 0)],
128+
stroke=True,
129+
show_dots=False,
130+
stroke_style={"width": 4},
131+
)
132+
# Add center point for the diamond (filled dot at center for visual emphasis)
133+
chart.add(None, [(pooled_effect, 0)], dots_size=28, stroke=False)
134+
135+
# Y-axis labels with study names and CIs
136+
y_labels = []
137+
for i, (study, _effect, ci_low, ci_high, _weight) in enumerate(studies):
138+
y_labels.append({"value": len(studies) - i, "label": f"{study} [{ci_low:.2f}, {ci_high:.2f}]"})
139+
y_labels.append({"value": 0, "label": f"Pooled [{pooled_ci_lower:.2f}, {pooled_ci_upper:.2f}]"})
140+
chart.y_labels = y_labels
141+
142+
# Save outputs
143+
chart.render_to_png("plot.png")
144+
chart.render_to_file("plot.html")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: pygal
2+
specification_id: forest-basic
3+
created: '2025-12-27T19:25:07Z'
4+
updated: '2025-12-27T19:52:15Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20543283240
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/forest-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/forest-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/forest-basic/pygal/plot.html
13+
quality_score: 75
14+
review:
15+
strengths:
16+
- Clean data representation with study names and CI bounds displayed on Y-axis labels
17+
- Good use of color contrast between study markers (blue) and pooled estimate (yellow)
18+
- Proper null reference line at x=0 as dashed vertical line
19+
- Weight-proportional marker sizing implemented correctly
20+
- Appropriate font sizes for the 4800x2700 canvas
21+
weaknesses:
22+
- Pooled estimate diamond is rendered as a circle rather than the traditional diamond
23+
shape specified in the requirements
24+
- CI whiskers extend beyond the visible plot area on the right side, creating visual
25+
artifacts
26+
- Legend at bottom is redundant and cluttered; forest plots typically do not need
27+
a legend as the visual encoding is standard
28+
- The diamond outline attempt using 4 line segments is not visible/effective

0 commit comments

Comments
 (0)