{"spec_id":"chernoff-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nchernoff-basic: Chernoff Faces for Multivariate Data\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette for species\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Iris dataset features for 12 representative flowers\nnp.random.seed(42)\n# Diverse samples from iris-like measurements (normalized 0-1)\ndata = pd.DataFrame(\n    {\n        \"observation\": [f\"Sample {i + 1}\" for i in range(12)],\n        \"sepal_length\": [0.22, 0.83, 0.45, 0.12, 0.91, 0.67, 0.33, 0.78, 0.55, 0.95, 0.28, 0.61],\n        \"sepal_width\": [0.63, 0.45, 0.78, 0.89, 0.32, 0.56, 0.71, 0.41, 0.65, 0.25, 0.82, 0.48],\n        \"petal_length\": [0.07, 0.69, 0.42, 0.05, 0.83, 0.55, 0.18, 0.76, 0.38, 0.95, 0.11, 0.62],\n        \"petal_width\": [0.04, 0.54, 0.33, 0.02, 0.79, 0.48, 0.12, 0.67, 0.29, 0.88, 0.08, 0.52],\n        \"species\": [\n            \"setosa\",\n            \"virginica\",\n            \"versicolor\",\n            \"setosa\",\n            \"virginica\",\n            \"versicolor\",\n            \"setosa\",\n            \"virginica\",\n            \"versicolor\",\n            \"virginica\",\n            \"setosa\",\n            \"versicolor\",\n        ],\n    }\n)\n\n# Map species to Okabe-Ito colors\nspecies_colors = {\"setosa\": IMPRINT[0], \"versicolor\": IMPRINT[1], \"virginica\": IMPRINT[2]}\ndata[\"color\"] = data[\"species\"].map(species_colors)\n\n# Create descriptive labels including species name\ndata[\"label\"] = [f\"{obs} ({sp})\" for obs, sp in zip(data[\"observation\"], data[\"species\"], strict=True)]\n\n# Grid positions for 12 faces (4 columns x 3 rows for better canvas utilization)\ndata[\"col\"] = [i % 4 for i in range(12)]\ndata[\"row\"] = [i // 4 for i in range(12)]\ndata[\"x_center\"] = data[\"col\"] * 200 + 130\ndata[\"y_center\"] = (2 - data[\"row\"]) * 240 + 160\n\n# Calculate face feature dimensions based on variables with more pronounced variation\n# face_width: sepal_length, face_height: sepal_width\n# eye_size: petal_length, mouth_width: petal_width\n# eyebrow_slant: derived from petal_length (maps to eyebrow angle)\ndata[\"face_width\"] = 40 + data[\"sepal_length\"] * 70  # 40-110\ndata[\"face_height\"] = 50 + data[\"sepal_width\"] * 80  # 50-130\ndata[\"eye_size\"] = 6 + data[\"petal_length\"] * 22  # 6-28\ndata[\"mouth_width\"] = 15 + data[\"petal_width\"] * 35  # 15-50\ndata[\"eyebrow_slant\"] = -15 + data[\"petal_length\"] * 30  # -15 to 15\n\n# Build face components using layered shapes\nface_records = []\n\nfor _, r in data.iterrows():\n    xc, yc = r[\"x_center\"], r[\"y_center\"]\n    fw, fh = r[\"face_width\"], r[\"face_height\"]\n    es = r[\"eye_size\"]\n    mw = r[\"mouth_width\"]\n    eb_slant = r[\"eyebrow_slant\"]\n\n    # Face outline - single smooth ellipse using many small points on perimeter\n    # This creates a clean ellipse shape instead of blobby overlapping circles\n    for angle in np.linspace(0, 2 * np.pi, 48, endpoint=False):\n        px = xc + (fw * 0.9) * np.cos(angle)\n        py = yc + (fh * 0.75) * np.sin(angle)\n        face_records.append(\n            {\n                \"x\": px,\n                \"y\": py,\n                \"size\": 350,\n                \"color\": r[\"color\"],\n                \"part\": \"outline\",\n                \"observation\": r[\"observation\"],\n                \"species\": r[\"species\"],\n                \"opacity\": 0.7,\n            }\n        )\n\n    # Face fill - concentric rings of points to fill the ellipse smoothly\n    for scale in [0.8, 0.6, 0.4, 0.2]:\n        for angle in np.linspace(0, 2 * np.pi, int(36 * scale) + 8, endpoint=False):\n            px = xc + (fw * 0.9 * scale) * np.cos(angle)\n            py = yc + (fh * 0.75 * scale) * np.sin(angle)\n            face_records.append(\n                {\n                    \"x\": px,\n                    \"y\": py,\n                    \"size\": 400,\n                    \"color\": r[\"color\"],\n                    \"part\": \"face_fill\",\n                    \"observation\": r[\"observation\"],\n                    \"species\": r[\"species\"],\n                    \"opacity\": 0.5,\n                }\n            )\n\n    # Center fill point\n    face_records.append(\n        {\n            \"x\": xc,\n            \"y\": yc,\n            \"size\": 600,\n            \"color\": r[\"color\"],\n            \"part\": \"face_fill\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.5,\n        }\n    )\n\n    # Left eyebrow (line represented by two points)\n    eyebrow_color = INK_SOFT\n    face_records.append(\n        {\n            \"x\": xc - fw * 0.38,\n            \"y\": yc + fh * 0.32 + eb_slant * 0.3,\n            \"size\": 120,\n            \"color\": eyebrow_color,\n            \"part\": \"eyebrow\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.9,\n        }\n    )\n    face_records.append(\n        {\n            \"x\": xc - fw * 0.22,\n            \"y\": yc + fh * 0.32 - eb_slant * 0.3,\n            \"size\": 120,\n            \"color\": eyebrow_color,\n            \"part\": \"eyebrow\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.9,\n        }\n    )\n    # Right eyebrow\n    face_records.append(\n        {\n            \"x\": xc + fw * 0.22,\n            \"y\": yc + fh * 0.32 - eb_slant * 0.3,\n            \"size\": 120,\n            \"color\": eyebrow_color,\n            \"part\": \"eyebrow\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.9,\n        }\n    )\n    face_records.append(\n        {\n            \"x\": xc + fw * 0.38,\n            \"y\": yc + fh * 0.32 + eb_slant * 0.3,\n            \"size\": 120,\n            \"color\": eyebrow_color,\n            \"part\": \"eyebrow\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.9,\n        }\n    )\n    # Left eye\n    face_records.append(\n        {\n            \"x\": xc - fw * 0.30,\n            \"y\": yc + fh * 0.15,\n            \"size\": es * 45,\n            \"color\": INK,\n            \"part\": \"eye\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 1.0,\n        }\n    )\n    # Right eye\n    face_records.append(\n        {\n            \"x\": xc + fw * 0.30,\n            \"y\": yc + fh * 0.15,\n            \"size\": es * 45,\n            \"color\": INK,\n            \"part\": \"eye\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 1.0,\n        }\n    )\n    # Left pupil (white/light highlight)\n    pupil_color = PAGE_BG if THEME == \"light\" else INK_SOFT\n    face_records.append(\n        {\n            \"x\": xc - fw * 0.30 + 3,\n            \"y\": yc + fh * 0.15 + 3,\n            \"size\": es * 12,\n            \"color\": pupil_color,\n            \"part\": \"pupil\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.95,\n        }\n    )\n    # Right pupil (white/light highlight)\n    face_records.append(\n        {\n            \"x\": xc + fw * 0.30 + 3,\n            \"y\": yc + fh * 0.15 + 3,\n            \"size\": es * 12,\n            \"color\": pupil_color,\n            \"part\": \"pupil\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.95,\n        }\n    )\n    # Nose\n    nose_color = INK_MUTED\n    face_records.append(\n        {\n            \"x\": xc,\n            \"y\": yc - fh * 0.05,\n            \"size\": 90,\n            \"color\": nose_color,\n            \"part\": \"nose\",\n            \"observation\": r[\"observation\"],\n            \"species\": r[\"species\"],\n            \"opacity\": 0.7,\n        }\n    )\n    # Mouth - using horizontal ellipse shape for better representation\n    mouth_color = IMPRINT[1] if THEME == \"light\" else IMPRINT[4]\n    mouth_y = yc - fh * 0.30\n    for dx in np.linspace(-mw * 0.4, mw * 0.4, 7):\n        # Parabolic curve for mouth (smiling effect based on width)\n        dy = -(dx**2) / (mw * 1.2) + mw * 0.08\n        face_records.append(\n            {\n                \"x\": xc + dx,\n                \"y\": mouth_y + dy,\n                \"size\": 80 if abs(dx) < mw * 0.3 else 50,\n                \"color\": mouth_color,\n                \"part\": \"mouth\",\n                \"observation\": r[\"observation\"],\n                \"species\": r[\"species\"],\n                \"opacity\": 0.9,\n            }\n        )\n\nface_df = pd.DataFrame(face_records)\n\n# Reorder facial features drawing order\npart_order = {\"outline\": 0, \"face_fill\": 1, \"eyebrow\": 2, \"nose\": 3, \"mouth\": 4, \"eye\": 5, \"pupil\": 6}\nface_df[\"order\"] = face_df[\"part\"].map(part_order)\nface_df = face_df.sort_values(\"order\")\n\n# Create labels for each face - positioned below faces with descriptive text\nlabel_df = data[[\"x_center\", \"y_center\", \"label\", \"face_height\"]].copy()\nlabel_df[\"y_label\"] = label_df[\"y_center\"] - label_df[\"face_height\"] * 0.7 - 35\n\n# Face features chart (includes outline, fill, and features)\nfeatures = (\n    alt.Chart(face_df)\n    .mark_point(filled=True)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[0, 900])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[0, 800])),\n        size=alt.Size(\"size:Q\", legend=None, scale=alt.Scale(range=[40, 1600])),\n        color=alt.Color(\"color:N\", legend=None, scale=None),\n        opacity=alt.Opacity(\"opacity:Q\", legend=None),\n        order=\"order:O\",\n    )\n)\n\n# Labels with species info\nlabels = (\n    alt.Chart(label_df)\n    .mark_text(fontSize=13, fontWeight=\"bold\", color=INK_SOFT)\n    .encode(x=alt.X(\"x_center:Q\", axis=None), y=alt.Y(\"y_label:Q\", axis=None), text=\"label:N\")\n)\n\n# Legend for species (positioned on right side, higher up to avoid overlap)\nlegend_data = pd.DataFrame(\n    {\n        \"species\": [\"setosa\", \"versicolor\", \"virginica\"],\n        \"x\": [850, 850, 850],\n        \"y\": [780, 730, 680],\n        \"color\": [IMPRINT[0], IMPRINT[1], IMPRINT[2]],\n    }\n)\n\nlegend_points = (\n    alt.Chart(legend_data)\n    .mark_point(filled=True, size=600, opacity=0.5)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), color=alt.Color(\"color:N\", scale=None, legend=None))\n)\n\nlegend_text = (\n    alt.Chart(legend_data)\n    .mark_text(align=\"right\", fontSize=14, dx=-25, fontWeight=\"bold\", color=INK_SOFT)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"species:N\")\n)\n\n# Feature mapping explanation - positioned at top left to avoid overlap with faces\nmapping_data = pd.DataFrame(\n    {\n        \"text\": [\n            \"Feature Mapping:\",\n            \"Face width ← sepal length\",\n            \"Face height ← sepal width\",\n            \"Eye size ← petal length\",\n            \"Mouth width ← petal width\",\n            \"Eyebrow slant ← petal length\",\n        ],\n        \"x\": [30, 30, 30, 30, 30, 30],\n        \"y\": [785, 760, 735, 710, 685, 660],\n    }\n)\n\nmapping_text = (\n    alt.Chart(mapping_data)\n    .mark_text(align=\"left\", fontSize=12, color=INK_MUTED)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Combine all layers\nchart = (\n    (features + labels + legend_points + legend_text + mapping_text)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"chernoff-basic · altair · anyplot.ai\",\n            fontSize=28,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Iris Dataset: Each face represents a flower sample with features encoding measurements\",\n            subtitleFontSize=16,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n)\n\n# Save as PNG and HTML\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}