Skip to content

Commit b0c8a08

Browse files
feat(matplotlib): implement radar-multi (#10298)
## Implementation: `radar-multi` - python/matplotlib Implements the **python/matplotlib** version of `radar-multi`. **File:** `plots/radar-multi/implementations/python/matplotlib.py` **Parent Issue:** #2026 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/32046021626)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 4d54914 commit b0c8a08

2 files changed

Lines changed: 178 additions & 110 deletions

File tree

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
radar-multi: Multi-Series Radar Chart
3-
Library: matplotlib 3.10.9 | Python 3.13.13
4-
Quality: 91/100 | Updated: 2026-05-07
3+
Library: matplotlib 3.11.1 | Python 3.13.15
4+
Quality: 86/100 | Updated: 2026-08-17
55
"""
66

77
import os
@@ -17,16 +17,17 @@
1717
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1818
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
1919

20-
# Okabe-Ito palette (first three series)
20+
# Imprint palette (first three series, canonical order)
2121
IMPRINT = ["#009E73", "#C475FD", "#4467A3"]
2222

23-
# Data: Product comparison across key attributes
23+
# Data: product comparison across key attributes — Product B is the overall leader
2424
categories = ["Performance", "Battery Life", "Camera", "Display", "Build Quality", "Value"]
2525
products = {
2626
"Product A": [85, 70, 90, 88, 75, 65],
2727
"Product B": [72, 95, 78, 82, 88, 80],
2828
"Product C": [90, 60, 85, 75, 70, 90],
2929
}
30+
leader_idx = 1 # Product B carries the highest average score — given visual emphasis
3031

3132
# Number of variables
3233
n_categories = len(categories)
@@ -36,38 +37,95 @@
3637
angles += angles[:1] # Close the polygon
3738

3839
# Create figure (square format for radar)
39-
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw={"polar": True}, facecolor=PAGE_BG)
40+
fig, ax = plt.subplots(figsize=(6, 6), dpi=400, subplot_kw={"polar": True}, facecolor=PAGE_BG)
4041
ax.set_facecolor(PAGE_BG)
4142

43+
# Orient the radar with the first category at the top, going clockwise, so
44+
# no category label lands due-east where the legend sits
45+
ax.set_theta_zero_location("N")
46+
ax.set_theta_direction(-1)
47+
48+
# Per-series visual weight — the leader draws bolder and on top, guiding the eye
49+
# to the strongest overall performer without adding any text annotation
50+
markers = ["o", "s", "^"]
51+
linewidths = [2.5, 2.5, 2.5]
52+
markersizes = [8, 8, 8]
53+
fill_alphas = [0.18, 0.18, 0.18]
54+
zorders = [1, 1, 1] # below the default radial-grid/tick-label zorder (~2.5)
55+
linewidths[leader_idx] = 3.5
56+
markersizes[leader_idx] = 12
57+
fill_alphas[leader_idx] = 0.35
58+
zorders[leader_idx] = 1.5
59+
4260
# Plot each product
4361
for idx, (product, values) in enumerate(products.items()):
4462
values_closed = values + values[:1] # Close the polygon
45-
ax.plot(angles, values_closed, "o-", linewidth=3, label=product, color=IMPRINT[idx], markersize=10)
46-
ax.fill(angles, values_closed, alpha=0.25, color=IMPRINT[idx])
47-
48-
# Set category labels at each axis
63+
ax.plot(
64+
angles,
65+
values_closed,
66+
marker=markers[idx],
67+
linestyle="-",
68+
linewidth=linewidths[idx],
69+
label=product,
70+
color=IMPRINT[idx],
71+
markersize=markersizes[idx],
72+
markeredgecolor=PAGE_BG,
73+
markeredgewidth=1,
74+
zorder=zorders[idx],
75+
)
76+
ax.fill(angles, values_closed, alpha=fill_alphas[idx], color=IMPRINT[idx], zorder=zorders[idx])
77+
78+
# Set category labels at each axis, nudged outward and aligned by their
79+
# rendered screen position (accounting for the N-zero/clockwise rotation
80+
# above) so labels on the right lean left, labels on the left lean right,
81+
# and top/bottom labels stay centered — none collide with their own markers
4982
ax.set_xticks(angles[:-1])
50-
ax.set_xticklabels(categories, fontsize=18, fontweight="bold", color=INK)
51-
52-
# Set radial grid
83+
tick_labels = ax.set_xticklabels(categories, fontsize=13, fontweight="bold", color=INK)
84+
ax.tick_params(axis="x", pad=14)
85+
for label, raw_angle in zip(tick_labels, angles[:-1], strict=True):
86+
cos_disp = np.cos(np.pi / 2 - raw_angle)
87+
if cos_disp > 0.3:
88+
label.set_horizontalalignment("left")
89+
elif cos_disp < -0.3:
90+
label.set_horizontalalignment("right")
91+
else:
92+
label.set_horizontalalignment("center")
93+
94+
# Set radial grid — fewer labels (25/50/75/100) placed in the empty sector
95+
# between Display and Build Quality, away from every category axis, so they
96+
# no longer collide with each other or with the data. Labels keep an opaque
97+
# halo so they stay legible where polygon fills cross the radial axis
5398
ax.set_ylim(0, 100)
54-
ax.set_yticks([20, 40, 60, 80, 100])
55-
ax.set_yticklabels(["20", "40", "60", "80", "100"], fontsize=16, color=INK_SOFT)
99+
ax.set_yticks([25, 50, 75, 100])
100+
r_tick_labels = ax.set_yticklabels(["25", "50", "75", "100"], fontsize=13, color=INK_SOFT)
101+
# set_rlabel_position takes a RAW (pre-rotation) angle; raw=210 (Build
102+
# Quality's own axis) renders at display angle 240 = between Display (270)
103+
# and Build Quality (210) after the theta_zero_location/direction rotation
104+
ax.set_rlabel_position(210)
105+
for label in r_tick_labels:
106+
label.set_bbox({"facecolor": PAGE_BG, "edgecolor": "none", "alpha": 0.9, "pad": 1.5})
56107

57108
# Grid styling
58109
ax.yaxis.grid(True, linestyle="-", alpha=0.15, linewidth=0.8, color=INK_SOFT)
59110
ax.xaxis.grid(True, linestyle="-", alpha=0.15, linewidth=0.8, color=INK_SOFT)
60111

61-
# Title
62-
ax.set_title("radar-multi · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK, pad=40)
112+
# Title — figure-level (not axes-level) so it stays centered on the full
113+
# canvas regardless of how the legend reshapes the polar axes below
114+
fig.suptitle("radar-multi · python · matplotlib · anyplot.ai", fontsize=12, fontweight="medium", color=INK, y=0.97)
63115

64-
# Legend
65-
leg = ax.legend(loc="upper left", bbox_to_anchor=(1.05, 1.0), fontsize=16, framealpha=1.0)
116+
# Legend outside the circular grid, vertically centered — leader label
117+
# rendered bold to echo its bolder polygon
118+
leg = ax.legend(loc="center left", bbox_to_anchor=(1.08, 0.5), fontsize=13, framealpha=1.0)
66119
leg.get_frame().set_facecolor(ELEVATED_BG)
67120
leg.get_frame().set_edgecolor(INK_SOFT)
68121
leg.get_frame().set_linewidth(0.8)
69-
for text in leg.get_texts():
122+
for idx, text in enumerate(leg.get_texts()):
70123
text.set_color(INK)
71-
72-
plt.tight_layout()
73-
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)
124+
if idx == leader_idx:
125+
text.set_fontweight("bold")
126+
127+
# Explicit margins (not tight_layout) so the polar axes sit vertically
128+
# centered in the square canvas — top/bottom margins balanced, right margin
129+
# reserved for the legend
130+
fig.subplots_adjust(left=0.32, right=0.66, top=0.80, bottom=0.20)
131+
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

0 commit comments

Comments
 (0)