Metadata-Version: 2.4
Name: p9beeswarm
Version: 0.3.0
Summary: Beeswarm and quasirandom geoms for plotnine
Keywords: beeswarm,plotnine,quasirandom,visualization
Author: Liang-Bo Wang
Author-email: Liang-Bo Wang <me@liang2.tw>
License-Expression: GPL-3.0-or-later
License-File: COPYING.txt
License-File: LICENSE.txt
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Dist: numpy>=1.23
Requires-Dist: pandas>=1.5
Requires-Dist: plotnine>=0.12
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/ccwang002/p9beeswarm
Project-URL: Repository, https://github.com/ccwang002/p9beeswarm
Project-URL: Issues, https://github.com/ccwang002/p9beeswarm/issues
Project-URL: Changelog, https://github.com/ccwang002/p9beeswarm/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# p9beeswarm

`p9beeswarm` provides beeswarm-style geoms for [plotnine][plotnine], modelled after the R [ggbeeswarm][ggbeeswarm-github] package.
The distribution also exposes the upstream algorithm compatibility modules as
`beeswarm` and `vipor`, mirroring the corresponding R packages.


## Upstream R tools

`p9beeswarm` is heavily inspired by the following upstream R packages.
The Python implementations and compatibility tests follow their algorithms and plotting behavior:

|         Package          |              License               |                                     Authors                                     |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------- |
| [beeswarm][beeswarm]     | [Artistic License 2.0][artistic-2] | [Aron Eklund, James Trimble, et al.][beeswarm-authors]                          |
| [vipor][vipor]           | [GPL (>= 2)][gpl-2]                | [Scott Sherrill-Mix, Erik Clarke, et al.][vipor-authors]                        |
| [ggbeeswarm][ggbeeswarm] | [GPL (>= 3)][gpl-3]                | [Erik Clarke, Scott Sherrill-Mix, Charlotte Dawson, et al.][ggbeeswarm-authors] |


## Installation

Install `p9beeswarm` from PyPI with:

```sh
python -m pip install p9beeswarm
```


## Usage

```python
import 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:

![Penguin body mass beeswarm plot by p9beeswarm][penguins-figure]

The [side-by-side ggbeeswarm examples][examples] recreate the upstream README plots with both the Python and R implementations.
Render them with:

```sh
quarto render docs/ggbeeswarm-examples.qmd
```

The README figure is generated by [docs/update_readme_figure.py][update-readme-figure].
If you change the example above, regenerate the image with:

```sh
uv run python docs/update_readme_figure.py
```


## Implementations and reproducibility

The 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.


## License

`p9beeswarm` is licensed under the [GNU GPLv3 or later][license].


## Testing

The package is managed with [uv].
The core test suite covers the `beeswarm` and `vipor` compatibility modules,
along with the plotnine position/geom integration:

```sh
uv run --no-default-groups --group test pytest
```

### Upstream comparison tests

Beyond the core suite, the tests include parity checks against the original R packages.
These tests use [rpy2][rpy2] to call the upstream R implementations and assert that the Python results match numerically:

```sh
uv run --no-default-groups --group test --group r-test pytest
```

The R dependencies are pinned in [`renv.lock`][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:

```sh
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 checking

Type-check using [mypy][mypy].

```sh
uv run --no-default-groups --group test --group r-test mypy src/ tests/
```

[plotnine]: https://plotnine.org/
[mypy]: https://mypy.readthedocs.io/
[rpy2]: https://rpy2.github.io/
[ggbeeswarm-github]: https://github.com/eclarke/ggbeeswarm
[beeswarm]: https://CRAN.R-project.org/package=beeswarm
[beeswarm-authors]: https://cran.r-project.org/web/packages/beeswarm/DESCRIPTION
[vipor]: https://CRAN.R-project.org/package=vipor
[vipor-authors]: https://cran.r-project.org/web/packages/vipor/DESCRIPTION
[ggbeeswarm]: https://CRAN.R-project.org/package=ggbeeswarm
[ggbeeswarm-authors]: https://cran.r-project.org/web/packages/ggbeeswarm/DESCRIPTION
[artistic-2]: https://opensource.org/license/artistic-2-0/
[gpl-2]: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
[gpl-3]: https://www.gnu.org/licenses/gpl-3.0.html

[penguins-figure]: https://raw.githubusercontent.com/ccwang002/p9beeswarm/main/docs/penguins-beeswarm.png
[examples]: docs/ggbeeswarm-examples.qmd
[update-readme-figure]: docs/update_readme_figure.py
[license]: LICENSE.txt
[renv-lock]: renv.lock
