Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 53 additions & 63 deletions plots/chernoff-basic/implementations/python/highcharts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
""" pyplots.ai
""" anyplot.ai
chernoff-basic: Chernoff Faces for Multivariate Data
Library: highcharts unknown | Python 3.13.11
Quality: 91/100 | Created: 2025-12-31
Library: highcharts unknown | Python 3.13.13
Quality: 80/100 | Updated: 2026-05-15
"""

import base64
import os
import tempfile
import time
from pathlib import Path
Expand All @@ -15,7 +16,16 @@
from sklearn.datasets import load_iris


# Data - Using Iris dataset (4 variables per flower)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

OKABE_ITO = ["#009E73", "#D55E00", "#0072B2"]

# Data
np.random.seed(42)
iris = load_iris()
X = iris.data
Expand All @@ -36,96 +46,80 @@
X_max = X_sample.max(axis=0)
X_norm = (X_sample - X_min) / (X_max - X_min + 1e-8)

# Colors for species - colorblind-safe
species_colors = ["#306998", "#FFD43B", "#9467BD"]

# SVG dimensions
svg_width = 4800
svg_height = 2700

# Build custom Chernoff faces HTML/SVG
# Feature mappings:
# - Variable 0 (sepal length): face width
# - Variable 1 (sepal width): eye size
# - Variable 2 (petal length): mouth curvature
# - Variable 3 (petal width): eyebrow slant


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

cx = x_pos + size // 2
cy = y_pos + size // 2
face_rx = int(size * 0.4 * face_width)
face_ry = int(size * 0.45)

# Eye positions and sizes
eye_cx_left = cx - int(size * 0.15)
eye_cx_right = cx + int(size * 0.15)
eye_cy = cy - int(size * 0.08)
eye_r = int(18 * eye_size)
pupil_r = int(9 * eye_size)

# Mouth (cubic bezier curve)
mouth_y = cy + int(size * 0.2)
mouth_width = int(size * 0.25)
mouth_curve_offset = int(mouth_curve * size * 0.12)

# Eyebrows
brow_y = eye_cy - int(size * 0.12)
brow_len = int(size * 0.12)

svg = f"""
<!-- Face {label} -->
<ellipse cx="{cx}" cy="{cy}" rx="{face_rx}" ry="{face_ry}"
fill="{color}" stroke="#333333" stroke-width="4"/>
fill="{color}" stroke="{INK}" stroke-width="4"/>

<!-- Left eye -->
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{eye_r}" fill="white" stroke="#333333" stroke-width="3"/>
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{pupil_r}" fill="#333333"/>
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{eye_r}" fill="{ELEVATED_BG}" stroke="{INK}" stroke-width="3"/>
<circle cx="{eye_cx_left}" cy="{eye_cy}" r="{pupil_r}" fill="{INK}"/>

<!-- Right eye -->
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{eye_r}" fill="white" stroke="#333333" stroke-width="3"/>
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{pupil_r}" fill="#333333"/>
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{eye_r}" fill="{ELEVATED_BG}" stroke="{INK}" stroke-width="3"/>
<circle cx="{eye_cx_right}" cy="{eye_cy}" r="{pupil_r}" fill="{INK}"/>

<!-- Left eyebrow -->
<line x1="{eye_cx_left - brow_len}" y1="{brow_y + int(eyebrow_slant)}"
x2="{eye_cx_left + brow_len}" y2="{brow_y - int(eyebrow_slant)}"
stroke="#333333" stroke-width="5" stroke-linecap="round"/>
stroke="{INK}" stroke-width="5" stroke-linecap="round"/>

<!-- Right eyebrow -->
<line x1="{eye_cx_right - brow_len}" y1="{brow_y - int(eyebrow_slant)}"
x2="{eye_cx_right + brow_len}" y2="{brow_y + int(eyebrow_slant)}"
stroke="#333333" stroke-width="5" stroke-linecap="round"/>
stroke="{INK}" stroke-width="5" stroke-linecap="round"/>

<!-- Nose -->
<line x1="{cx}" y1="{cy - int(size * 0.02)}" x2="{cx}" y2="{cy + int(size * 0.1)}"
stroke="#333333" stroke-width="4" stroke-linecap="round"/>
stroke="{INK}" stroke-width="4" stroke-linecap="round"/>

<!-- Mouth -->
<path d="M {cx - mouth_width} {mouth_y} Q {cx} {mouth_y + mouth_curve_offset} {cx + mouth_width} {mouth_y}"
fill="none" stroke="#333333" stroke-width="5" stroke-linecap="round"/>
fill="none" stroke="{INK}" stroke-width="5" stroke-linecap="round"/>

<!-- Label -->
<text x="{cx}" y="{y_pos + size + 50}" text-anchor="middle"
font-size="36" font-family="Arial, sans-serif" font-weight="bold">{label}</text>
font-size="36" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">{label}</text>
"""
return svg


# Create the complete HTML with embedded SVG
# Create faces grid
faces_svg = ""
face_size = 580
cols = 3
rows = 3

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

# Calculate cell size for even distribution
cell_width = grid_width // cols
cell_height = grid_height // rows

for idx in range(9):
row = idx // cols
col = idx % cols

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

species_idx = y_sample[idx]
color = species_colors[species_idx]
color = OKABE_ITO[species_idx]
label = f"{species_names[species_idx]} #{(idx % 3) + 1}"

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

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

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

# Feature mapping legend
feature_legend_y = legend_y + 450
feature_legend_svg = f"""
<rect x="{legend_x}" y="{feature_legend_y}" width="550" height="480" fill="#f8f8f8" stroke="#333333" stroke-width="3" rx="15"/>
<text x="{legend_x + 35}" y="{feature_legend_y + 60}" font-size="40" font-family="Arial, sans-serif" font-weight="bold">Feature Mapping</text>
<text x="{legend_x + 35}" y="{feature_legend_y + 130}" font-size="30" font-family="Arial, sans-serif">Face Width → Sepal Length</text>
<text x="{legend_x + 35}" y="{feature_legend_y + 190}" font-size="30" font-family="Arial, sans-serif">Eye Size → Sepal Width</text>
<text x="{legend_x + 35}" y="{feature_legend_y + 250}" font-size="30" font-family="Arial, sans-serif">Mouth Curve → Petal Length</text>
<text x="{legend_x + 35}" y="{feature_legend_y + 310}" font-size="30" font-family="Arial, sans-serif">Eyebrow Slant → Petal Width</text>
<line x1="{legend_x + 35}" y1="{feature_legend_y + 355}" x2="{legend_x + 515}" y2="{feature_legend_y + 355}" stroke="#cccccc" stroke-width="2"/>
<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>
<rect x="{legend_x}" y="{feature_legend_y}" width="550" height="480" fill="{ELEVATED_BG}" stroke="{INK_SOFT}" stroke-width="3" rx="15"/>
<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>
<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>
<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>
<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>
<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>
<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"/>
<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>
"""

# Complete HTML
Expand All @@ -188,24 +180,24 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
<head>
<meta charset="utf-8">
<style>
body {{ margin: 0; padding: 0; background: #ffffff; }}
body {{ margin: 0; padding: 0; background: {PAGE_BG}; }}
</style>
</head>
<body>
<svg width="{svg_width}" height="{svg_height}" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect width="100%" height="100%" fill="#ffffff"/>
<rect width="100%" height="100%" fill="{PAGE_BG}"/>

<!-- Title -->
<text x="{svg_width // 2}" y="100" text-anchor="middle"
font-size="64" font-family="Arial, sans-serif" font-weight="bold">
chernoff-basic · highcharts · pyplots.ai
font-size="64" font-family="Arial, sans-serif" font-weight="bold" fill="{INK}">
chernoff-basic · highcharts · anyplot.ai
</text>

<!-- Subtitle -->
<text x="{svg_width // 2}" y="175" text-anchor="middle"
font-size="40" font-family="Arial, sans-serif" fill="#666666">
Iris Dataset: 4 Variables Mapped to Facial Features (9 Samples, 3 Per Species)
font-size="40" font-family="Arial, sans-serif" fill="{INK_SOFT}">
Iris Dataset: 4 Variables Mapped to Facial Features
</text>

<!-- Faces Grid -->
Expand All @@ -221,7 +213,7 @@ def create_face_svg(values, color, label, x_pos, y_pos, size=450):
</html>"""

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

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

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

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

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

driver.quit()
Expand Down
Loading
Loading