Skip to content

Commit d7cdd4a

Browse files
feat(pygal): implement cat-box-strip (#2763)
## Implementation: `cat-box-strip` - pygal Implements the **pygal** version of `cat-box-strip`. **File:** `plots/cat-box-strip/implementations/pygal.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20601060592)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 54e70ee commit d7cdd4a

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
""" pyplots.ai
2+
cat-box-strip: Box Plot with Strip Overlay
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data - Plant growth measurements (cm) under different light conditions
13+
np.random.seed(42)
14+
categories = ["Full Sun", "Partial Shade", "Full Shade", "Artificial"]
15+
data = {
16+
"Full Sun": np.random.normal(45, 8, 35),
17+
"Partial Shade": np.random.normal(38, 10, 40),
18+
"Full Shade": np.random.normal(25, 6, 30),
19+
"Artificial": np.random.normal(35, 12, 38),
20+
}
21+
22+
# Add realistic variation and some outliers
23+
data["Full Sun"] = np.append(data["Full Sun"], [68, 72, 28])
24+
data["Partial Shade"] = np.append(data["Partial Shade"], [65, 15])
25+
data["Full Shade"] = np.append(data["Full Shade"], [42, 10])
26+
data["Artificial"] = np.append(data["Artificial"], [70, 12])
27+
28+
# Group colors - distinct for each category (colorblind-safe)
29+
group_colors = ["#306998", "#FFD43B", "#4CAF50", "#E07B39"]
30+
31+
# Build color sequence: 6 elements per box (box, median, 2 whiskers, 2 caps) + 1 strip = 7 per category
32+
# But we draw all boxes first (6*4=24), then all strips (4)
33+
# So color sequence needs: 6 of color1, 6 of color2, 6 of color3, 6 of color4, then 1 each for strips
34+
color_sequence = []
35+
for c in group_colors:
36+
color_sequence.extend([c] * 6) # 6 box elements per category
37+
for c in group_colors:
38+
color_sequence.append(c) # 1 strip series per category
39+
40+
# Custom style for 4800x2700 px canvas
41+
custom_style = Style(
42+
background="white",
43+
plot_background="white",
44+
foreground="#333333",
45+
foreground_strong="#333333",
46+
foreground_subtle="#999999",
47+
guide_stroke_color="#e0e0e0",
48+
colors=tuple(color_sequence),
49+
title_font_size=72,
50+
label_font_size=48,
51+
major_label_font_size=44,
52+
legend_font_size=44,
53+
value_font_size=36,
54+
opacity=0.6,
55+
opacity_hover=0.8,
56+
)
57+
58+
# Create XY chart for combined box plot with strip overlay
59+
# X-axis = Category position, Y-axis = Plant Height (cm)
60+
chart = pygal.XY(
61+
width=4800,
62+
height=2700,
63+
style=custom_style,
64+
title="cat-box-strip · pygal · pyplots.ai",
65+
x_title="Light Condition",
66+
y_title="Plant Height (cm)",
67+
show_legend=False,
68+
stroke=True,
69+
fill=True,
70+
dots_size=0,
71+
show_x_guides=False,
72+
show_y_guides=True,
73+
xrange=(0, 5),
74+
range=(0, 80),
75+
margin=80,
76+
explicit_size=True,
77+
)
78+
79+
# Layout parameters
80+
box_width = 0.25
81+
cap_width = 0.15
82+
83+
# Pre-compute all components
84+
strip_data = []
85+
box_data = []
86+
87+
for i, (category, values) in enumerate(data.items()):
88+
center_x = i + 1 # X position for this group (1, 2, 3, 4)
89+
values = np.array(values)
90+
color = group_colors[i]
91+
92+
# --- Box Plot Statistics ---
93+
median = float(np.median(values))
94+
q1 = float(np.percentile(values, 25))
95+
q3 = float(np.percentile(values, 75))
96+
iqr = q3 - q1
97+
whisker_low = float(max(values.min(), q1 - 1.5 * iqr))
98+
whisker_high = float(min(values.max(), q3 + 1.5 * iqr))
99+
box_data.append((center_x, median, q1, q3, whisker_low, whisker_high, color))
100+
101+
# --- Strip Points with Jitter ---
102+
np.random.seed(42 + i)
103+
jitter = np.random.uniform(-0.12, 0.12, len(values))
104+
strip_points = [(center_x + j, float(v)) for j, v in zip(jitter, values, strict=True)]
105+
strip_data.append((category, strip_points, color))
106+
107+
# First, draw box plots (so strip points appear on top)
108+
for center_x, median, q1, q3, whisker_low, whisker_high, _color in box_data:
109+
# IQR box (filled rectangle)
110+
quartile_box = [
111+
(center_x - box_width, q1),
112+
(center_x - box_width, q3),
113+
(center_x + box_width, q3),
114+
(center_x + box_width, q1),
115+
(center_x - box_width, q1),
116+
]
117+
chart.add("", quartile_box, stroke=True, fill=True, show_dots=False, stroke_style={"width": 6})
118+
119+
# Median line (horizontal line within box)
120+
median_line = [(center_x - box_width * 1.1, median), (center_x + box_width * 1.1, median)]
121+
chart.add("", median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 10})
122+
123+
# Whiskers (vertical lines from box to caps)
124+
whisker_bottom = [(center_x, q1), (center_x, whisker_low)]
125+
whisker_top = [(center_x, q3), (center_x, whisker_high)]
126+
chart.add("", whisker_bottom, stroke=True, fill=False, show_dots=False, stroke_style={"width": 6})
127+
chart.add("", whisker_top, stroke=True, fill=False, show_dots=False, stroke_style={"width": 6})
128+
129+
# Whisker caps (horizontal lines at ends)
130+
cap_bottom = [(center_x - cap_width, whisker_low), (center_x + cap_width, whisker_low)]
131+
cap_top = [(center_x - cap_width, whisker_high), (center_x + cap_width, whisker_high)]
132+
chart.add("", cap_bottom, stroke=True, fill=False, show_dots=False, stroke_style={"width": 6})
133+
chart.add("", cap_top, stroke=True, fill=False, show_dots=False, stroke_style={"width": 6})
134+
135+
# Add strip points on top with transparency
136+
for _category, strip_points, _color in strip_data:
137+
chart.add("", strip_points, stroke=False, fill=False, dots_size=18)
138+
139+
# X-axis labels for categories
140+
chart.x_labels = [
141+
{"value": 0, "label": ""},
142+
{"value": 1, "label": "Full Sun"},
143+
{"value": 2, "label": "Partial Shade"},
144+
{"value": 3, "label": "Full Shade"},
145+
{"value": 4, "label": "Artificial"},
146+
{"value": 5, "label": ""},
147+
]
148+
149+
# Save outputs
150+
chart.render_to_file("plot.html")
151+
chart.render_to_png("plot.png")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: pygal
2+
specification_id: cat-box-strip
3+
created: '2025-12-30T16:31:18Z'
4+
updated: '2025-12-30T16:42:46Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20601060592
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/cat-box-strip/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/cat-box-strip/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/cat-box-strip/pygal/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent creative solution using XY chart to achieve box plot with strip overlay
17+
(pygal.Box does not support strip overlay natively)
18+
- Well-designed color scheme with matching box and point colors for each category
19+
- Good use of custom Style for proper font sizing at 4800x2700 resolution
20+
- Clear statistical visualization with median, quartiles, whiskers, and caps properly
21+
constructed
22+
- Jitter and transparency on strip points effectively reduce overlap
23+
weaknesses:
24+
- Grid only shows on y-axis, no x-axis guides (minor visual consistency issue)
25+
- Some strip points in dense areas still overlap slightly despite jitter

0 commit comments

Comments
 (0)