Skip to content

Commit d9c175b

Browse files
update(violin-basic): pygal — comprehensive quality review (#4328)
## Summary Updated **pygal** implementation for **violin-basic**. **Changes:** Comprehensive quality review improving code quality, data choice, visual design, spec compliance, and library feature usage. ### Changes - Improved data generation with distinct distribution shapes per category - Enhanced visual design (explicit font sizes, refined color palette, layout balance) - Fixed review weaknesses from previous evaluation - Updated metadata with current library/Python versions - Preview images uploaded to GCS staging ## Test Plan - [x] Preview images uploaded to GCS staging - [x] Implementation file passes ruff format/check - [x] Metadata YAML updated with current versions - [ ] Automated review triggered --- Generated with [Claude Code](https://claude.com/claude-code) `/update` command --------- 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 dffd16e commit d9c175b

2 files changed

Lines changed: 206 additions & 166 deletions

File tree

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,47 @@
11
""" pyplots.ai
22
violin-basic: Basic Violin Plot
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 85/100 | Updated: 2026-02-21
55
"""
66

77
import numpy as np
88
import pygal
99
from pygal.style import Style
1010

1111

12-
# Data - Generate distributions for different categories
12+
# Data - Test scores across 4 class groups with distinct distribution shapes
1313
np.random.seed(42)
1414
data = {
15-
"Engineering": np.random.normal(85, 12, 200),
16-
"Marketing": np.random.normal(72, 15, 200),
17-
"Sales": np.random.normal(78, 20, 200),
18-
"Operations": np.random.normal(65, 10, 200),
15+
"Honors": np.clip(np.random.normal(88, 6, 200), 50, 100),
16+
"Standard": np.clip(60 + np.random.gamma(3.5, 4, 200), 40, 100),
17+
"Remedial": np.clip(np.random.normal(62, 8, 200), 30, 100),
18+
"Advanced": np.clip(np.concatenate([np.random.normal(75, 6, 120), np.random.normal(93, 4, 80)]), 45, 100),
1919
}
2020

21-
# Custom style for 4800x2700 px canvas
21+
# Colors: 3 series per violin (fill, IQR fill, median line)
22+
# Purple replaces green for deuteranopia accessibility; gold emphasizes bimodal Advanced
23+
violin_colors = ["#306998", "#E8875B", "#8B6FBF", "#D4A017"]
24+
iqr_colors = ["#1d3f5c", "#a35a38", "#5A3F82", "#8a6a10"]
25+
median_color = "#FFFFFF"
26+
palette = []
27+
for vc, ic in zip(violin_colors, iqr_colors, strict=True):
28+
palette.extend([vc, ic, median_color])
29+
2230
custom_style = Style(
2331
background="white",
2432
plot_background="white",
25-
foreground="#333333",
33+
foreground="#555555",
2634
foreground_strong="#333333",
27-
foreground_subtle="#666666",
28-
colors=("#306998", "#FFD43B", "#4CAF50", "#FF5722", "#666666", "#999999", "#333333", "#AAAAAA"),
35+
foreground_subtle="#e0e0e0",
36+
colors=tuple(palette),
2937
title_font_size=72,
3038
label_font_size=48,
3139
major_label_font_size=42,
3240
legend_font_size=36,
3341
value_font_size=36,
34-
opacity=0.7,
35-
opacity_hover=0.9,
42+
opacity=0.78,
43+
opacity_hover=0.92,
44+
transition="200ms ease-in",
3645
)
3746

3847
# Create XY chart for violin plot (pygal has no native violin)
@@ -41,37 +50,44 @@
4150
height=2700,
4251
style=custom_style,
4352
title="violin-basic · pygal · pyplots.ai",
44-
x_title="Category",
45-
y_title="Performance Score",
46-
show_legend=True,
47-
legend_at_bottom=True,
53+
x_title="Class Group",
54+
y_title="Test Score (%)",
55+
show_legend=False,
4856
stroke=True,
4957
fill=True,
5058
dots_size=0,
5159
show_x_guides=False,
5260
show_y_guides=True,
53-
range=(20, 130),
54-
xrange=(0, 6),
61+
range=(33, 103),
62+
xrange=(0, 5.0),
5563
margin=50,
64+
value_formatter=lambda x: f"{x:.0f}%",
65+
x_value_formatter=lambda x: "",
66+
tooltip_border_radius=10,
67+
tooltip_fancy_mode=True,
68+
human_readable=True,
69+
pretty_print=True,
5670
)
5771

58-
# Parameters for violin shapes
59-
violin_width = 0.4
72+
# Violin widths — Advanced is wider to visually highlight its bimodal shape
73+
base_width = 0.38
74+
widths = {"Honors": base_width, "Standard": base_width, "Remedial": base_width, "Advanced": 0.46}
6075
n_points = 100
6176

62-
# Add violins for each category
77+
# Build violins with quartile markers and median lines
6378
for i, (category, values) in enumerate(data.items()):
64-
center_x = i + 1.5
79+
center_x = i + 1.0
80+
violin_width = widths[category]
6581

66-
# Compute KDE using Silverman's rule
82+
# KDE using Silverman's rule
6783
n = len(values)
6884
std = np.std(values)
69-
iqr = np.percentile(values, 75) - np.percentile(values, 25)
70-
bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)
85+
iqr_val = np.percentile(values, 75) - np.percentile(values, 25)
86+
bandwidth = 0.9 * min(std, iqr_val / 1.34) * n ** (-0.2)
7187

72-
# Create range of y values for density
88+
# Y values for density estimation
7389
y_min, y_max = values.min(), values.max()
74-
y_range = np.linspace(y_min - 5, y_max + 5, n_points)
90+
y_range = np.linspace(y_min - 2, y_max + 2, n_points)
7591

7692
# Gaussian kernel density estimation
7793
density = np.zeros_like(y_range)
@@ -82,37 +98,36 @@
8298
# Normalize density to desired width
8399
density = density / density.max() * violin_width
84100

85-
# Create violin shape (mirrored density)
101+
# Mirrored violin shape with tooltip showing statistics
102+
median_val = float(np.median(values))
103+
q1 = float(np.percentile(values, 25))
104+
q3 = float(np.percentile(values, 75))
105+
tooltip = f"{category} — Median: {median_val:.1f}%, Q1: {q1:.1f}%, Q3: {q3:.1f}%"
106+
86107
left_points = [(center_x - d, y) for y, d in zip(y_range, density, strict=True)]
87108
right_points = [(center_x + d, y) for y, d in zip(y_range[::-1], density[::-1], strict=True)]
88109
violin_points = left_points + right_points + [left_points[0]]
110+
chart.add(category, violin_points, formatter=lambda x, t=tooltip: t, stroke_style={"width": 2})
89111

90-
chart.add(category, violin_points)
91-
92-
# Calculate quartiles and median for inner box
93-
median = float(np.median(values))
94-
q1 = float(np.percentile(values, 25))
95-
q3 = float(np.percentile(values, 75))
96-
box_width = 0.06
97-
98-
# Quartile box (IQR)
112+
# Quartile markers — filled box in darker shade for clear visibility
113+
box_w = 0.16
99114
quartile_box = [
100-
(center_x - box_width, q1),
101-
(center_x - box_width, q3),
102-
(center_x + box_width, q3),
103-
(center_x + box_width, q1),
104-
(center_x - box_width, q1),
115+
(center_x - box_w, q1),
116+
(center_x - box_w, q3),
117+
(center_x + box_w, q3),
118+
(center_x + box_w, q1),
119+
(center_x - box_w, q1),
105120
]
106-
chart.add(None, quartile_box, stroke=True, fill=False, show_dots=False)
121+
chart.add(None, quartile_box, stroke=True, fill=True, show_dots=False, stroke_style={"width": 5})
107122

108-
# Median line
109-
median_line = [(center_x - box_width * 1.5, median), (center_x + box_width * 1.5, median)]
110-
chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 4})
123+
# Median line — thick white for high contrast against dark IQR box
124+
median_line = [(center_x - box_w * 1.1, median_val), (center_x + box_w * 1.1, median_val)]
125+
chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 18})
111126

112127
# X-axis labels at violin positions
113-
chart.x_labels = ["", "Engineering", "Marketing", "Sales", "Operations", ""]
128+
chart.x_labels = ["", "Honors", "Standard", "Remedial", "Advanced", ""]
114129
chart.x_labels_major_count = 4
115130

116-
# Save outputs
131+
# Save
117132
chart.render_to_file("plot.html")
118133
chart.render_to_png("plot.png")

0 commit comments

Comments
 (0)