|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | chernoff-basic: Chernoff Faces for Multivariate Data |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-31 |
| 3 | +Library: plotly 6.7.0 | Python 3.13.13 |
| 4 | +Quality: 92/100 | Updated: 2026-05-15 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import numpy as np |
8 | 10 | import plotly.graph_objects as go |
9 | 11 | from sklearn.datasets import load_iris |
10 | 12 |
|
11 | 13 |
|
| 14 | +# Theme tokens |
| 15 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 17 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 18 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 19 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 20 | +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" |
| 21 | + |
| 22 | +# Okabe-Ito palette (first series is always #009E73) |
| 23 | +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2"] |
| 24 | + |
12 | 25 | # Data - use Iris dataset with 4 measurements per flower |
13 | 26 | np.random.seed(42) |
14 | 27 | iris = load_iris() |
|
18 | 31 | target_names = iris.target_names |
19 | 32 |
|
20 | 33 | # Select subset for clear visualization (5 samples per species = 15 faces) |
21 | | -# Choose samples with maximum variation within species by selecting spread across range |
22 | 34 | indices = [] |
23 | 35 | for species in range(3): |
24 | 36 | species_mask = y == species |
|
44 | 56 | # Normalize data to 0-1 range |
45 | 57 | X_norm = (X_subset - X_subset.min(axis=0)) / (X_subset.max(axis=0) - X_subset.min(axis=0)) |
46 | 58 |
|
47 | | -# Colors for each species |
48 | | -colors = ["#306998", "#FFD43B", "#2CA02C"] # Python Blue, Python Yellow, Green |
49 | | - |
50 | 59 | # Create figure |
51 | 60 | fig = go.Figure() |
52 | 61 |
|
|
66 | 75 | cx = col * spacing + spacing / 2 |
67 | 76 | cy = (n_rows - 1 - row) * spacing + spacing / 2 |
68 | 77 |
|
69 | | - color = colors[species] |
| 78 | + color = OKABE_ITO[species] |
70 | 79 |
|
71 | 80 | # Feature mapping with increased variation: |
72 | 81 | # - sepal_length (data[0]) -> face width |
|
90 | 99 | type="path", |
91 | 100 | path="M " + " L ".join([f"{x},{y}" for x, y in zip(face_x, face_y)]) + " Z", |
92 | 101 | fillcolor=color, |
93 | | - line=dict(color="#333333", width=2), |
| 102 | + line=dict(color=INK_SOFT, width=2), |
94 | 103 | opacity=0.35, |
95 | 104 | ) |
96 | 105 | ) |
|
108 | 117 | dict( |
109 | 118 | type="path", |
110 | 119 | path="M " + " L ".join([f"{x},{y}" for x, y in zip(left_eye_x, left_eye_y)]) + " Z", |
111 | | - fillcolor="white", |
112 | | - line=dict(color="#333333", width=2), |
| 120 | + fillcolor=ELEVATED_BG, |
| 121 | + line=dict(color=INK_SOFT, width=2), |
113 | 122 | ) |
114 | 123 | ) |
115 | 124 |
|
|
121 | 130 | dict( |
122 | 131 | type="path", |
123 | 132 | path="M " + " L ".join([f"{x},{y}" for x, y in zip(pupil_x, pupil_y)]) + " Z", |
124 | | - fillcolor="#333333", |
125 | | - line=dict(color="#333333", width=1), |
| 133 | + fillcolor=INK, |
| 134 | + line=dict(color=INK, width=1), |
126 | 135 | ) |
127 | 136 | ) |
128 | 137 |
|
|
133 | 142 | dict( |
134 | 143 | type="path", |
135 | 144 | path="M " + " L ".join([f"{x},{y}" for x, y in zip(right_eye_x, right_eye_y)]) + " Z", |
136 | | - fillcolor="white", |
137 | | - line=dict(color="#333333", width=2), |
| 145 | + fillcolor=ELEVATED_BG, |
| 146 | + line=dict(color=INK_SOFT, width=2), |
138 | 147 | ) |
139 | 148 | ) |
140 | 149 |
|
|
145 | 154 | dict( |
146 | 155 | type="path", |
147 | 156 | path="M " + " L ".join([f"{x},{y}" for x, y in zip(pupil_x, pupil_y)]) + " Z", |
148 | | - fillcolor="#333333", |
149 | | - line=dict(color="#333333", width=1), |
| 157 | + fillcolor=INK, |
| 158 | + line=dict(color=INK, width=1), |
150 | 159 | ) |
151 | 160 | ) |
152 | 161 |
|
|
155 | 164 | nose_w = face_w * 0.1 |
156 | 165 | nose_y_center = cy |
157 | 166 | nose_path = f"M {cx},{nose_y_center + nose_h * 0.5} L {cx - nose_w},{nose_y_center - nose_h * 0.5} L {cx + nose_w},{nose_y_center - nose_h * 0.5} Z" |
158 | | - all_shapes.append( |
159 | | - dict(type="path", path=nose_path, fillcolor="#333333", line=dict(color="#333333", width=1), opacity=0.5) |
160 | | - ) |
| 167 | + all_shapes.append(dict(type="path", path=nose_path, fillcolor=INK, line=dict(color=INK, width=1), opacity=0.5)) |
161 | 168 |
|
162 | 169 | # Mouth (curved line with much more variation) |
163 | 170 | mouth_y_base = cy - face_h * 0.35 |
|
167 | 174 | # Parabolic curve: positive = smile, negative = frown |
168 | 175 | mouth_y_vals = mouth_y_base + mouth_curve * (1 - ((mouth_x_vals - cx) / mouth_width) ** 2) * radius * 0.5 |
169 | 176 | mouth_path = "M " + " L ".join([f"{x},{y}" for x, y in zip(mouth_x_vals, mouth_y_vals)]) |
170 | | - all_shapes.append(dict(type="path", path=mouth_path, line=dict(color="#333333", width=3))) |
| 177 | + all_shapes.append(dict(type="path", path=mouth_path, line=dict(color=INK, width=3))) |
171 | 178 |
|
172 | 179 | # Eyebrows (angled based on face width feature) |
173 | 180 | brow_offset_y = eye_offset_y + eye_r + face_h * 0.1 |
|
182 | 189 | y0=cy + brow_offset_y - brow_angle * radius, |
183 | 190 | x1=cx - eye_offset_x + brow_width, |
184 | 191 | y1=cy + brow_offset_y + brow_angle * radius, |
185 | | - line=dict(color="#333333", width=3), |
| 192 | + line=dict(color=INK, width=3), |
186 | 193 | ) |
187 | 194 | ) |
188 | 195 |
|
|
194 | 201 | y0=cy + brow_offset_y + brow_angle * radius, |
195 | 202 | x1=cx + eye_offset_x + brow_width, |
196 | 203 | y1=cy + brow_offset_y - brow_angle * radius, |
197 | | - line=dict(color="#333333", width=3), |
| 204 | + line=dict(color=INK, width=3), |
198 | 205 | ) |
199 | 206 | ) |
200 | 207 |
|
201 | 208 | # Add invisible scatter for axis setup |
202 | 209 | fig.add_trace(go.Scatter(x=[0], y=[0], mode="markers", marker=dict(opacity=0), showlegend=False)) |
203 | 210 |
|
204 | 211 | # Add legend entries for species |
205 | | -for i, (name, color) in enumerate(zip(target_names, colors)): |
| 212 | +for i, (name, color) in enumerate(zip(target_names, OKABE_ITO)): |
206 | 213 | fig.add_trace( |
207 | 214 | go.Scatter( |
208 | 215 | x=[None], |
209 | 216 | y=[None], |
210 | 217 | mode="markers", |
211 | | - marker=dict(size=20, color=color, opacity=0.5, line=dict(color="#333333", width=2)), |
| 218 | + marker=dict(size=20, color=color, opacity=0.5, line=dict(color=INK_SOFT, width=2)), |
212 | 219 | name=name.capitalize(), |
213 | 220 | showlegend=True, |
214 | 221 | ) |
|
222 | 229 | y=row_y, |
223 | 230 | text=f"<b>{name.capitalize()}</b>", |
224 | 231 | showarrow=False, |
225 | | - font=dict(size=18, color="#333333"), |
| 232 | + font=dict(size=18, color=INK), |
226 | 233 | xanchor="right", |
227 | 234 | ) |
228 | 235 |
|
229 | 236 | # Column labels (sample numbers) |
230 | 237 | for col in range(n_cols): |
231 | 238 | col_x = col * spacing + spacing / 2 |
232 | 239 | fig.add_annotation( |
233 | | - x=col_x, |
234 | | - y=n_rows * spacing + 0.2, |
235 | | - text=f"Sample {col + 1}", |
236 | | - showarrow=False, |
237 | | - font=dict(size=16, color="#666666"), |
| 240 | + x=col_x, y=n_rows * spacing + 0.2, text=f"Sample {col + 1}", showarrow=False, font=dict(size=16, color=INK_SOFT) |
238 | 241 | ) |
239 | 242 |
|
240 | 243 | # Feature mapping legend |
|
250 | 253 | y=n_rows * spacing / 2, |
251 | 254 | text=mapping_text, |
252 | 255 | showarrow=False, |
253 | | - font=dict(size=14, color="#333333"), |
| 256 | + font=dict(size=14, color=INK_SOFT), |
254 | 257 | align="left", |
255 | 258 | xanchor="left", |
256 | | - bgcolor="rgba(255,255,255,0.9)", |
257 | | - bordercolor="#cccccc", |
| 259 | + bgcolor=ELEVATED_BG, |
| 260 | + bordercolor=INK_SOFT, |
258 | 261 | borderwidth=1, |
259 | 262 | borderpad=10, |
260 | 263 | ) |
261 | 264 |
|
262 | 265 | # Update layout - optimized for better space utilization |
263 | 266 | fig.update_layout( |
264 | | - title=dict( |
265 | | - text="chernoff-basic · plotly · pyplots.ai", font=dict(size=28, color="#333333"), x=0.5, xanchor="center" |
266 | | - ), |
| 267 | + title=dict(text="chernoff-basic · plotly · anyplot.ai", font=dict(size=28, color=INK), x=0.5, xanchor="center"), |
267 | 268 | shapes=all_shapes, |
268 | 269 | xaxis=dict(range=[-1.2, n_cols * spacing + 3.0], showgrid=False, zeroline=False, showticklabels=False, title=""), |
269 | 270 | yaxis=dict( |
|
275 | 276 | scaleanchor="x", |
276 | 277 | scaleratio=1, |
277 | 278 | ), |
278 | | - template="plotly_white", |
| 279 | + paper_bgcolor=PAGE_BG, |
| 280 | + plot_bgcolor=PAGE_BG, |
| 281 | + font=dict(color=INK), |
279 | 282 | legend=dict( |
280 | | - title=dict(text="<b>Species</b>", font=dict(size=18)), |
281 | | - font=dict(size=16), |
| 283 | + title=dict(text="<b>Species</b>", font=dict(size=18, color=INK)), |
| 284 | + font=dict(size=16, color=INK_SOFT), |
282 | 285 | x=1.02, |
283 | 286 | y=0.98, |
284 | 287 | xanchor="left", |
285 | | - bgcolor="rgba(255,255,255,0.9)", |
286 | | - bordercolor="#cccccc", |
| 288 | + bgcolor=ELEVATED_BG, |
| 289 | + bordercolor=INK_SOFT, |
287 | 290 | borderwidth=1, |
288 | 291 | ), |
289 | 292 | margin=dict(l=100, r=180, t=80, b=40), |
290 | | - plot_bgcolor="white", |
291 | 293 | ) |
292 | 294 |
|
293 | 295 | # Save as PNG |
294 | | -fig.write_image("plot.png", width=1600, height=900, scale=3) |
| 296 | +fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3) |
295 | 297 |
|
296 | 298 | # Save interactive HTML |
297 | | -fig.write_html("plot.html", include_plotlyjs=True, full_html=True) |
| 299 | +fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") |
0 commit comments