Skip to content

Commit 8975052

Browse files
feat(bokeh): implement violin-grouped-swarm (#3547)
## Implementation: `violin-grouped-swarm` - bokeh Implements the **bokeh** version of `violin-grouped-swarm`. **File:** `plots/violin-grouped-swarm/implementations/bokeh.py` **Parent Issue:** #3529 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20858844359)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent af1dd46 commit 8975052

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
""" pyplots.ai
2+
violin-grouped-swarm: Grouped Violin Plot with Swarm Overlay
3+
Library: bokeh 3.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-09
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png, save
9+
from bokeh.models import ColumnDataSource, Legend, LegendItem
10+
from bokeh.plotting import figure
11+
from bokeh.resources import CDN
12+
from scipy import stats
13+
14+
15+
# Data - Response times (ms) across 3 task types and 2 expertise levels
16+
np.random.seed(42)
17+
18+
categories = ["Simple", "Moderate", "Complex"]
19+
groups = ["Novice", "Expert"]
20+
21+
# Generate realistic response time data for each combination
22+
data = []
23+
for cat_idx, category in enumerate(categories):
24+
for _grp_idx, group in enumerate(groups):
25+
# Experts faster, complex tasks take longer
26+
base = 200 + cat_idx * 150
27+
expert_adjust = -80 if group == "Expert" else 0
28+
mean = base + expert_adjust
29+
std = 30 + cat_idx * 15
30+
values = np.random.normal(mean, std, 40)
31+
values = np.clip(values, 50, 900)
32+
for val in values:
33+
data.append({"category": category, "group": group, "value": val})
34+
35+
# Colors
36+
colors = {"Novice": "#306998", "Expert": "#FFD43B"}
37+
38+
# Create figure
39+
p = figure(
40+
width=4800,
41+
height=2700,
42+
title="violin-grouped-swarm · bokeh · pyplots.ai",
43+
x_axis_label="Task Type",
44+
y_axis_label="Response Time (ms)",
45+
x_range=[-0.5, 2.5],
46+
y_range=[0, 750],
47+
tools="",
48+
toolbar_location=None,
49+
)
50+
51+
# Styling
52+
p.title.text_font_size = "36pt"
53+
p.title.align = "center"
54+
p.xaxis.axis_label_text_font_size = "28pt"
55+
p.yaxis.axis_label_text_font_size = "28pt"
56+
p.xaxis.major_label_text_font_size = "22pt"
57+
p.yaxis.major_label_text_font_size = "20pt"
58+
59+
# Grid styling
60+
p.xgrid.grid_line_alpha = 0.3
61+
p.ygrid.grid_line_alpha = 0.3
62+
p.xgrid.grid_line_dash = "dashed"
63+
p.ygrid.grid_line_dash = "dashed"
64+
65+
# Positioning
66+
cat_positions = {cat: i for i, cat in enumerate(categories)}
67+
group_offsets = {"Novice": -0.2, "Expert": 0.2}
68+
69+
# Store legend items
70+
legend_items = []
71+
72+
# Draw violin shapes and swarm points for each category-group combination
73+
for _grp_idx, group in enumerate(groups):
74+
first_violin = None
75+
76+
for _cat_idx, category in enumerate(categories):
77+
# Get values for this category-group
78+
values = np.array([d["value"] for d in data if d["category"] == category and d["group"] == group])
79+
80+
base_x = cat_positions[category] + group_offsets[group]
81+
82+
# Compute kernel density estimate for violin
83+
kde = stats.gaussian_kde(values)
84+
y_range = np.linspace(values.min() - 15, values.max() + 15, 100)
85+
density = kde(y_range)
86+
87+
# Scale density to reasonable width
88+
max_width = 0.17
89+
density_scaled = density / density.max() * max_width
90+
91+
# Create violin polygon
92+
violin_x = np.concatenate([base_x - density_scaled, (base_x + density_scaled)[::-1]])
93+
violin_y = np.concatenate([y_range, y_range[::-1]])
94+
95+
# Draw violin
96+
v_glyph = p.patch(
97+
violin_x, violin_y, fill_color=colors[group], fill_alpha=0.5, line_color=colors[group], line_width=3
98+
)
99+
100+
if first_violin is None:
101+
first_violin = v_glyph
102+
103+
# Create swarm points - bin values and assign jittered x positions
104+
swarm_x = []
105+
bin_width = 20
106+
value_bins = {}
107+
108+
for val in values:
109+
bin_key = int(val // bin_width)
110+
if bin_key not in value_bins:
111+
value_bins[bin_key] = 0
112+
count = value_bins[bin_key]
113+
# Alternate sides with increasing offset
114+
offset = (count // 2 + 1) * 0.025 * (1 if count % 2 == 0 else -1)
115+
if count == 0:
116+
offset = 0
117+
# Clamp offset within violin width
118+
max_offset = density_scaled[min(int((val - y_range[0]) / (y_range[-1] - y_range[0]) * 99), 99)] * 0.7
119+
offset = np.clip(offset, -max_offset, max_offset)
120+
swarm_x.append(base_x + offset)
121+
value_bins[bin_key] += 1
122+
123+
swarm_source = ColumnDataSource(data={"x": swarm_x, "y": values})
124+
125+
p.scatter(
126+
"x",
127+
"y",
128+
source=swarm_source,
129+
size=14,
130+
fill_color=colors[group],
131+
fill_alpha=0.75,
132+
line_color="white",
133+
line_width=2,
134+
)
135+
136+
# Add legend item for this group
137+
legend_items.append(LegendItem(label=group, renderers=[first_violin]))
138+
139+
# Custom x-axis with category labels
140+
p.xaxis.ticker = list(range(len(categories)))
141+
p.xaxis.major_label_overrides = dict(enumerate(categories))
142+
143+
# Add legend
144+
legend = Legend(
145+
items=legend_items,
146+
location="top_right",
147+
label_text_font_size="22pt",
148+
glyph_width=40,
149+
glyph_height=40,
150+
spacing=15,
151+
padding=20,
152+
background_fill_alpha=0.8,
153+
border_line_color="#cccccc",
154+
border_line_width=2,
155+
)
156+
p.add_layout(legend, "right")
157+
158+
# Save PNG
159+
export_png(p, filename="plot.png")
160+
161+
# Save HTML for interactive version
162+
save(p, filename="plot.html", resources=CDN, title="violin-grouped-swarm · bokeh · pyplots.ai")
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
library: bokeh
2+
specification_id: violin-grouped-swarm
3+
created: '2026-01-09T16:49:54Z'
4+
updated: '2026-01-09T16:53:33Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20858844359
7+
issue: 3529
8+
python_version: 3.13.11
9+
library_version: 3.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/violin-grouped-swarm/bokeh/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/violin-grouped-swarm/bokeh/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/violin-grouped-swarm/bokeh/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent visual clarity with well-sized text and well-spaced grouped violins
17+
- Colorblind-safe blue/yellow color scheme with good contrast
18+
- Custom KDE-based violin shapes using patches demonstrate advanced Bokeh usage
19+
- Swarm point algorithm effectively places points within violin bounds
20+
- Clean KISS code structure with good reproducibility
21+
weaknesses:
22+
- Legend appears small relative to the large canvas size; could use larger glyph
23+
sizes
24+
- Does not leverage Bokeh interactive features like HoverTool for tooltips on swarm
25+
points
26+
- Some unused loop variables (_grp_idx, _cat_idx) could be cleaner
27+
image_description: 'The plot displays a grouped violin plot with swarm point overlay
28+
showing response times (ms) across three task types (Simple, Moderate, Complex)
29+
and two expertise levels (Novice in blue #306998 and Expert in yellow #FFD43B).
30+
Six violin shapes are arranged in pairs for each task type, with Novice violins
31+
positioned slightly left and Expert violins slightly right within each category.
32+
The violin shapes show the distribution density computed via KDE, with 50% alpha
33+
transparency. Individual data points are overlaid as small dots within each violin,
34+
matching the hue of their corresponding group. The violins clearly show that Novice
35+
users have higher response times than Experts across all task types, and that
36+
response times increase with task complexity. The title "violin-grouped-swarm
37+
· bokeh · pyplots.ai" appears at the top center. X-axis is labeled "Task Type"
38+
and Y-axis is labeled "Response Time (ms)" ranging from 0-700. A legend on the
39+
right side shows Novice (blue) and Expert (yellow) labels. The grid is subtle
40+
with dashed lines and low alpha.'
41+
criteria_checklist:
42+
visual_quality:
43+
score: 37
44+
max: 40
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 10
49+
max: 10
50+
passed: true
51+
comment: Title at 36pt, axis labels at 28pt/22pt, tick labels at 20pt+ - all
52+
perfectly readable
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 8
56+
max: 8
57+
passed: true
58+
comment: No overlapping text elements; violins well-spaced within categories
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 7
62+
max: 8
63+
passed: true
64+
comment: Swarm points sized well at 14px with good alpha; slight deduction
65+
as some points close together
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Blue and yellow provide excellent contrast and are colorblind-safe
72+
- id: VQ-05
73+
name: Layout Balance
74+
score: 5
75+
max: 5
76+
passed: true
77+
comment: Plot fills canvas well, balanced margins, legend positioned appropriately
78+
- id: VQ-06
79+
name: Axis Labels
80+
score: 2
81+
max: 2
82+
passed: true
83+
comment: Y-axis has units Response Time (ms), X-axis has descriptive Task
84+
Type
85+
- id: VQ-07
86+
name: Grid & Legend
87+
score: 0
88+
max: 2
89+
passed: false
90+
comment: Legend appears small relative to the large canvas size
91+
spec_compliance:
92+
score: 25
93+
max: 25
94+
items:
95+
- id: SC-01
96+
name: Plot Type
97+
score: 8
98+
max: 8
99+
passed: true
100+
comment: Correct grouped violin plot with swarm overlay
101+
- id: SC-02
102+
name: Data Mapping
103+
score: 5
104+
max: 5
105+
passed: true
106+
comment: Categories on X-axis, values on Y-axis, groups differentiated by
107+
color
108+
- id: SC-03
109+
name: Required Features
110+
score: 5
111+
max: 5
112+
passed: true
113+
comment: 'All spec features present: grouped violins, swarm points, transparency,
114+
dodging'
115+
- id: SC-04
116+
name: Data Range
117+
score: 3
118+
max: 3
119+
passed: true
120+
comment: Y-axis shows 0-750, comfortably displaying all data points
121+
- id: SC-05
122+
name: Legend Accuracy
123+
score: 2
124+
max: 2
125+
passed: true
126+
comment: Legend correctly shows Novice and Expert with matching colors
127+
- id: SC-06
128+
name: Title Format
129+
score: 2
130+
max: 2
131+
passed: true
132+
comment: Correct format violin-grouped-swarm · bokeh · pyplots.ai
133+
data_quality:
134+
score: 18
135+
max: 20
136+
items:
137+
- id: DQ-01
138+
name: Feature Coverage
139+
score: 7
140+
max: 8
141+
passed: true
142+
comment: Shows different distributions, varying spreads across groups and
143+
categories
144+
- id: DQ-02
145+
name: Realistic Context
146+
score: 7
147+
max: 7
148+
passed: true
149+
comment: Response times for cognitive tasks across expertise levels is realistic
150+
- id: DQ-03
151+
name: Appropriate Scale
152+
score: 4
153+
max: 5
154+
passed: true
155+
comment: Response times 50-700ms are realistic for UI tasks
156+
code_quality:
157+
score: 9
158+
max: 10
159+
items:
160+
- id: CQ-01
161+
name: KISS Structure
162+
score: 3
163+
max: 3
164+
passed: true
165+
comment: 'Linear structure: imports, data, plot, save'
166+
- id: CQ-02
167+
name: Reproducibility
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: np.random.seed(42) set
172+
- id: CQ-03
173+
name: Clean Imports
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: All imports are used
178+
- id: CQ-04
179+
name: No Deprecated API
180+
score: 0
181+
max: 1
182+
passed: false
183+
comment: Uses unused loop variables (_grp_idx, _cat_idx)
184+
- id: CQ-05
185+
name: Output Correct
186+
score: 1
187+
max: 1
188+
passed: true
189+
comment: Saves as plot.png and plot.html
190+
library_features:
191+
score: 2
192+
max: 5
193+
items:
194+
- id: LF-01
195+
name: Distinctive Features
196+
score: 2
197+
max: 5
198+
passed: false
199+
comment: Uses ColumnDataSource and custom Legend, but could leverage HoverTool
200+
for interactivity
201+
verdict: APPROVED
202+
impl_tags:
203+
dependencies:
204+
- scipy
205+
techniques:
206+
- patches
207+
- custom-legend
208+
- html-export
209+
- manual-ticks
210+
patterns:
211+
- data-generation
212+
- iteration-over-groups
213+
- columndatasource
214+
dataprep:
215+
- kde
216+
styling:
217+
- alpha-blending
218+
- edge-highlighting
219+
- grid-styling

0 commit comments

Comments
 (0)