Skip to content

Commit dfe5d23

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(pygal): implement chernoff-basic (#6833)
## Implementation: `chernoff-basic` - python/pygal Implements the **python/pygal** version of `chernoff-basic`. **File:** `plots/chernoff-basic/implementations/python/pygal.py` **Parent Issue:** #3003 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25925322877)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 4be09ad commit dfe5d23

2 files changed

Lines changed: 209 additions & 174 deletions

File tree

plots/chernoff-basic/implementations/python/pygal.py

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
chernoff-basic: Chernoff Faces for Multivariate Data
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 90/100 | Created: 2025-12-31
3+
Library: pygal 3.1.0 | Python 3.13.13
4+
Quality: 86/100 | Updated: 2026-05-15
55
"""
66

7+
import os
8+
import sys
79
import xml.etree.ElementTree as ET
810

9-
import cairosvg
10-
import numpy as np
11-
import pygal
12-
from pygal.style import Style
1311

12+
# Add site-packages to the beginning of sys.path to avoid local pygal.py conflict
13+
site_packages = None
14+
for path in sys.path:
15+
if "site-packages" in path:
16+
site_packages = path
17+
break
18+
19+
if site_packages:
20+
sys.path.insert(0, site_packages)
21+
22+
import cairosvg # noqa: E402
23+
import numpy as np # noqa: E402
24+
import pygal # noqa: E402
25+
from pygal.style import Style # noqa: E402
26+
27+
28+
# Theme-adaptive colors
29+
THEME = os.getenv("ANYPLOT_THEME", "light")
30+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
31+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
32+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
33+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
34+
35+
# Okabe-Ito palette (first series is brand green #009E73)
36+
OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00")
1437

1538
# Set seed for reproducibility
1639
np.random.seed(42)
@@ -31,21 +54,21 @@
3154
]
3255
)
3356

34-
# Group colors for cars (colorblind-safe palette)
35-
face_colors = ["#306998", "#FFD43B", "#4ECDC4", "#FF7043", "#9C88FF"]
57+
# Okabe-Ito palette for face colors
58+
face_colors = OKABE_ITO
3659

3760
# SVG namespace
3861
SVG_NS = "http://www.w3.org/2000/svg"
3962
ET.register_namespace("", SVG_NS)
4063

4164
# Custom style for pygal
4265
custom_style = Style(
43-
background="white",
44-
plot_background="white",
45-
foreground="#333333",
46-
foreground_strong="#333333",
47-
foreground_subtle="#666666",
48-
colors=tuple(face_colors),
66+
background=PAGE_BG,
67+
plot_background=PAGE_BG,
68+
foreground=INK,
69+
foreground_strong=INK,
70+
foreground_subtle=INK_SOFT,
71+
colors=OKABE_ITO,
4972
title_font_size=72,
5073
label_font_size=36,
5174
major_label_font_size=32,
@@ -129,16 +152,16 @@
129152
left_eye.set("cx", str(left_eye_x))
130153
left_eye.set("cy", str(left_eye_y))
131154
left_eye.set("r", str(eye_size))
132-
left_eye.set("fill", "white")
133-
left_eye.set("stroke", "#333")
155+
left_eye.set("fill", PAGE_BG)
156+
left_eye.set("stroke", INK)
134157
left_eye.set("stroke-width", str(max(3, 2.5 * scale)))
135158

136159
# Left pupil
137160
left_pupil = ET.SubElement(face_group, f"{{{SVG_NS}}}circle")
138161
left_pupil.set("cx", str(left_eye_x))
139162
left_pupil.set("cy", str(left_eye_y))
140163
left_pupil.set("r", str(eye_size * 0.4))
141-
left_pupil.set("fill", "#333")
164+
left_pupil.set("fill", INK)
142165

143166
# Right eye
144167
right_eye_x = cx + eye_spacing * 0.5
@@ -147,16 +170,16 @@
147170
right_eye.set("cx", str(right_eye_x))
148171
right_eye.set("cy", str(right_eye_y))
149172
right_eye.set("r", str(eye_size))
150-
right_eye.set("fill", "white")
151-
right_eye.set("stroke", "#333")
173+
right_eye.set("fill", PAGE_BG)
174+
right_eye.set("stroke", INK)
152175
right_eye.set("stroke-width", str(max(3, 2.5 * scale)))
153176

154177
# Right pupil
155178
right_pupil = ET.SubElement(face_group, f"{{{SVG_NS}}}circle")
156179
right_pupil.set("cx", str(right_eye_x))
157180
right_pupil.set("cy", str(right_eye_y))
158181
right_pupil.set("r", str(eye_size * 0.4))
159-
right_pupil.set("fill", "#333")
182+
right_pupil.set("fill", INK)
160183

161184
# Eyebrows
162185
brow_length = eye_size * 2.2
@@ -169,7 +192,7 @@
169192
left_brow.set("y1", str(left_eye_y - brow_y_offset + slant_offset))
170193
left_brow.set("x2", str(left_eye_x + brow_length * 0.5))
171194
left_brow.set("y2", str(left_eye_y - brow_y_offset - slant_offset))
172-
left_brow.set("stroke", "#333")
195+
left_brow.set("stroke", INK)
173196
left_brow.set("stroke-width", str(max(5, 4 * scale)))
174197
left_brow.set("stroke-linecap", "round")
175198

@@ -179,7 +202,7 @@
179202
right_brow.set("y1", str(right_eye_y - brow_y_offset - slant_offset))
180203
right_brow.set("x2", str(right_eye_x + brow_length * 0.5))
181204
right_brow.set("y2", str(right_eye_y - brow_y_offset + slant_offset))
182-
right_brow.set("stroke", "#333")
205+
right_brow.set("stroke", INK)
183206
right_brow.set("stroke-width", str(max(5, 4 * scale)))
184207
right_brow.set("stroke-linecap", "round")
185208

@@ -189,7 +212,7 @@
189212
nose.set("y1", str(cy - nose_length * 0.3))
190213
nose.set("x2", str(cx))
191214
nose.set("y2", str(cy + nose_length * 0.5))
192-
nose.set("stroke", "#333")
215+
nose.set("stroke", INK)
193216
nose.set("stroke-width", str(max(4, 3.5 * scale)))
194217
nose.set("stroke-linecap", "round")
195218

@@ -200,7 +223,7 @@
200223
mouth = ET.SubElement(face_group, f"{{{SVG_NS}}}path")
201224
mouth.set("d", mouth_path)
202225
mouth.set("fill", "none")
203-
mouth.set("stroke", "#333")
226+
mouth.set("stroke", INK)
204227
mouth.set("stroke-width", str(max(5, 4 * scale)))
205228
mouth.set("stroke-linecap", "round")
206229

@@ -212,7 +235,7 @@
212235
label_elem.set("font-family", "sans-serif")
213236
label_elem.set("font-size", str(max(36, 28 * scale)))
214237
label_elem.set("font-weight", "bold")
215-
label_elem.set("fill", "#333")
238+
label_elem.set("fill", INK)
216239
label_elem.text = name
217240

218241
# Add title
@@ -223,7 +246,7 @@
223246
title_elem.set("font-family", "sans-serif")
224247
title_elem.set("font-size", "72")
225248
title_elem.set("font-weight", "bold")
226-
title_elem.set("fill", "#333")
249+
title_elem.set("fill", INK)
227250
title_elem.text = "Car Performance Comparison · chernoff-basic · pygal · pyplots.ai"
228251

229252
# Add legend for attributes - positioned closer to faces
@@ -236,7 +259,7 @@
236259
legend_title.set("font-family", "sans-serif")
237260
legend_title.set("font-size", "44")
238261
legend_title.set("font-weight", "bold")
239-
legend_title.set("fill", "#333")
262+
legend_title.set("fill", INK)
240263
legend_title.text = "Feature Mappings:"
241264

242265
feature_mappings = [
@@ -258,7 +281,7 @@
258281
text_elem.set("y", str(legend_y + 80 + row * 70))
259282
text_elem.set("font-family", "sans-serif")
260283
text_elem.set("font-size", "36")
261-
text_elem.set("fill", "#555")
284+
text_elem.set("fill", INK_SOFT)
262285
text_elem.text = mapping
263286

264287
# Add color legend for car identification
@@ -271,7 +294,7 @@
271294
color_title.set("font-family", "sans-serif")
272295
color_title.set("font-size", "44")
273296
color_title.set("font-weight", "bold")
274-
color_title.set("fill", "#333")
297+
color_title.set("fill", INK)
275298
color_title.text = "Cars:"
276299

277300
for i, (name, color) in enumerate(zip(car_names, face_colors, strict=True)):
@@ -290,7 +313,7 @@
290313
name_elem.set("y", str(color_legend_y))
291314
name_elem.set("font-family", "sans-serif")
292315
name_elem.set("font-size", "36")
293-
name_elem.set("fill", "#555")
316+
name_elem.set("fill", INK_SOFT)
294317
name_elem.text = name
295318

296319
# Convert back to string
@@ -300,29 +323,29 @@
300323
final_svg = '<?xml version="1.0" encoding="UTF-8"?>\n' + final_svg
301324

302325
# Save as SVG file
303-
with open("plot.svg", "w") as f:
326+
with open(f"plot-{THEME}.svg", "w") as f:
304327
f.write(final_svg)
305328

306329
# Use cairosvg to convert to PNG
307-
cairosvg.svg2png(bytestring=final_svg.encode(), write_to="plot.png", output_width=4800, output_height=2700)
330+
cairosvg.svg2png(bytestring=final_svg.encode(), write_to=f"plot-{THEME}.png", output_width=4800, output_height=2700)
308331

309332
# Save HTML for interactive version
310-
with open("plot.html", "w") as f:
333+
with open(f"plot-{THEME}.html", "w") as f:
311334
f.write(
312-
"""<!DOCTYPE html>
335+
f"""<!DOCTYPE html>
313336
<html>
314337
<head>
315338
<title>chernoff-basic · pygal · pyplots.ai</title>
316339
<style>
317-
body { margin: 0; padding: 20px; background: #f5f5f5; font-family: sans-serif; }
318-
.container { max-width: 100%; margin: 0 auto; }
319-
h1 { text-align: center; color: #333; }
320-
object { width: 100%; height: auto; }
340+
body {{ margin: 0; padding: 20px; background: {PAGE_BG}; font-family: sans-serif; }}
341+
.container {{ max-width: 100%; margin: 0 auto; }}
342+
h1 {{ text-align: center; color: {INK}; }}
343+
object {{ width: 100%; height: auto; }}
321344
</style>
322345
</head>
323346
<body>
324347
<div class="container">
325-
<object type="image/svg+xml" data="plot.svg">
348+
<object type="image/svg+xml" data="plot-{THEME}.svg">
326349
Chernoff faces visualization not supported
327350
</object>
328351
</div>

0 commit comments

Comments
 (0)