1- """ pyplots .ai
1+ """ anyplot .ai
22chernoff-basic: Chernoff Faces for Multivariate Data
3- Library: plotnine 0.15.2 | Python 3.13.11
4- Quality: 91/100 | Created: 2025-12-31
3+ Library: plotnine 0.15.4 | Python 3.13.13
4+ Quality: 91/100 | Updated: 2026-05-15
55"""
66
7+ import os
8+
79import numpy as np
810import pandas as pd
911from plotnine import (
1012 aes ,
1113 coord_fixed ,
14+ element_rect ,
1215 element_text ,
1316 facet_wrap ,
1417 geom_path ,
2225)
2326
2427
28+ THEME = os .getenv ("ANYPLOT_THEME" , "light" )
29+ PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
30+ ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
31+ INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
32+ INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
33+
2534np .random .seed (42 )
2635
27- # Data: Car performance metrics (6 cars with 4 metrics each)
28- # Metrics: Engine Power, Fuel Efficiency, Safety Rating, Comfort Score
36+ # Data: Car performance metrics with more extreme outliers
2937car_data = {
3038 "observation_id" : ["Compact A" , "Compact B" , "Sedan A" , "Sedan B" , "SUV A" , "SUV B" ],
3139 "category" : ["Compact" , "Compact" , "Sedan" , "Sedan" , "SUV" , "SUV" ],
32- "engine_power" : [120 , 140 , 180 , 200 , 250 , 280 ], # HP
33- "fuel_efficiency" : [35 , 32 , 28 , 25 , 22 , 18 ], # MPG
34- "safety_rating" : [4.2 , 4.5 , 4.8 , 4.6 , 4.4 , 4.7 ], # 1-5 scale
35- "comfort_score" : [3.5 , 3.8 , 4.2 , 4.5 , 4.0 , 4.3 ], # 1-5 scale
40+ "engine_power" : [100 , 150 , 180 , 220 , 280 , 320 ],
41+ "fuel_efficiency" : [38 , 30 , 26 , 22 , 18 , 15 ],
42+ "safety_rating" : [3.9 , 4.5 , 4.8 , 4.2 , 4.6 , 4.9 ],
43+ "comfort_score" : [3.0 , 3.6 , 4.5 , 4.1 , 4.0 , 4.4 ],
3644}
3745sample_df = pd .DataFrame (car_data )
3846
39-
4047# Normalize data to 0-1 range for facial feature mapping
41- def normalize_column (col ):
42- return (col - col .min ()) / (col .max () - col .min () + 1e-10 )
43-
44-
45- normalized = sample_df [["engine_power" , "fuel_efficiency" , "safety_rating" , "comfort_score" ]].apply (normalize_column )
46-
47- # Feature mappings:
48- # engine_power -> face width (0.6 to 1.0)
49- # fuel_efficiency -> face height (0.8 to 1.2)
50- # safety_rating -> eye size (0.08 to 0.18)
51- # comfort_score -> mouth curvature (-0.3 to 0.3)
48+ normalized = sample_df [["engine_power" , "fuel_efficiency" , "safety_rating" , "comfort_score" ]].apply (
49+ lambda col : (col - col .min ()) / (col .max () - col .min () + 1e-10 )
50+ )
5251
5352face_widths = 0.6 + normalized ["engine_power" ] * 0.4
5453face_heights = 0.8 + normalized ["fuel_efficiency" ] * 0.4
5554eye_sizes = 0.08 + normalized ["safety_rating" ] * 0.1
5655mouth_curvatures = - 0.3 + normalized ["comfort_score" ] * 0.6
5756
58-
59- # Generate face polygon vertices (ellipse approximation)
60- def make_ellipse (cx , cy , rx , ry , n_points = 50 ):
61- theta = np .linspace (0 , 2 * np .pi , n_points )
62- x = cx + rx * np .cos (theta )
63- y = cy + ry * np .sin (theta )
64- return x , y
65-
66-
67- # Generate eye vertices
68- def make_eye (cx , cy , r , n_points = 20 ):
69- theta = np .linspace (0 , 2 * np .pi , n_points )
70- x = cx + r * np .cos (theta )
71- y = cy + r * np .sin (theta )
72- return x , y
73-
74-
75- # Generate mouth curve
76- def make_mouth (cx , cy , width , curvature , n_points = 20 ):
77- x = np .linspace (cx - width / 2 , cx + width / 2 , n_points )
78- y = cy + curvature * (((x - cx ) / (width / 2 )) ** 2 - 1 )
79- return x , y
80-
81-
82- # Generate eyebrow
83- def make_eyebrow (cx , cy , width , slant , n_points = 10 ):
84- x = np .linspace (cx - width / 2 , cx + width / 2 , n_points )
85- y = cy + slant * (x - cx ) / (width / 2 )
86- return x , y
87-
88-
8957# Build data for all faces
9058all_data = []
9159
@@ -99,146 +67,149 @@ def make_eyebrow(cx, cy, width, slant, n_points=10):
9967 mc = mouth_curvatures .iloc [idx ]
10068
10169 # Face outline
102- fx , fy = make_ellipse (0 , 0 , fw , fh )
70+ theta = np .linspace (0 , 2 * np .pi , 50 )
71+ fx = fw * np .cos (theta )
72+ fy = fh * np .sin (theta )
10373 for i in range (len (fx )):
104- all_data .append (
105- {"observation_id" : obs_id , "category" : category , "part" : "face" , "x" : fx [i ], "y" : fy [i ], "order" : i }
106- )
74+ all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "face" , "x" : fx [i ], "y" : fy [i ]})
10775
10876 # Left eye
109- ex , ey = make_eye (- fw * 0.35 , fh * 0.25 , es )
77+ theta = np .linspace (0 , 2 * np .pi , 20 )
78+ ex = - fw * 0.35 + es * np .cos (theta )
79+ ey = fh * 0.25 + es * np .sin (theta )
11080 for i in range (len (ex )):
111- all_data .append (
112- {"observation_id" : obs_id , "category" : category , "part" : "left_eye" , "x" : ex [i ], "y" : ey [i ], "order" : i }
113- )
81+ all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "left_eye" , "x" : ex [i ], "y" : ey [i ]})
11482
11583 # Right eye
116- ex , ey = make_eye (fw * 0.35 , fh * 0.25 , es )
84+ ex = fw * 0.35 + es * np .cos (theta )
85+ ey = fh * 0.25 + es * np .sin (theta )
11786 for i in range (len (ex )):
118- all_data .append (
119- {"observation_id" : obs_id , "category" : category , "part" : "right_eye" , "x" : ex [i ], "y" : ey [i ], "order" : i }
120- )
87+ all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "right_eye" , "x" : ex [i ], "y" : ey [i ]})
12188
122- # Left pupil (point )
89+ # Left pupil (larger for visibility )
12390 all_data .append (
124- {
125- "observation_id" : obs_id ,
126- "category" : category ,
127- "part" : "left_pupil" ,
128- "x" : - fw * 0.35 ,
129- "y" : fh * 0.25 ,
130- "order" : 0 ,
131- }
91+ {"observation_id" : obs_id , "category" : category , "part" : "left_pupil" , "x" : - fw * 0.35 , "y" : fh * 0.25 }
13292 )
13393
134- # Right pupil (point )
94+ # Right pupil (larger for visibility )
13595 all_data .append (
136- {
137- "observation_id" : obs_id ,
138- "category" : category ,
139- "part" : "right_pupil" ,
140- "x" : fw * 0.35 ,
141- "y" : fh * 0.25 ,
142- "order" : 0 ,
143- }
96+ {"observation_id" : obs_id , "category" : category , "part" : "right_pupil" , "x" : fw * 0.35 , "y" : fh * 0.25 }
14497 )
14598
14699 # Mouth
147- mx , my = make_mouth (0 , - fh * 0.35 , fw * 0.5 , mc )
148- for i in range (len (mx )):
100+ x_mouth = np .linspace (- fw * 0.25 , fw * 0.25 , 20 )
101+ y_mouth = - fh * 0.35 + mc * (((x_mouth ) / (fw * 0.25 )) ** 2 - 1 )
102+ for i in range (len (x_mouth )):
149103 all_data .append (
150- {"observation_id" : obs_id , "category" : category , "part" : "mouth" , "x" : mx [i ], "y" : my [i ], "order" : i }
104+ {"observation_id" : obs_id , "category" : category , "part" : "mouth" , "x" : x_mouth [i ], "y" : y_mouth [i ]}
151105 )
152106
153- # Nose (simple vertical line)
154- all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "nose" , "x" : 0 , "y" : fh * 0.1 , "order" : 0 })
155- all_data .append (
156- {"observation_id" : obs_id , "category" : category , "part" : "nose" , "x" : 0 , "y" : - fh * 0.1 , "order" : 1 }
157- )
107+ # Nose
108+ all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "nose" , "x" : 0 , "y" : fh * 0.1 })
109+ all_data .append ({"observation_id" : obs_id , "category" : category , "part" : "nose" , "x" : 0 , "y" : - fh * 0.1 })
158110
159111 # Left eyebrow
160- bx , by = make_eyebrow (- fw * 0.35 , fh * 0.45 , es * 2.5 , 0.05 )
161- for i in range (len (bx )):
112+ x_brow = np .linspace (- fw * 0.35 - es , - fw * 0.35 + es , 10 )
113+ y_brow = fh * 0.45 + 0.05 * (x_brow + fw * 0.35 ) / es
114+ for i in range (len (x_brow )):
162115 all_data .append (
163- {"observation_id" : obs_id , "category" : category , "part" : "left_eyebrow" , "x" : bx [i ], "y" : by [i ], "order" : i }
116+ {"observation_id" : obs_id , "category" : category , "part" : "left_eyebrow" , "x" : x_brow [i ], "y" : y_brow [i ]}
164117 )
165118
166119 # Right eyebrow
167- bx , by = make_eyebrow (fw * 0.35 , fh * 0.45 , es * 2.5 , - 0.05 )
168- for i in range (len (bx )):
120+ x_brow = np .linspace (fw * 0.35 - es , fw * 0.35 + es , 10 )
121+ y_brow = fh * 0.45 - 0.05 * (x_brow - fw * 0.35 ) / es
122+ for i in range (len (x_brow )):
169123 all_data .append (
170- {
171- "observation_id" : obs_id ,
172- "category" : category ,
173- "part" : "right_eyebrow" ,
174- "x" : bx [i ],
175- "y" : by [i ],
176- "order" : i ,
177- }
124+ {"observation_id" : obs_id , "category" : category , "part" : "right_eyebrow" , "x" : x_brow [i ], "y" : y_brow [i ]}
178125 )
179126
180127plot_df = pd .DataFrame (all_data )
181128
182- # Separate dataframes for different geoms
183- face_df = plot_df [plot_df ["part" ] == "face" ].copy ()
184- left_eye_df = plot_df [plot_df ["part" ] == "left_eye" ].copy ()
185- right_eye_df = plot_df [plot_df ["part" ] == "right_eye" ].copy ()
186- left_pupil_df = plot_df [plot_df ["part" ] == "left_pupil" ].copy ()
187- right_pupil_df = plot_df [plot_df ["part" ] == "right_pupil" ].copy ()
188- mouth_df = plot_df [plot_df ["part" ] == "mouth" ].copy ()
189- nose_df = plot_df [plot_df ["part" ] == "nose" ].copy ()
190- left_eyebrow_df = plot_df [plot_df ["part" ] == "left_eyebrow" ].copy ()
191- right_eyebrow_df = plot_df [plot_df ["part" ] == "right_eyebrow" ].copy ()
192-
193- # Category colors
194- category_colors = {"Compact" : "#306998" , "Sedan" : "#FFD43B" , "SUV" : "#4B8BBE" }
195-
196- # Create the plot using native plotnine geoms
129+ # Okabe-Ito palette
130+ category_colors = {"Compact" : "#009E73" , "Sedan" : "#D55E00" , "SUV" : "#0072B2" }
131+
132+ anyplot_theme = theme (
133+ figure_size = (16 , 9 ),
134+ plot_background = element_rect (fill = PAGE_BG , color = PAGE_BG ),
135+ panel_background = element_rect (fill = PAGE_BG , color = PAGE_BG ),
136+ plot_title = element_text (size = 24 , weight = "bold" , color = INK , ha = "center" ),
137+ plot_subtitle = element_text (size = 16 , color = INK_SOFT , ha = "center" ),
138+ legend_position = "bottom" ,
139+ legend_background = element_rect (fill = ELEVATED_BG , color = INK_SOFT ),
140+ legend_title = element_text (size = 16 , color = INK ),
141+ legend_text = element_text (size = 14 , color = INK_SOFT ),
142+ strip_text = element_text (size = 14 , color = INK , weight = "bold" ),
143+ )
144+
197145plot = (
198146 ggplot ()
199147 # Face outline (filled polygon)
200148 + geom_polygon (
201- data = face_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" , fill = "category" ), color = "#333333" , size = 1.5
149+ data = plot_df [plot_df ["part" ] == "face" ],
150+ mapping = aes (x = "x" , y = "y" , group = "observation_id" , fill = "category" ),
151+ color = INK_SOFT ,
152+ size = 1.5 ,
202153 )
203- # Eyes (white filled)
154+ # Eyes (white/elevated filled)
204155 + geom_polygon (
205- data = left_eye_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), fill = "white" , color = "#333333" , size = 0.8
156+ data = plot_df [plot_df ["part" ] == "left_eye" ],
157+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
158+ fill = ELEVATED_BG ,
159+ color = INK_SOFT ,
160+ size = 0.8 ,
206161 )
207162 + geom_polygon (
208- data = right_eye_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), fill = "white" , color = "#333333" , size = 0.8
163+ data = plot_df [plot_df ["part" ] == "right_eye" ],
164+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
165+ fill = ELEVATED_BG ,
166+ color = INK_SOFT ,
167+ size = 0.8 ,
209168 )
210- # Pupils
211- + geom_point (data = left_pupil_df , mapping = aes (x = "x" , y = "y" ), color = "#333333" , size = 3 )
212- + geom_point (data = right_pupil_df , mapping = aes (x = "x" , y = "y" ), color = "#333333" , size = 3 )
169+ # Pupils (larger for visibility)
170+ + geom_point (data = plot_df [ plot_df [ "part" ] == "left_pupil" ] , mapping = aes (x = "x" , y = "y" ), color = INK_SOFT , size = 5 )
171+ + geom_point (data = plot_df [ plot_df [ "part" ] == "right_pupil" ] , mapping = aes (x = "x" , y = "y" ), color = INK_SOFT , size = 5 )
213172 # Mouth
214- + geom_path (data = mouth_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), color = "#333333" , size = 1.2 )
173+ + geom_path (
174+ data = plot_df [plot_df ["part" ] == "mouth" ],
175+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
176+ color = INK_SOFT ,
177+ size = 1.2 ,
178+ )
215179 # Nose
216- + geom_path (data = nose_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), color = "#333333" , size = 1 )
180+ + geom_path (
181+ data = plot_df [plot_df ["part" ] == "nose" ],
182+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
183+ color = INK_SOFT ,
184+ size = 1 ,
185+ )
217186 # Eyebrows
218- + geom_path (data = left_eyebrow_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), color = "#333333" , size = 1.2 )
219- + geom_path (data = right_eyebrow_df , mapping = aes (x = "x" , y = "y" , group = "observation_id" ), color = "#333333" , size = 1.2 )
187+ + geom_path (
188+ data = plot_df [plot_df ["part" ] == "left_eyebrow" ],
189+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
190+ color = INK_SOFT ,
191+ size = 1.2 ,
192+ )
193+ + geom_path (
194+ data = plot_df [plot_df ["part" ] == "right_eyebrow" ],
195+ mapping = aes (x = "x" , y = "y" , group = "observation_id" ),
196+ color = INK_SOFT ,
197+ size = 1.2 ,
198+ )
220199 # Facet by observation
221200 + facet_wrap ("~observation_id" , ncol = 3 )
222- # Colors
201+ # Colors (Okabe-Ito palette)
223202 + scale_fill_manual (values = category_colors )
224203 # Labels
225204 + labs (
226- title = "chernoff-basic · plotnine · pyplots .ai" ,
205+ title = "chernoff-basic · plotnine · anyplot .ai" ,
227206 subtitle = "Car Performance: Power/Efficiency/Safety/Comfort mapped to facial features" ,
228207 fill = "Category" ,
229208 )
230209 # Theme
231210 + theme_void ()
232- + theme (
233- figure_size = (16 , 9 ),
234- plot_title = element_text (size = 24 , ha = "center" , weight = "bold" ),
235- plot_subtitle = element_text (size = 16 , ha = "center" ),
236- legend_title = element_text (size = 16 ),
237- legend_text = element_text (size = 14 ),
238- strip_text = element_text (size = 14 , weight = "bold" ),
239- legend_position = "bottom" ,
240- )
211+ + anyplot_theme
241212 + coord_fixed (ratio = 1 )
242213)
243214
244- plot .save ("plot.png" , dpi = 300 , width = 16 , height = 9 )
215+ plot .save (f "plot- { THEME } .png" , dpi = 300 )
0 commit comments