|
1 | 1 | """ anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import sys |
|
15 | 15 | import pandas as pd |
16 | 16 | from plotnine import ( |
17 | 17 | aes, |
| 18 | + element_blank, |
18 | 19 | element_line, |
19 | 20 | element_rect, |
20 | 21 | element_text, |
| 22 | + geom_line, |
21 | 23 | geom_point, |
22 | 24 | ggplot, |
23 | 25 | labs, |
24 | | - position_jitter, |
25 | 26 | scale_color_manual, |
26 | | - stat_summary, |
| 27 | + scale_x_continuous, |
27 | 28 | theme, |
28 | 29 | theme_minimal, |
29 | 30 | ) |
|
55 | 56 |
|
56 | 57 | df = pd.DataFrame(data, columns=["treatment", "biomarker"]) |
57 | 58 | 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) |
58 | 97 |
|
59 | 98 | # Plot |
60 | 99 | 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) |
64 | 111 | + scale_color_manual(values=IMPRINT) |
| 112 | + + scale_x_continuous(breaks=list(range(len(treatment_groups))), labels=treatment_groups) |
65 | 113 | + labs(x="Treatment Group", y="Biomarker Level (ng/mL)", title="swarm-basic · plotnine · anyplot.ai") |
66 | 114 | + theme_minimal() |
67 | 115 | + theme( |
68 | | - figure_size=(16, 9), |
69 | | - text=element_text(size=14), |
| 116 | + figure_size=(8, 4.5), |
| 117 | + text=element_text(size=7), |
70 | 118 | plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
71 | 119 | panel_background=element_rect(fill=PAGE_BG), |
| 120 | + panel_border=element_blank(), |
72 | 121 | panel_grid_major=element_line(color=INK, size=0.3, alpha=0.08), |
73 | 122 | 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), |
77 | 127 | legend_position="none", |
78 | 128 | ) |
79 | 129 | ) |
80 | 130 |
|
81 | 131 | # 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