Skip to content

Commit 21fd0c0

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

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
""" pyplots.ai
2+
facet-grid: Faceted Grid Plot
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 - Plant growth experiment across different soil types and light conditions
17+
np.random.seed(42)
18+
19+
# Create faceted data: 2 row categories (soil type) x 3 column categories (light level)
20+
row_cats = ["Sandy Soil", "Clay Soil"]
21+
col_cats = ["Low Light", "Medium Light", "High Light"]
22+
23+
# Generate plant growth data (height vs days) for each condition
24+
data = {}
25+
base_growth_rates = {
26+
("Sandy Soil", "Low Light"): 0.4,
27+
("Sandy Soil", "Medium Light"): 0.8,
28+
("Sandy Soil", "High Light"): 1.0,
29+
("Clay Soil", "Low Light"): 0.5,
30+
("Clay Soil", "Medium Light"): 1.2,
31+
("Clay Soil", "High Light"): 0.9,
32+
}
33+
34+
# Days as x-axis (shared across all facets)
35+
days = [0, 5, 10, 15, 20, 25, 30]
36+
37+
for row_cat in row_cats:
38+
for col_cat in col_cats:
39+
rate = base_growth_rates[(row_cat, col_cat)]
40+
# Generate growth curve with some variation
41+
heights = [d * rate + np.random.normal(0, 1) for d in days]
42+
heights = [max(0, h) for h in heights] # No negative heights
43+
data[(row_cat, col_cat)] = heights
44+
45+
# Custom style for pyplots
46+
custom_style = Style(
47+
background="white",
48+
plot_background="#fafafa",
49+
foreground="#333333",
50+
foreground_strong="#333333",
51+
foreground_subtle="#666666",
52+
colors=("#306998", "#FFD43B", "#4CAF50", "#FF5722"),
53+
font_family="sans-serif",
54+
title_font_size=40,
55+
label_font_size=28,
56+
major_label_font_size=24,
57+
legend_font_size=28,
58+
value_font_size=20,
59+
stroke_width=4,
60+
opacity=0.9,
61+
opacity_hover=1.0,
62+
)
63+
64+
# Create individual charts for each facet
65+
# Target: 4800 x 2700 px total grid (2 rows x 3 cols)
66+
cell_width = 1500
67+
cell_height = 1250
68+
69+
charts = []
70+
for row_idx, row_cat in enumerate(row_cats):
71+
row_charts = []
72+
for col_idx, col_cat in enumerate(col_cats):
73+
# Create chart for this facet
74+
chart = pygal.Line(
75+
width=cell_width,
76+
height=cell_height,
77+
style=custom_style,
78+
show_legend=False,
79+
show_y_guides=True,
80+
show_x_guides=False,
81+
x_title="Days" if row_idx == len(row_cats) - 1 else "",
82+
y_title="Height (cm)" if col_idx == 0 else "",
83+
title=f"{col_cat}" if row_idx == 0 else "",
84+
show_dots=True,
85+
dots_size=8,
86+
stroke_style={"width": 4},
87+
range=(0, 35),
88+
truncate_label=-1,
89+
)
90+
91+
# Add x-axis labels (days)
92+
chart.x_labels = [str(d) for d in days]
93+
94+
# Add data series
95+
chart.add(row_cat, data[(row_cat, col_cat)])
96+
97+
row_charts.append(chart)
98+
charts.append(row_charts)
99+
100+
# Render each chart to PNG and combine them
101+
images = []
102+
for _row_idx, row_charts in enumerate(charts):
103+
row_images = []
104+
for chart in row_charts:
105+
# Render to SVG bytes, then convert to PNG
106+
svg_bytes = chart.render()
107+
png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=cell_width, output_height=cell_height)
108+
img = Image.open(BytesIO(png_bytes))
109+
row_images.append(img)
110+
images.append(row_images)
111+
112+
# Create combined image (4800 x 2700 with space for title and row labels)
113+
title_height = 150
114+
row_label_width = 300
115+
total_width = 4800
116+
total_height = 2700
117+
118+
combined = Image.new("RGB", (total_width, total_height), "white")
119+
120+
# Calculate grid positioning
121+
grid_width = total_width - row_label_width
122+
grid_height = total_height - title_height
123+
actual_cell_width = grid_width // len(col_cats)
124+
actual_cell_height = grid_height // len(row_cats)
125+
126+
# Paste charts into grid
127+
for row_idx, row_images in enumerate(images):
128+
for col_idx, img in enumerate(row_images):
129+
# Resize to fit cell
130+
img_resized = img.resize((actual_cell_width, actual_cell_height), Image.LANCZOS)
131+
x = row_label_width + col_idx * actual_cell_width
132+
y = title_height + row_idx * actual_cell_height
133+
combined.paste(img_resized, (x, y))
134+
135+
# Add main title and row labels using PIL
136+
draw = ImageDraw.Draw(combined)
137+
138+
# Try to use a system font, fallback to default
139+
try:
140+
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 60)
141+
label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36)
142+
except OSError:
143+
title_font = ImageFont.load_default()
144+
label_font = ImageFont.load_default()
145+
146+
# Draw main title
147+
title_text = "facet-grid · pygal · pyplots.ai"
148+
bbox = draw.textbbox((0, 0), title_text, font=title_font)
149+
title_width = bbox[2] - bbox[0]
150+
title_x = (total_width - title_width) // 2
151+
draw.text((title_x, 40), title_text, fill="#333333", font=title_font)
152+
153+
# Draw row labels (rotated 90 degrees)
154+
for row_idx, row_cat in enumerate(row_cats):
155+
# Create a temporary image for rotated text
156+
temp_img = Image.new("RGBA", (400, 80), (255, 255, 255, 0))
157+
temp_draw = ImageDraw.Draw(temp_img)
158+
temp_draw.text((10, 10), row_cat, fill="#333333", font=label_font)
159+
temp_img = temp_img.rotate(90, expand=True)
160+
161+
# Position the rotated label
162+
label_y = title_height + row_idx * actual_cell_height + actual_cell_height // 2 - temp_img.height // 2
163+
combined.paste(temp_img, (50, label_y), temp_img)
164+
165+
# Save final image
166+
combined.save("plot.png", dpi=(300, 300))
167+
168+
# Also save as HTML (interactive SVG grid)
169+
html_content = """<!DOCTYPE html>
170+
<html>
171+
<head>
172+
<title>facet-grid · pygal · pyplots.ai</title>
173+
<style>
174+
body { font-family: sans-serif; background: white; margin: 20px; }
175+
h1 { text-align: center; color: #333; font-size: 28px; }
176+
.grid { display: grid; grid-template-columns: 100px repeat(3, 1fr); gap: 10px; max-width: 1600px; margin: 0 auto; }
177+
.row-label { writing-mode: vertical-rl; text-orientation: mixed; transform: rotate(180deg);
178+
display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: bold; color: #333; }
179+
.col-header { text-align: center; font-size: 18px; font-weight: bold; color: #333; padding: 10px; }
180+
.chart { width: 100%; }
181+
.empty { }
182+
</style>
183+
</head>
184+
<body>
185+
<h1>facet-grid · pygal · pyplots.ai</h1>
186+
<div class="grid">
187+
<div class="empty"></div>
188+
"""
189+
190+
# Add column headers
191+
for col_cat in col_cats:
192+
html_content += f' <div class="col-header">{col_cat}</div>\n'
193+
194+
# Add charts with row labels
195+
for row_idx, row_cat in enumerate(row_cats):
196+
html_content += f' <div class="row-label">{row_cat}</div>\n'
197+
for col_idx, _col_cat in enumerate(col_cats):
198+
chart = charts[row_idx][col_idx]
199+
svg_data = chart.render(is_unicode=True)
200+
# Remove XML declaration if present
201+
svg_data = svg_data.replace('<?xml version="1.0" encoding="utf-8"?>', "")
202+
html_content += f' <div class="chart">{svg_data}</div>\n'
203+
204+
html_content += """ </div>
205+
</body>
206+
</html>"""
207+
208+
with open("plot.html", "w") as f:
209+
f.write(html_content)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
library: pygal
2+
specification_id: facet-grid
3+
created: '2025-12-30T16:37:40Z'
4+
updated: '2025-12-30T16:45:38Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20601061346
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/facet-grid/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/facet-grid/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/facet-grid/pygal/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of faceted grid layout using creative PIL-based composition
17+
approach
18+
- Clear, readable labels with proper units (Height in cm, Days)
19+
- Consistent shared y-axis range across all facets for easy comparison
20+
- Realistic plant growth scenario with meaningful variation between conditions
21+
- Clean color scheme using pyplots primary blue (#306998)
22+
- Both PNG and interactive HTML outputs generated
23+
- Proper title format following spec requirements
24+
weaknesses:
25+
- Grid lines could be more subtle (currently visible but acceptable)
26+
- The faceted composition relies heavily on PIL rather than pygal native features
27+
- this is a reasonable workaround given pygal limitations
28+
- Row labels could be slightly larger for better visibility

0 commit comments

Comments
 (0)