Skip to main content

nbimplot

Jupyter-native, ImPlot-powered plotting for very large arrays.

PyPI npm GitHub Demo

nbimplot is built around three constraints:

  • notebook cell rendering only (no native windows, no side process)
  • binary transfer (numpy -> bytes -> wasm heap)
  • WASM-owned interaction, state, and LOD

The runtime is strict: ImPlot + WASM + WebGL2 are required.

Project note: nbimplot was vibe coded with Codex, then hardened with tests, packaging checks, and runtime validation.

nbimplot now has two surfaces:

  • nbimplot: Python/Jupyter notebook package
  • @nbimplot/web: standalone browser package for web apps

Public web demo:

AI/agent-readable docs:

Upstream Libraries

nbimplot is built on top of these upstream projects:

Thanks

Special thanks to the ImPlot and Dear ImGui maintainers and contributors for building and maintaining the core libraries that make nbimplot possible.

Install

python -m pip install -U nbimplot

Minimum recommended widget/runtime stack:

python -m pip install -U "nbimplot>=0.1.11" "anywidget>=0.9.21" ipywidgets jupyterlab_widgets

Compatibility

  • Python >=3.10
  • Jupyter widget stack: anywidget, ipywidgets, traitlets
  • Frontend: JupyterLab/Notebook with widget manager enabled
  • Browser/GPU: WebGL2 required
  • Runtime mode: strict WASM + ImPlot only (no JS renderer fallback)

Quick Start

import numpy as np
import nbimplot as ip

x = np.linspace(0, 100, 1_000_000, dtype=np.float32)
y = np.sin(x)

p = ip.Plot(width=900, height=450, title="Signal")
h = p.line("mid", y, x=x)
p.show()

# Update in place, then redraw
h.set_data((0.8 * y).astype(np.float32), x=x)
p.render()

Direct Web App Usage

Use @nbimplot/web when you want ImPlot/WASM rendering in a normal browser application without Jupyter:

npm install /path/to/nbimplot/packages/web

After publishing the web package to npm:

npm install @nbimplot/web
import { createPlot } from "@nbimplot/web";

const plot = await createPlot("#plot", {
  width: 900,
  height: 450,
  title: "Signal",
});

const x = new Float32Array(1_000_000);
const y = new Float32Array(x.length);
for (let i = 0; i < y.length; i += 1) {
  x[i] = i * 0.001;
  y[i] = Math.sin(x[i]);
}

const h = plot.line("mid", y, { x });
plot.render();

h.setData(y, { x });
plot.dispose();

The web package lives in packages/web. See docs/WEB.md and packages/web/examples/plain/index.html.

The repository also includes a full Next.js examples gallery that can run locally or as a static GitHub Pages site:

npm install
npm run dev -- --hostname 0.0.0.0 --port 3001

The gallery lazy-loads examples as their cards approach the viewport and releases offscreen canvases to keep active WebGL contexts bounded.

For GitHub Pages, the workflow in .github/workflows/pages.yml builds with:

NEXT_PUBLIC_BASE_PATH=/nbimplot npm run build

GitHub Pages should be configured once as:

  • Source: Deploy from a branch
  • Branch: gh-pages
  • Folder: /root

Interaction Defaults

  • initial X/Y view auto-fits to available data
  • double-click inside plot area resets view (autoscale)
  • right-drag box zoom, wheel zoom, drag pan, legend toggle
  • hover, click, drag-tool, selection, view-change, and performance callbacks are exposed from the WASM/ImPlot event path

Core API

import numpy as np
import nbimplot as ip

x = np.linspace(0, 10_000, 200_000, dtype=np.float32)
y = np.random.randn(x.size).astype(np.float32).cumsum()

p = ip.Plot(width=1000, height=420, title="Core API")
h = p.line("price", y, x=x, color="#22c55e", line_weight=2.0, marker="none")
p.set_plot_flags(no_legend=False, no_menus=False, no_box_select=False)
p.set_colormap("Viridis")
p.show()

# Later update
h.set_data((y * 1.01).astype(np.float32), x=x)
p.render()

Explicit X Data

Line plots accept explicit x coordinates on both public surfaces:

h = p.line("signal", y, x=x)
h.set_data(y_new, x=x_new)
const h = plot.line("signal", y, { x });
h.setData(yNew, { x: xNew });

Rules:

  • x and y must be 1D, finite, and equal length.
  • x must be sorted in non-decreasing order so the WASM LOD path can binary-search the visible range.
  • If a custom-x line keeps the same length, h.set_data(y_new) / h.setData(yNew) preserves the existing x buffer.
  • x_axis / xAxis selects the ImPlot axis slot (x1, x2, x3); it is not the x-data argument.
  • Streaming append uses implicit sample indices. For custom x streaming, update with set_data(y, x=x) / setData(y, { x }).

Interaction Callbacks

selected = {}

def on_hover(plot, event):
    print(event["series_name"], event["index"], event["x"], event["y"])

def on_click(plot, event):
    print("clicked", event["button"], event["x"], event["y"])

def on_select(plot, event):
    selected["event"] = event
    exact = plot.indices_for_selection(event)
    print({series_id: idx.size for series_id, idx in exact.items()})

p.on_hover(on_hover)
p.on_click(on_click)
p.on_select(on_select)

Selection callbacks include the ImPlot selection rectangle and per-series x-index ranges computed in WASM. indices_for_selection(...) applies the y bounds only when requested and returns exact NumPy index arrays.

Standalone web apps expose the same interaction layer:

plot.onHover((event) => console.log(event.seriesName, event.index, event.x, event.y));
plot.onClick((event) => console.log(event.button, event.x, event.y));
plot.onSelection((event) => {
  const exact = plot.indicesForSelection(event);
  console.log([...exact.entries()].map(([token, indices]) => [token, indices.length]));
});

Common Examples

1) Line + Streaming

import numpy as np
import nbimplot as ip

p = ip.Plot(width=1000, height=380, title="Streaming")
h = p.stream_line("ticks", capacity=200_000, initial=np.zeros(1000, dtype=np.float32))
p.show()

chunk = np.random.randn(20_000).astype(np.float32)
h.append(chunk)
p.render()

2) Scatter / Bars / Histogram

import numpy as np
import nbimplot as ip

rng = np.random.default_rng(7)
x = rng.normal(0, 1, 4000).astype(np.float32)
y = (0.5 * x + 0.2 * rng.normal(size=x.size)).astype(np.float32)

p = ip.Plot(width=1100, height=420, title="Stat Plots")
p.scatter("cloud", y, x=x, size=2.0)
p.vlines("cuts", np.array([-1.0, 1.0], dtype=np.float32))
p.hlines("zero", np.array([0.0], dtype=np.float32))
p.show()

p2 = ip.Plot(width=1100, height=360, title="Histogram")
p2.histogram("x-dist", x, bins=60)
p2.show()

3) Heatmap / Histogram2D / Image

import numpy as np
import nbimplot as ip

rng = np.random.default_rng(0)
z = rng.normal(size=(50, 80)).astype(np.float32)

p = ip.Plot(width=1100, height=420, title="Heatmap")
p.set_colormap("Plasma")
p.heatmap(
    "z",
    z,
    label_fmt="",  # empty format disables cell text
    show_colorbar=True,
    colorbar_label="Intensity",
    colorbar_format="%.3f",
)
p.show()

x = rng.normal(size=200_000).astype(np.float32)
y = (0.3 * x + rng.normal(size=x.size)).astype(np.float32)
p2 = ip.Plot(width=1100, height=420, title="Histogram2D")
p2.histogram2d(
    "h2d",
    x,
    y,
    x_bins=100,
    y_bins=80,
    label_fmt="",
    show_colorbar=True,
    colorbar_label="Count",
)
p2.show()

4) Subplots

import numpy as np
import nbimplot as ip

sp = ip.Subplots(
    2,
    2,
    title="Dashboard",
    width=1100,
    height=760,
    link_rows=True,
    link_cols=True,
    share_items=True,
)

t = np.linspace(0, 30, 4000, dtype=np.float32)
sp.subplot(0, 0).line("sin", np.sin(t))
sp.subplot(0, 1).scatter("noise", np.random.randn(3000).astype(np.float32))
sp.subplot(1, 0).bars("bars", np.abs(np.random.randn(120)).astype(np.float32))
sp.subplot(1, 1).histogram("hist", np.random.randn(20_000).astype(np.float32), bins=50)
sp.show()

Plot and Primitive Coverage

Implemented plot/primitive APIs include:

  • line, stream_line
  • scatter, bubbles, stairs, stems, digital
  • bars, bar_groups, bars_h, shaded
  • error_bars, error_bars_h
  • inf_lines, vlines, hlines
  • histogram, histogram2d, heatmap, image, pie_chart
  • text, annotation, dummy
  • tag_x, tag_y, colormap_slider, colormap_button, colormap_selector
  • drag_line_x, drag_line_y, drag_point, drag_rect
  • drag_drop_plot, drag_drop_axis, drag_drop_legend

For a broader cookbook, see docs/EXAMPLES.md.

Search-focused guides:

  • docs/FAST_JUPYTER_PLOTTING.md
  • docs/MILLION_POINT_NOTEBOOK_PLOTTING.md
  • docs/WEBAPP_INTEGRATION.md
  • docs/POSITIONING.md

View, Axes, and Performance Controls

  • p.set_view(x_min, x_max, y_min, y_max)
  • p.autoscale()
  • p.set_axis_scale(x="linear|log", y="linear|log")
  • p.set_axis_state("x2|x3|y2|y3", enabled=True|False, scale="linear|log|time")
  • p.set_secondary_axes(x2=..., x3=..., y2=..., y3=...)
  • p.set_time_axis("x1|x2|x3|y1|y2|y3")
  • p.set_axis_label(...), p.set_axis_format(...)
  • p.set_axis_ticks(...), p.clear_axis_ticks(...)
  • p.set_axis_limits_constraints(...), p.set_axis_zoom_constraints(...), p.set_axis_link(...)
  • p.hide_next_item()
  • p.on_perf_stats(callback, interval_ms=500)
  • p.on_tool_change(callback)
  • p.on_selection_change(callback)
  • p.on_select(callback)
  • p.on_hover(callback)
  • p.on_click(callback)
  • p.indices_for_selection(selection, series=None)

Example Notebooks

  • notebooks/nbimplot_examples.ipynb
  • notebooks/nbimplot_api_gallery.ipynb
  • notebooks/nbimplot_benchmarks.ipynb
  • notebooks/nbimplot_colab_complete.ipynb (Colab-ready complete examples)

Open the Colab notebook directly:

Open In Colab

Troubleshooting

Failed to load model class 'AnyModel' from module 'anywidget'

This is usually a server-kernel env mismatch or stale lab assets.

python -m pip install -U "nbimplot>=0.1.11" "anywidget>=0.9.21" ipywidgets jupyterlab_widgets
jupyter lab clean

Then restart the full JupyterLab server.

Quick verification:

import nbimplot as ip, anywidget, sys
print("python:", sys.executable)
print("anywidget:", anywidget.__version__)
print("has Plot:", hasattr(ip, "Plot"))

Unable to enable ImPlot in the WASM core or WebGL context errors

Strict mode requires WebGL2.

!!document.createElement("canvas").getContext("webgl2")

If this is false, run from a local desktop browser session with GPU acceleration enabled.

Known Limitations

  • Rendering requires WebGL2-capable browser/runtime.
  • Strict mode is enforced; non-ImPlot or non-WASM fallback is disabled.
  • Headless/browser-restricted environments may fail to create GL context.

Build WASM Core

Prerequisites:

Build:

scripts/build_wasm.sh

Expected outputs in nbimplot/wasm/:

  • nbimplot_wasm.js
  • nbimplot_wasm.wasm

Build explicitly with local ImGui/ImPlot sources:

NBIMPLOT_WITH_IMPLOT=ON \
NBIMPLOT_IMGUI_DIR=/path/to/imgui \
NBIMPLOT_IMPLOT_DIR=/path/to/implot \
scripts/build_wasm.sh

If you use vendored deps:

NBIMPLOT_WITH_IMPLOT=ON scripts/build_wasm.sh

Performance Model

  • raw path when visible points <= 3 * pixel_width
  • min/max LOD when visible points > 3 * pixel_width
  • LOD computed in WASM, complexity scales with screen pixels

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nbimplot-0.1.11.tar.gz (401.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nbimplot-0.1.11-py3-none-any.whl (402.8 kB view details)

Uploaded Python 3

File details

Details for the file nbimplot-0.1.11.tar.gz.

File metadata

  • Download URL: nbimplot-0.1.11.tar.gz
  • Upload date:
  • Size: 401.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for nbimplot-0.1.11.tar.gz
Algorithm Hash digest
SHA256 65494a4ddedb06ed2a4a1d46ee70742e96681587be29a47f1c232355e992ee99
MD5 0a49d77b1c85206a5eee4e687f35634a
BLAKE2b-256 86f9ee79850079edb798845fdc6be17769e7af54d1ce56f8579253d89a5af834

See more details on using hashes here.

File details

Details for the file nbimplot-0.1.11-py3-none-any.whl.

File metadata

  • Download URL: nbimplot-0.1.11-py3-none-any.whl
  • Upload date:
  • Size: 402.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for nbimplot-0.1.11-py3-none-any.whl
Algorithm Hash digest
SHA256 e6e4eff043feca89b29a49375578563c2dcf902ed39b437c1d0ac978f83eb256
MD5 950af0cb89eb8ad51dca14339bd5ec30
BLAKE2b-256 7043fc054218b6736346648d4de722e06274f8cf7c57268f709d7d83705e8e0a

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.13

2 files

This release

0.1.11 This release

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page