|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import sys |
7 | 9 | import xml.etree.ElementTree as ET |
8 | 10 |
|
9 | | -import cairosvg |
10 | | -import numpy as np |
11 | | -import pygal |
12 | | -from pygal.style import Style |
13 | 11 |
|
| 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") |
14 | 37 |
|
15 | 38 | # Set seed for reproducibility |
16 | 39 | np.random.seed(42) |
|
31 | 54 | ] |
32 | 55 | ) |
33 | 56 |
|
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 |
36 | 59 |
|
37 | 60 | # SVG namespace |
38 | 61 | SVG_NS = "http://www.w3.org/2000/svg" |
39 | 62 | ET.register_namespace("", SVG_NS) |
40 | 63 |
|
41 | 64 | # Custom style for pygal |
42 | 65 | 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, |
49 | 72 | title_font_size=72, |
50 | 73 | label_font_size=36, |
51 | 74 | major_label_font_size=32, |
|
129 | 152 | left_eye.set("cx", str(left_eye_x)) |
130 | 153 | left_eye.set("cy", str(left_eye_y)) |
131 | 154 | 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) |
134 | 157 | left_eye.set("stroke-width", str(max(3, 2.5 * scale))) |
135 | 158 |
|
136 | 159 | # Left pupil |
137 | 160 | left_pupil = ET.SubElement(face_group, f"{{{SVG_NS}}}circle") |
138 | 161 | left_pupil.set("cx", str(left_eye_x)) |
139 | 162 | left_pupil.set("cy", str(left_eye_y)) |
140 | 163 | left_pupil.set("r", str(eye_size * 0.4)) |
141 | | - left_pupil.set("fill", "#333") |
| 164 | + left_pupil.set("fill", INK) |
142 | 165 |
|
143 | 166 | # Right eye |
144 | 167 | right_eye_x = cx + eye_spacing * 0.5 |
|
147 | 170 | right_eye.set("cx", str(right_eye_x)) |
148 | 171 | right_eye.set("cy", str(right_eye_y)) |
149 | 172 | 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) |
152 | 175 | right_eye.set("stroke-width", str(max(3, 2.5 * scale))) |
153 | 176 |
|
154 | 177 | # Right pupil |
155 | 178 | right_pupil = ET.SubElement(face_group, f"{{{SVG_NS}}}circle") |
156 | 179 | right_pupil.set("cx", str(right_eye_x)) |
157 | 180 | right_pupil.set("cy", str(right_eye_y)) |
158 | 181 | right_pupil.set("r", str(eye_size * 0.4)) |
159 | | - right_pupil.set("fill", "#333") |
| 182 | + right_pupil.set("fill", INK) |
160 | 183 |
|
161 | 184 | # Eyebrows |
162 | 185 | brow_length = eye_size * 2.2 |
|
169 | 192 | left_brow.set("y1", str(left_eye_y - brow_y_offset + slant_offset)) |
170 | 193 | left_brow.set("x2", str(left_eye_x + brow_length * 0.5)) |
171 | 194 | 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) |
173 | 196 | left_brow.set("stroke-width", str(max(5, 4 * scale))) |
174 | 197 | left_brow.set("stroke-linecap", "round") |
175 | 198 |
|
|
179 | 202 | right_brow.set("y1", str(right_eye_y - brow_y_offset - slant_offset)) |
180 | 203 | right_brow.set("x2", str(right_eye_x + brow_length * 0.5)) |
181 | 204 | 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) |
183 | 206 | right_brow.set("stroke-width", str(max(5, 4 * scale))) |
184 | 207 | right_brow.set("stroke-linecap", "round") |
185 | 208 |
|
|
189 | 212 | nose.set("y1", str(cy - nose_length * 0.3)) |
190 | 213 | nose.set("x2", str(cx)) |
191 | 214 | nose.set("y2", str(cy + nose_length * 0.5)) |
192 | | - nose.set("stroke", "#333") |
| 215 | + nose.set("stroke", INK) |
193 | 216 | nose.set("stroke-width", str(max(4, 3.5 * scale))) |
194 | 217 | nose.set("stroke-linecap", "round") |
195 | 218 |
|
|
200 | 223 | mouth = ET.SubElement(face_group, f"{{{SVG_NS}}}path") |
201 | 224 | mouth.set("d", mouth_path) |
202 | 225 | mouth.set("fill", "none") |
203 | | - mouth.set("stroke", "#333") |
| 226 | + mouth.set("stroke", INK) |
204 | 227 | mouth.set("stroke-width", str(max(5, 4 * scale))) |
205 | 228 | mouth.set("stroke-linecap", "round") |
206 | 229 |
|
|
212 | 235 | label_elem.set("font-family", "sans-serif") |
213 | 236 | label_elem.set("font-size", str(max(36, 28 * scale))) |
214 | 237 | label_elem.set("font-weight", "bold") |
215 | | - label_elem.set("fill", "#333") |
| 238 | + label_elem.set("fill", INK) |
216 | 239 | label_elem.text = name |
217 | 240 |
|
218 | 241 | # Add title |
|
223 | 246 | title_elem.set("font-family", "sans-serif") |
224 | 247 | title_elem.set("font-size", "72") |
225 | 248 | title_elem.set("font-weight", "bold") |
226 | | -title_elem.set("fill", "#333") |
| 249 | +title_elem.set("fill", INK) |
227 | 250 | title_elem.text = "Car Performance Comparison · chernoff-basic · pygal · pyplots.ai" |
228 | 251 |
|
229 | 252 | # Add legend for attributes - positioned closer to faces |
|
236 | 259 | legend_title.set("font-family", "sans-serif") |
237 | 260 | legend_title.set("font-size", "44") |
238 | 261 | legend_title.set("font-weight", "bold") |
239 | | -legend_title.set("fill", "#333") |
| 262 | +legend_title.set("fill", INK) |
240 | 263 | legend_title.text = "Feature Mappings:" |
241 | 264 |
|
242 | 265 | feature_mappings = [ |
|
258 | 281 | text_elem.set("y", str(legend_y + 80 + row * 70)) |
259 | 282 | text_elem.set("font-family", "sans-serif") |
260 | 283 | text_elem.set("font-size", "36") |
261 | | - text_elem.set("fill", "#555") |
| 284 | + text_elem.set("fill", INK_SOFT) |
262 | 285 | text_elem.text = mapping |
263 | 286 |
|
264 | 287 | # Add color legend for car identification |
|
271 | 294 | color_title.set("font-family", "sans-serif") |
272 | 295 | color_title.set("font-size", "44") |
273 | 296 | color_title.set("font-weight", "bold") |
274 | | -color_title.set("fill", "#333") |
| 297 | +color_title.set("fill", INK) |
275 | 298 | color_title.text = "Cars:" |
276 | 299 |
|
277 | 300 | for i, (name, color) in enumerate(zip(car_names, face_colors, strict=True)): |
|
290 | 313 | name_elem.set("y", str(color_legend_y)) |
291 | 314 | name_elem.set("font-family", "sans-serif") |
292 | 315 | name_elem.set("font-size", "36") |
293 | | - name_elem.set("fill", "#555") |
| 316 | + name_elem.set("fill", INK_SOFT) |
294 | 317 | name_elem.text = name |
295 | 318 |
|
296 | 319 | # Convert back to string |
|
300 | 323 | final_svg = '<?xml version="1.0" encoding="UTF-8"?>\n' + final_svg |
301 | 324 |
|
302 | 325 | # Save as SVG file |
303 | | -with open("plot.svg", "w") as f: |
| 326 | +with open(f"plot-{THEME}.svg", "w") as f: |
304 | 327 | f.write(final_svg) |
305 | 328 |
|
306 | 329 | # 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) |
308 | 331 |
|
309 | 332 | # Save HTML for interactive version |
310 | | -with open("plot.html", "w") as f: |
| 333 | +with open(f"plot-{THEME}.html", "w") as f: |
311 | 334 | f.write( |
312 | | - """<!DOCTYPE html> |
| 335 | + f"""<!DOCTYPE html> |
313 | 336 | <html> |
314 | 337 | <head> |
315 | 338 | <title>chernoff-basic · pygal · pyplots.ai</title> |
316 | 339 | <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; }} |
321 | 344 | </style> |
322 | 345 | </head> |
323 | 346 | <body> |
324 | 347 | <div class="container"> |
325 | | - <object type="image/svg+xml" data="plot.svg"> |
| 348 | + <object type="image/svg+xml" data="plot-{THEME}.svg"> |
326 | 349 | Chernoff faces visualization not supported |
327 | 350 | </object> |
328 | 351 | </div> |
|
0 commit comments