Skip to content

Commit 9bb36d0

Browse files
feat(makie): implement chernoff-basic
1 parent 8cac985 commit 9bb36d0

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

  • plots/chernoff-basic/implementations/julia
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# anyplot.ai
2+
# chernoff-basic: Chernoff Faces for Multivariate Data
3+
# Library: Makie.jl | Julia 1.11
4+
# Quality: pending | Created: 2026-09-02
5+
6+
using CairoMakie
7+
using Colors
8+
using Random
9+
10+
Random.seed!(42)
11+
12+
# --- Theme tokens -------------------------------------------------------------
13+
const THEME = get(ENV, "ANYPLOT_THEME", "light")
14+
const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
15+
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
16+
const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
17+
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
18+
const BRAND = colorant"#009E73" # Imprint palette position 1 -- ALWAYS first series
19+
20+
# --- Data: patient vital-sign profiles -----------------------------------------
21+
n = 12
22+
patient_ids = [string("P", lpad(i, 2, '0')) for i in 1:n]
23+
24+
resting_heart_rate = clamp.(72 .+ 12 .* randn(n), 50, 110) # bpm -> eye size
25+
systolic_bp = clamp.(122 .+ 14 .* randn(n), 95, 165) # mmHg -> face width
26+
cholesterol = clamp.(195 .+ 30 .* randn(n), 130, 280) # mg/dL -> eyebrow slant
27+
bmi = clamp.(26 .+ 4 .* randn(n), 18, 38) # kg/m^2 -> face height
28+
blood_glucose = clamp.(100 .+ 18 .* randn(n), 75, 160) # mg/dL -> mouth curvature
29+
sleep_hours = clamp.(6.8 .+ 1.1 .* randn(n), 4.5, 9.0) # hours -> mouth width
30+
respiratory_rate = clamp.(15 .+ 2.5 .* randn(n), 11, 22) # breaths/minute -> nose length
31+
32+
# Min-max normalize each variable to [0, 1] before mapping to a facial feature
33+
eye_size_n = (resting_heart_rate .- minimum(resting_heart_rate)) ./ (maximum(resting_heart_rate) - minimum(resting_heart_rate))
34+
face_width_n = (systolic_bp .- minimum(systolic_bp)) ./ (maximum(systolic_bp) - minimum(systolic_bp))
35+
eyebrow_n = (cholesterol .- minimum(cholesterol)) ./ (maximum(cholesterol) - minimum(cholesterol))
36+
face_height_n = (bmi .- minimum(bmi)) ./ (maximum(bmi) - minimum(bmi))
37+
mouth_curve_n = (blood_glucose .- minimum(blood_glucose)) ./ (maximum(blood_glucose) - minimum(blood_glucose))
38+
mouth_width_n = (sleep_hours .- minimum(sleep_hours)) ./ (maximum(sleep_hours) - minimum(sleep_hours))
39+
nose_len_n = (respiratory_rate .- minimum(respiratory_rate)) ./ (maximum(respiratory_rate) - minimum(respiratory_rate))
40+
41+
# --- Grid layout: 4 columns x 3 rows -------------------------------------------
42+
ncols, nrows = 4, 3
43+
spacing_x, spacing_y = 2.0, 2.6
44+
45+
centers = Point2f[]
46+
for i in 1:n
47+
row = div(i - 1, ncols)
48+
col = mod(i - 1, ncols)
49+
push!(centers, Point2f(col * spacing_x, -row * spacing_y))
50+
end
51+
52+
rx_max = 0.55 * 1.30
53+
ry_max = 0.68 * 1.30
54+
55+
# --- Figure -------------------------------------------------------------------
56+
fig = Figure(
57+
resolution = (1200, 1200),
58+
fontsize = 14,
59+
backgroundcolor = PAGE_BG,
60+
)
61+
62+
ax = Axis(
63+
fig[1, 1];
64+
title = "chernoff-basic · julia · makie · anyplot.ai",
65+
titlesize = 20,
66+
titlecolor = INK,
67+
backgroundcolor = PAGE_BG,
68+
aspect = DataAspect(),
69+
)
70+
hidedecorations!(ax)
71+
hidespines!(ax)
72+
73+
xlims!(ax, -rx_max - 0.3, (ncols - 1) * spacing_x + rx_max + 0.3)
74+
ylims!(ax, -(nrows - 1) * spacing_y - ry_max - 0.55, ry_max + 0.3)
75+
76+
# --- Draw one Chernoff face per patient ----------------------------------------
77+
for i in 1:n
78+
cx, cy = centers[i]
79+
80+
rx = 0.55 * (0.75 + 0.55 * face_width_n[i])
81+
ry = 0.68 * (0.75 + 0.55 * face_height_n[i])
82+
83+
face_theta = range(0, 2π; length=80)
84+
face_pts = [Point2f(cx + rx * cos(t), cy + ry * sin(t)) for t in face_theta]
85+
poly!(ax, face_pts; color=ELEVATED_BG, strokecolor=BRAND, strokewidth=3)
86+
87+
eye_r = 0.05 + 0.09 * eye_size_n[i]
88+
eye_dx = rx * 0.42
89+
eye_y = cy + ry * 0.15
90+
eye_theta = range(0, 2π; length=40)
91+
92+
for side in (-1, 1)
93+
ex = cx + side * eye_dx
94+
eye_pts = [Point2f(ex + eye_r * cos(t), eye_y + eye_r * sin(t)) for t in eye_theta]
95+
poly!(ax, eye_pts; color=PAGE_BG, strokecolor=INK, strokewidth=2)
96+
pupil_pts = [Point2f(ex + 0.4 * eye_r * cos(t), eye_y + 0.4 * eye_r * sin(t)) for t in eye_theta]
97+
poly!(ax, pupil_pts; color=INK, strokewidth=0)
98+
end
99+
100+
brow_half_len = rx * 0.32
101+
brow_y = eye_y + eye_r * 1.9
102+
brow_slope = (0.5 - eyebrow_n[i]) * 0.32 * ry
103+
104+
for (side, mirror) in ((-1, 1), (1, -1))
105+
bx = cx + side * eye_dx
106+
dy = mirror * brow_slope
107+
lines!(ax, [Point2f(bx - brow_half_len, brow_y - dy), Point2f(bx + brow_half_len, brow_y + dy)];
108+
color=INK_SOFT, linewidth=4)
109+
end
110+
111+
nose_len = ry * (0.22 + 0.30 * nose_len_n[i])
112+
nose_top = cy + ry * 0.02
113+
lines!(ax, [Point2f(cx, nose_top), Point2f(cx, nose_top - nose_len)]; color=INK_SOFT, linewidth=2.5)
114+
115+
mouth_width = rx * (0.55 + 0.55 * mouth_width_n[i])
116+
mouth_base_y = cy - ry * 0.42
117+
mouth_a = (0.5 - mouth_curve_n[i]) * 0.9 * ry / max((mouth_width / 2)^2, 1e-6)
118+
mouth_xs = range(-mouth_width / 2, mouth_width / 2; length=30)
119+
mouth_pts = [Point2f(cx + xv, mouth_base_y + mouth_a * xv^2) for xv in mouth_xs]
120+
lines!(ax, mouth_pts; color=INK, linewidth=3.5)
121+
122+
text!(ax, cx, cy - ry - 0.16; text=patient_ids[i], color=INK_SOFT, fontsize=13,
123+
align=(:center, :top))
124+
end
125+
126+
# --- Save -----------------------------------------------------------------------
127+
save("plot-$(THEME).png", fig; px_per_unit = 2)

0 commit comments

Comments
 (0)