Skip to content

Commit 52a1e6f

Browse files
feat(pygal): implement subplot-mosaic (#3035)
## Implementation: `subplot-mosaic` - pygal Implements the **pygal** version of `subplot-mosaic`. **File:** `plots/subplot-mosaic/implementations/pygal.py` **Parent Issue:** #3002 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617512290)* --------- 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 468c53a commit 52a1e6f

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
""" pyplots.ai
2+
subplot-mosaic: Mosaic Subplot Layout with Varying Sizes
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
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 - Dashboard showing sales performance across different dimensions
17+
np.random.seed(42)
18+
19+
# Time series data for main overview chart (Panel A - wide)
20+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
21+
revenue = [120, 135, 142, 138, 155, 168, 172, 185, 178, 192, 205, 218]
22+
costs = [85, 88, 92, 90, 98, 105, 108, 115, 112, 120, 128, 135]
23+
24+
# Category data for bar chart (Panel B)
25+
categories = ["Electronics", "Clothing", "Home", "Sports", "Books"]
26+
category_sales = [450, 320, 280, 195, 165]
27+
28+
# Pie chart data for market share (Panel C)
29+
regions = ["North", "South", "East", "West"]
30+
region_shares = [35, 28, 22, 15]
31+
32+
# Scatter data for correlation (Panel D)
33+
n_points = 40
34+
marketing_spend = np.random.uniform(10, 100, n_points)
35+
sales_response = marketing_spend * 2.5 + np.random.normal(0, 25, n_points)
36+
37+
# Gauge data for KPI (Panel E)
38+
current_target_pct = 78
39+
40+
# Custom style for pyplots
41+
custom_style = Style(
42+
background="white",
43+
plot_background="#fafafa",
44+
foreground="#333333",
45+
foreground_strong="#333333",
46+
foreground_subtle="#666666",
47+
colors=("#306998", "#FFD43B", "#4CAF50", "#FF5722", "#9C27B0", "#00BCD4"),
48+
font_family="sans-serif",
49+
title_font_size=36,
50+
label_font_size=24,
51+
major_label_font_size=22,
52+
legend_font_size=22,
53+
value_font_size=18,
54+
stroke_width=4,
55+
opacity=0.9,
56+
opacity_hover=1.0,
57+
)
58+
59+
# Mosaic layout pattern: "AAB;AAC;DDE"
60+
# A = large chart (2x2), B = medium chart (1x1), C = medium chart (1x1)
61+
# D = medium chart (2x1), E = small chart (1x1)
62+
63+
# Grid dimensions
64+
total_width = 4800
65+
total_height = 2700
66+
title_height = 120
67+
padding = 20
68+
69+
# Calculate cell sizes for 3-column, 3-row grid
70+
grid_width = total_width - 2 * padding
71+
grid_height = total_height - title_height - 2 * padding
72+
col_width = grid_width // 3
73+
row_height = grid_height // 3
74+
75+
# Panel A: Line chart (spans 2 cols, 2 rows) - Revenue & Costs over time
76+
chart_a = pygal.Line(
77+
width=col_width * 2,
78+
height=row_height * 2,
79+
style=custom_style,
80+
show_legend=True,
81+
legend_at_bottom=True,
82+
show_y_guides=True,
83+
show_x_guides=False,
84+
x_title="Month",
85+
y_title="Amount ($K)",
86+
title="Monthly Revenue vs Costs",
87+
show_dots=True,
88+
dots_size=10,
89+
stroke_style={"width": 5},
90+
truncate_label=-1,
91+
)
92+
chart_a.x_labels = months
93+
chart_a.add("Revenue", revenue)
94+
chart_a.add("Costs", costs)
95+
96+
# Panel B: Horizontal bar chart (1 col, 1 row) - Category sales
97+
chart_b = pygal.HorizontalBar(
98+
width=col_width,
99+
height=row_height,
100+
style=custom_style,
101+
show_legend=False,
102+
show_y_guides=True,
103+
title="Sales by Category",
104+
truncate_label=-1,
105+
print_values=True,
106+
print_values_position="center",
107+
value_font_size=16,
108+
)
109+
for cat, val in zip(categories, category_sales, strict=True):
110+
chart_b.add(cat, val)
111+
112+
# Panel C: Pie chart (1 col, 1 row) - Regional distribution
113+
chart_c = pygal.Pie(
114+
width=col_width,
115+
height=row_height,
116+
style=custom_style,
117+
show_legend=True,
118+
legend_at_bottom=True,
119+
title="Regional Share",
120+
inner_radius=0.4,
121+
truncate_label=-1,
122+
)
123+
for region, share in zip(regions, region_shares, strict=True):
124+
chart_c.add(region, share)
125+
126+
# Panel D: XY scatter chart (2 cols, 1 row) - Marketing vs Sales
127+
chart_d = pygal.XY(
128+
width=col_width * 2,
129+
height=row_height,
130+
style=custom_style,
131+
show_legend=False,
132+
show_y_guides=True,
133+
x_title="Marketing Spend ($K)",
134+
y_title="Sales ($K)",
135+
title="Marketing ROI Correlation",
136+
stroke=False,
137+
dots_size=12,
138+
truncate_label=-1,
139+
)
140+
# Convert to list of tuples for XY chart
141+
scatter_data = [(float(x), float(y)) for x, y in zip(marketing_spend, sales_response, strict=True)]
142+
chart_d.add("Correlation", scatter_data)
143+
144+
# Panel E: Gauge chart (1 col, 1 row) - Target achievement
145+
chart_e = pygal.SolidGauge(
146+
width=col_width,
147+
height=row_height,
148+
style=custom_style,
149+
show_legend=False,
150+
title="Target Achievement",
151+
inner_radius=0.6,
152+
half_pie=True,
153+
)
154+
chart_e.add("Progress", [{"value": current_target_pct, "max_value": 100}])
155+
156+
157+
# Helper function to render chart to PIL Image
158+
def render_chart_to_image(chart, width, height):
159+
svg_bytes = chart.render()
160+
png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=width, output_height=height)
161+
return Image.open(BytesIO(png_bytes))
162+
163+
164+
# Render all charts
165+
img_a = render_chart_to_image(chart_a, col_width * 2, row_height * 2)
166+
img_b = render_chart_to_image(chart_b, col_width, row_height)
167+
img_c = render_chart_to_image(chart_c, col_width, row_height)
168+
img_d = render_chart_to_image(chart_d, col_width * 2, row_height)
169+
img_e = render_chart_to_image(chart_e, col_width, row_height)
170+
171+
# Create combined image
172+
combined = Image.new("RGB", (total_width, total_height), "white")
173+
174+
# Place charts according to mosaic pattern: "AAB;AAC;DDE"
175+
# Row 0: A (cols 0-1), B (col 2)
176+
# Row 1: A (cols 0-1), C (col 2)
177+
# Row 2: D (cols 0-1), E (col 2)
178+
179+
x_offset = padding
180+
y_offset = title_height + padding
181+
182+
# Panel A: top-left, spans 2 cols x 2 rows
183+
combined.paste(img_a, (x_offset, y_offset))
184+
185+
# Panel B: top-right, 1 col x 1 row
186+
combined.paste(img_b, (x_offset + col_width * 2, y_offset))
187+
188+
# Panel C: middle-right, 1 col x 1 row
189+
combined.paste(img_c, (x_offset + col_width * 2, y_offset + row_height))
190+
191+
# Panel D: bottom-left, 2 cols x 1 row
192+
combined.paste(img_d, (x_offset, y_offset + row_height * 2))
193+
194+
# Panel E: bottom-right, 1 col x 1 row
195+
combined.paste(img_e, (x_offset + col_width * 2, y_offset + row_height * 2))
196+
197+
# Add main title
198+
draw = ImageDraw.Draw(combined)
199+
200+
try:
201+
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
202+
except OSError:
203+
title_font = ImageFont.load_default()
204+
205+
title_text = "subplot-mosaic · pygal · pyplots.ai"
206+
bbox = draw.textbbox((0, 0), title_text, font=title_font)
207+
title_width = bbox[2] - bbox[0]
208+
title_x = (total_width - title_width) // 2
209+
draw.text((title_x, 30), title_text, fill="#333333", font=title_font)
210+
211+
# Save final image
212+
combined.save("plot.png", dpi=(300, 300))
213+
214+
# Also save as HTML with interactive SVG grid
215+
html_content = """<!DOCTYPE html>
216+
<html>
217+
<head>
218+
<title>subplot-mosaic · pygal · pyplots.ai</title>
219+
<style>
220+
body { font-family: sans-serif; background: white; margin: 20px; }
221+
h1 { text-align: center; color: #333; font-size: 32px; margin-bottom: 20px; }
222+
.mosaic {
223+
display: grid;
224+
grid-template-columns: 1fr 1fr 1fr;
225+
grid-template-rows: 1fr 1fr 1fr;
226+
gap: 10px;
227+
max-width: 1600px;
228+
margin: 0 auto;
229+
height: 900px;
230+
}
231+
.panel-a { grid-column: 1 / 3; grid-row: 1 / 3; }
232+
.panel-b { grid-column: 3; grid-row: 1; }
233+
.panel-c { grid-column: 3; grid-row: 2; }
234+
.panel-d { grid-column: 1 / 3; grid-row: 3; }
235+
.panel-e { grid-column: 3; grid-row: 3; }
236+
.panel svg { width: 100%; height: 100%; }
237+
</style>
238+
</head>
239+
<body>
240+
<h1>subplot-mosaic · pygal · pyplots.ai</h1>
241+
<div class="mosaic">
242+
"""
243+
244+
245+
def get_svg_content(chart):
246+
svg = chart.render(is_unicode=True)
247+
return svg.replace('<?xml version="1.0" encoding="utf-8"?>', "")
248+
249+
250+
html_content += f' <div class="panel panel-a">{get_svg_content(chart_a)}</div>\n'
251+
html_content += f' <div class="panel panel-b">{get_svg_content(chart_b)}</div>\n'
252+
html_content += f' <div class="panel panel-c">{get_svg_content(chart_c)}</div>\n'
253+
html_content += f' <div class="panel panel-d">{get_svg_content(chart_d)}</div>\n'
254+
html_content += f' <div class="panel panel-e">{get_svg_content(chart_e)}</div>\n'
255+
256+
html_content += """ </div>
257+
</body>
258+
</html>"""
259+
260+
with open("plot.html", "w") as f:
261+
f.write(html_content)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: pygal
2+
specification_id: subplot-mosaic
3+
created: '2025-12-31T10:57:45Z'
4+
updated: '2025-12-31T11:08:07Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617512290
7+
issue: 3002
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/subplot-mosaic/pygal/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent demonstration of mosaic layout concept with 5 diverse chart types showing
17+
different spanning patterns (2×2, 1×1, 2×1)
18+
- Creative solution using PIL to composite multiple pygal charts into a cohesive
19+
dashboard
20+
- Well-designed custom style with appropriate font sizes for the large canvas
21+
- Bonus HTML output with interactive SVG grid demonstrates pygal web-native strength
22+
- Realistic business dashboard scenario with coherent data narrative
23+
weaknesses:
24+
- Uses helper functions (render_chart_to_image, get_svg_content) which violates
25+
KISS principle for plot implementations
26+
- Does not demonstrate empty cell placeholder feature mentioned in spec

0 commit comments

Comments
 (0)