Skip to content

Commit f96dc53

Browse files
feat(matplotlib): implement chernoff-basic (#3041)
## Implementation: `chernoff-basic` - matplotlib Implements the **matplotlib** version of `chernoff-basic`. **File:** `plots/chernoff-basic/implementations/matplotlib.py` **Parent Issue:** #3003 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617517947)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 62374b1 commit f96dc53

2 files changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
""" pyplots.ai
2+
chernoff-basic: Chernoff Faces for Multivariate Data
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.patches as patches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
12+
# Data - Car performance metrics (9 vehicles with 4 attributes)
13+
# Attributes: fuel efficiency, power, reliability, comfort (all 0-1 normalized)
14+
np.random.seed(42)
15+
16+
# Create 3 categories of cars with distinct characteristics
17+
# Economy cars: high efficiency, low power, medium reliability, medium comfort
18+
# Sports cars: low efficiency, high power, medium reliability, low comfort
19+
# Luxury cars: medium efficiency, medium power, high reliability, high comfort
20+
categories = ["Economy", "Sports", "Luxury"]
21+
n_per_category = 3
22+
23+
# Generate synthetic data with category-specific distributions
24+
data = []
25+
labels = []
26+
category_ids = []
27+
28+
# Economy cars - high efficiency, low power
29+
for i in range(n_per_category):
30+
data.append(
31+
[
32+
0.7 + np.random.rand() * 0.25, # fuel_efficiency: 0.7-0.95
33+
0.2 + np.random.rand() * 0.2, # power: 0.2-0.4
34+
0.4 + np.random.rand() * 0.3, # reliability: 0.4-0.7
35+
0.3 + np.random.rand() * 0.3, # comfort: 0.3-0.6
36+
]
37+
)
38+
labels.append(f"Economy {i + 1}")
39+
category_ids.append(0)
40+
41+
# Sports cars - low efficiency, high power
42+
for i in range(n_per_category):
43+
data.append(
44+
[
45+
0.15 + np.random.rand() * 0.2, # fuel_efficiency: 0.15-0.35
46+
0.75 + np.random.rand() * 0.2, # power: 0.75-0.95
47+
0.4 + np.random.rand() * 0.25, # reliability: 0.4-0.65
48+
0.25 + np.random.rand() * 0.25, # comfort: 0.25-0.5
49+
]
50+
)
51+
labels.append(f"Sports {i + 1}")
52+
category_ids.append(1)
53+
54+
# Luxury cars - high reliability and comfort
55+
for i in range(n_per_category):
56+
data.append(
57+
[
58+
0.35 + np.random.rand() * 0.25, # fuel_efficiency: 0.35-0.6
59+
0.5 + np.random.rand() * 0.25, # power: 0.5-0.75
60+
0.7 + np.random.rand() * 0.25, # reliability: 0.7-0.95
61+
0.7 + np.random.rand() * 0.25, # comfort: 0.7-0.95
62+
]
63+
)
64+
labels.append(f"Luxury {i + 1}")
65+
category_ids.append(2)
66+
67+
X_norm = np.array(data)
68+
colors = ["#306998", "#FFD43B", "#4CAF50"] # Python Blue, Yellow, Green
69+
70+
# Create figure - 3x3 grid of faces (square format for symmetric grid)
71+
fig, ax = plt.subplots(figsize=(12, 12))
72+
73+
# Calculate grid positions with better spacing
74+
n_cols = 3
75+
n_rows = 3
76+
x_positions = np.linspace(0.22, 0.78, n_cols)
77+
y_positions = np.linspace(0.76, 0.28, n_rows) # More space between rows
78+
79+
# Draw each Chernoff face
80+
for idx in range(len(X_norm)):
81+
row = idx // n_cols
82+
col = idx % n_cols
83+
x_center = x_positions[col]
84+
y_center = y_positions[row]
85+
features = X_norm[idx]
86+
color = colors[category_ids[idx]]
87+
label = labels[idx]
88+
89+
# Feature mappings (all features in 0-1 range):
90+
# - features[0]: face width (fuel efficiency)
91+
# - features[1]: face height (power)
92+
# - features[2]: eye size (reliability)
93+
# - features[3]: mouth curvature (comfort) - happy = high comfort
94+
95+
# Scale down faces to prevent overlap
96+
face_width = 0.12 + features[0] * 0.06 # 0.12-0.18
97+
face_height = 0.14 + features[1] * 0.06 # 0.14-0.20
98+
eye_size = 0.015 + features[2] * 0.015 # 0.015-0.03
99+
mouth_curve = -0.05 + features[3] * 0.10 # -0.05 to 0.05 (sad to happy)
100+
101+
# Face ellipse
102+
face = patches.Ellipse(
103+
(x_center, y_center), face_width, face_height, facecolor=color, edgecolor="black", linewidth=2.5, alpha=0.75
104+
)
105+
ax.add_patch(face)
106+
107+
# Eyes - position relative to face
108+
eye_y = y_center + face_height * 0.18
109+
eye_x_offset = face_width * 0.22
110+
111+
# Left eye (white with pupil)
112+
left_eye = patches.Ellipse(
113+
(x_center - eye_x_offset, eye_y), eye_size * 1.6, eye_size, facecolor="white", edgecolor="black", linewidth=1.5
114+
)
115+
ax.add_patch(left_eye)
116+
left_pupil = patches.Circle((x_center - eye_x_offset, eye_y), eye_size * 0.35, facecolor="black")
117+
ax.add_patch(left_pupil)
118+
119+
# Right eye
120+
right_eye = patches.Ellipse(
121+
(x_center + eye_x_offset, eye_y), eye_size * 1.6, eye_size, facecolor="white", edgecolor="black", linewidth=1.5
122+
)
123+
ax.add_patch(right_eye)
124+
right_pupil = patches.Circle((x_center + eye_x_offset, eye_y), eye_size * 0.35, facecolor="black")
125+
ax.add_patch(right_pupil)
126+
127+
# Eyebrows - angle based on power (higher power = more intense)
128+
brow_y = eye_y + eye_size * 1.4
129+
brow_length = eye_size * 1.3
130+
brow_angle = (features[1] - 0.5) * 0.015 # Angle variation
131+
132+
ax.plot(
133+
[x_center - eye_x_offset - brow_length / 2, x_center - eye_x_offset + brow_length / 2],
134+
[brow_y + brow_angle, brow_y - brow_angle],
135+
color="black",
136+
linewidth=2.5,
137+
solid_capstyle="round",
138+
)
139+
ax.plot(
140+
[x_center + eye_x_offset - brow_length / 2, x_center + eye_x_offset + brow_length / 2],
141+
[brow_y - brow_angle, brow_y + brow_angle],
142+
color="black",
143+
linewidth=2.5,
144+
solid_capstyle="round",
145+
)
146+
147+
# Nose - simple vertical line with base
148+
nose_height = 0.015 + features[1] * 0.01
149+
nose_y_top = y_center + nose_height * 0.3
150+
nose_y_bottom = y_center - nose_height * 0.7
151+
ax.plot([x_center, x_center], [nose_y_top, nose_y_bottom], color="black", linewidth=2)
152+
# Nose base
153+
ax.plot([x_center - 0.005, x_center + 0.005], [nose_y_bottom, nose_y_bottom], color="black", linewidth=2)
154+
155+
# Mouth - curved based on comfort
156+
mouth_y = y_center - face_height * 0.28
157+
mouth_width_val = 0.02 + features[0] * 0.015
158+
159+
mouth_x = np.linspace(-mouth_width_val / 2, mouth_width_val / 2, 30)
160+
mouth_y_curve = mouth_y + mouth_curve * (1 - (2 * mouth_x / mouth_width_val) ** 2)
161+
ax.plot(x_center + mouth_x, mouth_y_curve, color="black", linewidth=3, solid_capstyle="round")
162+
163+
# Label below face (positioned further down to avoid overlap)
164+
ax.text(
165+
x_center, y_center - face_height * 0.65 - 0.02, label, ha="center", va="top", fontsize=13, fontweight="bold"
166+
)
167+
168+
# Styling
169+
ax.set_xlim(0, 1)
170+
ax.set_ylim(0, 1)
171+
ax.set_aspect("equal")
172+
ax.axis("off")
173+
174+
# Title
175+
ax.set_title("Car Ratings · chernoff-basic · matplotlib · pyplots.ai", fontsize=24, fontweight="bold", pad=20)
176+
177+
# Feature mapping legend (bottom left, moved down to avoid overlap)
178+
legend_text = (
179+
"Feature Mapping:\nFace Width = Fuel Efficiency\nFace Height = Power\nEye Size = Reliability\nMouth Curve = Comfort"
180+
)
181+
ax.text(
182+
0.02,
183+
0.01,
184+
legend_text,
185+
transform=ax.transAxes,
186+
fontsize=11,
187+
verticalalignment="bottom",
188+
fontfamily="monospace",
189+
bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.95, "edgecolor": "gray"},
190+
)
191+
192+
# Category legend (upper right)
193+
for i, category in enumerate(categories):
194+
ax.scatter([], [], c=colors[i], s=250, label=category, alpha=0.75, edgecolors="black")
195+
ax.legend(loc="upper right", fontsize=14, title="Category", title_fontsize=16, framealpha=0.95, edgecolor="gray")
196+
197+
plt.tight_layout()
198+
plt.savefig("plot.png", dpi=300, bbox_inches="tight", facecolor="white")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
library: matplotlib
2+
specification_id: chernoff-basic
3+
created: '2025-12-31T11:00:11Z'
4+
updated: '2025-12-31T11:10:43Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617517947
7+
issue: 3003
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/chernoff-basic/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/chernoff-basic/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of Chernoff faces with clear visual differentiation between
17+
car categories
18+
- 'Well-designed feature mapping: face width for efficiency, height for power, eye
19+
size for reliability, mouth curve for comfort creates intuitive visual patterns'
20+
- Clean, informative legends that explain both the category colors and feature mappings
21+
- Good use of square figure format (12x12) for the 3x3 grid layout
22+
- Category-specific data generation creates meaningful visual clusters (Economy=wide
23+
faces, Sports=tall faces, Luxury=happy faces)
24+
- Professional appearance with consistent styling (line widths, alpha values, edge
25+
colors)
26+
weaknesses:
27+
- Some wasted vertical space at the top of the figure above the first row of faces
28+
could be reduced
29+
- The within-category variation could be more pronounced to better demonstrate the
30+
multivariate nature of the visualization
31+
- Could potentially add more facial features (e.g., ear size, hair) to map additional
32+
variables as mentioned in the spec

0 commit comments

Comments
 (0)