Skip to content

Commit 088f71f

Browse files
feat(ggplot2): implement swarm-basic (#9942)
## Implementation: `swarm-basic` - r/ggplot2 Implements the **r/ggplot2** version of `swarm-basic`. **File:** `plots/swarm-basic/implementations/r/ggplot2.R` **Parent Issue:** #974 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/30192366615)* --------- 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 f578efd commit 088f71f

2 files changed

Lines changed: 366 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#' anyplot.ai
2+
#' swarm-basic: Basic Swarm Plot
3+
#' Library: ggplot2 3.5.1 | R 4.4.1
4+
#' Quality: 84/100 | Created: 2026-07-26
5+
6+
library(ggplot2)
7+
library(dplyr)
8+
library(ragg)
9+
library(scales)
10+
11+
set.seed(42)
12+
13+
# --- Theme tokens ------------------------------------------------------------
14+
THEME <- Sys.getenv("ANYPLOT_THEME", "light")
15+
PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
16+
INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
17+
INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
18+
IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233",
19+
"#AE3030", "#2ABCCD", "#954477", "#99B314")
20+
21+
# --- Data ----------------------------------------------------------------
22+
# Reaction times across a cognitive-load experiment (psychology research)
23+
conditions <- c("Baseline", "Low Load", "High Load", "Time Pressure")
24+
means <- c(420, 460, 540, 580)
25+
sds <- c(45, 50, 65, 70)
26+
n_per_condition <- 45
27+
28+
reaction_df <- bind_rows(lapply(seq_along(conditions), function(i) {
29+
tibble::tibble(
30+
condition = conditions[i],
31+
reaction_time = rnorm(n_per_condition, mean = means[i], sd = sds[i])
32+
)
33+
})) %>%
34+
mutate(condition = factor(condition, levels = conditions))
35+
36+
# --- Swarm layout ----------------------------------------------------------
37+
# Bin observations along the value axis, then rank within each bin so
38+
# points that fall close in value are spread symmetrically around the
39+
# category's center line - a lightweight beeswarm approximation built
40+
# entirely from geom_point (ggplot2 has no native beeswarm geom).
41+
bin_width <- diff(range(reaction_df$reaction_time)) / 24
42+
point_spacing <- 0.05
43+
max_offset <- 0.42
44+
45+
reaction_df <- reaction_df %>%
46+
mutate(value_bin = round(reaction_time / bin_width)) %>%
47+
group_by(condition, value_bin) %>%
48+
mutate(
49+
bin_rank = rank(reaction_time, ties.method = "first"),
50+
bin_n = n(),
51+
x_offset = pmin(pmax((bin_rank - (bin_n + 1) / 2) * point_spacing, -max_offset), max_offset)
52+
) %>%
53+
ungroup() %>%
54+
mutate(condition_x = as.numeric(condition) + x_offset)
55+
56+
condition_means <- reaction_df %>%
57+
group_by(condition) %>%
58+
summarise(mean_rt = mean(reaction_time), x_pos = as.numeric(condition[1]), .groups = "drop")
59+
60+
# --- Plot ------------------------------------------------------------------
61+
p <- ggplot(reaction_df, aes(x = condition_x, y = reaction_time)) +
62+
geom_point(
63+
aes(fill = condition), shape = 21, size = 3.1, stroke = 0.3,
64+
color = PAGE_BG, alpha = 0.85
65+
) +
66+
geom_line(
67+
data = condition_means,
68+
aes(x = x_pos, y = mean_rt),
69+
color = scales::alpha(INK, 0.4), linewidth = 0.5, linetype = "dashed"
70+
) +
71+
geom_point(
72+
data = condition_means,
73+
aes(x = x_pos, y = mean_rt),
74+
shape = 18, size = 3.4, color = INK, alpha = 0.9
75+
) +
76+
scale_fill_manual(values = IMPRINT_PALETTE[seq_along(conditions)], guide = "none") +
77+
scale_x_continuous(
78+
breaks = seq_along(conditions),
79+
labels = conditions,
80+
expand = expansion(mult = c(0.1, 0.1))
81+
) +
82+
labs(
83+
title = "swarm-basic · r · ggplot2 · anyplot.ai",
84+
x = "Experimental Condition",
85+
y = "Reaction Time (ms)"
86+
) +
87+
theme_minimal(base_size = 8) +
88+
theme(
89+
plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),
90+
panel.background = element_rect(fill = PAGE_BG, color = NA),
91+
panel.grid.major.x = element_blank(),
92+
panel.grid.minor = element_blank(),
93+
panel.grid.major.y = element_line(color = scales::alpha(INK, 0.15), linewidth = 0.3),
94+
axis.title = element_text(color = INK, size = 10),
95+
axis.text = element_text(color = INK_SOFT, size = 8),
96+
axis.ticks = element_blank(),
97+
axis.line = element_line(color = INK_SOFT, linewidth = 0.3),
98+
plot.title = element_text(color = INK, size = 12)
99+
)
100+
101+
# --- Save --------------------------------------------------------------
102+
ggsave(
103+
filename = sprintf("plot-%s.png", THEME),
104+
plot = p,
105+
device = ragg::agg_png,
106+
width = 8,
107+
height = 4.5,
108+
units = "in",
109+
dpi = 400
110+
)
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
library: ggplot2
2+
language: r
3+
specification_id: swarm-basic
4+
created: '2026-07-26T07:17:07Z'
5+
updated: '2026-07-26T07:31:37Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 30192366615
8+
issue: 974
9+
language_version: 4.4.1
10+
library_version: 3.5.1
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/r/ggplot2/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/r/ggplot2/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 84
16+
review:
17+
strengths:
18+
- Clever custom beeswarm layout built entirely from geom_point (bin the value axis,
19+
then rank and symmetrically offset points within each bin) since ggplot2 has no
20+
native beeswarm geom — a genuine library-mastery technique.
21+
- 'Imprint palette applied correctly in canonical order with first series #009E73;
22+
theme-adaptive chrome (background, ink, grid) flips correctly between light and
23+
dark while data colors stay identical.'
24+
- 'Strong spec compliance: correct title format, descriptive axis labels with units,
25+
per-category mean markers, clear spacing between category groups, and a realistic
26+
psychology reaction-time dataset with plausible escalating means across cognitive-load
27+
conditions.'
28+
- Correct 3200×1800 canvas, theme_minimal with spines/heavy grid stripped, generous
29+
whitespace, and explicit font sizes throughout.
30+
weaknesses:
31+
- The dashed mean-trend connector (geom_line over condition_means, color = scales::alpha(INK,
32+
0.4)) does not render at all in either theme — pixel-level inspection of both
33+
PNGs found zero line pixels anywhere between the four mean-diamond markers, only
34+
the diamonds themselves are visible. This is dead/non-functional code; either
35+
fix the rendering (verify the 8-digit alpha hex string is honoured by the ragg
36+
device, check the layer isn't being clipped or dropped) or remove the geom_line
37+
layer entirely since the diamond markers alone already satisfy the spec's 'subtle
38+
mean marker' note.
39+
- Points overlap fairly densely in the Low Load / High Load / Time Pressure clusters
40+
at fill alpha 0.85, making some individual observations hard to distinguish at
41+
the busiest bins — slightly smaller point_spacing increments or a touch more alpha
42+
reduction would improve separation.
43+
- No legend for the categorical fill (guide = "none") — acceptable here since x-axis
44+
tick labels already identify each category, but worth confirming this is the intended
45+
encoding rather than leftover from copying a legend-bearing pattern.
46+
image_description: |-
47+
Light render (plot-light.png):
48+
Background: Warm off-white, matches #FAF8F1 target, not pure white.
49+
Chrome: Title "swarm-basic · r · ggplot2 · anyplot.ai" top-left in dark ink; axis titles "Experimental Condition" (x) and "Reaction Time (ms)" (y) in dark ink; tick labels in soft grey-ink; subtle horizontal gridlines only. All clearly readable against the light background.
50+
Data: Four category swarms (Baseline, Low Load, High Load, Time Pressure) as filled circles with light-background-colored strokes, colors #009E73 (green), #C475FD (lavender), #4467A3 (blue), #BD8233 (ochre) in canonical Imprint order, first series correctly green. A dark diamond mean-marker sits at the center of each swarm. No dashed connector line between the diamonds is visible despite being present in the code (see weaknesses).
51+
Legibility verdict: PASS
52+
53+
Dark render (plot-dark.png):
54+
Background: Warm near-black, matches #1A1A17 target, not pure black.
55+
Chrome: Same title and axis titles now in light ink (#F0EFE8), tick labels in lighter soft-ink (#B8B7B0), gridlines subtle against the dark background. No dark-on-dark text anywhere — all chrome is clearly legible.
56+
Data: Identical swatch of four colors (#009E73, #C475FD, #4467A3, #BD8233) to the light render, confirming data colors are unchanged between themes; mean diamonds now render as light markers against the dark background (theme-correctly flipped from dark in light mode). Same missing connector line as in the light render.
57+
Legibility verdict: PASS
58+
criteria_checklist:
59+
visual_quality:
60+
score: 24
61+
max: 30
62+
items:
63+
- id: VQ-01
64+
name: Text Legibility
65+
score: 7
66+
max: 8
67+
passed: true
68+
comment: Explicit font sizes throughout; all text readable in both themes,
69+
no dark-on-dark or light-on-light.
70+
- id: VQ-02
71+
name: No Overlap
72+
score: 4
73+
max: 6
74+
passed: true
75+
comment: No text collisions; data points overlap noticeably in the denser
76+
High Load / Time Pressure clusters.
77+
- id: VQ-03
78+
name: Element Visibility
79+
score: 3
80+
max: 6
81+
passed: false
82+
comment: Swarm markers and mean diamonds are clearly visible, but the intended
83+
dashed mean-trend connector line does not render at all (confirmed via pixel
84+
inspection of both renders).
85+
- id: VQ-04
86+
name: Color Accessibility
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: Imprint palette hues are distinct and CVD-safe; no red-green as sole
91+
signal.
92+
- id: VQ-05
93+
name: Layout & Canvas
94+
score: 4
95+
max: 4
96+
passed: true
97+
comment: Canvas gate passed (3200x1800); title and axis proportions balanced,
98+
nothing cut off.
99+
- id: VQ-06
100+
name: Axis Labels & Title
101+
score: 2
102+
max: 2
103+
passed: true
104+
comment: Descriptive axis labels with units (ms).
105+
- id: VQ-07
106+
name: Palette Compliance
107+
score: 2
108+
max: 2
109+
passed: true
110+
comment: 'First series #009E73, canonical Imprint order, correct theme-adaptive
111+
backgrounds in both renders.'
112+
design_excellence:
113+
score: 14
114+
max: 20
115+
items:
116+
- id: DE-01
117+
name: Aesthetic Sophistication
118+
score: 6
119+
max: 8
120+
passed: true
121+
comment: Custom beeswarm binning algorithm and polished diamond mean markers
122+
show real design effort.
123+
- id: DE-02
124+
name: Visual Refinement
125+
score: 5
126+
max: 6
127+
passed: true
128+
comment: Spines removed, grid limited to subtle horizontal lines, generous
129+
whitespace.
130+
- id: DE-03
131+
name: Data Storytelling
132+
score: 3
133+
max: 6
134+
passed: false
135+
comment: Escalating trend across conditions is visible from swarm position/color
136+
alone, but the intended trend-connector guide line is invisible, weakening
137+
the storytelling device.
138+
spec_compliance:
139+
score: 15
140+
max: 15
141+
items:
142+
- id: SC-01
143+
name: Plot Type
144+
score: 5
145+
max: 5
146+
passed: true
147+
comment: Correct swarm/beeswarm plot via custom binned-offset layout.
148+
- id: SC-02
149+
name: Required Features
150+
score: 4
151+
max: 4
152+
passed: true
153+
comment: Per-category mean markers, color-coded categories, clear group spacing,
154+
appropriately sized points.
155+
- id: SC-03
156+
name: Data Mapping
157+
score: 3
158+
max: 3
159+
passed: true
160+
comment: Category on x, reaction time on y, correct and complete.
161+
- id: SC-04
162+
name: Title & Legend
163+
score: 3
164+
max: 3
165+
passed: true
166+
comment: Title matches mandated format; legend intentionally omitted since
167+
categories are already labeled on the x-axis.
168+
data_quality:
169+
score: 14
170+
max: 15
171+
items:
172+
- id: DQ-01
173+
name: Feature Coverage
174+
score: 5
175+
max: 6
176+
passed: true
177+
comment: Shows full distribution shape, density, and an outlier per condition.
178+
- id: DQ-02
179+
name: Realistic Context
180+
score: 5
181+
max: 5
182+
passed: true
183+
comment: Cognitive-load reaction-time experiment is realistic and neutral.
184+
- id: DQ-03
185+
name: Appropriate Scale
186+
score: 4
187+
max: 4
188+
passed: true
189+
comment: 300-700ms range and escalating means are sensible for psychology
190+
RT data.
191+
code_quality:
192+
score: 9
193+
max: 10
194+
items:
195+
- id: CQ-01
196+
name: KISS Structure
197+
score: 3
198+
max: 3
199+
passed: true
200+
comment: No functions/classes, straightforward pipeline.
201+
- id: CQ-02
202+
name: Reproducibility
203+
score: 2
204+
max: 2
205+
passed: true
206+
comment: set.seed(42) used.
207+
- id: CQ-03
208+
name: Clean Imports
209+
score: 2
210+
max: 2
211+
passed: true
212+
comment: ggplot2, dplyr, ragg, scales all actually used.
213+
- id: CQ-04
214+
name: Code Elegance
215+
score: 1
216+
max: 2
217+
passed: false
218+
comment: Non-functional geom_line layer that renders nothing is dead code.
219+
- id: CQ-05
220+
name: Output & API
221+
score: 1
222+
max: 1
223+
passed: true
224+
comment: Saves plot-{THEME}.png via ragg::agg_png.
225+
library_mastery:
226+
score: 8
227+
max: 10
228+
items:
229+
- id: LM-01
230+
name: Idiomatic Usage
231+
score: 4
232+
max: 5
233+
passed: true
234+
comment: Idiomatic dplyr + ggplot2 grammar composition.
235+
- id: LM-02
236+
name: Distinctive Features
237+
score: 4
238+
max: 5
239+
passed: true
240+
comment: Manual bin-and-rank beeswarm approximation is a distinctive technique
241+
given ggplot2 has no native beeswarm geom.
242+
verdict: APPROVED
243+
impl_tags:
244+
dependencies: []
245+
techniques:
246+
- manual-ticks
247+
- layer-composition
248+
patterns:
249+
- data-generation
250+
- groupby-aggregation
251+
- iteration-over-groups
252+
dataprep:
253+
- binning
254+
styling:
255+
- alpha-blending
256+
- edge-highlighting

0 commit comments

Comments
 (0)