Skip to content

Commit c508b2a

Browse files
fix(makie): address review feedback for chernoff-basic
Attempt 1/4 - fixes based on AI review
1 parent 5dceca5 commit c508b2a

1 file changed

Lines changed: 115 additions & 48 deletions

File tree

  • plots/chernoff-basic/implementations/julia

plots/chernoff-basic/implementations/julia/makie.jl

Lines changed: 115 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,90 @@ const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
1616
const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
1717
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
1818
const BRAND = colorant"#009E73" # Imprint palette position 1 -- ALWAYS first series
19+
const ANYPLOT_AMBER = colorant"#DDCC77" # warning / caution -- flags the outlier patient
20+
21+
# --- Chernoff face recipe -------------------------------------------------------
22+
# A custom Makie recipe: `chernoffface!` is a reusable, self-contained glyph
23+
# (face outline + eyes/pupils + eyebrows + nose + mouth + label) whose shape is
24+
# entirely driven by declarative attributes. This leans on Makie's recipe
25+
# system (`@recipe`, attribute-linked sub-plots) rather than a generic
26+
# poly!/lines!/text! loop.
27+
@recipe(ChernoffFace, cx, cy) do scene
28+
Attributes(
29+
face_width = 0.5,
30+
face_height = 0.5,
31+
eye_size = 0.5,
32+
eye_spacing = 0.5,
33+
eyebrow_slant = 0.5,
34+
nose_length = 0.5,
35+
mouth_curve = 0.5,
36+
mouth_width = 0.5,
37+
rx_base = 0.55,
38+
ry_base = 0.68,
39+
facecolor = :white,
40+
outlinecolor = :black,
41+
outlinewidth = 3.0,
42+
ink = :black,
43+
ink_soft = :gray,
44+
label = "",
45+
labelcolor = :gray,
46+
labelsize = 13.0,
47+
)
48+
end
49+
50+
function Makie.plot!(cf::ChernoffFace)
51+
cx = cf[1][]
52+
cy = cf[2][]
53+
54+
rx = cf.rx_base[] * (0.75 + 0.55 * cf.face_width[])
55+
ry = cf.ry_base[] * (0.75 + 0.55 * cf.face_height[])
56+
57+
θ_face = range(0, 2π; length = 80)
58+
face_pts = [Point2f(cx + rx * cos(t), cy + ry * sin(t)) for t in θ_face]
59+
poly!(cf, face_pts; color = cf.facecolor, strokecolor = cf.outlinecolor,
60+
strokewidth = cf.outlinewidth)
61+
62+
eye_r = 0.05 + 0.09 * cf.eye_size[]
63+
eye_dx = rx * (0.30 + 0.24 * cf.eye_spacing[])
64+
eye_y = cy + ry * 0.15
65+
θ_eye = range(0, 2π; length = 40)
66+
67+
for side in (-1, 1)
68+
ex = cx + side * eye_dx
69+
eye_pts = [Point2f(ex + eye_r * cos(t), eye_y + eye_r * sin(t)) for t in θ_eye]
70+
poly!(cf, eye_pts; color = cf.facecolor, strokecolor = cf.ink, strokewidth = 2)
71+
pupil_pts = [Point2f(ex + 0.4 * eye_r * cos(t), eye_y + 0.4 * eye_r * sin(t)) for t in θ_eye]
72+
poly!(cf, pupil_pts; color = cf.ink, strokewidth = 0)
73+
end
74+
75+
brow_half_len = rx * 0.32
76+
brow_y = eye_y + eye_r * 1.9
77+
brow_slope = (0.5 - cf.eyebrow_slant[]) * 0.32 * ry
78+
79+
for (side, mirror) in ((-1, 1), (1, -1))
80+
bx = cx + side * eye_dx
81+
dy = mirror * brow_slope
82+
lines!(cf, [Point2f(bx - brow_half_len, brow_y - dy), Point2f(bx + brow_half_len, brow_y + dy)];
83+
color = cf.ink_soft, linewidth = 4)
84+
end
85+
86+
nose_len = ry * (0.22 + 0.30 * cf.nose_length[])
87+
nose_top = cy + ry * 0.02
88+
lines!(cf, [Point2f(cx, nose_top), Point2f(cx, nose_top - nose_len)];
89+
color = cf.ink_soft, linewidth = 2.5)
90+
91+
mouth_width = rx * (0.55 + 0.55 * cf.mouth_width[])
92+
mouth_base_y = cy - ry * 0.42
93+
mouth_a = (0.5 - cf.mouth_curve[]) * 0.9 * ry / max((mouth_width / 2)^2, 1e-6)
94+
mouth_xs = range(-mouth_width / 2, mouth_width / 2; length = 30)
95+
mouth_pts = [Point2f(cx + xv, mouth_base_y + mouth_a * xv^2) for xv in mouth_xs]
96+
lines!(cf, mouth_pts; color = cf.ink, linewidth = 3.5)
97+
98+
text!(cf, cx, cy - ry - 0.16; text = cf.label, color = cf.labelcolor,
99+
fontsize = cf.labelsize, align = (:center, :top))
100+
101+
cf
102+
end
19103

20104
# --- Data: patient vital-sign profiles -----------------------------------------
21105
n = 12
@@ -28,6 +112,7 @@ bmi = clamp.(26 .+ 4 .* randn(n), 18, 38) # kg/m^2
28112
blood_glucose = clamp.(100 .+ 18 .* randn(n), 75, 160) # mg/dL -> mouth curvature
29113
sleep_hours = clamp.(6.8 .+ 1.1 .* randn(n), 4.5, 9.0) # hours -> mouth width
30114
respiratory_rate = clamp.(15 .+ 2.5 .* randn(n), 11, 22) # breaths/minute -> nose length
115+
body_temperature = clamp.(98.2 .+ 0.6 .* randn(n), 96.8, 100.4) # deg F -> eye spacing
31116

32117
# Min-max normalize each variable to [0, 1] before mapping to a facial feature
33118
eye_size_n = (resting_heart_rate .- minimum(resting_heart_rate)) ./ (maximum(resting_heart_rate) - minimum(resting_heart_rate))
@@ -37,6 +122,15 @@ face_height_n = (bmi .- minimum(bmi)) ./ (maximum(bmi) - minimum(bmi))
37122
mouth_curve_n = (blood_glucose .- minimum(blood_glucose)) ./ (maximum(blood_glucose) - minimum(blood_glucose))
38123
mouth_width_n = (sleep_hours .- minimum(sleep_hours)) ./ (maximum(sleep_hours) - minimum(sleep_hours))
39124
nose_len_n = (respiratory_rate .- minimum(respiratory_rate)) ./ (maximum(respiratory_rate) - minimum(respiratory_rate))
125+
eye_spacing_n = (body_temperature .- minimum(body_temperature)) ./ (maximum(body_temperature) - minimum(body_temperature))
126+
127+
# Flag the most extreme combined profile -- largest total deviation from the
128+
# cohort midpoint (0.5) across all 8 normalized variables -- as a visual entry
129+
# point into the comparison.
130+
normalized = hcat(eye_size_n, face_width_n, eyebrow_n, face_height_n,
131+
mouth_curve_n, mouth_width_n, nose_len_n, eye_spacing_n)
132+
extremity = vec(sum((normalized .- 0.5) .^ 2; dims = 2))
133+
outlier_idx = argmax(extremity)
40134

41135
# --- Grid layout: 4 columns x 3 rows -------------------------------------------
42136
ncols, nrows = 4, 3
@@ -62,7 +156,7 @@ fig = Figure(
62156
ax = Axis(
63157
fig[1, 1];
64158
title = "chernoff-basic · julia · makie · anyplot.ai",
65-
titlesize = 20,
159+
titlesize = 26,
66160
titlecolor = INK,
67161
backgroundcolor = PAGE_BG,
68162
aspect = DataAspect(),
@@ -73,54 +167,27 @@ hidespines!(ax)
73167
xlims!(ax, -rx_max - 0.3, (ncols - 1) * spacing_x + rx_max + 0.3)
74168
ylims!(ax, -(nrows - 1) * spacing_y - ry_max - 0.55, ry_max + 0.3)
75169

76-
# --- Draw one Chernoff face per patient ----------------------------------------
170+
# --- Draw one Chernoff face per patient via the custom recipe -------------------
77171
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))
172+
is_outlier = i == outlier_idx
173+
chernoffface!(ax, centers[i][1], centers[i][2];
174+
face_width = face_width_n[i],
175+
face_height = face_height_n[i],
176+
eye_size = eye_size_n[i],
177+
eye_spacing = eye_spacing_n[i],
178+
eyebrow_slant = eyebrow_n[i],
179+
nose_length = nose_len_n[i],
180+
mouth_curve = mouth_curve_n[i],
181+
mouth_width = mouth_width_n[i],
182+
facecolor = ELEVATED_BG,
183+
outlinecolor = is_outlier ? ANYPLOT_AMBER : BRAND,
184+
outlinewidth = is_outlier ? 5.0 : 3.0,
185+
ink = INK,
186+
ink_soft = INK_SOFT,
187+
label = patient_ids[i],
188+
labelcolor = INK_SOFT,
189+
labelsize = 13,
190+
)
124191
end
125192

126193
# --- Save -----------------------------------------------------------------------

0 commit comments

Comments
 (0)