{"spec_id":"chernoff-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# chernoff-basic: Chernoff Faces for Multivariate Data\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 96/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -------------------------------------------------------------\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG      = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG  = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK          = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT     = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst BRAND        = colorant\"#009E73\"  # Imprint palette position 1 -- ALWAYS first series\nconst ANYPLOT_AMBER = colorant\"#DDCC77\" # warning / caution -- flags the outlier patient\n\n# --- Chernoff face recipe -------------------------------------------------------\n# A custom Makie recipe: `chernoffface!` is a reusable, self-contained glyph\n# (face outline + eyes/pupils + eyebrows + nose + mouth + label) whose shape is\n# entirely driven by declarative attributes. This leans on Makie's recipe\n# system (`@recipe`, attribute-linked sub-plots) rather than a generic\n# poly!/lines!/text! loop.\n@recipe(ChernoffFace, cx, cy) do scene\n    Attributes(\n        face_width    = 0.5,\n        face_height   = 0.5,\n        eye_size      = 0.5,\n        eye_spacing   = 0.5,\n        eyebrow_slant = 0.5,\n        nose_length   = 0.5,\n        mouth_curve   = 0.5,\n        mouth_width   = 0.5,\n        rx_base       = 0.55,\n        ry_base       = 0.68,\n        facecolor     = :white,\n        outlinecolor  = :black,\n        outlinewidth  = 3.0,\n        ink           = :black,\n        ink_soft      = :gray,\n        label         = \"\",\n        labelcolor    = :gray,\n        labelsize     = 13.0,\n    )\nend\n\nfunction Makie.plot!(cf::ChernoffFace)\n    cx = cf[1][]\n    cy = cf[2][]\n\n    rx = cf.rx_base[] * (0.75 + 0.55 * cf.face_width[])\n    ry = cf.ry_base[] * (0.75 + 0.55 * cf.face_height[])\n\n    θ_face = range(0, 2π; length = 80)\n    face_pts = [Point2f(cx + rx * cos(t), cy + ry * sin(t)) for t in θ_face]\n    poly!(cf, face_pts; color = cf.facecolor, strokecolor = cf.outlinecolor,\n          strokewidth = cf.outlinewidth)\n\n    eye_r  = 0.05 + 0.09 * cf.eye_size[]\n    eye_dx = rx * (0.30 + 0.24 * cf.eye_spacing[])\n    eye_y  = cy + ry * 0.15\n    θ_eye  = range(0, 2π; length = 40)\n\n    for side in (-1, 1)\n        ex = cx + side * eye_dx\n        eye_pts = [Point2f(ex + eye_r * cos(t), eye_y + eye_r * sin(t)) for t in θ_eye]\n        poly!(cf, eye_pts; color = cf.facecolor, strokecolor = cf.ink, strokewidth = 2)\n        pupil_pts = [Point2f(ex + 0.4 * eye_r * cos(t), eye_y + 0.4 * eye_r * sin(t)) for t in θ_eye]\n        poly!(cf, pupil_pts; color = cf.ink, strokewidth = 0)\n    end\n\n    brow_half_len = rx * 0.32\n    brow_y = eye_y + eye_r * 1.9\n    brow_slope = (0.5 - cf.eyebrow_slant[]) * 0.32 * ry\n\n    for (side, mirror) in ((-1, 1), (1, -1))\n        bx = cx + side * eye_dx\n        dy = mirror * brow_slope\n        lines!(cf, [Point2f(bx - brow_half_len, brow_y - dy), Point2f(bx + brow_half_len, brow_y + dy)];\n               color = cf.ink_soft, linewidth = 4)\n    end\n\n    nose_len = ry * (0.22 + 0.30 * cf.nose_length[])\n    nose_top = cy + ry * 0.02\n    lines!(cf, [Point2f(cx, nose_top), Point2f(cx, nose_top - nose_len)];\n           color = cf.ink_soft, linewidth = 2.5)\n\n    mouth_width = rx * (0.55 + 0.55 * cf.mouth_width[])\n    mouth_base_y = cy - ry * 0.42\n    mouth_a = (0.5 - cf.mouth_curve[]) * 0.9 * ry / max((mouth_width / 2)^2, 1e-6)\n    mouth_xs = range(-mouth_width / 2, mouth_width / 2; length = 30)\n    mouth_pts = [Point2f(cx + xv, mouth_base_y + mouth_a * xv^2) for xv in mouth_xs]\n    lines!(cf, mouth_pts; color = cf.ink, linewidth = 3.5)\n\n    text!(cf, cx, cy - ry - 0.16; text = cf.label, color = cf.labelcolor,\n          fontsize = cf.labelsize, align = (:center, :top))\n\n    cf\nend\n\n# --- Data: patient vital-sign profiles -----------------------------------------\nn = 12\npatient_ids = [string(\"P\", lpad(i, 2, '0')) for i in 1:n]\n\nresting_heart_rate = clamp.(72 .+ 12 .* randn(n), 50, 110)     # bpm            -> eye size\nsystolic_bp        = clamp.(122 .+ 14 .* randn(n), 95, 165)    # mmHg           -> face width\ncholesterol        = clamp.(195 .+ 30 .* randn(n), 130, 280)   # mg/dL          -> eyebrow slant\nbmi                = clamp.(26 .+ 4 .* randn(n), 18, 38)       # kg/m^2         -> face height\nblood_glucose      = clamp.(100 .+ 18 .* randn(n), 75, 160)    # mg/dL          -> mouth curvature\nsleep_hours        = clamp.(6.8 .+ 1.1 .* randn(n), 4.5, 9.0)  # hours          -> mouth width\nrespiratory_rate   = clamp.(15 .+ 2.5 .* randn(n), 11, 22)     # breaths/minute -> nose length\nbody_temperature   = clamp.(98.2 .+ 0.6 .* randn(n), 96.8, 100.4) # deg F       -> eye spacing\n\n# Min-max normalize each variable to [0, 1] before mapping to a facial feature\neye_size_n    = (resting_heart_rate .- minimum(resting_heart_rate)) ./ (maximum(resting_heart_rate) - minimum(resting_heart_rate))\nface_width_n  = (systolic_bp .- minimum(systolic_bp)) ./ (maximum(systolic_bp) - minimum(systolic_bp))\neyebrow_n     = (cholesterol .- minimum(cholesterol)) ./ (maximum(cholesterol) - minimum(cholesterol))\nface_height_n = (bmi .- minimum(bmi)) ./ (maximum(bmi) - minimum(bmi))\nmouth_curve_n = (blood_glucose .- minimum(blood_glucose)) ./ (maximum(blood_glucose) - minimum(blood_glucose))\nmouth_width_n = (sleep_hours .- minimum(sleep_hours)) ./ (maximum(sleep_hours) - minimum(sleep_hours))\nnose_len_n    = (respiratory_rate .- minimum(respiratory_rate)) ./ (maximum(respiratory_rate) - minimum(respiratory_rate))\neye_spacing_n = (body_temperature .- minimum(body_temperature)) ./ (maximum(body_temperature) - minimum(body_temperature))\n\n# Flag the most extreme combined profile -- largest total deviation from the\n# cohort midpoint (0.5) across all 8 normalized variables -- as a visual entry\n# point into the comparison.\nnormalized = hcat(eye_size_n, face_width_n, eyebrow_n, face_height_n,\n                   mouth_curve_n, mouth_width_n, nose_len_n, eye_spacing_n)\nextremity = vec(sum((normalized .- 0.5) .^ 2; dims = 2))\noutlier_idx = argmax(extremity)\n\n# --- Grid layout: 4 columns x 3 rows -------------------------------------------\nncols, nrows = 4, 3\nspacing_x, spacing_y = 2.0, 2.6\n\ncenters = Point2f[]\nfor i in 1:n\n    row = div(i - 1, ncols)\n    col = mod(i - 1, ncols)\n    push!(centers, Point2f(col * spacing_x, -row * spacing_y))\nend\n\nrx_max = 0.55 * 1.30\nry_max = 0.68 * 1.30\n\n# --- Figure -------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = \"chernoff-basic · julia · makie · anyplot.ai\",\n    titlesize         = 26,\n    titlecolor        = INK,\n    backgroundcolor   = PAGE_BG,\n    aspect            = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\n\nxlims!(ax, -rx_max - 0.3, (ncols - 1) * spacing_x + rx_max + 0.3)\nylims!(ax, -(nrows - 1) * spacing_y - ry_max - 0.55, ry_max + 0.3)\n\n# --- Draw one Chernoff face per patient via the custom recipe -------------------\nfor i in 1:n\n    is_outlier = i == outlier_idx\n    chernoffface!(ax, centers[i][1], centers[i][2];\n        face_width    = face_width_n[i],\n        face_height   = face_height_n[i],\n        eye_size      = eye_size_n[i],\n        eye_spacing   = eye_spacing_n[i],\n        eyebrow_slant = eyebrow_n[i],\n        nose_length   = nose_len_n[i],\n        mouth_curve   = mouth_curve_n[i],\n        mouth_width   = mouth_width_n[i],\n        facecolor     = ELEVATED_BG,\n        outlinecolor  = is_outlier ? ANYPLOT_AMBER : BRAND,\n        outlinewidth  = is_outlier ? 5.0 : 3.0,\n        ink           = INK,\n        ink_soft      = INK_SOFT,\n        label         = patient_ids[i],\n        labelcolor    = INK_SOFT,\n        labelsize     = 13,\n    )\nend\n\n# --- Save -----------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}