Skip to content

Commit af1dd46

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

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
""" pyplots.ai
2+
violin-swarm: Violin Plot with Overlaid Swarm Points
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, FactorRange
10+
from bokeh.plotting import figure
11+
from bokeh.resources import CDN
12+
from scipy import stats
13+
14+
15+
# Data - Reaction times (ms) across 4 experimental conditions
16+
np.random.seed(42)
17+
18+
categories = ["Control", "Low Dose", "Medium Dose", "High Dose"]
19+
n_per_group = 50
20+
21+
# Generate different distributions for each condition
22+
data = {
23+
"Control": np.random.normal(350, 50, n_per_group),
24+
"Low Dose": np.random.normal(320, 45, n_per_group),
25+
"Medium Dose": np.random.normal(280, 60, n_per_group),
26+
"High Dose": np.random.normal(250, 40, n_per_group),
27+
}
28+
29+
# Colors: Python Blue for violin, Python Yellow for points
30+
violin_color = "#306998"
31+
point_color = "#FFD43B"
32+
33+
# Create figure with padding for violins
34+
p = figure(
35+
width=4800,
36+
height=2700,
37+
title="violin-swarm · bokeh · pyplots.ai",
38+
x_range=FactorRange(*categories, range_padding=0.15),
39+
y_axis_label="Reaction Time (ms)",
40+
x_axis_label="Experimental Condition",
41+
)
42+
43+
# Styling - larger text for 4800x2700 canvas
44+
p.title.text_font_size = "36pt"
45+
p.xaxis.axis_label_text_font_size = "28pt"
46+
p.yaxis.axis_label_text_font_size = "28pt"
47+
p.xaxis.major_label_text_font_size = "24pt"
48+
p.yaxis.major_label_text_font_size = "22pt"
49+
p.grid.grid_line_alpha = 0.3
50+
p.grid.grid_line_dash = [6, 4]
51+
p.outline_line_color = None
52+
53+
# Build violin shapes and swarm points
54+
violin_patches_x = []
55+
violin_patches_y = []
56+
swarm_x = []
57+
swarm_y = []
58+
59+
for i, cat in enumerate(categories):
60+
values = data[cat]
61+
62+
# Kernel density estimation for violin
63+
kde = stats.gaussian_kde(values)
64+
y_range = np.linspace(values.min() - 20, values.max() + 20, 200)
65+
density = kde(y_range)
66+
67+
# Normalize density to max width of 0.4 (so violin fits within category space)
68+
max_width = 0.35
69+
density_normalized = density / density.max() * max_width
70+
71+
# Create violin shape (mirrored density)
72+
x_violin = np.concatenate([i - density_normalized, (i + density_normalized)[::-1]])
73+
y_violin = np.concatenate([y_range, y_range[::-1]])
74+
75+
violin_patches_x.append(x_violin.tolist())
76+
violin_patches_y.append(y_violin.tolist())
77+
78+
# Create swarm points (jitter within violin boundary)
79+
for val in values:
80+
# Get the density at this y value to determine jitter range
81+
val_density = kde(val)[0]
82+
jitter_range = (val_density / density.max()) * max_width * 0.8
83+
jitter = np.random.uniform(-jitter_range, jitter_range)
84+
swarm_x.append(i + jitter)
85+
swarm_y.append(val)
86+
87+
# Draw violins as patches (semi-transparent)
88+
for vx, vy in zip(violin_patches_x, violin_patches_y, strict=True):
89+
p.patch(vx, vy, fill_color=violin_color, fill_alpha=0.4, line_color=violin_color, line_width=2)
90+
91+
# Draw swarm points - larger size for visibility
92+
swarm_source = ColumnDataSource(data={"x": swarm_x, "y": swarm_y})
93+
p.scatter("x", "y", source=swarm_source, size=18, color=point_color, alpha=0.9, line_color="#333333", line_width=1.5)
94+
95+
# Save PNG and HTML
96+
export_png(p, filename="plot.png")
97+
save(p, filename="plot.html", resources=CDN, title="violin-swarm · bokeh · pyplots.ai")
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
library: bokeh
2+
specification_id: violin-swarm
3+
created: '2026-01-09T16:49:31Z'
4+
updated: '2026-01-09T16:52:47Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20858842134
7+
issue: 3526
8+
python_version: 3.13.11
9+
library_version: 3.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/violin-swarm/bokeh/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/violin-swarm/bokeh/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/violin-swarm/bokeh/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of KDE-based violin shapes using scipy.stats.gaussian_kde
17+
with proper normalization
18+
- Smart swarm point jittering algorithm that respects violin boundaries by scaling
19+
jitter to local density
20+
- Good color contrast between semi-transparent blue violins and opaque yellow points
21+
- Proper text sizing for 4800x2700 canvas with all elements clearly readable
22+
- Clean, KISS code structure with good reproducibility (seed=42)
23+
- Both PNG and HTML outputs provided
24+
weaknesses:
25+
- Missing HoverTool for interactive tooltips on swarm points (would showcase Bokeh
26+
interactivity strength)
27+
- Small Bokeh toolbar visible in corner is not very useful for PNG output
28+
- Could benefit from slight median/quartile indicators within the violins
29+
image_description: 'The plot displays four violin shapes arranged horizontally representing
30+
different experimental conditions (Control, Low Dose, Medium Dose, High Dose)
31+
for reaction time data. Each violin is rendered in Python Blue (#306998) with
32+
approximately 40% transparency (semi-transparent light blue appearance). Yellow/gold
33+
circular markers (Python Yellow #FFD43B) with dark gray outlines are overlaid
34+
as swarm points within each violin, showing individual data observations. The
35+
title "violin-swarm · bokeh · pyplots.ai" appears at the top-left. The Y-axis
36+
shows "Reaction Time (ms)" ranging from approximately 150-450ms, and the X-axis
37+
shows "Experimental Condition" with the four category labels. The violins display
38+
different distribution shapes - Control and Low Dose are more symmetric, Medium
39+
Dose has a wider spread, and High Dose is more compact and lower on the scale.
40+
Grid lines are subtle with dashed styling. There is a small Bokeh toolbar visible
41+
in the top-right corner.'
42+
criteria_checklist:
43+
visual_quality:
44+
score: 36
45+
max: 40
46+
items:
47+
- id: VQ-01
48+
name: Text Legibility
49+
score: 10
50+
max: 10
51+
passed: true
52+
comment: Title at 36pt, axis labels at 28pt/22pt, tick labels at 24pt/22pt
53+
- all perfectly readable at 4800x2700
54+
- id: VQ-02
55+
name: No Overlap
56+
score: 8
57+
max: 8
58+
passed: true
59+
comment: No overlapping text or elements, category labels well-spaced
60+
- id: VQ-03
61+
name: Element Visibility
62+
score: 7
63+
max: 8
64+
passed: true
65+
comment: Markers size=18 with alpha=0.9 are clearly visible, violin alpha=0.4
66+
provides good contrast; slightly dense in some areas
67+
- id: VQ-04
68+
name: Color Accessibility
69+
score: 5
70+
max: 5
71+
passed: true
72+
comment: Blue/yellow contrast is excellent and colorblind-safe
73+
- id: VQ-05
74+
name: Layout Balance
75+
score: 4
76+
max: 5
77+
passed: true
78+
comment: Good canvas utilization, violins well-distributed, slight extra whitespace
79+
on right side
80+
- id: VQ-06
81+
name: Axis Labels
82+
score: 2
83+
max: 2
84+
passed: true
85+
comment: 'Descriptive with units: Reaction Time (ms), Experimental Condition'
86+
- id: VQ-07
87+
name: Grid & Legend
88+
score: 0
89+
max: 2
90+
passed: false
91+
comment: Grid is subtle but no legend present; not strictly needed for this
92+
plot type but could help identify violin vs swarm
93+
spec_compliance:
94+
score: 25
95+
max: 25
96+
items:
97+
- id: SC-01
98+
name: Plot Type
99+
score: 8
100+
max: 8
101+
passed: true
102+
comment: Correct violin plot with overlaid swarm points
103+
- id: SC-02
104+
name: Data Mapping
105+
score: 5
106+
max: 5
107+
passed: true
108+
comment: Categories on X-axis, continuous values on Y-axis
109+
- id: SC-03
110+
name: Required Features
111+
score: 5
112+
max: 5
113+
passed: true
114+
comment: Violin shows KDE distribution, swarm points overlaid and centered
115+
within violin shape, transparency applied, contrasting colors used
116+
- id: SC-04
117+
name: Data Range
118+
score: 3
119+
max: 3
120+
passed: true
121+
comment: All data visible within axis range
122+
- id: SC-05
123+
name: Legend Accuracy
124+
score: 2
125+
max: 2
126+
passed: true
127+
comment: No legend needed for this single-series plot
128+
- id: SC-06
129+
name: Title Format
130+
score: 2
131+
max: 2
132+
passed: true
133+
comment: 'Uses correct format: violin-swarm · bokeh · pyplots.ai'
134+
data_quality:
135+
score: 18
136+
max: 20
137+
items:
138+
- id: DQ-01
139+
name: Feature Coverage
140+
score: 7
141+
max: 8
142+
passed: true
143+
comment: Shows 4 different distributions with varying means, spreads, and
144+
shapes; could show more extreme outliers
145+
- id: DQ-02
146+
name: Realistic Context
147+
score: 7
148+
max: 7
149+
passed: true
150+
comment: Reaction time drug dose study is a realistic scientific scenario
151+
- id: DQ-03
152+
name: Appropriate Scale
153+
score: 4
154+
max: 5
155+
passed: true
156+
comment: Reaction times 150-450ms are plausible; decreasing reaction time
157+
with increasing dose makes scientific sense
158+
code_quality:
159+
score: 10
160+
max: 10
161+
items:
162+
- id: CQ-01
163+
name: KISS Structure
164+
score: 3
165+
max: 3
166+
passed: true
167+
comment: 'Linear flow: imports, data, plot, save - no functions/classes'
168+
- id: CQ-02
169+
name: Reproducibility
170+
score: 3
171+
max: 3
172+
passed: true
173+
comment: Uses np.random.seed(42)
174+
- id: CQ-03
175+
name: Clean Imports
176+
score: 2
177+
max: 2
178+
passed: true
179+
comment: All imports used (numpy, bokeh modules, scipy.stats)
180+
- id: CQ-04
181+
name: No Deprecated API
182+
score: 1
183+
max: 1
184+
passed: true
185+
comment: Using current Bokeh API
186+
- id: CQ-05
187+
name: Output Correct
188+
score: 1
189+
max: 1
190+
passed: true
191+
comment: Saves both plot.png and plot.html
192+
library_features:
193+
score: 2
194+
max: 5
195+
items:
196+
- id: LF-01
197+
name: Distinctive Features
198+
score: 2
199+
max: 5
200+
passed: false
201+
comment: Uses ColumnDataSource and patch() for violins, but does not leverage
202+
Bokeh's distinctive interactivity (HoverTool could show point values on
203+
hover)
204+
verdict: APPROVED
205+
impl_tags:
206+
dependencies:
207+
- scipy
208+
techniques:
209+
- patches
210+
- html-export
211+
patterns:
212+
- data-generation
213+
- columndatasource
214+
- iteration-over-groups
215+
dataprep:
216+
- kde
217+
styling:
218+
- alpha-blending
219+
- grid-styling
220+
- edge-highlighting

0 commit comments

Comments
 (0)