Skip to content

Commit c8e78f8

Browse files
feat(makie): implement swarm-basic (#9943)
## Implementation: `swarm-basic` - julia/makie Implements the **julia/makie** version of `swarm-basic`. **File:** `plots/swarm-basic/implementations/julia/makie.jl` **Parent Issue:** #974 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/30192426062)* --------- 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 33dbfc2 commit c8e78f8

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# anyplot.ai
2+
# swarm-basic: Basic Swarm Plot
3+
# Library: makie 0.21.9 | Julia 1.11.9
4+
# Quality: 91/100 | Created: 2026-07-26
5+
6+
using CairoMakie
7+
using Colors
8+
using Random
9+
using Statistics
10+
11+
Random.seed!(42)
12+
13+
# --- Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome") ----
14+
const THEME = get(ENV, "ANYPLOT_THEME", "light")
15+
const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
16+
const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
17+
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
18+
19+
# Imprint categorical palette — first 4 positions used, one per department
20+
const IMPRINT_PALETTE = [
21+
colorant"#009E73", # 1 — brand green (always first series)
22+
colorant"#C475FD", # 2 — lavender
23+
colorant"#4467A3", # 3 — blue
24+
colorant"#BD8233", # 4 — ochre
25+
]
26+
27+
# --- Data ---------------------------------------------------------------------
28+
# Employee performance scores by department
29+
departments = ["Engineering", "Sales", "Support", "Marketing"]
30+
group_sizes = [45, 52, 38, 47]
31+
group_means = [78.0, 71.0, 82.0, 75.0]
32+
group_stds = [7.0, 11.0, 5.5, 9.0]
33+
34+
# Swarm layout: greedy nearest-free-slot placement so points spread
35+
# horizontally (category axis) without overlapping, while keeping the
36+
# true value on the vertical axis. Rectangular collision check (separate
37+
# x/y thresholds) since the two axes carry different units.
38+
min_dist_y = 1.1 # vertical threshold below which points compete for space (score points)
39+
step_x = 0.055 # horizontal offset increment (category-axis units)
40+
41+
swarm_x = Float64[]
42+
swarm_y = Float64[]
43+
swarm_color = eltype(IMPRINT_PALETTE)[]
44+
group_medians = Float64[]
45+
max_offset = 0.0
46+
47+
for (group_index, (department, n, group_mean, group_std)) in
48+
enumerate(zip(departments, group_sizes, group_means, group_stds))
49+
scores = clamp.(randn(n) .* group_std .+ group_mean, 0.0, 100.0)
50+
order = sortperm(scores)
51+
placed_offsets = Float64[]
52+
placed_scores = Float64[]
53+
offsets = zeros(n)
54+
55+
for i in order
56+
score = scores[i]
57+
level = 0
58+
chosen_offset = 0.0
59+
while true
60+
candidates = level == 0 ? (0.0,) : (level * step_x, -level * step_x)
61+
free_slot = 0.0
62+
found = false
63+
for candidate in candidates
64+
conflict = false
65+
for j in eachindex(placed_scores)
66+
if abs(placed_scores[j] - score) < min_dist_y &&
67+
abs(placed_offsets[j] - candidate) < step_x
68+
conflict = true
69+
break
70+
end
71+
end
72+
if !conflict
73+
free_slot = candidate
74+
found = true
75+
break
76+
end
77+
end
78+
if found
79+
chosen_offset = free_slot
80+
break
81+
end
82+
level += 1
83+
end
84+
offsets[i] = chosen_offset
85+
push!(placed_offsets, chosen_offset)
86+
push!(placed_scores, score)
87+
end
88+
89+
append!(swarm_x, group_index .+ offsets)
90+
append!(swarm_y, scores)
91+
append!(swarm_color, fill(IMPRINT_PALETTE[group_index], n))
92+
push!(group_medians, median(scores))
93+
global max_offset = max(max_offset, maximum(abs, offsets))
94+
end
95+
96+
# Headline comparison for the subtitle + callout: the department with the
97+
# highest median score drives the data story beyond the per-group medians.
98+
best_idx = argmax(group_medians)
99+
subtitle_text = "$(departments[best_idx]) leads with the highest median score " *
100+
"($(round(Int, group_medians[best_idx])))"
101+
102+
# --- Plot -----------------------------------------------------------------------
103+
fig = Figure(
104+
resolution = (1600, 900),
105+
fontsize = 14,
106+
backgroundcolor = PAGE_BG,
107+
)
108+
109+
ax = Axis(
110+
fig[1, 1];
111+
title = "swarm-basic · julia · makie · anyplot.ai",
112+
titlesize = 20,
113+
titlecolor = INK,
114+
subtitle = subtitle_text,
115+
subtitlesize = 14,
116+
subtitlecolor = INK_SOFT,
117+
xlabel = "Department",
118+
ylabel = "Performance Score (0–100)",
119+
xlabelsize = 16,
120+
ylabelsize = 16,
121+
xlabelcolor = INK,
122+
ylabelcolor = INK,
123+
xticklabelsize = 13,
124+
yticklabelsize = 13,
125+
xticklabelcolor = INK_SOFT,
126+
yticklabelcolor = INK_SOFT,
127+
xtickcolor = INK_SOFT,
128+
ytickcolor = INK_SOFT,
129+
backgroundcolor = PAGE_BG,
130+
topspinevisible = false,
131+
rightspinevisible = false,
132+
leftspinecolor = INK_SOFT,
133+
bottomspinecolor = INK_SOFT,
134+
xgridvisible = false,
135+
ygridcolor = RGBAf(INK.r, INK.g, INK.b, 0.15),
136+
yminorgridvisible = false,
137+
xticks = (1:length(departments), departments),
138+
)
139+
140+
scatter!(ax, swarm_x, swarm_y;
141+
color = swarm_color, markersize = 11,
142+
strokewidth = 0.75, strokecolor = INK)
143+
144+
# Median marker per department; the top performer gets a bolder line so the
145+
# subtitle's callout has a visible anchor on the chart.
146+
for (group_index, group_median) in enumerate(group_medians)
147+
lines!(ax, [group_index - 0.32, group_index + 0.32], [group_median, group_median];
148+
color = INK, linewidth = group_index == best_idx ? 3.5 : 2.5)
149+
end
150+
151+
# Symmetric padding derived from the widest swarm actually placed, so the
152+
# canvas margins stay balanced regardless of how far any one department spreads.
153+
pad = max_offset + 0.12
154+
xlims!(ax, 1 - pad, length(departments) + pad)
155+
156+
# --- Save -------------------------------------------------------------------
157+
save("plot-$(THEME).png", fig; px_per_unit = 2)
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
library: makie
2+
language: julia
3+
specification_id: swarm-basic
4+
created: '2026-07-26T07:19:24Z'
5+
updated: '2026-07-26T07:33:48Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 30192426062
8+
issue: 974
9+
language_version: 1.11.9
10+
library_version: 0.21.9
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/julia/makie/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/swarm-basic/julia/makie/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 91
16+
review:
17+
strengths:
18+
- Correct Imprint palette with brand green first series, verified identical across
19+
both themes
20+
- Theme-adaptive chrome fully correct in both light and dark renders — no legibility
21+
failures
22+
- 'Sound engineering for a library gap: Makie has no native swarm/beeswarm geom,
23+
so the greedy nearest-free-slot collision algorithm is a legitimate, well-commented
24+
solution rather than a workaround'
25+
- Subtitle explicitly ties the data story to the visual (bolded median line for
26+
the leading department)
27+
- Realistic, well-varied data across all four departments (different means, spreads,
28+
and outliers)
29+
weaknesses:
30+
- LM-02 is low — the implementation doesn't lean on anything distinctive to Makie
31+
beyond colorant/RGBAf tokens; consider a Makie-specific touch (e.g. a Legend block,
32+
a custom recipe, or GeometryBasics-based marker shapes) to raise library mastery
33+
- Busiest swarm bands (Sales ~76-81, Marketing ~74-79) read slightly dense — a marginal
34+
reduction in markersize or increase in step_x/min_dist_y would improve point-level
35+
visibility without changing the overall shape
36+
- DE-01/DE-02 are solid but not yet publication-tier — a touch more restraint on
37+
y-axis padding and a slightly stronger visual differentiator for the winning department
38+
(beyond line-width) would push this further
39+
image_description: |-
40+
Light render (plot-light.png):
41+
Background: Warm off-white, consistent with #FAF8F1 — not pure white, not dark.
42+
Chrome: Bold centered title "swarm-basic · julia · makie · anyplot.ai" in dark ink; soft-ink subtitle "Support leads with the highest median score (83)"; axis titles "Department" / "Performance Score (0-100)" and tick labels all in dark/soft-ink tones, fully readable. Subtle horizontal-only gridlines every 10 units.
43+
Data: Four beeswarm columns — Engineering=#009E73 (first series, brand green confirmed), Sales=#C475FD lavender, Support=#4467A3 blue, Marketing=#BD8233 ochre. Solid median line per department, bolder for Support (the leader).
44+
Legibility verdict: PASS
45+
46+
Dark render (plot-dark.png):
47+
Background: Warm near-black, consistent with #1A1A17 — not pure black, not light.
48+
Chrome: Title, subtitle, axis titles, and tick labels flip to light/soft-light ink and remain fully legible; gridlines and median lines switch to light tone appropriate for dark surface. No dark-on-dark failures found.
49+
Data: Colors identical to light render (#009E73/#C475FD/#4467A3/#BD8233) — only chrome flipped, confirmed by direct comparison.
50+
Legibility verdict: PASS
51+
criteria_checklist:
52+
visual_quality:
53+
score: 29
54+
max: 30
55+
items:
56+
- id: VQ-01
57+
name: Text Legibility
58+
score: 8
59+
max: 8
60+
passed: true
61+
comment: All font sizes explicit, readable in both themes, no overflow
62+
- id: VQ-02
63+
name: No Overlap
64+
score: 6
65+
max: 6
66+
passed: true
67+
comment: No text collisions
68+
- id: VQ-03
69+
name: Element Visibility
70+
score: 5
71+
max: 6
72+
passed: true
73+
comment: Collision-avoidance layout works, but busiest bands read visually
74+
dense
75+
- id: VQ-04
76+
name: Color Accessibility
77+
score: 2
78+
max: 2
79+
passed: true
80+
comment: Four hues, good luminance separation, no red-green-only signal
81+
- id: VQ-05
82+
name: Layout & Canvas
83+
score: 4
84+
max: 4
85+
passed: true
86+
comment: Plot fills majority of canvas, balanced margins
87+
- id: VQ-06
88+
name: Axis Labels & Title
89+
score: 2
90+
max: 2
91+
passed: true
92+
comment: Descriptive labels with scale range
93+
- id: VQ-07
94+
name: Palette Compliance
95+
score: 2
96+
max: 2
97+
passed: true
98+
comment: 'First series #009E73, canonical order, theme-correct chrome in both
99+
renders'
100+
design_excellence:
101+
score: 16
102+
max: 20
103+
items:
104+
- id: DE-01
105+
name: Aesthetic Sophistication
106+
score: 6
107+
max: 8
108+
passed: true
109+
comment: Custom palette, intentional hierarchy, above defaults, short of publication-tier
110+
- id: DE-02
111+
name: Visual Refinement
112+
score: 5
113+
max: 6
114+
passed: true
115+
comment: Spines removed, subtle grid, generous whitespace
116+
- id: DE-03
117+
name: Data Storytelling
118+
score: 5
119+
max: 6
120+
passed: true
121+
comment: Subtitle callout + bolder median line create a clear focal point
122+
spec_compliance:
123+
score: 15
124+
max: 15
125+
items:
126+
- id: SC-01
127+
name: Plot Type
128+
score: 5
129+
max: 5
130+
passed: true
131+
comment: Genuine hand-rolled beeswarm layout
132+
- id: SC-02
133+
name: Required Features
134+
score: 4
135+
max: 4
136+
passed: true
137+
comment: Consistent sizes, median marker, color-coded categories, clear spacing
138+
- id: SC-03
139+
name: Data Mapping
140+
score: 3
141+
max: 3
142+
passed: true
143+
comment: Category on x, value on y, correct
144+
- id: SC-04
145+
name: Title & Legend
146+
score: 3
147+
max: 3
148+
passed: true
149+
comment: Title format exact; no separate legend needed
150+
data_quality:
151+
score: 15
152+
max: 15
153+
items:
154+
- id: DQ-01
155+
name: Feature Coverage
156+
score: 6
157+
max: 6
158+
passed: true
159+
comment: Distinct means/spreads/outliers across departments
160+
- id: DQ-02
161+
name: Realistic Context
162+
score: 5
163+
max: 5
164+
passed: true
165+
comment: Neutral business scenario
166+
- id: DQ-03
167+
name: Appropriate Scale
168+
score: 4
169+
max: 4
170+
passed: true
171+
comment: 0-100 scale, plausible department differences
172+
code_quality:
173+
score: 10
174+
max: 10
175+
items:
176+
- id: CQ-01
177+
name: KISS Structure
178+
score: 3
179+
max: 3
180+
passed: true
181+
comment: Linear structure, no functions/classes
182+
- id: CQ-02
183+
name: Reproducibility
184+
score: 2
185+
max: 2
186+
passed: true
187+
comment: Random.seed!(42)
188+
- id: CQ-03
189+
name: Clean Imports
190+
score: 2
191+
max: 2
192+
passed: true
193+
comment: All imports used
194+
- id: CQ-04
195+
name: Code Elegance
196+
score: 2
197+
max: 2
198+
passed: true
199+
comment: Well-scoped custom collision algorithm, no fake functionality
200+
- id: CQ-05
201+
name: Output & API
202+
score: 1
203+
max: 1
204+
passed: true
205+
comment: Correct save call, current API
206+
library_mastery:
207+
score: 6
208+
max: 10
209+
items:
210+
- id: LM-01
211+
name: Idiomatic Usage
212+
score: 4
213+
max: 5
214+
passed: true
215+
comment: Clean high-level Figure/Axis/scatter! usage
216+
- id: LM-02
217+
name: Distinctive Features
218+
score: 2
219+
max: 5
220+
passed: false
221+
comment: Generic hand-rolled swarm algorithm, no Makie-distinctive features
222+
verdict: APPROVED
223+
impl_tags:
224+
dependencies: []
225+
techniques:
226+
- manual-ticks
227+
patterns:
228+
- data-generation
229+
- iteration-over-groups
230+
dataprep: []
231+
styling:
232+
- edge-highlighting
233+
- grid-styling

0 commit comments

Comments
 (0)