p9beeswarm provides beeswarm-style geoms for plotnine, modelled after the R ggbeeswarm package.
The distribution also exposes the upstream algorithm compatibility modules as
beeswarm and vipor, mirroring the corresponding R packages.
p9beeswarm is heavily inspired by the following upstream R packages.
The Python implementations and compatibility tests follow their algorithms and plotting behavior:
Install p9beeswarm from PyPI with:
python -m pip install p9beeswarmimport plotnine as p9
from plotnine.data import penguins
from p9beeswarm import geom_quasirandom
penguins_plot = (
p9.ggplot(
penguins,
p9.aes(x="species", y="body_mass_g", color="species"),
)
+ geom_quasirandom()
+ p9.labs(
title="Penguin body mass by species",
x="Species",
y="Body mass (g)",
)
+ p9.theme_bw()
)Rendered output:
The side-by-side ggbeeswarm examples recreate the upstream README plots with both the Python and R implementations. Render them with:
quarto render docs/ggbeeswarm-examples.qmdThe README figure is generated by docs/update_readme_figure.py. If you change the example above, regenerate the image with:
uv run python docs/update_readme_figure.pyThe table below records the Python implementation and its upstream R implementation.
“100% reproducible” means that repeated builds with the same data and parameters do not draw random numbers.
Methods marked as random can still be made reproducible in Python by passing a fixed random_state; without one, their point positions may change between builds.
| Plotnine API / method | Upstream R implementation | 100% reproducible? | Notes |
|---|---|---|---|
geom_quasirandom(method="quasirandom") |
ggbeeswarm::geom_quasirandom → vipor::offsetX / vipor::offsetSingleGroup |
Yes | Uses the deterministic van der Corput sequence. This is the default quasirandom method. |
geom_quasirandom(method="maxout|minout|smiley|frowney") |
ggbeeswarm::geom_quasirandom → vipor::offsetX / vipor::offsetSingleGroup |
Yes | Density and alternating extreme placement do not use random draws. |
geom_quasirandom(method="pseudorandom") |
ggbeeswarm::geom_quasirandom → vipor::offsetX / vipor::offsetSingleGroup |
No (unless seeded) | Explicitly generates random offsets; pass random_state for repeatable Python plots. |
geom_quasirandom(method="tukey|tukeyDense") |
ggbeeswarm::geom_quasirandom → vipor::offsetX / vipor::tukeyTexture |
No (unless seeded) | Tukey texture generates random permutations and jitter; pass random_state for repeatable Python plots. |
geom_beeswarm(method="swarm") |
ggbeeswarm::geom_beeswarm → beeswarm::swarmx |
Yes* | The default priority="ascending" and corral="none" are deterministic. priority="random" or corral="random" introduces randomness. |
geom_beeswarm(method="compactswarm") |
ggbeeswarm::geom_beeswarm → beeswarm::swarmx(compact=TRUE) |
Yes* | Same priority and corral caveat as swarm. |
geom_beeswarm(method="center|square|hex") |
ggbeeswarm::geom_beeswarm internal grid placement (center, square, or hex) |
Yes* | Grid placement is deterministic; corral="random" is the exception. |
*The beeswarm rows are deterministic with their default options. Any method that uses a random priority or corral is not 100% reproducible unless a seed is supplied.
p9beeswarm is licensed under the GNU GPLv3 or later.
The package is managed with [uv].
The core test suite covers the beeswarm and vipor compatibility modules,
along with the plotnine position/geom integration:
uv run --no-default-groups --group test pytestBeyond the core suite, the tests include parity checks against the original R packages. These tests use rpy2 to call the upstream R implementations and assert that the Python results match numerically:
uv run --no-default-groups --group test --group r-test pytestThe R dependencies are pinned in renv.lock and restored by the GitHub Actions workflow with r-lib/actions/setup-renv.
To update the pinned versions, run these commands from the repository root:
Rscript -e 'if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv", repos = "https://cloud.r-project.org")'
Rscript -e 'renv::restore(prompt = FALSE)'
Rscript -e 'renv::update(c("beeswarm", "vipor", "ggbeeswarm", "ggplot2"), prompt = FALSE)'
Rscript -e 'renv::snapshot(prompt = FALSE)'Review the resulting renv.lock diff, run the upstream comparison tests, and commit the updated lockfile along with any dependency changes.
Type-check using mypy.
uv run --no-default-groups --group test --group r-test mypy src/ tests/