How ninejs works under the hood
ninejs turns a normal plotnine chart into a self-contained HTML document.
The important idea is simple: plotnine still draws the chart, and ninejs
adds browser behavior on top of the SVG that plotnine produces.
flowchart TD
A["plotnine ggplot"] --> B["interactive(gg)"]
B --> C["draw or reuse Matplotlib figure"]
C --> D["save figure as deterministic SVG"]
B --> E["extract tooltip, hover_group, and on_click mappings"]
D --> F["_InteractivePlot"]
E --> F
F --> G["append css(...) and javascript(...) additions"]
F --> H["terminal export: save, to_html, to_iframe, or show"]
H --> I["Jinja HTML template"]
I --> J["standalone HTML: SVG, JSON config, CSS, D3, DOMPurify, parser"]
J --> K["initPlot() in the browser"]
K --> L["find SVG elements and attach hover/click behavior"]
L --> M["✔ HTML ready page for users"]
The mental model¶
There is no custom plotting engine in ninejs. The chart is still a plotnine chart. plotnine builds the data, creates the Matplotlib artists, and Matplotlib writes the SVG. This is very practical because it means we're 100% sure that the displayed chart is exactly as expected. ninejs then does two extra things:
- It extracts the interactivity metadata from the plotnine object.
- It ships JavaScript that reconnects that metadata to the matching SVG elements in the browser.
That second step is the delicate part. The browser parser depends on the SVG structure emitted by Matplotlib, so changes to SVG parsing should be small, tested, and checked in a browser.
Important
This is possible because an SVG element inside an HTML page is part of the DOM, which means we can attach any browser side behavior to it.
Python side¶
The public entry point is interactive(gg) in ninejs/main.py.
When an interactive plot is created, ninejs:
- Checks that
ggis a plotnineggplot. - Reuses an already drawn figure when possible, otherwise calls
gg.draw()(learn more why here). - Reads the special aesthetics from the plot:
tooltip,hover_group, andon_click. - Extracts layer-level and panel-level tooltip data for supported geoms.
- Saves the Matplotlib figure to SVG.
- Builds a JSON configuration that describes the axes, labels, hover groups, click handlers, and hover options.
The supported geom names are normalized in ninejs/const.py:
| Plotnine geoms | Browser geom kind |
|---|---|
geom_point, geom_jitter |
points |
geom_line, geom_path, geom_step |
lines |
geom_bar, geom_col, geom_histogram |
bars |
geom_area, geom_ribbon |
areas |
geom_map |
polygons |
Points, bars, jittered points, and polygons usually use one tooltip per row. Lines, paths, steps, areas, and ribbons usually use one tooltip per rendered group.
Browser side¶
It reads the JSON config, creates a PlotSVGParser, then loops over each
configured axes group such as axes_1. For each axes group, it finds the SVG
elements that correspond to supported geom kinds:
| Browser class | SVG selector target |
|---|---|
.point |
Matplotlib PathCollection nodes |
.line |
Matplotlib line2d paths |
.bar |
Matplotlib PolyCollection paths |
.area |
Matplotlib FillBetweenPolyCollection paths |
.polygon |
Matplotlib PatchCollection paths |
The parser adds .plot-element plus the geom-specific class to each matched
node. Hover behavior then uses .hovered and .not-hovered.
Tooltips are written as HTML, but sanitized with DOMPurify before display.
Custom JavaScript from javascript(...) and JavaScript stored in on_click
columns is trusted code and runs directly in the output page.
After editing any PlotParser*.js source file, run:
Note that tests will fail if the minified bundle is stale.
What to test¶
Use the smallest test that covers the behavior you changed:
For SVG parsing or browser behavior, do not rely only on Python tests. Add or update a JavaScript parser test and a browser test when the behavior is visible only after the HTML runs in a browser.