{"spec_id":"chernoff-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nchernoff-basic: Chernoff Faces for Multivariate Data\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 79/100 | Updated: 2026-05-15\n\"\"\"\n\nimport numpy as np\nfrom bokeh.io import export_png\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\n\n\n# Generate synthetic company performance data (4 metrics for 12 companies)\n# Metrics: Revenue Growth, Profit Margin, Customer Satisfaction, Market Share\nnp.random.seed(42)\n\n# Three company sectors with different profiles\nsectors = [\"Tech\", \"Retail\", \"Energy\"]\nsector_idx = np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2])\n\n# Tech companies: high growth, high margin\ntech_data = np.column_stack(\n    [\n        np.random.uniform(0.6, 1.0, 4),  # Revenue Growth\n        np.random.uniform(0.5, 0.9, 4),  # Profit Margin\n        np.random.uniform(0.6, 0.95, 4),  # Customer Satisfaction\n        np.random.uniform(0.3, 0.7, 4),  # Market Share\n    ]\n)\n\n# Retail companies: moderate growth, moderate margin\nretail_data = np.column_stack(\n    [\n        np.random.uniform(0.2, 0.5, 4),  # Revenue Growth\n        np.random.uniform(0.15, 0.4, 4),  # Profit Margin\n        np.random.uniform(0.5, 0.8, 4),  # Customer Satisfaction\n        np.random.uniform(0.4, 0.8, 4),  # Market Share\n    ]\n)\n\n# Energy companies: low growth, variable margin\nenergy_data = np.column_stack(\n    [\n        np.random.uniform(0.05, 0.3, 4),  # Revenue Growth\n        np.random.uniform(0.3, 0.6, 4),  # Profit Margin\n        np.random.uniform(0.3, 0.6, 4),  # Customer Satisfaction\n        np.random.uniform(0.5, 0.9, 4),  # Market Share\n    ]\n)\n\ndata = np.vstack([tech_data, retail_data, energy_data])\n\n# Normalize each feature to 0-1\ndata_norm = (data - data.min(axis=0)) / (data.max(axis=0) - data.min(axis=0) + 1e-10)\n\n# Colors for sectors\ncolors = [\"#306998\", \"#FFD43B\", \"#8B4513\"]\n\n# Store face center data for hover tooltips using ColumnDataSource\nface_centers_x = []\nface_centers_y = []\nface_labels = []\nface_revenue = []\nface_margin = []\nface_satisfaction = []\nface_market_share = []\n\n# Create figure with 4x3 grid for 12 faces\np = figure(\n    width=4800,\n    height=2700,\n    title=\"chernoff-basic · bokeh · pyplots.ai\",\n    x_range=(-0.1, 4.1),\n    y_range=(-0.2, 3.2),\n    tools=\"\",\n)\n\n# Style\np.title.text_font_size = \"32pt\"\np.title.align = \"center\"\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\np.outline_line_color = None\np.background_fill_color = \"#FAFAFA\"\n\n# Draw faces in a 4x3 grid (inline, no helper function)\nface_size = 0.4\nfor i, (features, sec_idx) in enumerate(zip(data_norm, sector_idx, strict=True)):\n    col = i % 4\n    row = 2 - i // 4  # Start from top row\n    cx = col + 0.5\n    cy = row + 0.5\n    color = colors[sec_idx]\n    label_text = f\"{sectors[sec_idx]} #{i % 4 + 1}\"\n\n    # Store data for hover tooltip\n    face_centers_x.append(cx)\n    face_centers_y.append(cy)\n    face_labels.append(label_text)\n    face_revenue.append(f\"{data[i, 0] * 100:.1f}%\")\n    face_margin.append(f\"{data[i, 1] * 100:.1f}%\")\n    face_satisfaction.append(f\"{data[i, 2] * 100:.1f}%\")\n    face_market_share.append(f\"{data[i, 3] * 100:.1f}%\")\n\n    # Features mapping:\n    # - revenue_growth (features[0]) -> face width\n    # - profit_margin (features[1]) -> face height\n    # - customer_satisfaction (features[2]) -> eye size\n    # - market_share (features[3]) -> mouth curvature\n    face_width = (0.3 + features[0] * 0.3) * face_size\n    face_height = (0.35 + features[1] * 0.25) * face_size\n    eye_size = (0.03 + features[2] * 0.05) * face_size\n    mouth_curve = features[3]\n\n    # Draw face outline (ellipse approximation using patches)\n    theta = np.linspace(0, 2 * np.pi, 50)\n    face_x = cx + face_width * np.cos(theta)\n    face_y = cy + face_height * np.sin(theta)\n    p.patch(face_x, face_y, fill_color=color, fill_alpha=0.3, line_color=color, line_width=3)\n\n    # Draw eyes\n    eye_spacing = face_width * 0.5\n    eye_y = cy + face_height * 0.25\n    eye_theta = np.linspace(0, 2 * np.pi, 30)\n\n    # Left eye\n    left_eye_x = cx - eye_spacing\n    left_ex = left_eye_x + eye_size * np.cos(eye_theta)\n    left_ey = eye_y + eye_size * np.sin(eye_theta)\n    p.patch(left_ex, left_ey, fill_color=\"white\", line_color=\"#333333\", line_width=2)\n\n    # Left pupil\n    pupil_size = eye_size * 0.5\n    left_px = left_eye_x + pupil_size * np.cos(eye_theta) * 0.6\n    left_py = eye_y + pupil_size * np.sin(eye_theta) * 0.6\n    p.patch(left_px, left_py, fill_color=\"#333333\", line_color=\"#333333\")\n\n    # Right eye\n    right_eye_x = cx + eye_spacing\n    right_ex = right_eye_x + eye_size * np.cos(eye_theta)\n    right_ey = eye_y + eye_size * np.sin(eye_theta)\n    p.patch(right_ex, right_ey, fill_color=\"white\", line_color=\"#333333\", line_width=2)\n\n    # Right pupil\n    right_px = right_eye_x + pupil_size * np.cos(eye_theta) * 0.6\n    right_py = eye_y + pupil_size * np.sin(eye_theta) * 0.6\n    p.patch(right_px, right_py, fill_color=\"#333333\", line_color=\"#333333\")\n\n    # Draw eyebrows\n    brow_y = eye_y + eye_size * 1.8\n    brow_width = eye_size * 1.2\n    eyebrow_slant = (features[0] - 0.5) * 0.02 * face_size\n\n    p.line(\n        [left_eye_x - brow_width, left_eye_x + brow_width],\n        [brow_y + eyebrow_slant, brow_y - eyebrow_slant],\n        line_color=\"#333333\",\n        line_width=3,\n    )\n    p.line(\n        [right_eye_x - brow_width, right_eye_x + brow_width],\n        [brow_y - eyebrow_slant, brow_y + eyebrow_slant],\n        line_color=\"#333333\",\n        line_width=3,\n    )\n\n    # Draw nose\n    nose_length = (0.02 + features[1] * 0.03) * face_size\n    nose_y_top = cy + face_height * 0.1\n    nose_y_bottom = cy - face_height * 0.1\n    p.line([cx, cx], [nose_y_top, nose_y_bottom], line_color=\"#333333\", line_width=2)\n    p.line(\n        [cx - nose_length * 0.5, cx, cx + nose_length * 0.5],\n        [nose_y_bottom, nose_y_bottom - nose_length * 0.3, nose_y_bottom],\n        line_color=\"#333333\",\n        line_width=2,\n    )\n\n    # Draw mouth (curved based on market_share)\n    mouth_y = cy - face_height * 0.4\n    mouth_width = face_width * 0.5\n    mouth_x = np.linspace(cx - mouth_width, cx + mouth_width, 20)\n    curve_amount = (mouth_curve - 0.5) * 0.08 * face_size\n    mouth_y_curve = mouth_y + curve_amount * (1 - ((mouth_x - cx) / mouth_width) ** 2) * 4\n    p.line(mouth_x, mouth_y_curve, line_color=\"#333333\", line_width=3)\n\n    # Add label below face\n    label_obj = Label(\n        x=cx,\n        y=cy - face_height - 0.1,\n        text=label_text,\n        text_align=\"center\",\n        text_font_size=\"20pt\",\n        text_color=\"#333333\",\n    )\n    p.add_layout(label_obj)\n\n# Create ColumnDataSource for hover tooltips (Bokeh-specific feature)\nhover_source = ColumnDataSource(\n    data={\n        \"x\": face_centers_x,\n        \"y\": face_centers_y,\n        \"label\": face_labels,\n        \"revenue\": face_revenue,\n        \"margin\": face_margin,\n        \"satisfaction\": face_satisfaction,\n        \"market_share\": face_market_share,\n    }\n)\n\n# Add invisible scatter for hover interaction\nhover_renderer = p.scatter(\"x\", \"y\", source=hover_source, size=80, fill_alpha=0, line_alpha=0)\n\n# Add HoverTool for interactivity (distinctive Bokeh feature)\nhover_tool = HoverTool(\n    renderers=[hover_renderer],\n    tooltips=[\n        (\"Company\", \"@label\"),\n        (\"Revenue Growth\", \"@revenue\"),\n        (\"Profit Margin\", \"@margin\"),\n        (\"Satisfaction\", \"@satisfaction\"),\n        (\"Market Share\", \"@market_share\"),\n    ],\n)\np.add_tools(hover_tool)\n\n# Add legend manually using patches and labels (positioned below grid)\nlegend_y_base = 2.95\nlegend_x_positions = [0.5, 1.5, 2.5]\nfor i, (name, color) in enumerate(zip(sectors, colors, strict=True)):\n    lx_center = legend_x_positions[i]\n    theta = np.linspace(0, 2 * np.pi, 30)\n    lx = lx_center + 0.06 * np.cos(theta)\n    ly = legend_y_base + 0.06 * np.sin(theta)\n    p.patch(lx, ly, fill_color=color, fill_alpha=0.3, line_color=color, line_width=2)\n    legend_label = Label(\n        x=lx_center + 0.12, y=legend_y_base - 0.02, text=name, text_font_size=\"20pt\", text_color=\"#333333\"\n    )\n    p.add_layout(legend_label)\n\n# Add subtitle with feature mapping explanation (increased font size)\nsubtitle = Label(\n    x=2.0,\n    y=-0.02,\n    text=\"Face width=Revenue Growth, Face height=Profit Margin, Eye size=Satisfaction, Mouth=Market Share\",\n    text_align=\"center\",\n    text_font_size=\"22pt\",\n    text_color=\"#666666\",\n)\np.add_layout(subtitle)\n\n# Save\nexport_png(p, filename=\"plot.png\")\n"}