Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 51 additions & 39 deletions plots/violin-basic/implementations/pygal.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
""" pyplots.ai
violin-basic: Basic Violin Plot
Library: pygal 3.1.0 | Python 3.13.11
Quality: 91/100 | Created: 2025-12-23
Library: pygal 3.1.0 | Python 3.14.3
Quality: 80/100 | Updated: 2026-02-21
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation header docstring no longer follows the repository’s standard 4-line format: the quality score is missing ("Quality: /100") and the date label changed to "Updated" instead of "Created". If any tooling parses these headers, this will break consistency with other violin-basic implementations (e.g., matplotlib.py uses "Quality: 92/100 | Created: ..."). Update the header to include an actual score and keep the conventional "Created" field (or whichever field the repo expects).

Suggested change
Quality: 80/100 | Updated: 2026-02-21
Quality: 92/100 | Created: 2026-02-21

Copilot uses AI. Check for mistakes.
"""

import numpy as np
import pygal
from pygal.style import Style


# Data - Generate distributions for different categories
# Data - Test scores across 4 class groups with distinct distribution shapes
np.random.seed(42)
data = {
"Engineering": np.random.normal(85, 12, 200),
"Marketing": np.random.normal(72, 15, 200),
"Sales": np.random.normal(78, 20, 200),
"Operations": np.random.normal(65, 10, 200),
"Honors": np.clip(np.random.normal(88, 6, 200), 50, 100),
"Standard": np.clip(np.random.normal(74, 10, 200), 40, 100),
"Remedial": np.clip(np.random.normal(62, 8, 200), 30, 100),
"Advanced": np.clip(np.random.normal(82, 14, 200), 45, 100),
}

# Custom style for 4800x2700 px canvas
# Colors: each violin gets 3 series (fill, IQR box, median line)
# Palette cycles per series, so position matters
custom_style = Style(
background="white",
plot_background="white",
foreground="#333333",
foreground_strong="#333333",
foreground_subtle="#666666",
colors=("#306998", "#FFD43B", "#4CAF50", "#FF5722", "#666666", "#999999", "#333333", "#AAAAAA"),
foreground_subtle="#cccccc",
colors=(
"#306998",
"#1a1a1a",
"#1a1a1a",
"#E8875B",
"#1a1a1a",
"#1a1a1a",
"#5BA37E",
"#1a1a1a",
"#1a1a1a",
"#C4A23D",
"#1a1a1a",
"#1a1a1a",
),
title_font_size=72,
label_font_size=48,
major_label_font_size=42,
legend_font_size=36,
value_font_size=36,
opacity=0.7,
opacity=0.75,
opacity_hover=0.9,
)

Expand All @@ -41,37 +55,36 @@
height=2700,
style=custom_style,
title="violin-basic · pygal · pyplots.ai",
x_title="Category",
y_title="Performance Score",
show_legend=True,
legend_at_bottom=True,
x_title="Class Group",
y_title="Test Score",
show_legend=False,
stroke=True,
fill=True,
dots_size=0,
show_x_guides=False,
show_y_guides=True,
range=(20, 130),
xrange=(0, 6),
range=(30, 105),
xrange=(0, 5.5),
margin=50,
)

# Parameters for violin shapes
# Violin shape parameters
violin_width = 0.4
n_points = 100

# Add violins for each category
# Build violins with quartile markers and median lines
for i, (category, values) in enumerate(data.items()):
center_x = i + 1.5
center_x = i + 1.25

# Compute KDE using Silverman's rule
# KDE using Silverman's rule
n = len(values)
std = np.std(values)
iqr = np.percentile(values, 75) - np.percentile(values, 25)
bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)

# Create range of y values for density
# Y values for density estimation
y_min, y_max = values.min(), values.max()
y_range = np.linspace(y_min - 5, y_max + 5, n_points)
y_range = np.linspace(y_min - 3, y_max + 3, n_points)

# Gaussian kernel density estimation
density = np.zeros_like(y_range)
Expand All @@ -82,37 +95,36 @@
# Normalize density to desired width
density = density / density.max() * violin_width

# Create violin shape (mirrored density)
# Mirrored violin shape
left_points = [(center_x - d, y) for y, d in zip(y_range, density, strict=True)]
right_points = [(center_x + d, y) for y, d in zip(y_range[::-1], density[::-1], strict=True)]
violin_points = left_points + right_points + [left_points[0]]

chart.add(category, violin_points)

# Calculate quartiles and median for inner box
# Quartile markers and median
median = float(np.median(values))
q1 = float(np.percentile(values, 25))
q3 = float(np.percentile(values, 75))
box_width = 0.06
box_w = 0.15

# Quartile box (IQR)
# IQR box — filled dark for strong contrast
quartile_box = [
(center_x - box_width, q1),
(center_x - box_width, q3),
(center_x + box_width, q3),
(center_x + box_width, q1),
(center_x - box_width, q1),
(center_x - box_w, q1),
(center_x - box_w, q3),
(center_x + box_w, q3),
(center_x + box_w, q1),
(center_x - box_w, q1),
]
chart.add(None, quartile_box, stroke=True, fill=False, show_dots=False)
chart.add(None, quartile_box, stroke=True, fill=True, show_dots=False, stroke_style={"width": 3})

# Median line
median_line = [(center_x - box_width * 1.5, median), (center_x + box_width * 1.5, median)]
chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 4})
# Median line — thick dark stroke
median_line = [(center_x - box_w * 1.3, median), (center_x + box_w * 1.3, median)]
chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 8})

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

# Save outputs
# Save
chart.render_to_file("plot.html")
chart.render_to_png("plot.png")
Loading