Skip to content

Commit 3567577

Browse files
feat(pygal): implement histogram-returns-distribution (#3920)
## Implementation: `histogram-returns-distribution` - pygal Implements the **pygal** version of `histogram-returns-distribution`. **File:** `plots/histogram-returns-distribution/implementations/pygal.py` **Parent Issue:** #3751 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21082372884)* --------- 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 105a1ce commit 3567577

2 files changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
""" pyplots.ai
2+
histogram-returns-distribution: Returns Distribution Histogram
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 58/100 | Created: 2026-01-16
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data - Generate 252 daily returns (1 year of trading data)
13+
np.random.seed(42)
14+
returns = np.random.normal(loc=0.0005, scale=0.015, size=252) * 100 # Convert to percentage
15+
16+
# Calculate statistics
17+
n = len(returns)
18+
mean_return = np.mean(returns)
19+
std_return = np.std(returns, ddof=1)
20+
21+
# Skewness calculation
22+
skewness = (n / ((n - 1) * (n - 2))) * np.sum(((returns - mean_return) / std_return) ** 3)
23+
24+
# Kurtosis calculation (excess kurtosis)
25+
kurtosis = ((n * (n + 1)) / ((n - 1) * (n - 2) * (n - 3))) * np.sum(((returns - mean_return) / std_return) ** 4) - (
26+
3 * (n - 1) ** 2
27+
) / ((n - 2) * (n - 3))
28+
29+
# Create histogram bins with density normalization
30+
n_bins = 25
31+
counts, bin_edges = np.histogram(returns, bins=n_bins, density=True)
32+
33+
# Identify tail regions (beyond 2 standard deviations)
34+
lower_tail = mean_return - 2 * std_return
35+
upper_tail = mean_return + 2 * std_return
36+
37+
# Generate normal distribution curve points
38+
x_curve = np.linspace(returns.min() - 0.5, returns.max() + 0.5, 150)
39+
normal_pdf = (1 / (std_return * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x_curve - mean_return) / std_return) ** 2)
40+
41+
# Custom style for 4800x2700 px canvas with larger legend
42+
custom_style = Style(
43+
background="white",
44+
plot_background="white",
45+
foreground="#333",
46+
foreground_strong="#333",
47+
foreground_subtle="#666",
48+
colors=("#306998", "#C0392B", "#1E8449"), # Blue for normal, red for tails, green for curve
49+
title_font_size=64,
50+
label_font_size=44,
51+
major_label_font_size=40,
52+
legend_font_size=44,
53+
value_font_size=32,
54+
stroke_width=5,
55+
opacity=0.85,
56+
opacity_hover=1.0,
57+
)
58+
59+
# Create stats subtitle
60+
stats_text = f"Mean: {mean_return:.3f}% | Std: {std_return:.3f}% | Skew: {skewness:.2f} | Kurt: {kurtosis:.2f}"
61+
62+
# Use XY chart to render both histogram bars and normal curve on same y-axis
63+
chart = pygal.XY(
64+
width=4800,
65+
height=2700,
66+
style=custom_style,
67+
title=f"histogram-returns-distribution · pygal · pyplots.ai\n{stats_text}",
68+
x_title="Returns (%)",
69+
y_title="Probability Density",
70+
show_legend=True,
71+
legend_at_bottom=True,
72+
legend_at_bottom_columns=3,
73+
legend_box_size=32,
74+
show_y_guides=True,
75+
show_x_guides=False,
76+
margin_bottom=200,
77+
margin_left=120,
78+
margin_right=80,
79+
print_values=False,
80+
explicit_size=True,
81+
show_dots=False,
82+
stroke=True,
83+
fill=True,
84+
)
85+
86+
# Build histogram as step polygons for XY chart
87+
# Each bar is rendered as a box shape for filled step histogram effect
88+
normal_bars_xy = []
89+
tail_bars_xy = []
90+
91+
for i, count in enumerate(counts):
92+
left = float(bin_edges[i])
93+
right = float(bin_edges[i + 1])
94+
center = (left + right) / 2
95+
height = float(count)
96+
97+
box = [(left, 0), (left, height), (right, height), (right, 0)]
98+
99+
if center < lower_tail or center > upper_tail:
100+
tail_bars_xy.extend(box)
101+
else:
102+
normal_bars_xy.extend(box)
103+
104+
# Add histogram series as filled areas
105+
chart.add("Returns (within 2σ)", normal_bars_xy, fill=True, stroke_style={"width": 2})
106+
chart.add("Tails (beyond ±2σ)", tail_bars_xy, fill=True, stroke_style={"width": 2})
107+
108+
# Add normal distribution curve - not filled, just line
109+
curve_data = [(float(x), float(y)) for x, y in zip(x_curve, normal_pdf, strict=True)]
110+
chart.add("Normal Distribution", curve_data, fill=False, stroke_style={"width": 6})
111+
112+
# Save outputs
113+
chart.render_to_file("plot.html")
114+
chart.render_to_png("plot.png")
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
library: pygal
2+
specification_id: histogram-returns-distribution
3+
created: '2026-01-16T22:12:03Z'
4+
updated: '2026-01-18T22:39:21Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21082372884
7+
issue: 3751
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/histogram-returns-distribution/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/histogram-returns-distribution/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/histogram-returns-distribution/pygal/plot.html
13+
quality_score: 58
14+
review:
15+
strengths:
16+
- Histogram bars render correctly with proper density normalization
17+
- Tail highlighting beyond ±2σ works well with distinct coloring
18+
- Statistics (mean, std, skewness, kurtosis) calculated and displayed correctly
19+
- Good use of XY chart type to create step-polygon histogram effect
20+
- Code is well-structured and reproducible with seed(42)
21+
weaknesses:
22+
- Normal distribution curve overlay is not visible in the rendered output despite
23+
being added to the chart
24+
- Y-axis label is misleading (Frequency Density x 100 instead of Probability Density)
25+
- Title format does not follow the standard spec-id library pyplots.ai pattern
26+
- Legend entries do not match the full series descriptions and normal curve legend
27+
is missing
28+
- X-axis tick labels are rotated and somewhat difficult to read
29+
image_description: 'The plot displays a histogram of daily returns distribution
30+
with approximately 25 bins. The main histogram bars are rendered in steel blue
31+
(#306998) for returns within 2 standard deviations, while tail regions (beyond
32+
±2σ) are shown in coral/salmon red. The title at the top reads "Daily Returns
33+
Distribution (Mean: 0.044% | Std: 1.451% | Skew: 0.30 | Kurt: 0.62) · histogram-returns-distribution
34+
· pygal · pyplots.ai". The x-axis is labeled "Returns (%)" with rotated tick labels
35+
showing values from approximately -3.7% to 4.3%. The y-axis is labeled "Frequency
36+
(Density × 100)" with values from 0 to 36. A legend appears at the bottom left
37+
showing "Returns" and "Tails (>2σ)". Critical issue: The normal distribution curve
38+
overlay specified in the requirements is NOT visible in the rendered plot - only
39+
the histogram bars appear.'
40+
criteria_checklist:
41+
visual_quality:
42+
score: 28
43+
max: 40
44+
items:
45+
- id: VQ-01
46+
name: Text Legibility
47+
score: 7
48+
max: 10
49+
passed: true
50+
comment: Title and axis labels readable but x-axis tick labels are rotated
51+
and somewhat cramped
52+
- id: VQ-02
53+
name: No Overlap
54+
score: 6
55+
max: 8
56+
passed: true
57+
comment: Minor overlap in x-axis tick labels due to rotation
58+
- id: VQ-03
59+
name: Element Visibility
60+
score: 6
61+
max: 8
62+
passed: true
63+
comment: Histogram bars clearly visible, but normal curve is missing
64+
- id: VQ-04
65+
name: Color Accessibility
66+
score: 4
67+
max: 5
68+
passed: true
69+
comment: Blue/red distinction is colorblind-accessible
70+
- id: VQ-05
71+
name: Layout Balance
72+
score: 3
73+
max: 5
74+
passed: true
75+
comment: Good use of canvas but legend placement at bottom-left is awkward
76+
- id: VQ-06
77+
name: Axis Labels
78+
score: 1
79+
max: 2
80+
passed: false
81+
comment: Y-axis label says Frequency (Density x 100) which is misleading
82+
- id: VQ-07
83+
name: Grid & Legend
84+
score: 1
85+
max: 2
86+
passed: false
87+
comment: Horizontal grid lines present, legend incomplete (missing normal
88+
curve entry)
89+
spec_compliance:
90+
score: 15
91+
max: 25
92+
items:
93+
- id: SC-01
94+
name: Plot Type
95+
score: 6
96+
max: 8
97+
passed: true
98+
comment: Histogram is correct, but missing the required normal distribution
99+
overlay curve
100+
- id: SC-02
101+
name: Data Mapping
102+
score: 4
103+
max: 5
104+
passed: true
105+
comment: Returns correctly mapped to x-axis, density to y-axis
106+
- id: SC-03
107+
name: Required Features
108+
score: 2
109+
max: 5
110+
passed: false
111+
comment: Missing normal distribution curve overlay - a key specification requirement
112+
- id: SC-04
113+
name: Data Range
114+
score: 3
115+
max: 3
116+
passed: true
117+
comment: All data visible, appropriate bin width
118+
- id: SC-05
119+
name: Legend Accuracy
120+
score: 0
121+
max: 2
122+
passed: false
123+
comment: Legend shows Returns instead of Returns (within 2σ) and normal curve
124+
legend entry missing
125+
- id: SC-06
126+
name: Title Format
127+
score: 0
128+
max: 2
129+
passed: false
130+
comment: Title format is non-standard (stats first, then spec-id)
131+
data_quality:
132+
score: 17
133+
max: 20
134+
items:
135+
- id: DQ-01
136+
name: Feature Coverage
137+
score: 6
138+
max: 8
139+
passed: true
140+
comment: Shows distribution shape and tail highlighting, but missing curve
141+
comparison
142+
- id: DQ-02
143+
name: Realistic Context
144+
score: 6
145+
max: 7
146+
passed: true
147+
comment: Daily stock returns with realistic mean (~0.05%) and std (~1.5%)
148+
- id: DQ-03
149+
name: Appropriate Scale
150+
score: 5
151+
max: 5
152+
passed: true
153+
comment: 252 observations, appropriate for 1 year of trading data
154+
code_quality:
155+
score: 10
156+
max: 10
157+
items:
158+
- id: CQ-01
159+
name: KISS Structure
160+
score: 3
161+
max: 3
162+
passed: true
163+
comment: 'Simple linear structure: imports, data, plot, save'
164+
- id: CQ-02
165+
name: Reproducibility
166+
score: 3
167+
max: 3
168+
passed: true
169+
comment: np.random.seed(42) used
170+
- id: CQ-03
171+
name: Clean Imports
172+
score: 2
173+
max: 2
174+
passed: true
175+
comment: Only numpy and pygal imported
176+
- id: CQ-04
177+
name: No Deprecated API
178+
score: 1
179+
max: 1
180+
passed: true
181+
comment: Current pygal API
182+
- id: CQ-05
183+
name: Output Correct
184+
score: 1
185+
max: 1
186+
passed: true
187+
comment: Saves plot.png and plot.html
188+
library_features:
189+
score: 3
190+
max: 5
191+
items:
192+
- id: LF-01
193+
name: Distinctive Features
194+
score: 3
195+
max: 5
196+
passed: true
197+
comment: Uses XY chart type creatively for histogram, custom Style, but approach
198+
for normal curve rendering failed
199+
verdict: APPROVED
200+
impl_tags:
201+
dependencies: []
202+
techniques: []
203+
patterns:
204+
- data-generation
205+
dataprep:
206+
- binning
207+
styling:
208+
- alpha-blending

0 commit comments

Comments
 (0)