|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | chernoff-basic: Chernoff Faces for Multivariate Data |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 87/100 | Created: 2025-12-31 |
| 3 | +Library: altair 6.1.0 | Python 3.13.13 |
| 4 | +Quality: 89/100 | Updated: 2026-05-15 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import altair as alt |
8 | 10 | import numpy as np |
9 | 11 | import pandas as pd |
10 | 12 |
|
11 | 13 |
|
| 14 | +# Theme tokens |
| 15 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 17 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 18 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 19 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 20 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
| 21 | + |
| 22 | +# Okabe-Ito palette for species |
| 23 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442"] |
| 24 | + |
12 | 25 | # Data - Iris dataset features for 12 representative flowers |
13 | 26 | np.random.seed(42) |
14 | 27 | # Diverse samples from iris-like measurements (normalized 0-1) |
|
36 | 49 | } |
37 | 50 | ) |
38 | 51 |
|
39 | | -# Map species to colors |
40 | | -species_colors = {"setosa": "#306998", "versicolor": "#FFD43B", "virginica": "#4B8BBE"} |
| 52 | +# Map species to Okabe-Ito colors |
| 53 | +species_colors = {"setosa": OKABE_ITO[0], "versicolor": OKABE_ITO[1], "virginica": OKABE_ITO[2]} |
41 | 54 | data["color"] = data["species"].map(species_colors) |
42 | 55 |
|
43 | 56 | # Create descriptive labels including species name |
|
120 | 133 | ) |
121 | 134 |
|
122 | 135 | # Left eyebrow (line represented by two points) |
| 136 | + eyebrow_color = INK_SOFT |
123 | 137 | face_records.append( |
124 | 138 | { |
125 | 139 | "x": xc - fw * 0.38, |
126 | 140 | "y": yc + fh * 0.32 + eb_slant * 0.3, |
127 | 141 | "size": 120, |
128 | | - "color": "#2C3E50", |
| 142 | + "color": eyebrow_color, |
129 | 143 | "part": "eyebrow", |
130 | 144 | "observation": r["observation"], |
131 | 145 | "species": r["species"], |
|
137 | 151 | "x": xc - fw * 0.22, |
138 | 152 | "y": yc + fh * 0.32 - eb_slant * 0.3, |
139 | 153 | "size": 120, |
140 | | - "color": "#2C3E50", |
| 154 | + "color": eyebrow_color, |
141 | 155 | "part": "eyebrow", |
142 | 156 | "observation": r["observation"], |
143 | 157 | "species": r["species"], |
|
150 | 164 | "x": xc + fw * 0.22, |
151 | 165 | "y": yc + fh * 0.32 - eb_slant * 0.3, |
152 | 166 | "size": 120, |
153 | | - "color": "#2C3E50", |
| 167 | + "color": eyebrow_color, |
154 | 168 | "part": "eyebrow", |
155 | 169 | "observation": r["observation"], |
156 | 170 | "species": r["species"], |
|
162 | 176 | "x": xc + fw * 0.38, |
163 | 177 | "y": yc + fh * 0.32 + eb_slant * 0.3, |
164 | 178 | "size": 120, |
165 | | - "color": "#2C3E50", |
| 179 | + "color": eyebrow_color, |
166 | 180 | "part": "eyebrow", |
167 | 181 | "observation": r["observation"], |
168 | 182 | "species": r["species"], |
|
175 | 189 | "x": xc - fw * 0.30, |
176 | 190 | "y": yc + fh * 0.15, |
177 | 191 | "size": es * 45, |
178 | | - "color": "#1A252F", |
| 192 | + "color": INK, |
179 | 193 | "part": "eye", |
180 | 194 | "observation": r["observation"], |
181 | 195 | "species": r["species"], |
|
188 | 202 | "x": xc + fw * 0.30, |
189 | 203 | "y": yc + fh * 0.15, |
190 | 204 | "size": es * 45, |
191 | | - "color": "#1A252F", |
| 205 | + "color": INK, |
192 | 206 | "part": "eye", |
193 | 207 | "observation": r["observation"], |
194 | 208 | "species": r["species"], |
195 | 209 | "opacity": 1.0, |
196 | 210 | } |
197 | 211 | ) |
198 | | - # Left pupil (white highlight) |
| 212 | + # Left pupil (white/light highlight) |
| 213 | + pupil_color = PAGE_BG if THEME == "light" else INK_SOFT |
199 | 214 | face_records.append( |
200 | 215 | { |
201 | 216 | "x": xc - fw * 0.30 + 3, |
202 | 217 | "y": yc + fh * 0.15 + 3, |
203 | 218 | "size": es * 12, |
204 | | - "color": "#FFFFFF", |
| 219 | + "color": pupil_color, |
205 | 220 | "part": "pupil", |
206 | 221 | "observation": r["observation"], |
207 | 222 | "species": r["species"], |
208 | 223 | "opacity": 0.95, |
209 | 224 | } |
210 | 225 | ) |
211 | | - # Right pupil (white highlight) |
| 226 | + # Right pupil (white/light highlight) |
212 | 227 | face_records.append( |
213 | 228 | { |
214 | 229 | "x": xc + fw * 0.30 + 3, |
215 | 230 | "y": yc + fh * 0.15 + 3, |
216 | 231 | "size": es * 12, |
217 | | - "color": "#FFFFFF", |
| 232 | + "color": pupil_color, |
218 | 233 | "part": "pupil", |
219 | 234 | "observation": r["observation"], |
220 | 235 | "species": r["species"], |
221 | 236 | "opacity": 0.95, |
222 | 237 | } |
223 | 238 | ) |
224 | 239 | # Nose |
| 240 | + nose_color = INK_MUTED |
225 | 241 | face_records.append( |
226 | 242 | { |
227 | 243 | "x": xc, |
228 | 244 | "y": yc - fh * 0.05, |
229 | 245 | "size": 90, |
230 | | - "color": "#5D6D7E", |
| 246 | + "color": nose_color, |
231 | 247 | "part": "nose", |
232 | 248 | "observation": r["observation"], |
233 | 249 | "species": r["species"], |
234 | 250 | "opacity": 0.7, |
235 | 251 | } |
236 | 252 | ) |
237 | 253 | # Mouth - using horizontal ellipse shape for better representation |
| 254 | + mouth_color = OKABE_ITO[1] if THEME == "light" else OKABE_ITO[4] |
238 | 255 | mouth_y = yc - fh * 0.30 |
239 | 256 | for dx in np.linspace(-mw * 0.4, mw * 0.4, 7): |
240 | 257 | # Parabolic curve for mouth (smiling effect based on width) |
|
244 | 261 | "x": xc + dx, |
245 | 262 | "y": mouth_y + dy, |
246 | 263 | "size": 80 if abs(dx) < mw * 0.3 else 50, |
247 | | - "color": "#C0392B", |
| 264 | + "color": mouth_color, |
248 | 265 | "part": "mouth", |
249 | 266 | "observation": r["observation"], |
250 | 267 | "species": r["species"], |
|
280 | 297 | # Labels with species info |
281 | 298 | labels = ( |
282 | 299 | alt.Chart(label_df) |
283 | | - .mark_text(fontSize=13, fontWeight="bold", color="#2C3E50") |
| 300 | + .mark_text(fontSize=13, fontWeight="bold", color=INK_SOFT) |
284 | 301 | .encode(x=alt.X("x_center:Q", axis=None), y=alt.Y("y_label:Q", axis=None), text="label:N") |
285 | 302 | ) |
286 | 303 |
|
|
290 | 307 | "species": ["setosa", "versicolor", "virginica"], |
291 | 308 | "x": [850, 850, 850], |
292 | 309 | "y": [780, 730, 680], |
293 | | - "color": ["#306998", "#FFD43B", "#4B8BBE"], |
| 310 | + "color": [OKABE_ITO[0], OKABE_ITO[1], OKABE_ITO[2]], |
294 | 311 | } |
295 | 312 | ) |
296 | 313 |
|
|
302 | 319 |
|
303 | 320 | legend_text = ( |
304 | 321 | alt.Chart(legend_data) |
305 | | - .mark_text(align="right", fontSize=14, dx=-25, fontWeight="bold") |
| 322 | + .mark_text(align="right", fontSize=14, dx=-25, fontWeight="bold", color=INK_SOFT) |
306 | 323 | .encode(x="x:Q", y="y:Q", text="species:N") |
307 | 324 | ) |
308 | 325 |
|
|
324 | 341 |
|
325 | 342 | mapping_text = ( |
326 | 343 | alt.Chart(mapping_data) |
327 | | - .mark_text(align="left", fontSize=12, color="#34495E") |
| 344 | + .mark_text(align="left", fontSize=12, color=INK_MUTED) |
328 | 345 | .encode(x="x:Q", y="y:Q", text="text:N") |
329 | 346 | ) |
330 | 347 |
|
|
334 | 351 | .properties( |
335 | 352 | width=1600, |
336 | 353 | height=900, |
| 354 | + background=PAGE_BG, |
337 | 355 | title=alt.Title( |
338 | | - "chernoff-basic · altair · pyplots.ai", |
| 356 | + "chernoff-basic · altair · anyplot.ai", |
339 | 357 | fontSize=28, |
340 | 358 | anchor="middle", |
| 359 | + color=INK, |
341 | 360 | subtitle="Iris Dataset: Each face represents a flower sample with features encoding measurements", |
342 | 361 | subtitleFontSize=16, |
| 362 | + subtitleColor=INK_SOFT, |
343 | 363 | ), |
344 | 364 | ) |
345 | | - .configure_view(strokeWidth=0) |
| 365 | + .configure_view(strokeWidth=0, fill=PAGE_BG) |
346 | 366 | ) |
347 | 367 |
|
348 | 368 | # Save as PNG and HTML |
349 | | -chart.save("plot.png", scale_factor=3.0) |
350 | | -chart.save("plot.html") |
| 369 | +chart.save(f"plot-{THEME}.png", scale_factor=3.0) |
| 370 | +chart.save(f"plot-{THEME}.html") |
0 commit comments