Skip to content

Commit a243786

Browse files
feat(plotnine): implement swarm-basic (#9938)
## Implementation: `swarm-basic` - python/plotnine Implements the **python/plotnine** version of `swarm-basic`. **File:** `plots/swarm-basic/implementations/python/plotnine.py` **Parent Issue:** #974 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/30192131037)* --------- 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 28f6952 commit a243786

2 files changed

Lines changed: 156 additions & 109 deletions

File tree

plots/swarm-basic/implementations/python/plotnine.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
swarm-basic: Basic Swarm Plot
3-
Library: plotnine 0.15.3 | Python 3.13.13
4-
Quality: 87/100 | Updated: 2026-05-05
3+
Library: plotnine 0.15.7 | Python 3.13.14
4+
Quality: 89/100 | Updated: 2026-07-26
55
"""
66

77
import sys
@@ -15,15 +15,16 @@
1515
import pandas as pd
1616
from plotnine import (
1717
aes,
18+
element_blank,
1819
element_line,
1920
element_rect,
2021
element_text,
22+
geom_line,
2123
geom_point,
2224
ggplot,
2325
labs,
24-
position_jitter,
2526
scale_color_manual,
26-
stat_summary,
27+
scale_x_continuous,
2728
theme,
2829
theme_minimal,
2930
)
@@ -55,28 +56,77 @@
5556

5657
df = pd.DataFrame(data, columns=["treatment", "biomarker"])
5758
df["treatment"] = pd.Categorical(df["treatment"], categories=treatment_groups, ordered=True)
59+
df["x_num"] = df["treatment"].cat.codes.astype(float)
60+
61+
62+
# Deterministic beeswarm packing: sweep points in ascending value order and
63+
# place each one in the nearest-to-center offset slot (alternating sides)
64+
# whose most recent occupant already cleared a minimum vertical gap — a slot
65+
# only frees up once its last point is far enough below the new one, so
66+
# offsets keep growing in dense stretches instead of every sparse column
67+
# resetting back to center and stacking near-concentrically with its neighbor.
68+
# min_gap is fixed to the shared y-axis scale (not each group's own spread)
69+
# since the marker's on-canvas footprint is the same regardless of group.
70+
def beeswarm_offsets(values, min_gap, spacing=0.09):
71+
offsets = np.zeros(len(values))
72+
slot_last_y = {} # offset slot (int) -> value of the last point placed there
73+
for idx in np.argsort(values):
74+
y = values[idx]
75+
step = 0
76+
while True:
77+
for slot in (0,) if step == 0 else (step, -step):
78+
last_y = slot_last_y.get(slot)
79+
if last_y is None or y - last_y >= min_gap:
80+
offsets[idx] = slot * spacing
81+
slot_last_y[slot] = y
82+
step = None
83+
break
84+
if step is None:
85+
break
86+
step += 1
87+
return offsets
88+
89+
90+
swarm_min_gap = (df["biomarker"].max() - df["biomarker"].min()) * 0.05
91+
for group in treatment_groups:
92+
mask = df["treatment"] == group
93+
df.loc[mask, "x_num"] += beeswarm_offsets(df.loc[mask, "biomarker"].to_numpy(), swarm_min_gap)
94+
95+
medians_df = df.groupby("treatment", observed=True)["biomarker"].median().reset_index()
96+
medians_df["x_num"] = medians_df["treatment"].cat.codes.astype(float)
5897

5998
# Plot
6099
plot = (
61-
ggplot(df, aes(x="treatment", y="biomarker", color="treatment"))
62-
+ geom_point(position=position_jitter(width=0.3, height=0, random_state=42), size=3.5, alpha=0.75)
63-
+ stat_summary(fun_y=np.median, geom="point", size=8, shape="D", color=INK)
100+
ggplot(df, aes(x="x_num", y="biomarker", color="treatment"))
101+
+ geom_point(size=2.2, alpha=0.75)
102+
+ geom_line(
103+
medians_df,
104+
aes(x="x_num", y="biomarker", group=1),
105+
linetype="dashed",
106+
color=INK_SOFT,
107+
size=1.0,
108+
inherit_aes=False,
109+
)
110+
+ geom_point(medians_df, aes(x="x_num", y="biomarker"), size=6, shape="D", color=INK, inherit_aes=False)
64111
+ scale_color_manual(values=IMPRINT)
112+
+ scale_x_continuous(breaks=list(range(len(treatment_groups))), labels=treatment_groups)
65113
+ labs(x="Treatment Group", y="Biomarker Level (ng/mL)", title="swarm-basic · plotnine · anyplot.ai")
66114
+ theme_minimal()
67115
+ theme(
68-
figure_size=(16, 9),
69-
text=element_text(size=14),
116+
figure_size=(8, 4.5),
117+
text=element_text(size=7),
70118
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
71119
panel_background=element_rect(fill=PAGE_BG),
120+
panel_border=element_blank(),
72121
panel_grid_major=element_line(color=INK, size=0.3, alpha=0.08),
73122
panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.04),
74-
axis_title=element_text(color=INK, size=20),
75-
axis_text=element_text(color=INK_SOFT, size=16),
76-
plot_title=element_text(color=INK, size=24),
123+
axis_ticks_major=element_blank(),
124+
axis_title=element_text(color=INK, size=10),
125+
axis_text=element_text(color=INK_SOFT, size=8),
126+
plot_title=element_text(color=INK, size=13),
77127
legend_position="none",
78128
)
79129
)
80130

81131
# Save
82-
plot.save(f"plot-{THEME}.png", dpi=300)
132+
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in")

0 commit comments

Comments
 (0)