Skip to content

feat: exploring bumplot as plotnine geoms - #15

Draft
machow wants to merge 4 commits into
y-sunflower:mainfrom
machow:feat-plotnine-geoms
Draft

feat: exploring bumplot as plotnine geoms#15
machow wants to merge 4 commits into
y-sunflower:mainfrom
machow:feat-plotnine-geoms

Conversation

@machow

@machow machow commented Oct 1, 2025

Copy link
Copy Markdown

(edit) TODO:

  • features:
    • 2 geoms geom_bump() & geom_bezier()
    • to discuss: option to set curve_force: float (nice feature IMO)
  • tests:
    • TODO: define how plotnine should be tested, but I assume something like aes are valid, maybe colors, etc?
    • categorical variables for the x axis is ok
    • maybe other things?
  • doc:
    • basic example in the landing (docs/index.md) and readme (docs/REAME.qmd): maybe the same made with matplotlib?
    • example in docs/examples.md and (nice to have but takes a significant amount of time, feel free to skip this one, see add good/advanced examples in the doc #3) example in docs/advanced-usage.md
    • use same doc style as in bumplot() (just make sure things are consistent)
    • update readme to say that it supports plotnine too
    • maybe other things?





Hey @JosephBARBIERDARNAL! Don't hesitate to punt this PR, but I've been really interested in trying bumplot, while also working a bit on plotnine extensions. I decided to try out using bumplot from plotnine to see what kind of work is involved.

This is still a WIP, but I implemented a geom_bezier() using the bumplot bezier_curve() function. I think if you're up for it @has2k1 is down to help with geom_bezier() and a bigger geom_bumplot(). It could either live here or somewhere else. WDYT?!

Here it is recreating the plot from the README.

import pandas as pd

from plotnine import *
from bumplot.geoms import geom_bezier

data = pd.DataFrame(
    {
        "x": [2020, 2021, 2022, 2023],
        "A": [10, 50, 20, 80],
        "B": [40, 30, 60, 10],
        "C": [90, 20, 70, 40],
    }
)
data_long = data.melt(id_vars="x", value_vars=["A", "B", "C"])

# TODO: I think a plotnine stat could do the ranking
# TODO I just use geom_bezier w/ geom_point, but geom_bumplot could do both
ranked = data_long.assign(
    rank = data_long.groupby("x")["value"].rank(ascending=False).astype(int)
)

(
    ggplot(ranked, aes("x", "rank"))
    # geom_bumplot would essentialy be bezier + point ----
    # plus the rank stat calculation
    + geom_bezier(aes(color="variable"), size=1)
    + geom_point(aes(fill="variable"), size=3, color="black")
    # match readme plot ----
    + theme_minimal()
    + scale_fill_manual(values=["#ffbe0b", "#ff006e", "#3a86ff"])
    + scale_color_manual(values=["#ffbe0b", "#ff006e", "#3a86ff"])
    + scale_y_reverse()
)
image

It seems like this kind of bumplot stuff would be neat to show off in the 2025 Plotnine Plotting Contest 😁

@JosephBARBIERDARNAL

Copy link
Copy Markdown
Contributor

Hi @machow

That's a great idea! I hadn't thought of that. In my opinion, it makes sense for geom_bump() to be in bumplot and think of it as a plotnine extension, but I don't have a strong opinion either way.

In ggbump, they included a geom_sigmoid() function, and I think that makes a little more sense to users than geom_bezier() (in terms of the name)?

I don't have much time to think about it right now, but it seems like you already have a pretty good idea on how this could/should be implemented, so if you're willing to work on a more "complete" PR (with @has2k1 ?), I'd be happy to review it, as this would be a very cool addition to bumplot!

@machow
machow marked this pull request as draft October 2, 2025 14:07
@machow

machow commented Oct 2, 2025

Copy link
Copy Markdown
Author

Thanks for getting back on this! The TLDR is I'll work on getting this PR ready (and check it with Hassan)! It's been helpful to dig a bit more into matplotlib, and also see narwhals plugged into bumplot.

it makes sense for geom_bump() to be in bumplot and think of it as a plotnine extension

That seems great to me! A lot of my interest is in how it gets extended, so I'll largely just try to squirrel away notes on extensions to add to the plotnine user guide 😁.

In ggbump, they included a geom_sigmoid() function, and I think that makes a little more sense to users than geom_bezier() (in terms of the name)?

geom_sigmoid() seems great (I have to admit I didn't know about ggbump 😅)

@JosephBARBIERDARNAL

Copy link
Copy Markdown
Contributor

(I see you're working on this, thanks! feel free to ping me whenever you need review/help/whatever)

@machow

machow commented Nov 7, 2025

Copy link
Copy Markdown
Author

Alright -- after looking a bit more at ggbump, I've implemented a stat called stat_rank to do the ordinal ranking. So basically:

  • geom_bump: uses stat rank by default
  • geom_bezier: doesn't use rank

RE bezier vs sigmoid: I looked a bit and it seems bezier curves might not use a sigmoid function? I left it as bezier, in case this detail is important. But I didn't look into the bezier code, and am happy to change it if sigmoid seems like a better name!

@has2k1 do you want to take a look at this, in case I messed up any plotnine bits? I added you as a collaborator on the fork, so feel free to push updates!

@machow

machow commented Nov 7, 2025

Copy link
Copy Markdown
Author

@JosephBARBIERDARNAL thanks for approving the CI run! One piece I wanted to check -- do you have a sense for how you might want this tested? I'm happy to try reproducing any examples / creating plots that use any paths (e.g. setting the different aesthetics!)

edit:

here's the code I used to test the geom!

# %%
import pandas as pd

from plotnine import (
    ggplot,
    aes,
    geom_point,
    theme_minimal,
    scale_color_manual,
    scale_fill_manual,
    scale_y_reverse,
)
from bumplot.geoms import geom_bump, geom_bezier

data = pd.DataFrame(
    {
        "x": [2020, 2021, 2022, 2023],
        "A": [10, 50, 20, 80],
        "B": [40, 30, 60, 10],
        "C": [90, 20, 70, 40],
    }
)
data_long = data.melt(id_vars="x", value_vars=["A", "B", "C"])

(
    ggplot(data_long, aes("x", "value"))
    + geom_bump(aes(color="variable"), size=1)

    # note the rank stat used below
    + geom_point(aes(fill="variable"), size=3, color="black", stat="rank")

    # match readme plot ----
    + theme_minimal()
    + scale_fill_manual(values=["#ffbe0b", "#ff006e", "#3a86ff"])
    + scale_color_manual(values=["#ffbe0b", "#ff006e", "#3a86ff"])
    # TODO: scale y reverse showing negative numbers?
    # + scale_y_reverse()
)

Comment thread bumplot/geoms.py

"""

# TODO: I get an error if I don't set geom here,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@has2k1 I got an error if I didn't have "geom" in the DEFAULT_PARAMS. Do you know if all stats are required to have a default geom, or is there a way to not have a default?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes, all stats should have a default geom. The goal of every geom or stat is to create a layer, they simplify creating layers. A layer has both a geom and a stat. That is why geoms have default stats and stats have default geoms.

@JosephBARBIERDARNAL

Copy link
Copy Markdown
Contributor

RE bezier vs sigmoid: I looked a bit and it seems bezier curves might not use a sigmoid function? I left it as bezier, in case this detail is important. But I didn't look into the bezier code, and am happy to change it if sigmoid seems like a better name!

You're right about sigmoid, it does not make that much sense mathematically. It just sounds more intuitive for users (or not?), so geom_bezier() is fine for me!

@JosephBARBIERDARNAL

JosephBARBIERDARNAL commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

@JosephBARBIERDARNAL thanks for approving the CI run! One piece I wanted to check -- do you have a sense for how you might want this tested? I'm happy to try reproducing any examples / creating plots that use any paths (e.g. setting the different aesthetics!)

I'm not entirely sure what is the best way to test a plotnine object, but you can take inspirations of what I did in tests/test_main.py with test_bumplot() where I check the number PathCollection (e.g., scatter plots), etc. Maybe also check that categorical variables are fine for the x axis?

Edit while I'm writing: I'll make a todo for this PR at the top of this page with everything I think make sense.

@has2k1

has2k1 commented Nov 10, 2025

Copy link
Copy Markdown

I'm not entirely sure what is the best way to test a plotnine object, ...

With plotnine you can also get the Axes object and inspect it for the expected artists.

p = ggplot(...) + geom_bump(...)
fig = p.draw()
ax = fig.get_axes()[0]
...

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.

3 participants