Skip to content

Implement geom_beeswarm using a quasi-random algorithm - #1068

Merged
has2k1 merged 12 commits into
has2k1:mainfrom
const-ae:beeswarm
Sep 14, 2026
Merged

has2k1 merged 12 commits into
has2k1:mainfrom
const-ae:beeswarm

Conversation

@const-ae

@const-ae const-ae commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #318.

This PR implements a beeswarm algorithm that produces a (in my opinion) visually more appealing output than the random sampling implemented in geom_sina.

n = 200
df = pd.concat([
    pd.DataFrame({'y': np.random.RandomState(125).normal(0, 10, n), 'x': "beeswarm"}),
    pd.DataFrame({'y': np.random.RandomState(125).normal(0, 10, n), 'x': "sina"}),
])
(ggplot(df, aes(x = 'x', y = 'y')) +
    geom_beeswarm(data = lambda df: df[df['x'] == 'beeswarm'], maxwidth = 0.4) +
    geom_sina(data = lambda df: df[df['x'] == 'sina'], maxwidth = 0.4)
)
image

The code and unit testes are mostly copied from [geom|stat]_sina. I removed the random_state parameter as it isn't needed anymore (except when jitter is called). I wasn't sure what the best way to do here is, so any feedback would be appreciated.

I could also refactor the PR, so that stat_sina and stat_beeswarm use a common code base.

@const-ae

Copy link
Copy Markdown
Contributor Author

Just checking if this feature is of interest?

@has2k1

has2k1 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Just checking if this feature is of interest?

Yes. I will get to it before the next minor release v0.16.0.

@has2k1 has2k1 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could also refactor the PR, so that stat_sina and stat_beeswarm use a common code base.

They do have a lot of code in common and it would be make sense if the refactor can extract some meaningful structure beyond the stat methods. Otherwise we can leave it as it is.

Comment thread plotnine/stats/stat_beeswarm.py Outdated
Comment thread tests/test_geom_beeswarm.py Outdated
@const-ae

Copy link
Copy Markdown
Contributor Author

Hey, I don't think I will get around to making the changes this week. If you would like to include the feature in the upcoming release, feel free to make any edits necessary. Otherwise, I hope to have some more free time for this in September :)

const-ae and others added 12 commits September 14, 2026 17:44
The method uses the van der Corput algorithm to produce a low discrepancy sequence between 0 and 1.
Add `random_state` to `stat_beeswarm` and use it when integer `y`
values need jittering. This replaces the fixed seed and matches
`stat_sina`'s random-state interface.

Reduce the test's univariate chi-square degrees of freedom from 17 to
3 so all three distributions occupy comparable portions of the panel.
Move density estimation, width scaling, point offsets, and style
mirroring into a shared swarm module. Keep each stat's distinct offset
strategy and sparse-group density while removing the duplicated layout
pipeline.

Resolve `random_state` once during parameter setup. A seeded
`stat_sina` layer therefore advances one random stream across its
offset and jitter draws.
Pass the resolved stat parameters into the uniform-offset calculation
and read `random_state` there. Both the offset calculation and integer
`y` jitter now consume the same resolved random stream.
Multiply the pandas width fractions before the NumPy random values so
the uniform-offset calculation retains the input index and returns a
Series.
Rename the uniform and van der Corput offset calculations to
`pseudorandom` and `quasirandom`. These names match the public spread
strategies and prepare the calculations for shared dispatch.
Add `smiley` and `frowney` offset calculations. Both divide each group
into `y` neighbourhoods and alternate ranked values across the swarm;
`smiley` places extreme values near the edges, while `frowney` places
them near the centre.
Map each spread name to its offset calculation and reject unknown
values. This gives the beeswarm stat one entry point for selecting
quasirandom, pseudorandom, smiley, or frowney placement.
Add the `spread` parameter to `stat_beeswarm` and `geom_beeswarm`.
Users can select quasirandom, pseudorandom, smiley, or frowney point
placement; quasirandom remains the default.

Add visual coverage for the smiley and frowney strategies.
Mark the wrapped `spread` signature as a continued line so numpydoc
parses its choices and default as one parameter declaration.
Add `geom_beeswarm` and `stat_beeswarm` to the API reference and
describe their swarm placement in the changelog and class docs.

Document the shared offset calculations, clarify how `random_state`
affects pseudorandom placement, and list valid spread values in errors.
Add pseudorandom visual coverage and align test names with their
assertions.
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.28079% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.09%. Comparing base (019a08b) to head (13175eb).

Files with missing lines Patch % Lines
plotnine/stats/_swarm.py 77.48% 20 Missing and 14 partials ⚠️
plotnine/stats/stat_beeswarm.py 88.88% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1068      +/-   ##
==========================================
+ Coverage   88.05%   88.09%   +0.03%     
==========================================
  Files         223      226       +3     
  Lines       16440    16550     +110     
  Branches     2108     2116       +8     
==========================================
+ Hits        14477    14580     +103     
- Misses       1352     1357       +5     
- Partials      611      613       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@has2k1
has2k1 merged commit cb5a46f into has2k1:main Sep 14, 2026
8 of 9 checks passed
@has2k1

has2k1 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

@const-ae thanks for initiating this. I refactored it to use a common code base with geom_sina and also parameterised the spread of the points within the "swarm" to give the four variants quasirandom, pseudorandom, smiley and frowney. And geom_beeswarm with spread="pseudorandom" is really indistinguishable from geom_sina!

@const-ae

Copy link
Copy Markdown
Contributor Author

Nice, looking forward to using this in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

swarmplot

2 participants