{"spec_id":"chernoff-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nchernoff-basic: Chernoff Faces for Multivariate Data\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 80/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_fill_manual,\n    theme,\n)\nfrom sklearn.datasets import load_iris\n\n\nLetsPlot.setup_html()\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\"\n\n# Data - Iris dataset\nnp.random.seed(42)\niris = load_iris()\ndf = pd.DataFrame(iris.data, columns=[\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"])\ndf[\"species\"] = [iris.target_names[i] for i in iris.target]\n\n# Sample 12 flowers (4 per species)\nsample_idx = []\nfor species in range(3):\n    species_idx = np.where(iris.target == species)[0]\n    sample_idx.extend(np.random.choice(species_idx, 4, replace=False))\ndf_sample = df.iloc[sample_idx].reset_index(drop=True)\n\n# Normalize data to 0-1 range\nfeatures = [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]\nfor col in features:\n    min_val = df_sample[col].min()\n    max_val = df_sample[col].max()\n    df_sample[col + \"_norm\"] = (df_sample[col] - min_val) / (max_val - min_val)\n\n# Generate faces in a grid\ngrid_rows = 3\ngrid_cols = 4\nall_face_data = []\nlabel_data = []\nspecies_colors = {\"setosa\": \"#009E73\", \"versicolor\": \"#C475FD\", \"virginica\": \"#4467A3\"}\n\nfor idx, row in df_sample.iterrows():\n    col = idx % grid_cols\n    row_pos = idx // grid_cols\n    center_x = col + 0.5\n    center_y = (grid_rows - 1 - row_pos) + 0.5\n\n    sepal_len = row[\"sepal_length_norm\"]\n    sepal_wid = row[\"sepal_width_norm\"]\n    petal_len = row[\"petal_length_norm\"]\n    petal_wid = row[\"petal_width_norm\"]\n\n    scale = 0.42\n    face_data = []\n\n    # Face outline (ellipse)\n    face_width = 0.35 + 0.2 * sepal_len\n    face_height = 0.45\n    theta = np.linspace(0, 2 * np.pi, 50)\n    face_x = center_x + scale * face_width * np.cos(theta)\n    face_y = center_y + scale * face_height * np.sin(theta)\n    for i in range(len(theta)):\n        face_data.append({\"x\": face_x[i], \"y\": face_y[i], \"part\": \"face\", \"order\": i})\n\n    # Eyes\n    eye_size = 0.03 + 0.04 * sepal_wid\n    eye_y = center_y + scale * 0.12\n    eye_spacing = 0.12\n\n    # Left eye\n    theta_eye = np.linspace(0, 2 * np.pi, 20)\n    left_eye_x = center_x - scale * eye_spacing + scale * eye_size * np.cos(theta_eye)\n    left_eye_y = eye_y + scale * eye_size * np.sin(theta_eye)\n    for i in range(len(theta_eye)):\n        face_data.append({\"x\": left_eye_x[i], \"y\": left_eye_y[i], \"part\": \"left_eye\", \"order\": i})\n\n    # Right eye\n    right_eye_x = center_x + scale * eye_spacing + scale * eye_size * np.cos(theta_eye)\n    right_eye_y = eye_y + scale * eye_size * np.sin(theta_eye)\n    for i in range(len(theta_eye)):\n        face_data.append({\"x\": right_eye_x[i], \"y\": right_eye_y[i], \"part\": \"right_eye\", \"order\": i})\n\n    # Pupils\n    pupil_size = eye_size * 0.4\n    left_pupil_x = center_x - scale * eye_spacing + scale * pupil_size * np.cos(theta_eye)\n    left_pupil_y = eye_y + scale * pupil_size * np.sin(theta_eye)\n    for i in range(len(theta_eye)):\n        face_data.append({\"x\": left_pupil_x[i], \"y\": left_pupil_y[i], \"part\": \"left_pupil\", \"order\": i})\n\n    right_pupil_x = center_x + scale * eye_spacing + scale * pupil_size * np.cos(theta_eye)\n    right_pupil_y = eye_y + scale * pupil_size * np.sin(theta_eye)\n    for i in range(len(theta_eye)):\n        face_data.append({\"x\": right_pupil_x[i], \"y\": right_pupil_y[i], \"part\": \"right_pupil\", \"order\": i})\n\n    # Mouth\n    mouth_y = center_y - scale * 0.15\n    mouth_width = 0.12\n    curvature = -0.08 + 0.16 * petal_len\n    mouth_x = np.linspace(-mouth_width, mouth_width, 20)\n    mouth_curve_y = mouth_y + scale * curvature * (1 - (mouth_x / mouth_width) ** 2)\n    mouth_curve_x = center_x + scale * mouth_x\n    for i in range(len(mouth_x)):\n        face_data.append({\"x\": mouth_curve_x[i], \"y\": mouth_curve_y[i], \"part\": \"mouth\", \"order\": i})\n\n    # Eyebrows\n    brow_y = center_y + scale * 0.22\n    brow_slant = -0.03 + 0.06 * petal_wid\n    brow_length = 0.06\n\n    # Left eyebrow\n    face_data.append(\n        {\n            \"x\": center_x - scale * (eye_spacing + brow_length),\n            \"y\": brow_y - scale * brow_slant,\n            \"part\": \"left_brow\",\n            \"order\": 0,\n        }\n    )\n    face_data.append(\n        {\n            \"x\": center_x - scale * (eye_spacing - brow_length),\n            \"y\": brow_y + scale * brow_slant,\n            \"part\": \"left_brow\",\n            \"order\": 1,\n        }\n    )\n\n    # Right eyebrow\n    face_data.append(\n        {\n            \"x\": center_x + scale * (eye_spacing - brow_length),\n            \"y\": brow_y + scale * brow_slant,\n            \"part\": \"right_brow\",\n            \"order\": 0,\n        }\n    )\n    face_data.append(\n        {\n            \"x\": center_x + scale * (eye_spacing + brow_length),\n            \"y\": brow_y - scale * brow_slant,\n            \"part\": \"right_brow\",\n            \"order\": 1,\n        }\n    )\n\n    # Nose\n    nose_top = center_y + scale * 0.02\n    nose_bottom = center_y - scale * 0.08\n    face_data.append({\"x\": center_x, \"y\": nose_top, \"part\": \"nose\", \"order\": 0})\n    face_data.append({\"x\": center_x, \"y\": nose_bottom, \"part\": \"nose\", \"order\": 1})\n\n    # Convert to DataFrame and add metadata\n    face_df = pd.DataFrame(face_data)\n    face_df[\"face_id\"] = idx\n    face_df[\"species\"] = row[\"species\"]\n    all_face_data.append(face_df)\n\n    # Add species label\n    label_data.append({\"x\": center_x, \"y\": center_y - 0.45, \"label\": row[\"species\"].title(), \"species\": row[\"species\"]})\n\n# Combine all face data\nfaces_df = pd.concat(all_face_data, ignore_index=True)\nlabels_df = pd.DataFrame(label_data)\n\n# Separate face parts for layering\nface_outline = faces_df[faces_df[\"part\"] == \"face\"]\neyes = faces_df[faces_df[\"part\"].isin([\"left_eye\", \"right_eye\"])]\npupils = faces_df[faces_df[\"part\"].isin([\"left_pupil\", \"right_pupil\"])]\nmouth = faces_df[faces_df[\"part\"] == \"mouth\"]\nbrows = faces_df[faces_df[\"part\"].isin([\"left_brow\", \"right_brow\"])]\nnose = faces_df[faces_df[\"part\"] == \"nose\"]\n\n# Create plot with theme-adaptive styling\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_grid=element_blank(),\n    axis_title=element_blank(),\n    axis_text=element_blank(),\n    axis_ticks=element_blank(),\n    plot_title=element_text(size=24, face=\"bold\", color=INK),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_title=element_text(size=18, color=INK),\n    legend_text=element_text(size=16, color=INK_SOFT),\n    legend_position=\"right\",\n    plot_margin=[40, 20, 20, 20],\n)\n\nplot = (\n    ggplot()\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"face_id\", fill=\"species\"), data=face_outline, color=INK_SOFT, size=1.5, alpha=0.3\n    )\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=[\"face_id\", \"part\"]), data=eyes, fill=\"white\", color=INK_SOFT, size=1.0)\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=[\"face_id\", \"part\"]), data=pupils, fill=INK, color=INK, size=0.5)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"face_id\"), data=mouth, color=INK, size=2.0)\n    + geom_path(aes(x=\"x\", y=\"y\", group=[\"face_id\", \"part\"]), data=brows, color=INK, size=2.5)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"face_id\"), data=nose, color=INK, size=1.5)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=labels_df, color=INK_SOFT, size=12, fontface=\"bold\")\n    + scale_fill_manual(values=species_colors)\n    + labs(title=\"chernoff-basic · letsplot · anyplot.ai\", fill=\"Species\")\n    + anyplot_theme\n    + ggsize(1600, 900)\n)\n\n# Save outputs\nggsave(plot, f\"plot-{THEME}.png\", scale=3, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}