Skip to content

Commit eceeacc

Browse files
feat(pygal): implement slider-control-basic (#3114)
## Implementation: `slider-control-basic` - pygal Implements the **pygal** version of `slider-control-basic`. **File:** `plots/slider-control-basic/implementations/pygal.py` **Parent Issue:** #3071 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620365742)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 120f08e commit eceeacc

2 files changed

Lines changed: 342 additions & 0 deletions

File tree

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
""" pyplots.ai
2+
slider-control-basic: Interactive Plot with Slider Control
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 55/100 | Created: 2025-12-31
5+
"""
6+
7+
import json
8+
9+
import numpy as np
10+
import pygal
11+
from pygal.style import Style
12+
13+
14+
# Data - Quarterly sales data across years (slider will filter by year)
15+
np.random.seed(42)
16+
17+
years = [2020, 2021, 2022, 2023, 2024]
18+
quarters = ["Q1", "Q2", "Q3", "Q4"]
19+
20+
# Generate realistic sales data with growth trend
21+
base_sales = 100
22+
sales_data = {}
23+
for i, year in enumerate(years):
24+
growth_factor = 1 + 0.15 * i + np.random.uniform(-0.05, 0.05)
25+
seasonal = [0.8, 1.0, 0.9, 1.3] # Q4 is strongest
26+
sales_data[year] = [base_sales * growth_factor * s * (1 + np.random.uniform(-0.1, 0.1)) for s in seasonal]
27+
28+
# Custom style for pyplots - large canvas with distinct colors
29+
custom_style = Style(
30+
background="white",
31+
plot_background="white",
32+
foreground="#333333",
33+
foreground_strong="#333333",
34+
foreground_subtle="#666666",
35+
colors=("#306998",), # Primary blue for the single series
36+
title_font_size=72,
37+
label_font_size=48,
38+
major_label_font_size=42,
39+
legend_font_size=48,
40+
value_font_size=42,
41+
stroke_width=3,
42+
opacity="0.9",
43+
opacity_hover="1.0",
44+
transition="300ms ease-in-out",
45+
tooltip_font_size=40,
46+
)
47+
48+
# Create bar chart showing single year (slider switches years)
49+
# Start with first year (2020)
50+
initial_year = years[0]
51+
52+
chart = pygal.Bar(
53+
width=4800,
54+
height=2700,
55+
style=custom_style,
56+
title=f"Quarterly Sales for Year {initial_year} · slider-control-basic · pygal · pyplots.ai",
57+
x_title="Quarter",
58+
y_title="Sales (thousands USD)",
59+
show_x_guides=False,
60+
show_y_guides=True,
61+
legend_at_bottom=False,
62+
show_legend=True,
63+
margin=100,
64+
margin_top=150,
65+
margin_bottom=120,
66+
spacing=50,
67+
value_formatter=lambda x: f"${x:.0f}K",
68+
print_values=True,
69+
print_values_position="top",
70+
truncate_legend=-1,
71+
x_label_rotation=0,
72+
y_labels_major_every=2,
73+
range=(0, 220), # Fixed range for consistent comparison across years
74+
)
75+
76+
# Set x-axis labels
77+
chart.x_labels = quarters
78+
79+
# Add the initial year's data
80+
chart.add(
81+
f"Year {initial_year}",
82+
[{"value": v, "label": f"{q}: ${v:.1f}K"} for q, v in zip(quarters, sales_data[initial_year], strict=True)],
83+
)
84+
85+
# Save PNG with initial year (static preview)
86+
chart.render_to_png("plot.png")
87+
88+
# Create interactive HTML with slider control
89+
# We'll generate separate SVG for each year and embed with JavaScript slider
90+
html_template = """<!DOCTYPE html>
91+
<html>
92+
<head>
93+
<meta charset="utf-8">
94+
<title>Quarterly Sales by Year - Interactive Slider | pyplots.ai</title>
95+
<style>
96+
* {{ box-sizing: border-box; }}
97+
body {{
98+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
99+
background: white;
100+
margin: 0;
101+
padding: 40px;
102+
display: flex;
103+
flex-direction: column;
104+
align-items: center;
105+
}}
106+
.container {{
107+
width: 100%;
108+
max-width: 1600px;
109+
}}
110+
.slider-container {{
111+
background: #f8f9fa;
112+
border-radius: 12px;
113+
padding: 30px 40px;
114+
margin-bottom: 30px;
115+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
116+
}}
117+
.slider-label {{
118+
font-size: 24px;
119+
font-weight: 600;
120+
color: #333;
121+
margin-bottom: 15px;
122+
display: flex;
123+
justify-content: space-between;
124+
align-items: center;
125+
}}
126+
.year-display {{
127+
font-size: 36px;
128+
font-weight: 700;
129+
color: #306998;
130+
background: white;
131+
padding: 10px 25px;
132+
border-radius: 8px;
133+
border: 2px solid #306998;
134+
}}
135+
.slider-wrapper {{
136+
display: flex;
137+
align-items: center;
138+
gap: 20px;
139+
}}
140+
.year-label {{
141+
font-size: 18px;
142+
font-weight: 500;
143+
color: #666;
144+
min-width: 50px;
145+
}}
146+
input[type="range"] {{
147+
flex: 1;
148+
height: 12px;
149+
-webkit-appearance: none;
150+
appearance: none;
151+
background: #ddd;
152+
border-radius: 6px;
153+
outline: none;
154+
}}
155+
input[type="range"]::-webkit-slider-thumb {{
156+
-webkit-appearance: none;
157+
appearance: none;
158+
width: 32px;
159+
height: 32px;
160+
background: #306998;
161+
border-radius: 50%;
162+
cursor: pointer;
163+
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
164+
transition: transform 0.2s;
165+
}}
166+
input[type="range"]::-webkit-slider-thumb:hover {{
167+
transform: scale(1.15);
168+
}}
169+
input[type="range"]::-moz-range-thumb {{
170+
width: 32px;
171+
height: 32px;
172+
background: #306998;
173+
border-radius: 50%;
174+
cursor: pointer;
175+
border: none;
176+
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
177+
}}
178+
.chart-container {{
179+
position: relative;
180+
width: 100%;
181+
background: white;
182+
border-radius: 12px;
183+
overflow: hidden;
184+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
185+
}}
186+
.chart {{
187+
display: none;
188+
width: 100%;
189+
}}
190+
.chart.active {{
191+
display: block;
192+
}}
193+
.chart svg {{
194+
width: 100%;
195+
height: auto;
196+
}}
197+
.summary {{
198+
margin-top: 20px;
199+
padding: 20px;
200+
background: #f0f7ff;
201+
border-radius: 8px;
202+
text-align: center;
203+
}}
204+
.summary-text {{
205+
font-size: 20px;
206+
color: #333;
207+
}}
208+
.summary-value {{
209+
font-weight: 700;
210+
color: #306998;
211+
}}
212+
</style>
213+
</head>
214+
<body>
215+
<div class="container">
216+
<div class="slider-container">
217+
<div class="slider-label">
218+
<span>Select Year to View</span>
219+
<span class="year-display" id="yearDisplay">{initial_year}</span>
220+
</div>
221+
<div class="slider-wrapper">
222+
<span class="year-label">{min_year}</span>
223+
<input type="range" id="yearSlider" min="0" max="{max_index}" value="0" step="1">
224+
<span class="year-label">{max_year}</span>
225+
</div>
226+
</div>
227+
<div class="chart-container">
228+
{charts_html}
229+
</div>
230+
<div class="summary">
231+
<span class="summary-text">
232+
Total Annual Sales: <span class="summary-value" id="totalSales"></span>
233+
</span>
234+
</div>
235+
</div>
236+
<script>
237+
const years = {years_json};
238+
const totals = {totals_json};
239+
const slider = document.getElementById('yearSlider');
240+
const yearDisplay = document.getElementById('yearDisplay');
241+
const totalSales = document.getElementById('totalSales');
242+
const charts = document.querySelectorAll('.chart');
243+
244+
function updateChart() {{
245+
const index = parseInt(slider.value);
246+
const year = years[index];
247+
yearDisplay.textContent = year;
248+
totalSales.textContent = '$' + totals[index].toFixed(1) + 'K';
249+
250+
charts.forEach((chart, i) => {{
251+
chart.classList.toggle('active', i === index);
252+
}});
253+
}}
254+
255+
slider.addEventListener('input', updateChart);
256+
updateChart();
257+
</script>
258+
</body>
259+
</html>
260+
"""
261+
262+
# Generate SVG for each year
263+
charts_html_parts = []
264+
totals = []
265+
266+
for idx, year in enumerate(years):
267+
year_chart = pygal.Bar(
268+
width=4800,
269+
height=2700,
270+
style=custom_style,
271+
title=f"Quarterly Sales for Year {year} · slider-control-basic · pygal · pyplots.ai",
272+
x_title="Quarter",
273+
y_title="Sales (thousands USD)",
274+
show_x_guides=False,
275+
show_y_guides=True,
276+
legend_at_bottom=False,
277+
show_legend=True,
278+
margin=100,
279+
margin_top=150,
280+
margin_bottom=120,
281+
spacing=50,
282+
value_formatter=lambda x: f"${x:.0f}K",
283+
print_values=True,
284+
print_values_position="top",
285+
truncate_legend=-1,
286+
x_label_rotation=0,
287+
y_labels_major_every=2,
288+
range=(0, 220), # Fixed range for consistent comparison
289+
)
290+
year_chart.x_labels = quarters
291+
year_chart.add(
292+
f"Year {year}",
293+
[{"value": v, "label": f"{q}: ${v:.1f}K"} for q, v in zip(quarters, sales_data[year], strict=True)],
294+
)
295+
296+
svg_content = year_chart.render().decode("utf-8")
297+
active_class = "active" if idx == 0 else ""
298+
charts_html_parts.append(f'<div class="chart {active_class}" data-year="{year}">{svg_content}</div>')
299+
totals.append(sum(sales_data[year]))
300+
301+
# Combine into final HTML
302+
final_html = html_template.format(
303+
initial_year=years[0],
304+
min_year=years[0],
305+
max_year=years[-1],
306+
max_index=len(years) - 1,
307+
charts_html="\n".join(charts_html_parts),
308+
years_json=json.dumps(years),
309+
totals_json=json.dumps(totals),
310+
)
311+
312+
with open("plot.html", "w", encoding="utf-8") as f:
313+
f.write(final_html)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
library: pygal
2+
specification_id: slider-control-basic
3+
created: '2025-12-31T13:57:39Z'
4+
updated: '2025-12-31T15:25:45Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620365742
7+
issue: 3071
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/slider-control-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/slider-control-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/slider-control-basic/pygal/plot.html
13+
quality_score: 55
14+
review:
15+
strengths:
16+
- Excellent HTML implementation with functional slider control, year display, and
17+
smooth transitions between years
18+
- Good use of pygal SVG capabilities to pre-render charts for each year in the HTML
19+
version
20+
- Clean, well-organized code with realistic seasonal sales data showing year-over-year
21+
growth
22+
- Proper use of fixed y-axis range (0-220) for consistent comparison across years
23+
in the interactive version
24+
- Nice styling with custom colors, value formatting, and professional appearance
25+
weaknesses:
26+
- PNG preview shows grouped bar chart with ALL years instead of single-year view
27+
with slider indication - fundamentally misrepresents the slider-control spec
28+
- Static PNG cannot demonstrate the core slider functionality that defines this
29+
plot type

0 commit comments

Comments
 (0)