Skip to content

Commit bfdc335

Browse files
feat(pygal): implement subplot-grid (#2809)
## Implementation: `subplot-grid` - pygal Implements the **pygal** version of `subplot-grid`. **File:** `plots/subplot-grid/implementations/pygal.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20602452625)* --------- 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 c89bd83 commit bfdc335

2 files changed

Lines changed: 317 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
""" pyplots.ai
2+
subplot-grid: Subplot Grid Layout
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
from io import BytesIO
8+
9+
import cairosvg
10+
import numpy as np
11+
import pygal
12+
from PIL import Image, ImageDraw, ImageFont
13+
from pygal.style import Style
14+
15+
16+
# Data - Business performance dashboard with multiple metrics
17+
np.random.seed(42)
18+
19+
# Monthly revenue trend (line chart)
20+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
21+
revenue = [120, 135, 128, 145, 162, 158, 175, 189, 195, 210, 225, 248] # in thousands
22+
23+
# Sales by category (bar chart)
24+
categories = ["Electronics", "Apparel", "Home", "Sports", "Books"]
25+
sales = [45.2, 32.8, 28.5, 19.7, 15.3] # in thousands
26+
27+
# Advertising spend vs ROI (scatter)
28+
ad_spend = np.random.uniform(5, 50, 30) # advertising spend in thousands
29+
roi = ad_spend * 0.12 + np.random.normal(0, 1.5, 30) # ROI percentage
30+
31+
# Daily order volume distribution (histogram)
32+
daily_orders = np.random.normal(loc=150, scale=35, size=365)
33+
daily_orders = np.clip(daily_orders, 50, 300)
34+
n_bins = 15
35+
counts, bin_edges = np.histogram(daily_orders, bins=n_bins)
36+
hist_data = [(int(count), float(bin_edges[i]), float(bin_edges[i + 1])) for i, count in enumerate(counts)]
37+
38+
# Custom styles for each chart
39+
base_style = Style(
40+
background="white",
41+
plot_background="#fafafa",
42+
foreground="#333333",
43+
foreground_strong="#333333",
44+
foreground_subtle="#666666",
45+
font_family="sans-serif",
46+
title_font_size=42,
47+
label_font_size=30,
48+
major_label_font_size=26,
49+
legend_font_size=28,
50+
value_font_size=22,
51+
stroke_width=4,
52+
opacity=0.85,
53+
opacity_hover=1.0,
54+
)
55+
56+
# Style for different charts
57+
line_style = Style(
58+
background="white",
59+
plot_background="#fafafa",
60+
foreground="#333333",
61+
foreground_strong="#333333",
62+
foreground_subtle="#666666",
63+
colors=("#306998",), # Python Blue
64+
font_family="sans-serif",
65+
title_font_size=42,
66+
label_font_size=30,
67+
major_label_font_size=26,
68+
legend_font_size=28,
69+
value_font_size=22,
70+
stroke_width=4,
71+
opacity=0.9,
72+
)
73+
74+
bar_style = Style(
75+
background="white",
76+
plot_background="#fafafa",
77+
foreground="#333333",
78+
foreground_strong="#333333",
79+
foreground_subtle="#666666",
80+
colors=("#FFD43B",), # Python Yellow
81+
font_family="sans-serif",
82+
title_font_size=42,
83+
label_font_size=30,
84+
major_label_font_size=26,
85+
legend_font_size=28,
86+
value_font_size=22,
87+
)
88+
89+
scatter_style = Style(
90+
background="white",
91+
plot_background="#fafafa",
92+
foreground="#333333",
93+
foreground_strong="#333333",
94+
foreground_subtle="#666666",
95+
colors=("#306998",), # Python Blue
96+
font_family="sans-serif",
97+
title_font_size=42,
98+
label_font_size=30,
99+
major_label_font_size=26,
100+
legend_font_size=28,
101+
value_font_size=22,
102+
opacity=0.7,
103+
)
104+
105+
hist_style = Style(
106+
background="white",
107+
plot_background="#fafafa",
108+
foreground="#333333",
109+
foreground_strong="#333333",
110+
foreground_subtle="#666666",
111+
colors=("#FFD43B",), # Python Yellow
112+
font_family="sans-serif",
113+
title_font_size=42,
114+
label_font_size=30,
115+
major_label_font_size=26,
116+
legend_font_size=28,
117+
value_font_size=22,
118+
)
119+
120+
# Create individual charts for the 2x2 grid
121+
cell_width = 2200
122+
cell_height = 1200
123+
124+
# Top-left: Line chart - Monthly Revenue Trend
125+
line_chart = pygal.Line(
126+
width=cell_width,
127+
height=cell_height,
128+
style=line_style,
129+
title="Monthly Revenue ($K)",
130+
x_title="Month",
131+
y_title="Revenue ($K)",
132+
show_legend=False,
133+
show_dots=True,
134+
dots_size=10,
135+
show_y_guides=True,
136+
show_x_guides=False,
137+
truncate_label=-1,
138+
)
139+
line_chart.x_labels = months
140+
line_chart.add("Revenue", revenue)
141+
142+
# Top-right: Bar chart - Sales by Category
143+
bar_chart = pygal.Bar(
144+
width=cell_width,
145+
height=cell_height,
146+
style=bar_style,
147+
title="Sales by Category ($K)",
148+
x_title="Category",
149+
y_title="Sales ($K)",
150+
show_legend=False,
151+
print_values=False,
152+
show_y_guides=True,
153+
show_x_guides=False,
154+
spacing=20,
155+
truncate_label=-1,
156+
)
157+
bar_chart.x_labels = categories
158+
bar_chart.add("Sales", sales)
159+
160+
# Bottom-left: Scatter plot - Ad Spend vs ROI
161+
scatter_chart = pygal.XY(
162+
width=cell_width,
163+
height=cell_height,
164+
style=scatter_style,
165+
title="Ad Spend vs Return on Investment",
166+
x_title="Ad Spend ($K)",
167+
y_title="ROI (%)",
168+
show_legend=False,
169+
stroke=False,
170+
dots_size=12,
171+
show_y_guides=True,
172+
show_x_guides=True,
173+
)
174+
scatter_points = [(float(x), float(y)) for x, y in zip(ad_spend, roi, strict=True)]
175+
scatter_chart.add("Campaigns", scatter_points)
176+
177+
# Bottom-right: Histogram - Daily Order Distribution
178+
hist_chart = pygal.Histogram(
179+
width=cell_width,
180+
height=cell_height,
181+
style=hist_style,
182+
title="Daily Order Volume Distribution",
183+
x_title="Orders per Day",
184+
y_title="Frequency",
185+
show_legend=False,
186+
show_y_guides=True,
187+
show_x_guides=False,
188+
)
189+
hist_chart.add("Orders", hist_data)
190+
191+
# Render each chart to PNG
192+
charts = [[line_chart, bar_chart], [scatter_chart, hist_chart]]
193+
194+
images = []
195+
for row_charts in charts:
196+
row_images = []
197+
for chart in row_charts:
198+
svg_bytes = chart.render()
199+
png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=cell_width, output_height=cell_height)
200+
img = Image.open(BytesIO(png_bytes))
201+
row_images.append(img)
202+
images.append(row_images)
203+
204+
# Create combined image (4800 x 2700 with space for main title)
205+
title_height = 180
206+
total_width = 4800
207+
total_height = 2700
208+
209+
combined = Image.new("RGB", (total_width, total_height), "white")
210+
211+
# Calculate grid positioning
212+
grid_height = total_height - title_height
213+
margin_x = 100
214+
grid_width = total_width - 2 * margin_x
215+
actual_cell_width = grid_width // 2
216+
actual_cell_height = grid_height // 2
217+
218+
# Paste charts into 2x2 grid
219+
for row_idx, row_images in enumerate(images):
220+
for col_idx, img in enumerate(row_images):
221+
img_resized = img.resize((actual_cell_width, actual_cell_height), Image.LANCZOS)
222+
x = margin_x + col_idx * actual_cell_width
223+
y = title_height + row_idx * actual_cell_height
224+
combined.paste(img_resized, (x, y))
225+
226+
# Add main title using PIL
227+
draw = ImageDraw.Draw(combined)
228+
229+
try:
230+
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 72)
231+
except OSError:
232+
title_font = ImageFont.load_default()
233+
234+
title_text = "subplot-grid · pygal · pyplots.ai"
235+
bbox = draw.textbbox((0, 0), title_text, font=title_font)
236+
title_width = bbox[2] - bbox[0]
237+
title_x = (total_width - title_width) // 2
238+
draw.text((title_x, 50), title_text, fill="#333333", font=title_font)
239+
240+
# Save final PNG
241+
combined.save("plot.png", dpi=(300, 300))
242+
243+
# Create interactive HTML version
244+
html_content = """<!DOCTYPE html>
245+
<html>
246+
<head>
247+
<title>subplot-grid · pygal · pyplots.ai</title>
248+
<style>
249+
body { font-family: sans-serif; background: white; margin: 20px; }
250+
h1 { text-align: center; color: #333; font-size: 32px; margin-bottom: 30px; }
251+
.grid {
252+
display: grid;
253+
grid-template-columns: repeat(2, 1fr);
254+
gap: 20px;
255+
max-width: 1600px;
256+
margin: 0 auto;
257+
}
258+
.chart {
259+
width: 100%;
260+
border: 1px solid #eee;
261+
border-radius: 8px;
262+
overflow: hidden;
263+
}
264+
.chart svg { width: 100%; height: auto; }
265+
</style>
266+
</head>
267+
<body>
268+
<h1>subplot-grid · pygal · pyplots.ai</h1>
269+
<div class="grid">
270+
"""
271+
272+
# Add all four charts
273+
all_charts = [line_chart, bar_chart, scatter_chart, hist_chart]
274+
for chart in all_charts:
275+
svg_data = chart.render(is_unicode=True)
276+
svg_data = svg_data.replace('<?xml version="1.0" encoding="utf-8"?>', "")
277+
html_content += f' <div class="chart">{svg_data}</div>\n'
278+
279+
html_content += """ </div>
280+
</body>
281+
</html>"""
282+
283+
with open("plot.html", "w") as f:
284+
f.write(html_content)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
library: pygal
2+
specification_id: subplot-grid
3+
created: '2025-12-30T17:48:29Z'
4+
updated: '2025-12-30T17:56:39Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20602452625
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/pygal/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent demonstration of pygal versatility with four distinct chart types (Line,
17+
Bar, XY scatter, Histogram) in a cohesive grid
18+
- Creative solution for subplot grid using PIL composition since pygal lacks native
19+
subplots
20+
- Realistic business dashboard scenario with interconnected metrics (revenue, sales,
21+
advertising ROI, order volume)
22+
- 'Proper use of Python brand colors (#306998 blue, #FFD43B yellow) maintaining
23+
visual consistency'
24+
- Both PNG and interactive HTML outputs generated, showcasing pygal web-native SVG
25+
capabilities
26+
- Clean, readable axis labels with appropriate units throughout all subplots
27+
weaknesses:
28+
- Chart titles repeat y-axis information (e.g., Monthly Revenue title + Revenue
29+
y-label) - could be more concise
30+
- Scatter plot dots could be slightly larger for improved visibility at the 30-point
31+
density level
32+
- Code structure uses loops for chart rendering which adds slight complexity beyond
33+
pure KISS style

0 commit comments

Comments
 (0)