|
1 | 1 | """ pyplots.ai |
2 | 2 | density-basic: Basic Density 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: 90/100 | Updated: 2026-02-23 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import numpy as np |
8 | 8 | import pygal |
9 | 9 | from pygal.style import Style |
10 | 10 |
|
11 | 11 |
|
12 | | -# Data - simulated test scores showing slightly left-skewed distribution |
| 12 | +# Data - test scores with clear bimodal structure (two student groups) |
13 | 13 | np.random.seed(42) |
14 | | -values = np.concatenate( |
15 | | - [ |
16 | | - np.random.normal(75, 8, 200), # Main cluster around 75 |
17 | | - np.random.normal(60, 5, 50), # Smaller cluster showing slight left skew |
18 | | - ] |
19 | | -) |
| 14 | +main_scores = np.random.normal(76, 8, 200) # Main group around 76 |
| 15 | +secondary_scores = np.random.normal(52, 5, 70) # Distinct lower-scoring group |
| 16 | +scores = np.concatenate([main_scores, secondary_scores]) |
20 | 17 |
|
| 18 | +# KDE with Gaussian kernel and Scott's rule bandwidth |
| 19 | +x_range = np.linspace(scores.min() - 8, scores.max() + 8, 400) |
| 20 | +n = len(scores) |
| 21 | +bandwidth = n ** (-1 / 5) * np.std(scores) |
21 | 22 |
|
22 | | -# Compute KDE using Gaussian kernel |
23 | | -x_range = np.linspace(values.min() - 10, values.max() + 10, 200) |
24 | | -n = len(values) |
25 | | -bandwidth = n ** (-1 / 5) * np.std(values) # Scott's rule |
| 23 | +# Combined density |
26 | 24 | density = np.zeros_like(x_range) |
27 | | -for xi in values: |
| 25 | +for xi in scores: |
28 | 26 | density += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2) |
29 | 27 | density /= n * bandwidth * np.sqrt(2 * np.pi) |
30 | 28 |
|
31 | | -# Custom style for 4800x2700 px (scaled 3x from template for large canvas) |
| 29 | +# Secondary component density (weighted by proportion) for visual storytelling |
| 30 | +density_sec = np.zeros_like(x_range) |
| 31 | +for xi in secondary_scores: |
| 32 | + density_sec += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2) |
| 33 | +density_sec /= n * bandwidth * np.sqrt(2 * np.pi) |
| 34 | + |
| 35 | +# Refined style with warm accent for secondary group |
32 | 36 | custom_style = Style( |
33 | 37 | background="white", |
34 | 38 | plot_background="white", |
35 | | - foreground="#333", |
36 | | - foreground_strong="#333", |
37 | | - foreground_subtle="#666", |
38 | | - colors=("#306998",), |
39 | | - title_font_size=56, |
40 | | - label_font_size=42, |
41 | | - major_label_font_size=36, |
| 39 | + foreground="#333333", |
| 40 | + foreground_strong="#333333", |
| 41 | + foreground_subtle="#e0e0e0", |
| 42 | + colors=("#306998", "#c47a3a", "#1a3d5c"), |
| 43 | + title_font_size=72, |
| 44 | + label_font_size=44, |
| 45 | + major_label_font_size=40, |
42 | 46 | legend_font_size=36, |
43 | | - value_font_size=32, |
| 47 | + value_font_size=28, |
| 48 | + stroke_width=5, |
| 49 | + opacity=0.60, |
| 50 | + opacity_hover=0.85, |
| 51 | + font_family="'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", |
44 | 52 | ) |
45 | 53 |
|
46 | | -# Create XY chart for continuous density curve |
| 54 | +# Chart with refined axes — spines removed for clean floating-axis look |
47 | 55 | chart = pygal.XY( |
48 | 56 | width=4800, |
49 | 57 | height=2700, |
50 | 58 | style=custom_style, |
51 | 59 | title="density-basic · pygal · pyplots.ai", |
52 | 60 | x_title="Test Score (points)", |
53 | | - y_title="Probability Density", |
| 61 | + y_title="Density", |
54 | 62 | show_dots=False, |
55 | | - stroke_style={"width": 3}, |
56 | 63 | fill=True, |
57 | | - show_legend=False, |
| 64 | + show_legend=True, |
| 65 | + legend_at_bottom=True, |
| 66 | + legend_box_size=28, |
58 | 67 | show_y_guides=True, |
59 | 68 | show_x_guides=False, |
| 69 | + stroke_style={"width": 5, "linecap": "round"}, |
| 70 | + truncate_label=-1, |
| 71 | + margin_top=40, |
| 72 | + margin_right=40, |
| 73 | + margin_bottom=30, |
| 74 | + margin_left=20, |
| 75 | + x_value_formatter=lambda x: f"{x:.0f}", |
| 76 | + y_value_formatter=lambda y: f"{y:.3f}", |
| 77 | + css=[ |
| 78 | + "file://style.css", |
| 79 | + "inline:.plot .background {fill: white !important; stroke: none !important; stroke-width: 0 !important;}", |
| 80 | + "inline:.graph > .background {fill: white !important; stroke: none !important;}", |
| 81 | + "inline:.axis .guides .line {stroke: #e0e0e0 !important; stroke-width: 0.8px;}", |
| 82 | + "inline:.axis.x > path.line {stroke: none !important; stroke-width: 0 !important;}", |
| 83 | + "inline:.axis.y > path.line {stroke: none !important; stroke-width: 0 !important;}", |
| 84 | + "inline:.axis .guides text {fill: #666666 !important;}", |
| 85 | + "inline:text.title {font-weight: 600 !important; fill: #222222 !important;}", |
| 86 | + "inline:.axis text {font-weight: 400 !important;}", |
| 87 | + "inline:.legends text {font-weight: 400 !important; fill: #444444 !important;}", |
| 88 | + ], |
| 89 | + js=[], |
60 | 90 | ) |
61 | 91 |
|
62 | | -# Add density curve data as XY points |
63 | | -xy_data = [(float(x), float(y)) for x, y in zip(x_range, density, strict=True)] |
64 | | -chart.add("Density", xy_data) |
| 92 | +# Main density curve (combined) — prominent filled area |
| 93 | +xy_combined = [(float(x), float(y)) for x, y in zip(x_range, density, strict=True)] |
| 94 | +chart.add("Test score distribution", xy_combined) |
| 95 | + |
| 96 | +# Secondary component — warm accent highlighting bimodal structure |
| 97 | +xy_sec = [(float(x), float(y)) for x, y in zip(x_range, density_sec, strict=True)] |
| 98 | +chart.add("Lower-scoring group", xy_sec, stroke_style={"width": 3.5, "linecap": "round"}, fill=True) |
65 | 99 |
|
66 | | -# Add rug plot as small vertical marks along x-axis (sampled for clarity) |
67 | | -rug_height = max(density) * 0.02 # Small height for rug marks |
68 | | -rug_sample = values[::5] # Sample every 5th point to avoid clutter |
| 100 | +# Rug plot — individual observation marks with increased prominence |
| 101 | +rug_height = max(density) * 0.10 |
69 | 102 | rug_data = [] |
70 | | -for xi in rug_sample: |
71 | | - rug_data.append((float(xi), 0)) |
| 103 | +for xi in sorted(scores): |
| 104 | + rug_data.append((float(xi), 0.0)) |
72 | 105 | rug_data.append((float(xi), float(rug_height))) |
73 | | - rug_data.append((float(xi), 0)) |
| 106 | + rug_data.append((float(xi), 0.0)) |
74 | 107 |
|
75 | | -# Add rug marks as a separate series with thinner stroke |
76 | | -chart.add("Observations", rug_data, stroke_style={"width": 1}, show_dots=False, fill=False) |
| 108 | +chart.add("Individual scores", rug_data, stroke_style={"width": 4.5, "linecap": "round"}, show_dots=False, fill=False) |
77 | 109 |
|
78 | | -# Save as PNG and HTML |
| 110 | +# Save outputs |
79 | 111 | chart.render_to_png("plot.png") |
80 | 112 | chart.render_to_file("plot.html") |
0 commit comments