Skip to content


ninejs logo

ninejs

Bringing ✨interactivity✨ to plotnine.

Coverage Python Versions

ninejs adds interactive behavior to plotnine charts with a minimal API. You can attach tooltips, hover grouping, and on click events directly from aes(), then export the result as a standalone HTML plot. All of this with just 2 lines of code!

Quick start

Specify the tooltip and hover_group aesthetic mappings, and then pass your plotnine chart to interactive():

from plotnine import ggplot, aes, geom_point, theme_minimal
from plotnine.data import anscombe_quartet

from ninejs import interactive, save

gg = (
    ggplot(
        data=anscombe_quartet,
        mapping=aes(x="x", y="y", color="dataset", tooltip="dataset", hover_group="dataset"),
    )
    + geom_point(size=7, alpha=0.5)
    + theme_minimal()
)

interactive(gg) + save("plot.html")

Installation

pip install ninejs
uv add ninejs
pixi add ninejs

Examples

map_plot = (
    ggplot()
    + geom_map(
        data=regions,
        mapping=aes(fill="region", tooltip="tooltip", hover_key="region"),
    )
    + theme_void()
)

bar_plot = (
    ggplot(bars, aes("reorder(region, value)", "value"))
    + geom_col(aes(fill="region", tooltip="tooltip", hover_key="region"))
    + theme_void()
)

plot = map_plot | bar_plot

interactive(plot) + save("docs/iframes/linked-map-bars.html")

gg = (
    ggplot(df, aes(x="date", y="value", fill="group", tooltip="group"))
    + geom_area(alpha=0.8)
    + theme_minimal()
    + labs(title="Monthly Growth by Product", x="Date", y="Value", fill="Category")
    + scale_x_date(date_labels="%b")
    + theme(
        figure_size=(10, 5),
        plot_title=element_text(size=16, weight="bold"),
        axis_title=element_text(size=11),
        axis_text=element_text(size=10),
        legend_title=element_text(size=11),
        legend_text=element_text(size=10),
    )
)

interactive(gg) + save("docs/iframes/area-chart.html")

from plotnine.data import economics

df = economics[economics["date"].dt.year >= 2000].copy()
df["tooltip"] = [
    f"{date:%b %Y}<br>Saving rate: {value:.1f}%"
    for date, value in zip(df["date"], df["psavert"], strict=True)
]

gg = (
    ggplot(df, aes("date", "psavert"))
    + geom_line(color="#2f6f73", size=1)
    + geom_point(aes(tooltip="tooltip"), color="#d95f02", size=3, alpha=0.7)
    + labs(title="U.S. personal saving rate since 2000", x="", y="Saving rate (%)")
    + theme_minimal()
)

interactive(gg, hover_nearest=True) + save("docs/iframes/saving-rate.html")

anscombe_quartet["open_url"] = "window.open('https://www.ysunflower.com/')"

gg = (
    ggplot(
        data=anscombe_quartet,
        mapping=aes(
            x="x",
            y="y",
            color="dataset",
            tooltip="dataset",
            on_click="open_url",
        ),
    )
    + geom_point(size=7, alpha=0.7)
    + theme_minimal()
)

interactive(gg) + save("docs/iframes/on-click-new-window.html")

gg = (
    ggplot(df, aes(x="category", y="value", tooltip="tooltip"))
    + geom_col()
    + theme_classic()
)

interactive(gg) + save("docs/iframes/bar.html")

gg = (
    ggplot(anscombe_quartet, aes("x", "y", tooltip="x"))
    + geom_point(color="sienna", fill="orange", size=3)
    + geom_smooth(method="lm", se=False, fullrange=True, color="steelblue", size=1)
    + facet_wrap("dataset")
    + labs(title="Anscombe’s Quartet")
    + scale_y_continuous(breaks=(4, 8, 12))
    + coord_fixed(xlim=(3, 22), ylim=(2, 14))
    + theme_tufte(base_family="Futura", base_size=16)
    + theme(
        axis_line=element_line(color="#4d4d4d"),
        axis_ticks_major=element_line(color="#00000000"),
        axis_title=element_blank(),
        panel_spacing=0.09,
    )
)

interactive(gg) + save("docs/iframes/facet_wrap.html")

goo_css = """
svg {filter: contrast(20);}

.point {
    filter: blur(5px);
    animation: goo 3s ease-in-out infinite alternate;
}
.point:nth-child(2n) {animation-duration: 4s;}
.point:nth-child(3n) {animation-duration: 8s;}

@keyframes goo {
    from {transform: translate(-12px, -12px) scale(1);}
    to {transform: translate(12px, 12px) scale(1.3);}
}
"""

interactive(gg) + css(goo_css) + save("docs/iframes/animation-art.html")

This is just a small subset of what's possible with ninejs. Check out the gallery and the guides for more.

Tools for AI and agents

A single-file overview of the ninejs API, written for AI/LLMs and coding agents. This file contains everything an agent needs to know to use ninejs properly!

Download

The file is also available at this URL: llms.txt

A ninejs skill that your agents will know when to use automatically. Once installed, your agent will automatically know when to use the skill, or you can mention it in a prompt (inside Codex / Claude Code) using the $skill-name syntax:

Use $ninejs to make my plotnine chart interactive.
  • Install for Claude Code:
claude plugin marketplace add y-sunflower/skills && claude plugin install ninejs@y-sunflower-skills
  • Install for Codex:
codex plugin marketplace add y-sunflower/skills && codex plugin add ninejs@y-sunflower-skills

Next steps?

For more in-depth explanations and feature overviews, check out: