@@ -16,6 +16,90 @@ const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
1616const INK = THEME == " light" ? colorant " #1A1A17" : colorant " #F0EFE8"
1717const INK_SOFT = THEME == " light" ? colorant " #4A4A44" : colorant " #B8B7B0"
1818const 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 -----------------------------------------
21105n = 12
@@ -28,6 +112,7 @@ bmi = clamp.(26 .+ 4 .* randn(n), 18, 38) # kg/m^2
28112blood_glucose = clamp .(100 .+ 18 .* randn (n), 75 , 160 ) # mg/dL -> mouth curvature
29113sleep_hours = clamp .(6.8 .+ 1.1 .* randn (n), 4.5 , 9.0 ) # hours -> mouth width
30114respiratory_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
33118eye_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))
37122mouth_curve_n = (blood_glucose .- minimum (blood_glucose)) ./ (maximum (blood_glucose) - minimum (blood_glucose))
38123mouth_width_n = (sleep_hours .- minimum (sleep_hours)) ./ (maximum (sleep_hours) - minimum (sleep_hours))
39124nose_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 -------------------------------------------
42136ncols, nrows = 4 , 3
@@ -62,7 +156,7 @@ fig = Figure(
62156ax = 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)
73167xlims! (ax, - rx_max - 0.3 , (ncols - 1 ) * spacing_x + rx_max + 0.3 )
74168ylims! (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 -------------------
77171for 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+ )
124191end
125192
126193# --- Save -----------------------------------------------------------------------
0 commit comments