Skip to content

Commit 4be09ad

Browse files
feat(altair): implement chernoff-basic (#6831)
## Implementation: `chernoff-basic` - python/altair Implements the **python/altair** version of `chernoff-basic`. **File:** `plots/chernoff-basic/implementations/python/altair.py` **Parent Issue:** #3003 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25925130696)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent c21c341 commit 4be09ad

2 files changed

Lines changed: 291 additions & 52 deletions

File tree

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

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import os
8+
79
import altair as alt
810
import numpy as np
911
import pandas as pd
1012

1113

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+
1225
# Data - Iris dataset features for 12 representative flowers
1326
np.random.seed(42)
1427
# Diverse samples from iris-like measurements (normalized 0-1)
@@ -36,8 +49,8 @@
3649
}
3750
)
3851

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]}
4154
data["color"] = data["species"].map(species_colors)
4255

4356
# Create descriptive labels including species name
@@ -120,12 +133,13 @@
120133
)
121134

122135
# Left eyebrow (line represented by two points)
136+
eyebrow_color = INK_SOFT
123137
face_records.append(
124138
{
125139
"x": xc - fw * 0.38,
126140
"y": yc + fh * 0.32 + eb_slant * 0.3,
127141
"size": 120,
128-
"color": "#2C3E50",
142+
"color": eyebrow_color,
129143
"part": "eyebrow",
130144
"observation": r["observation"],
131145
"species": r["species"],
@@ -137,7 +151,7 @@
137151
"x": xc - fw * 0.22,
138152
"y": yc + fh * 0.32 - eb_slant * 0.3,
139153
"size": 120,
140-
"color": "#2C3E50",
154+
"color": eyebrow_color,
141155
"part": "eyebrow",
142156
"observation": r["observation"],
143157
"species": r["species"],
@@ -150,7 +164,7 @@
150164
"x": xc + fw * 0.22,
151165
"y": yc + fh * 0.32 - eb_slant * 0.3,
152166
"size": 120,
153-
"color": "#2C3E50",
167+
"color": eyebrow_color,
154168
"part": "eyebrow",
155169
"observation": r["observation"],
156170
"species": r["species"],
@@ -162,7 +176,7 @@
162176
"x": xc + fw * 0.38,
163177
"y": yc + fh * 0.32 + eb_slant * 0.3,
164178
"size": 120,
165-
"color": "#2C3E50",
179+
"color": eyebrow_color,
166180
"part": "eyebrow",
167181
"observation": r["observation"],
168182
"species": r["species"],
@@ -175,7 +189,7 @@
175189
"x": xc - fw * 0.30,
176190
"y": yc + fh * 0.15,
177191
"size": es * 45,
178-
"color": "#1A252F",
192+
"color": INK,
179193
"part": "eye",
180194
"observation": r["observation"],
181195
"species": r["species"],
@@ -188,53 +202,56 @@
188202
"x": xc + fw * 0.30,
189203
"y": yc + fh * 0.15,
190204
"size": es * 45,
191-
"color": "#1A252F",
205+
"color": INK,
192206
"part": "eye",
193207
"observation": r["observation"],
194208
"species": r["species"],
195209
"opacity": 1.0,
196210
}
197211
)
198-
# Left pupil (white highlight)
212+
# Left pupil (white/light highlight)
213+
pupil_color = PAGE_BG if THEME == "light" else INK_SOFT
199214
face_records.append(
200215
{
201216
"x": xc - fw * 0.30 + 3,
202217
"y": yc + fh * 0.15 + 3,
203218
"size": es * 12,
204-
"color": "#FFFFFF",
219+
"color": pupil_color,
205220
"part": "pupil",
206221
"observation": r["observation"],
207222
"species": r["species"],
208223
"opacity": 0.95,
209224
}
210225
)
211-
# Right pupil (white highlight)
226+
# Right pupil (white/light highlight)
212227
face_records.append(
213228
{
214229
"x": xc + fw * 0.30 + 3,
215230
"y": yc + fh * 0.15 + 3,
216231
"size": es * 12,
217-
"color": "#FFFFFF",
232+
"color": pupil_color,
218233
"part": "pupil",
219234
"observation": r["observation"],
220235
"species": r["species"],
221236
"opacity": 0.95,
222237
}
223238
)
224239
# Nose
240+
nose_color = INK_MUTED
225241
face_records.append(
226242
{
227243
"x": xc,
228244
"y": yc - fh * 0.05,
229245
"size": 90,
230-
"color": "#5D6D7E",
246+
"color": nose_color,
231247
"part": "nose",
232248
"observation": r["observation"],
233249
"species": r["species"],
234250
"opacity": 0.7,
235251
}
236252
)
237253
# Mouth - using horizontal ellipse shape for better representation
254+
mouth_color = OKABE_ITO[1] if THEME == "light" else OKABE_ITO[4]
238255
mouth_y = yc - fh * 0.30
239256
for dx in np.linspace(-mw * 0.4, mw * 0.4, 7):
240257
# Parabolic curve for mouth (smiling effect based on width)
@@ -244,7 +261,7 @@
244261
"x": xc + dx,
245262
"y": mouth_y + dy,
246263
"size": 80 if abs(dx) < mw * 0.3 else 50,
247-
"color": "#C0392B",
264+
"color": mouth_color,
248265
"part": "mouth",
249266
"observation": r["observation"],
250267
"species": r["species"],
@@ -280,7 +297,7 @@
280297
# Labels with species info
281298
labels = (
282299
alt.Chart(label_df)
283-
.mark_text(fontSize=13, fontWeight="bold", color="#2C3E50")
300+
.mark_text(fontSize=13, fontWeight="bold", color=INK_SOFT)
284301
.encode(x=alt.X("x_center:Q", axis=None), y=alt.Y("y_label:Q", axis=None), text="label:N")
285302
)
286303

@@ -290,7 +307,7 @@
290307
"species": ["setosa", "versicolor", "virginica"],
291308
"x": [850, 850, 850],
292309
"y": [780, 730, 680],
293-
"color": ["#306998", "#FFD43B", "#4B8BBE"],
310+
"color": [OKABE_ITO[0], OKABE_ITO[1], OKABE_ITO[2]],
294311
}
295312
)
296313

@@ -302,7 +319,7 @@
302319

303320
legend_text = (
304321
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)
306323
.encode(x="x:Q", y="y:Q", text="species:N")
307324
)
308325

@@ -324,7 +341,7 @@
324341

325342
mapping_text = (
326343
alt.Chart(mapping_data)
327-
.mark_text(align="left", fontSize=12, color="#34495E")
344+
.mark_text(align="left", fontSize=12, color=INK_MUTED)
328345
.encode(x="x:Q", y="y:Q", text="text:N")
329346
)
330347

@@ -334,17 +351,20 @@
334351
.properties(
335352
width=1600,
336353
height=900,
354+
background=PAGE_BG,
337355
title=alt.Title(
338-
"chernoff-basic · altair · pyplots.ai",
356+
"chernoff-basic · altair · anyplot.ai",
339357
fontSize=28,
340358
anchor="middle",
359+
color=INK,
341360
subtitle="Iris Dataset: Each face represents a flower sample with features encoding measurements",
342361
subtitleFontSize=16,
362+
subtitleColor=INK_SOFT,
343363
),
344364
)
345-
.configure_view(strokeWidth=0)
365+
.configure_view(strokeWidth=0, fill=PAGE_BG)
346366
)
347367

348368
# 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

Comments
 (0)