Skip to content

Commit 106f873

Browse files
feat(highcharts): implement chernoff-basic (#6834)
## Implementation: `chernoff-basic` - python/highcharts Implements the **python/highcharts** version of `chernoff-basic`. **File:** `plots/chernoff-basic/implementations/python/highcharts.py` **Parent Issue:** #3003 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25925422377)* --------- 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 d12ea75 commit 106f873

2 files changed

Lines changed: 222 additions & 181 deletions

File tree

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

Lines changed: 53 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
chernoff-basic: Chernoff Faces for Multivariate Data
3-
Library: highcharts unknown | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-31
3+
Library: highcharts unknown | Python 3.13.13
4+
Quality: 80/100 | Updated: 2026-05-15
55
"""
66

77
import base64
8+
import os
89
import tempfile
910
import time
1011
from pathlib import Path
@@ -15,7 +16,16 @@
1516
from sklearn.datasets import load_iris
1617

1718

18-
# Data - Using Iris dataset (4 variables per flower)
19+
THEME = os.getenv("ANYPLOT_THEME", "light")
20+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
21+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
22+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
23+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
24+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
25+
26+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2"]
27+
28+
# Data
1929
np.random.seed(42)
2030
iris = load_iris()
2131
X = iris.data
@@ -36,96 +46,80 @@
3646
X_max = X_sample.max(axis=0)
3747
X_norm = (X_sample - X_min) / (X_max - X_min + 1e-8)
3848

39-
# Colors for species - colorblind-safe
40-
species_colors = ["#306998", "#FFD43B", "#9467BD"]
41-
4249
# SVG dimensions
4350
svg_width = 4800
4451
svg_height = 2700
4552

46-
# Build custom Chernoff faces HTML/SVG
47-
# Feature mappings:
48-
# - Variable 0 (sepal length): face width
49-
# - Variable 1 (sepal width): eye size
50-
# - Variable 2 (petal length): mouth curvature
51-
# - Variable 3 (petal width): eyebrow slant
52-
5353

5454
def create_face_svg(values, color, label, x_pos, y_pos, size=450):
5555
"""Create SVG for a single Chernoff face."""
56-
# Extract normalized values (0-1)
57-
face_width = 0.7 + values[0] * 0.3 # 0.7 to 1.0 multiplier
58-
eye_size = 0.7 + values[1] * 0.5 # 0.7 to 1.2 multiplier
59-
mouth_curve = values[2] * 2 - 1 # -1 to 1 (sad to happy)
60-
eyebrow_slant = (values[3] - 0.5) * 30 # -15 to +15 degrees
56+
face_width = 0.7 + values[0] * 0.3
57+
eye_size = 0.7 + values[1] * 0.5
58+
mouth_curve = values[2] * 2 - 1
59+
eyebrow_slant = (values[3] - 0.5) * 30
6160

6261
cx = x_pos + size // 2
6362
cy = y_pos + size // 2
6463
face_rx = int(size * 0.4 * face_width)
6564
face_ry = int(size * 0.45)
6665

67-
# Eye positions and sizes
6866
eye_cx_left = cx - int(size * 0.15)
6967
eye_cx_right = cx + int(size * 0.15)
7068
eye_cy = cy - int(size * 0.08)
7169
eye_r = int(18 * eye_size)
7270
pupil_r = int(9 * eye_size)
7371

74-
# Mouth (cubic bezier curve)
7572
mouth_y = cy + int(size * 0.2)
7673
mouth_width = int(size * 0.25)
7774
mouth_curve_offset = int(mouth_curve * size * 0.12)
7875

79-
# Eyebrows
8076
brow_y = eye_cy - int(size * 0.12)
8177
brow_len = int(size * 0.12)
8278

8379
svg = f"""
8480
<!-- Face {label} -->
8581
<ellipse cx="{cx}" cy="{cy}" rx="{face_rx}" ry="{face_ry}"
86-
fill="{color}" stroke="#333333" stroke-width="4"/>
82+
fill="{color}" stroke="{INK}" stroke-width="4"/>
8783
8884
<!-- Left eye -->
89-
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{eye_r}" fill="white" stroke="#333333" stroke-width="3"/>
90-
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{pupil_r}" fill="#333333"/>
85+
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{eye_r}" fill="{ELEVATED_BG}" stroke="{INK}" stroke-width="3"/>
86+
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{pupil_r}" fill="{INK}"/>
9187
9288
<!-- Right eye -->
93-
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{eye_r}" fill="white" stroke="#333333" stroke-width="3"/>
94-
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{pupil_r}" fill="#333333"/>
89+
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{eye_r}" fill="{ELEVATED_BG}" stroke="{INK}" stroke-width="3"/>
90+
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{pupil_r}" fill="{INK}"/>
9591
9692
<!-- Left eyebrow -->
9793
<line x1="{eye_cx_left - brow_len}" y1="{brow_y + int(eyebrow_slant)}"
9894
x2="{eye_cx_left + brow_len}" y2="{brow_y - int(eyebrow_slant)}"
99-
stroke="#333333" stroke-width="5" stroke-linecap="round"/>
95+
stroke="{INK}" stroke-width="5" stroke-linecap="round"/>
10096
10197
<!-- Right eyebrow -->
10298
<line x1="{eye_cx_right - brow_len}" y1="{brow_y - int(eyebrow_slant)}"
10399
x2="{eye_cx_right + brow_len}" y2="{brow_y + int(eyebrow_slant)}"
104-
stroke="#333333" stroke-width="5" stroke-linecap="round"/>
100+
stroke="{INK}" stroke-width="5" stroke-linecap="round"/>
105101
106102
<!-- Nose -->
107103
<line x1="{cx}" y1="{cy - int(size * 0.02)}" x2="{cx}" y2="{cy + int(size * 0.1)}"
108-
stroke="#333333" stroke-width="4" stroke-linecap="round"/>
104+
stroke="{INK}" stroke-width="4" stroke-linecap="round"/>
109105
110106
<!-- Mouth -->
111107
<path d="M {cx - mouth_width} {mouth_y} Q {cx} {mouth_y + mouth_curve_offset} {cx + mouth_width} {mouth_y}"
112-
fill="none" stroke="#333333" stroke-width="5" stroke-linecap="round"/>
108+
fill="none" stroke="{INK}" stroke-width="5" stroke-linecap="round"/>
113109
114110
<!-- Label -->
115111
<text x="{cx}" y="{y_pos + size + 50}" text-anchor="middle"
116-
font-size="36" font-family="Arial, sans-serif" font-weight="bold">{label}</text>
112+
font-size="36" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">{label}</text>
117113
"""
118114
return svg
119115

120116

121-
# Create the complete HTML with embedded SVG
117+
# Create faces grid
122118
faces_svg = ""
123119
face_size = 580
124120
cols = 3
125121
rows = 3
126122

127-
# Calculate grid to center faces properly across the canvas
128-
# Leave space for legends on right (about 600px) and title at top (about 220px)
129123
grid_left = 100
130124
grid_right = 3350
131125
grid_top = 250
@@ -134,52 +128,50 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
134128
grid_width = grid_right - grid_left
135129
grid_height = grid_bottom - grid_top
136130

137-
# Calculate cell size for even distribution
138131
cell_width = grid_width // cols
139132
cell_height = grid_height // rows
140133

141134
for idx in range(9):
142135
row = idx // cols
143136
col = idx % cols
144137

145-
# Center face within its cell
146138
cell_x = grid_left + col * cell_width
147139
cell_y = grid_top + row * cell_height
148140
x_pos = cell_x + (cell_width - face_size) // 2
149-
y_pos = cell_y + (cell_height - face_size - 60) // 2 # -60 for label space
141+
y_pos = cell_y + (cell_height - face_size - 60) // 2
150142

151143
species_idx = y_sample[idx]
152-
color = species_colors[species_idx]
144+
color = OKABE_ITO[species_idx]
153145
label = f"{species_names[species_idx]} #{(idx % 3) + 1}"
154146

155147
faces_svg += create_face_svg(X_norm[idx], color, label, x_pos, y_pos, face_size)
156148

157-
# Create legend - positioned in right column, vertically centered
149+
# Species legend
158150
legend_x = 3550
159151
legend_y = 450
160152
legend_svg = f"""
161-
<rect x="{legend_x}" y="{legend_y}" width="550" height="380" fill="#f8f8f8" stroke="#333333" stroke-width="3" rx="15"/>
162-
<text x="{legend_x + 35}" y="{legend_y + 60}" font-size="44" font-family="Arial, sans-serif" font-weight="bold">Species Legend</text>
153+
<rect x="{legend_x}" y="{legend_y}" width="550" height="380" fill="{ELEVATED_BG}" stroke="{INK_SOFT}" stroke-width="3" rx="15"/>
154+
<text x="{legend_x + 35}" y="{legend_y + 60}" font-size="44" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">Species</text>
163155
"""
164156

165-
for i, (species, color) in enumerate(zip(species_names, species_colors, strict=True)):
157+
for i, (species, color) in enumerate(zip(species_names, OKABE_ITO, strict=True)):
166158
ly_item = legend_y + 130 + i * 80
167159
legend_svg += f"""
168-
<circle cx="{legend_x + 60}" cy="{ly_item}" r="30" fill="{color}" stroke="#333333" stroke-width="3"/>
169-
<text x="{legend_x + 110}" y="{ly_item + 14}" font-size="38" font-family="Arial, sans-serif">{species}</text>
160+
<circle cx="{legend_x + 60}" cy="{ly_item}" r="30" fill="{color}" stroke="{INK}" stroke-width="3"/>
161+
<text x="{legend_x + 110}" y="{ly_item + 14}" font-size="38" font-family="Arial, sans-serif" fill="{INK}">{species}</text>
170162
"""
171163

172164
# Feature mapping legend
173165
feature_legend_y = legend_y + 450
174166
feature_legend_svg = f"""
175-
<rect x="{legend_x}" y="{feature_legend_y}" width="550" height="480" fill="#f8f8f8" stroke="#333333" stroke-width="3" rx="15"/>
176-
<text x="{legend_x + 35}" y="{feature_legend_y + 60}" font-size="40" font-family="Arial, sans-serif" font-weight="bold">Feature Mapping</text>
177-
<text x="{legend_x + 35}" y="{feature_legend_y + 130}" font-size="30" font-family="Arial, sans-serif">Face Width → Sepal Length</text>
178-
<text x="{legend_x + 35}" y="{feature_legend_y + 190}" font-size="30" font-family="Arial, sans-serif">Eye Size → Sepal Width</text>
179-
<text x="{legend_x + 35}" y="{feature_legend_y + 250}" font-size="30" font-family="Arial, sans-serif">Mouth Curve → Petal Length</text>
180-
<text x="{legend_x + 35}" y="{feature_legend_y + 310}" font-size="30" font-family="Arial, sans-serif">Eyebrow Slant → Petal Width</text>
181-
<line x1="{legend_x + 35}" y1="{feature_legend_y + 355}" x2="{legend_x + 515}" y2="{feature_legend_y + 355}" stroke="#cccccc" stroke-width="2"/>
182-
<text x="{legend_x + 35}" y="{feature_legend_y + 410}" font-size="26" font-family="Arial, sans-serif" fill="#666666">All values normalized to 0-1 range</text>
167+
<rect x="{legend_x}" y="{feature_legend_y}" width="550" height="480" fill="{ELEVATED_BG}" stroke="{INK_SOFT}" stroke-width="3" rx="15"/>
168+
<text x="{legend_x + 35}" y="{feature_legend_y + 60}" font-size="40" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">Mapping</text>
169+
<text x="{legend_x + 35}" y="{feature_legend_y + 130}" font-size="30" font-family="Arial, sans-serif" fill="{INK_SOFT}">Face W → Sepal L</text>
170+
<text x="{legend_x + 35}" y="{feature_legend_y + 190}" font-size="30" font-family="Arial, sans-serif" fill="{INK_SOFT}">Eye Size → Sepal W</text>
171+
<text x="{legend_x + 35}" y="{feature_legend_y + 250}" font-size="30" font-family="Arial, sans-serif" fill="{INK_SOFT}">Mouth → Petal L</text>
172+
<text x="{legend_x + 35}" y="{feature_legend_y + 310}" font-size="30" font-family="Arial, sans-serif" fill="{INK_SOFT}">Brow → Petal W</text>
173+
<line x1="{legend_x + 35}" y1="{feature_legend_y + 355}" x2="{legend_x + 515}" y2="{feature_legend_y + 355}" stroke="{INK_MUTED}" stroke-width="2"/>
174+
<text x="{legend_x + 35}" y="{feature_legend_y + 410}" font-size="26" font-family="Arial, sans-serif" fill="{INK_MUTED}">Normalized 0–1</text>
183175
"""
184176

185177
# Complete HTML
@@ -188,24 +180,24 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
188180
<head>
189181
<meta charset="utf-8">
190182
<style>
191-
body {{ margin: 0; padding: 0; background: #ffffff; }}
183+
body {{ margin: 0; padding: 0; background: {PAGE_BG}; }}
192184
</style>
193185
</head>
194186
<body>
195187
<svg width="{svg_width}" height="{svg_height}" xmlns="http://www.w3.org/2000/svg">
196188
<!-- Background -->
197-
<rect width="100%" height="100%" fill="#ffffff"/>
189+
<rect width="100%" height="100%" fill="{PAGE_BG}"/>
198190
199191
<!-- Title -->
200192
<text x="{svg_width // 2}" y="100" text-anchor="middle"
201-
font-size="64" font-family="Arial, sans-serif" font-weight="bold">
202-
chernoff-basic · highcharts · pyplots.ai
193+
font-size="64" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">
194+
chernoff-basic · highcharts · anyplot.ai
203195
</text>
204196
205197
<!-- Subtitle -->
206198
<text x="{svg_width // 2}" y="175" text-anchor="middle"
207-
font-size="40" font-family="Arial, sans-serif" fill="#666666">
208-
Iris Dataset: 4 Variables Mapped to Facial Features (9 Samples, 3 Per Species)
199+
font-size="40" font-family="Arial, sans-serif" fill="{INK_SOFT}">
200+
Iris Dataset: 4 Variables Mapped to Facial Features
209201
</text>
210202
211203
<!-- Faces Grid -->
@@ -221,7 +213,7 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
221213
</html>"""
222214

223215
# Save HTML version
224-
with open("plot.html", "w", encoding="utf-8") as f:
216+
with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
225217
f.write(html_content)
226218

227219
# Export to PNG via Selenium
@@ -241,13 +233,11 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
241233
driver.get(f"file://{temp_path}")
242234
time.sleep(3)
243235

244-
# Use CDP to capture full page at exact dimensions
245236
driver.execute_cdp_cmd(
246237
"Emulation.setDeviceMetricsOverride", {"width": 4800, "height": 2700, "deviceScaleFactor": 1, "mobile": False}
247238
)
248239
time.sleep(1)
249240

250-
# Take screenshot with clip to exact dimensions
251241
result = driver.execute_cdp_cmd(
252242
"Page.captureScreenshot",
253243
{
@@ -257,7 +247,7 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
257247
},
258248
)
259249

260-
with open("plot.png", "wb") as f:
250+
with open(f"plot-{THEME}.png", "wb") as f:
261251
f.write(base64.b64decode(result["data"]))
262252

263253
driver.quit()

0 commit comments

Comments
 (0)