ninejs, bringing interactivity to plotnine

Jul 2026

5 min

ninejs is a Python package that adds tooltips, hover effects, click events, and standalone HTML export to plotnine charts.

Why another plotting package?

I use plotnine a lot. It has a grammar that feels natural when you want to build a chart layer by layer, and it fits nicely into Python data workflows. But once a chart is rendered, it is static. That is often fine, until you want a reader to inspect a point, follow a line, or connect the same observation across multiple panels.

ninejs is my attempt to keep the plotnine workflow and add a thin layer of browser interactivity on top of it. You build the chart as a normal plotnine object, add a few extra aesthetics such as tooltip, hover_group, hover_key, or on_click, then export a standalone HTML file.

ninejs isn't a plotting library but rather a plotnine extension. There is no plotting engine and just adds the interactivity touch, which keeps it fairly lightweight!

Introducing: ninejs

The chart below is a regular plotnine chart; the only difference is that the data mapped in aes() also contains interaction metadata.

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

from ninejs import css, interactive, save

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

interactive(gg, hover_nearest=True)

The code is almost identical to the original plotnine chart. The interactive part is not a separate JavaScript chart specification; it is attached to the same variables that already define the plot.

Linked hover between panels

One feature I wanted early was linked hover. If the same row appears in two views, hovering it in one view should highlight it in the other.

python
import plotnine as gg
from plotnine.data import mtcars

from ninejs import css, interactive, save

mtcars = mtcars.rename(columns={"name": "carname"}).copy()
mtcars["car_id"] = mtcars["carname"].str.replace(r"\W+", "-", regex=True)
mtcars["tooltip"] = [
    f"{carname}<br>MPG: {mpg}<br>Displacement: {disp}<br>Quarter mile: {qsec}s"
    for carname, mpg, disp, qsec in zip(
        mtcars["carname"],
        mtcars["mpg"],
        mtcars["disp"],
        mtcars["qsec"],
        strict=True,
    )
]

mapping = gg.aes(tooltip="tooltip", hover_group="car_id", hover_key="car_id")

scatter_plot = (
    gg.ggplot(mtcars, gg.aes("disp", "qsec"))
    + gg.geom_point(mapping=mapping, size=3, fill="#333333", color="#333333")
    + gg.theme_minimal(base_size=10)
)

bar_plot = (
    gg.ggplot(mtcars, gg.aes("reorder(carname, mpg)", "mpg"))
    + gg.geom_col(mapping=mapping, fill="#8ecae6", color="white", size=0.35)
    + gg.coord_flip()
    + gg.theme_minimal(base_size=10)
)

plot = scatter_plot | bar_plot

(
    interactive(plot, hover_nearest=True)
    + css(from_dict={".plot-element.hovered": {"fill": "#d62828"}})
)

The important part is hover_key. The scatter point and the bar are two different SVG elements in two different panels, but they carry the same key. ninejs uses that key in the browser to apply the hover state to both elements at the same time.

What it can do

The package currently supports all common plotnine geoms such as points, lines, paths, bars, histograms, areas, ribbons, and maps. Facets also work, and the generated output is a self-contained HTML document with the required JavaScript and CSS included.

You can also bring your own styling and behavior. css() lets you override the tooltip or hover states, while javascript() and on_click let you add custom browser-side behavior when a chart needs something more specific.

Learn more

This is just a short overview of ninejs. You can read the documentation here, browse the gallery of examples here, and find the source code here.