Skip to content

Commit 68babbc

Browse files
feat(ggplot2): implement radar-multi (#10303)
## Implementation: `radar-multi` - r/ggplot2 Implements the **r/ggplot2** version of `radar-multi`. **File:** `plots/radar-multi/implementations/r/ggplot2.R` **Parent Issue:** #2026 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/32047475179)* --------- 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 a22bab4 commit 68babbc

2 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#' anyplot.ai
2+
#' radar-multi: Multi-Series Radar Chart
3+
#' Library: ggplot2 3.5.1 | R 4.4.1
4+
#' Quality: 83/100 | Created: 2026-08-17
5+
6+
library(ggplot2)
7+
library(ragg)
8+
9+
set.seed(42)
10+
11+
# --- Theme tokens -------------------------------------------------------
12+
THEME <- Sys.getenv("ANYPLOT_THEME", "light")
13+
PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
14+
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
15+
INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
16+
INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
17+
18+
IMPRINT_PALETTE <- c(
19+
"#009E73", # 1 — brand green, always first series
20+
"#C475FD", # 2 — lavender
21+
"#4467A3", # 3 — blue
22+
"#BD8233", # 4 — ochre
23+
"#AE3030", # 5 — matte red
24+
"#2ABCCD", # 6 — cyan
25+
"#954477", # 7 — rose
26+
"#99B314" # 8 — lime
27+
)
28+
29+
# coord_polar() munges straight polygon edges into arcs when interpolating
30+
# between vertices; is_linear = TRUE keeps the radar spokes and value rings
31+
# as straight-edged polygons, matching the spec's "closed polygon" per axis.
32+
coord_radar <- function(start = 0, direction = 1) {
33+
ggproto("CoordRadar", CoordPolar,
34+
theta = "x", r = "y", start = start, direction = sign(direction),
35+
is_linear = function(coord) TRUE
36+
)
37+
}
38+
39+
# --- Data -----------------------------------------------------------------
40+
attributes <- c("Battery Life", "Camera", "Performance", "Display", "Value", "Durability")
41+
42+
radar_df <- tibble::tibble(
43+
phone = rep(c("Aurora X12", "Nimbus S8", "Ridgeline Pro"), each = length(attributes)),
44+
attribute = factor(rep(attributes, times = 3), levels = attributes),
45+
score = c(
46+
72, 65, 88, 80, 55, 70, # Aurora X12 — performance-focused flagship
47+
90, 78, 60, 68, 82, 75, # Nimbus S8 — battery + value pick
48+
58, 92, 75, 95, 45, 85 # Ridgeline Pro — camera + display, premium price
49+
)
50+
)
51+
52+
# Faint alternating ring bands (drawn back-to-front: wide band first, then a
53+
# narrower band painted in the page color to punch out the "gap") give the
54+
# grid a touch of depth beyond the mandated gridlines alone.
55+
ring_hi <- data.frame(attribute = factor(attributes, levels = attributes), score = 80)
56+
ring_lo <- data.frame(attribute = factor(attributes, levels = attributes), score = 60)
57+
58+
# --- Plot -------------------------------------------------------------------
59+
p <- ggplot(radar_df, aes(x = attribute, y = score, group = phone, color = phone, fill = phone)) +
60+
geom_polygon(data = ring_hi, aes(x = attribute, y = score), inherit.aes = FALSE, fill = INK, alpha = 0.05) +
61+
geom_polygon(data = ring_lo, aes(x = attribute, y = score), inherit.aes = FALSE, fill = PAGE_BG) +
62+
geom_polygon(alpha = 0.25, linewidth = 1.0) +
63+
geom_point(size = 3.2) +
64+
coord_radar(start = -pi / 2) +
65+
scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 20), expand = expansion(mult = c(0, 0.18))) +
66+
scale_color_manual(values = IMPRINT_PALETTE) +
67+
scale_fill_manual(values = IMPRINT_PALETTE) +
68+
labs(
69+
title = "radar-multi · r · ggplot2 · anyplot.ai",
70+
caption = "Each phone leads on a different axis — no single winner across all six attributes",
71+
x = NULL, y = "Score (0-100)", color = NULL, fill = NULL
72+
) +
73+
theme_minimal(base_size = 8) +
74+
theme(
75+
plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),
76+
panel.background = element_rect(fill = PAGE_BG, color = NA),
77+
panel.border = element_blank(),
78+
axis.line = element_blank(),
79+
axis.ticks = element_blank(),
80+
panel.grid.major = element_line(color = INK, linewidth = 0.3),
81+
panel.grid.minor = element_blank(),
82+
axis.text.x = element_text(color = INK, size = 10),
83+
axis.text.y = element_text(color = INK_SOFT, size = 8),
84+
axis.title.y = element_text(color = INK_SOFT, size = 7, hjust = 0.85, margin = margin(r = 4)),
85+
plot.title = element_text(color = INK, size = 12, face = "bold", hjust = 0.5),
86+
plot.caption = element_text(color = INK_SOFT, size = 7, hjust = 0.5, margin = margin(t = 8)),
87+
legend.position = "bottom",
88+
legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT),
89+
legend.text = element_text(color = INK_SOFT, size = 8),
90+
legend.title = element_blank()
91+
) +
92+
guides(fill = guide_legend(override.aes = list(alpha = 0.5)))
93+
94+
# --- Save -------------------------------------------------------------------
95+
ggsave(
96+
filename = sprintf("plot-%s.png", THEME),
97+
plot = p,
98+
device = ragg::agg_png,
99+
width = 6,
100+
height = 6,
101+
units = "in",
102+
dpi = 400
103+
)
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
library: ggplot2
2+
language: r
3+
specification_id: radar-multi
4+
created: '2026-08-17T16:56:54Z'
5+
updated: '2026-08-17T17:15:11Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 32047475179
8+
issue: 2026
9+
language_version: 4.4.1
10+
library_version: 3.5.1
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/radar-multi/r/ggplot2/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/radar-multi/r/ggplot2/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 83
16+
review:
17+
strengths:
18+
- Unused imports from attempt 1 (library(dplyr), library(tidyr), library(scales))
19+
are gone — every remaining import is used.
20+
- Y-axis now carries a descriptive 'Score (0-100)' label with units, fixing the
21+
previous VQ-06 gap.
22+
- New caption ('Each phone leads on a different axis — no single winner across all
23+
six attributes') states the comparative insight directly, giving the chart a clear
24+
storytelling takeaway.
25+
- Marker size increased from 2.5 to 3.2, more prominent for the sparse 18-point
26+
dataset.
27+
- 'Perfect Imprint palette compliance: first series is #009E73, positions 2-3 follow
28+
canonical order (#C475FD, #4467A3), and chrome (background, text, grid, legend
29+
box) is correctly theme-adaptive in both renders.'
30+
- Custom coord_radar() ggproto extension (is_linear=TRUE) correctly keeps polygon
31+
edges straight instead of letting coord_polar() arc-interpolate them — a distinctive,
32+
idiomatic ggplot2-specific technique for radar charts.
33+
- Realistic, neutral phone-comparison dataset with genuine trade-offs across series
34+
(each phone leads on a different axis), avoiding flat/uniform data.
35+
- Title and legend format exactly match the spec requirement; all required spec
36+
features (filled polygons with alpha, distinct colors, legend, regular gridlines,
37+
closed polygons, fill+outline) are present.
38+
weaknesses:
39+
- 'The outer grid ring (and, for ''Durability'', the axis spoke line) visually crosses
40+
directly through 3 of the 6 axis category labels — ''Camera'', ''Value'', and
41+
''Durability'' — in both light and dark renders, producing a strikethrough look
42+
where the line cuts across the letters. Text stays legible but the collision looks
43+
unfinished. Fix: give axis.text.x more clearance from the panel (e.g. axis.text.x
44+
= element_text(margin = margin(t = 10, b = 10))) or increase the y-axis expansion
45+
(currently expansion(mult = c(0, 0.18))) so category labels sit clear of the outermost
46+
gridline in both directions.'
47+
- The alternating ring-band effect (ring_hi at alpha=0.05, ring_lo painted in the
48+
page color) is barely perceptible at normal viewing size — either strengthen it
49+
slightly so it reads as an intentional design touch, or drop it since it isn't
50+
currently adding visible depth.
51+
- No differential visual weight among the three phone polygons — all three carry
52+
identical alpha/linewidth, so beyond the caption text the viewer must still compare
53+
all three series from scratch rather than being visually guided to the comparison.
54+
- Marker size (3.2) is still on the conservative side for only 18 total data points
55+
— a further bump would add more visual pop, especially at thumbnail/mobile scale.
56+
image_description: |-
57+
Light render (plot-light.png):
58+
Background: Warm off-white, consistent with #FAF8F1 — not pure white.
59+
Chrome: Bold title "radar-multi · r · ggplot2 · anyplot.ai" is dark, centered, fully visible at the top. Six category axis labels (Camera, Performance, Display, Value, Durability, Battery Life) are dark and legible around the polar grid, but the outermost grid ring visibly crosses through the middle of "Camera" and "Value", and the axis spoke line cuts diagonally through "Durability" — a strikethrough effect that survives at full resolution (text remains readable, but the collision is a real visual defect). Radial value ticks (0/20/40/60/80/100) plus a new "Score (0-100)" axis title sit on the left, legible without colliding with the "Battery Life" label. Legend sits centered below the plot in a bordered box with square color-and-dot swatches for "Aurora X12", "Nimbus S8", "Ridgeline Pro" — all readable. A caption below the legend states the comparative insight ("Each phone leads on a different axis — no single winner across all six attributes").
60+
Data: Three translucent (alpha ~0.25) filled polygons with matching outline and point markers (size increased to 3.2) — green (#009E73, Aurora X12), lavender (#C475FD, Nimbus S8), blue (#4467A3, Ridgeline Pro). Each phone clearly leads on a different axis (Ridgeline Pro on Camera/Display, Nimbus S8 on Battery Life/Value, Aurora X12 on Performance), consistent with the spec's comparative-analysis intent. A very faint alternating ring band between the 60 and 80 gridlines is present but barely perceptible.
61+
Legibility verdict: PASS overall — all text is readable against the light background, no dark-on-light or light-on-light failures, but the grid-ring/label collision on 3 of 6 category labels is a notable cosmetic flaw (see weaknesses).
62+
63+
Dark render (plot-dark.png):
64+
Background: Warm near-black, consistent with #1A1A17 — not pure black.
65+
Chrome: Title, axis category labels, and radial tick numbers/axis title all render in light/off-white tones and remain fully legible against the dark background — no dark-on-dark failures observed. The same grid-ring/spoke-line collision through "Camera", "Value", and "Durability" is present and equally visible here. Legend box background flips to the elevated dark surface with light text, fully readable, and the caption is legible in muted light text.
66+
Data: Data colors are pixel-identical to the light render — green (#009E73), lavender (#C475FD), blue (#4467A3) — confirming only chrome (not data) flips between themes.
67+
Legibility verdict: PASS overall — all text is clearly readable against the dark background; no dark-on-dark or light-on-dark issues found, aside from the same grid/label collision noted in the light render.
68+
criteria_checklist:
69+
visual_quality:
70+
score: 23
71+
max: 30
72+
items:
73+
- id: VQ-01
74+
name: Text Legibility
75+
score: 6
76+
max: 8
77+
passed: true
78+
comment: All font sizes explicitly set and readable in both themes, but the
79+
grid-ring/label collision on 3 axes (see VQ-02) makes the result feel less
80+
than perfectly proportioned
81+
- id: VQ-02
82+
name: No Overlap
83+
score: 3
84+
max: 6
85+
passed: false
86+
comment: The outer grid ring crosses through 'Camera' and 'Value' labels,
87+
and the axis spoke line cuts through 'Durability', in both light and dark
88+
renders — text stays legible but the collision is a clear, reproducible
89+
overlap
90+
- id: VQ-03
91+
name: Element Visibility
92+
score: 5
93+
max: 6
94+
passed: true
95+
comment: Marker size increased to 3.2 for the sparse 18-point dataset, more
96+
visible than attempt 1 but still on the conservative side
97+
- id: VQ-04
98+
name: Color Accessibility
99+
score: 2
100+
max: 2
101+
passed: true
102+
comment: Green/lavender/blue combination is CVD-safe, no red-green sole signal,
103+
adequate contrast with alpha 0.25 fills
104+
- id: VQ-05
105+
name: Layout & Canvas
106+
score: 3
107+
max: 4
108+
passed: true
109+
comment: Polar plot fills a large majority of the square canvas with balanced
110+
margins and legend close to the plot; the axis-label/grid-ring collision
111+
indicates panel margin allocated for labels is slightly insufficient
112+
- id: VQ-06
113+
name: Axis Labels & Title
114+
score: 2
115+
max: 2
116+
passed: true
117+
comment: Category labels are descriptive and the radial axis now carries a
118+
'Score (0-100)' title with units, fixing the attempt-1 gap
119+
- id: VQ-07
120+
name: Palette Compliance
121+
score: 2
122+
max: 2
123+
passed: true
124+
comment: 'First series is #009E73, positions 2-3 follow canonical Imprint
125+
order, backgrounds and chrome are theme-correct in both renders'
126+
design_excellence:
127+
score: 12
128+
max: 20
129+
items:
130+
- id: DE-01
131+
name: Aesthetic Sophistication
132+
score: 5
133+
max: 8
134+
passed: false
135+
comment: Bold title, refined legend swatch alpha, and alternating ring bands
136+
lift it slightly above a bare default, but the unresolved label/grid-ring
137+
collision undercuts the polish claim
138+
- id: DE-02
139+
name: Visual Refinement
140+
score: 3
141+
max: 6
142+
passed: false
143+
comment: Spines/ticks removed and legend styled with themed border, but the
144+
visible grid-ring/label collision on 3 axes is the opposite of 'every detail
145+
polished'
146+
- id: DE-03
147+
name: Data Storytelling
148+
score: 4
149+
max: 6
150+
passed: true
151+
comment: New caption explicitly states the comparative insight ('no single
152+
winner'), giving the viewer a clear takeaway even though the three series
153+
still carry equal visual weight
154+
spec_compliance:
155+
score: 15
156+
max: 15
157+
items:
158+
- id: SC-01
159+
name: Plot Type
160+
score: 5
161+
max: 5
162+
passed: true
163+
comment: Correct multi-series radar chart
164+
- id: SC-02
165+
name: Required Features
166+
score: 4
167+
max: 4
168+
passed: true
169+
comment: Filled polygons with alpha, distinct colors, legend, regular gridlines,
170+
clear axis labels, closed polygons, fill+outline all present
171+
- id: SC-03
172+
name: Data Mapping
173+
score: 3
174+
max: 3
175+
passed: true
176+
comment: Categories on angular axis, 0-100 scores on radial axis, all data
177+
visible
178+
- id: SC-04
179+
name: Title & Legend
180+
score: 3
181+
max: 3
182+
passed: true
183+
comment: Title matches required format exactly; legend labels match the phone
184+
series
185+
data_quality:
186+
score: 14
187+
max: 15
188+
items:
189+
- id: DQ-01
190+
name: Feature Coverage
191+
score: 5
192+
max: 6
193+
passed: true
194+
comment: Each phone leads on different axes, showing real comparative trade-offs,
195+
though value ranges cluster in a moderate 45-95 band
196+
- id: DQ-02
197+
name: Realistic Context
198+
score: 5
199+
max: 5
200+
passed: true
201+
comment: Neutral, plausible consumer-tech product comparison
202+
- id: DQ-03
203+
name: Appropriate Scale
204+
score: 4
205+
max: 4
206+
passed: true
207+
comment: Values and relative proportions are all plausible for phone attribute
208+
scoring
209+
code_quality:
210+
score: 10
211+
max: 10
212+
items:
213+
- id: CQ-01
214+
name: KISS Structure
215+
score: 3
216+
max: 3
217+
passed: true
218+
comment: Linear imports -> theme tokens -> data -> plot -> save; the coord_radar()
219+
ggproto extension is a necessary, well-commented exception since ggplot2
220+
has no native radar coord
221+
- id: CQ-02
222+
name: Reproducibility
223+
score: 2
224+
max: 2
225+
passed: true
226+
comment: set.seed(42) present; data is fully deterministic regardless
227+
- id: CQ-03
228+
name: Clean Imports
229+
score: 2
230+
max: 2
231+
passed: true
232+
comment: Unused library(dplyr)/library(tidyr)/library(scales) removed — every
233+
remaining import is used
234+
- id: CQ-04
235+
name: Code Elegance
236+
score: 2
237+
max: 2
238+
passed: true
239+
comment: Clean, no over-engineering, no fake functionality
240+
- id: CQ-05
241+
name: Output & API
242+
score: 1
243+
max: 1
244+
passed: true
245+
comment: Saves plot-{theme}.png via ragg::agg_png, current API
246+
library_mastery:
247+
score: 9
248+
max: 10
249+
items:
250+
- id: LM-01
251+
name: Idiomatic Usage
252+
score: 5
253+
max: 5
254+
passed: true
255+
comment: 'Idiomatic ggplot2 layered grammar: geom_polygon + geom_point + scale_manual
256+
+ theme composition'
257+
- id: LM-02
258+
name: Distinctive Features
259+
score: 4
260+
max: 5
261+
passed: true
262+
comment: Custom CoordRadar ggproto subclass overriding is_linear is a genuinely
263+
ggplot2-specific technique not easily replicated in another library's API
264+
verdict: APPROVED
265+
impl_tags:
266+
dependencies: []
267+
techniques:
268+
- polar-projection
269+
- layer-composition
270+
patterns: []
271+
dataprep: []
272+
styling:
273+
- alpha-blending
274+
- minimal-chrome

0 commit comments

Comments
 (0)