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.13" "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()

Arrays Or DataFrame Columns

The same methods accept NumPy arrays, pandas/polars Series, or dataframe-like objects with column selectors. There is no separate *_df() API:

import pandas as pd

df = pd.DataFrame({"time": x, "mid": y, "vwap": y2, "returns": returns})
df_next = df.assign(mid=(0.8 * df["mid"]).astype("float32"))

p = ip.Plot(width=900, height=450, title="DataFrame Columns")
h = p.line("mid", df, x="time", y="mid")
p.line("vwap", df, x="time", y="vwap")
p.histogram("returns", df, y="returns", bins=80)
p.show()

h.set_data(df_next, x="time", y="mid")
p.render()

Column extraction is a Python input-normalization step only. The frontend still receives contiguous float32 binary buffers, and the WASM core still owns LOD and rendering.

Notebook Cell Renderer

Plot, Subplots, AlignedPlots, and Dashboard implement a Jupyter widget MIME renderer. Put the object as the last expression in a notebook cell to render it directly:

p = ip.Plot(width=900, height=450, title="Cell renderer")
p.line("mid", y, x=x)
p

Use p.show() when you want to display the plot before the last line of a cell, or when the final expression is a printout, table, or another object.

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.
  • With a dataframe-like positional argument, x="column" and y="column" select columns; there is no separate dataframe API.
  • 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 supports explicit x chunks: h.append(y_chunk, x=x_chunk) / h.append(yChunk, { x: xChunk }).

Batch Lines, Datetime, And Categories

Use lines(...) to upload several line series through one notebook widget message. This reduces Python-to-browser overhead when creating dashboards with many related signals:

p = ip.Plot(width=1100, height=420, title="Batch Lines")
handles = p.lines(
    {
        "mid": {"x": ts, "y": mid},
        "vwap": {"x": ts, "y": vwap, "color": "#b74b2b"},
    },
    line_weight=1.5,
)
p.show()
const handles = plot.lines({
  mid: { x: timestamps, y: mid },
  vwap: { x: timestamps, y: vwap, color: "#b74b2b" },
});

numpy.datetime64, pandas datetime columns, Python datetime values, and JavaScript Date values are normalized to ImPlot time axes automatically. Categorical x values are converted to integer tick locations with labels:

p = ip.Plot(width=900, height=360, title="Datetime + Categories")
p.line("sessions", df, x="timestamp", y="latency")
p.scatter("rank", np.array([4, 7, 3], dtype=np.float32), x=["A", "B", "C"])
p.show()
plot.line("sessions", latency, { x: dates });
plot.scatter("rank", new Float32Array([4, 7, 3]), { x: ["A", "B", "C"] });

For high-frequency modern timestamps, prefer relative numeric seconds when sub-second precision matters because the hot rendering path stores float32.

Themes And Standalone HTML

The C++/WASM layer owns the theme presets. Available presets are nbimplot, notebook, publication, finance, lab, and dark-terminal:

p.set_theme("finance")
p.set_colormap("Viridis")
plot.setTheme("publication");
plot.setColormap("Plasma");

Notebook plots can export a standalone HTML file that reloads the same data through @nbimplot/web and the WASM/ImPlot core:

p.export_html("signal.html")
const html = plot.exportHTML({ title: "Signal Export" });

The HTML export is for sharing a fixed plot state. It still requires browser WebGL2 and WASM asset loading; it is not a static SVG/PNG fallback.

PNG Export

Notebook widgets can request a browser-side PNG download of the current canvas:

p.export_png("nbimplot-signal.png")
p.copy_png_to_clipboard()

Web apps can download directly or keep the image for app-specific workflows:

await plot.downloadPNG("nbimplot-signal.png");
const dataUrl = plot.toDataURL("image/png");
const blob = await plot.toBlob("image/png");
await plot.copy_png_to_clipboard();

Export redraws the existing WASM/ImPlot canvas immediately before reading pixels; it does not use a JavaScript plotting fallback.

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 helper workflow:

bounds = p.selection_bounds(selected["event"])
indices = p.indices_for_selection(selected["event"], series="signal")
p.highlight_selection(selected["event"], series="signal", name="picked")
csv_text = p.export_csv_selection(selected["event"], series="signal")

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()

5) Specialty Scientific And Financial Plots

import numpy as np
import nbimplot as ip

rng = np.random.default_rng(22)
x = np.arange(120, dtype=np.float32)
close = (100 + rng.normal(0, 1, x.size).cumsum()).astype(np.float32)
open_ = np.r_[close[0], close[:-1]].astype(np.float32)
high = (np.maximum(open_, close) + rng.random(x.size) * 1.8).astype(np.float32)
low = (np.minimum(open_, close) - rng.random(x.size) * 1.8).astype(np.float32)

p = ip.Plot(width=1100, height=420, title="Finance")
p.set_theme("finance")
p.candlestick("candles", x=x, open=open_, high=high, low=low, close=close)
p.ohlc("ohlc", x=x, open=open_, high=high, low=low, close=close)
p.show()
grid = np.linspace(-3, 3, 80, dtype=np.float32)
xx, yy = np.meshgrid(grid, grid)
z = np.sin(xx * yy).astype(np.float32)

p = ip.Plot(width=1100, height=420, title="Scientific")
p.set_colormap("Viridis")
p.contour("contour", z, levels=np.linspace(-1, 1, 9, dtype=np.float32))
p.quiver("field", xx[::8, ::8].ravel(), yy[::8, ::8].ravel(),
         -yy[::8, ::8].ravel(), xx[::8, ::8].ravel(), scale=0.08, normalize=True)
p.waterfall("waterfall", z[::4], scale=0.18)
p.spectrogram("spectrogram", z, label_fmt="", show_colorbar=True)
p.show()

These specialty methods use ImPlot/WASM draw paths. Candles, OHLC, quiver, and contour lines use ImPlot coordinate transforms and draw-list item integration; spectrograms reuse ImPlot heatmaps.

6) Dashboard, State, Theme, and Linked Crosshair

t = np.linspace(0, 30, 4000, dtype=np.float32)
dash = ip.Dashboard(2, 2, title="Realtime Desk", link_x=True, theme="nbimplot")
dash.set_linked_crosshair("desk", axis="x")
dash.subplot(0, 0).line("a", np.sin(t), x=t)
dash.subplot(0, 1).line("b", np.cos(t), x=t)
dash.show()

state = dash.get_state(include_data=True)
json_text = dash.export_json_state(include_data=True)
dash.set_state(state)
dash.set_theme("notebook")
dash.export_csv_selection({"x_min": 0, "x_max": 1, "y_min": -1, "y_max": 1})
dash.copy_png_to_clipboard()
plot.setTheme("nbimplot");
plot.setLinkedCrosshair("desk", { axis: "x" });
const bounds = plot.selectionBounds(selection);
plot.highlightSelection(selection, handle, { name: "picked" });
const csv = plot.exportCSVSelection(selection, handle);
const state = plot.getState({ includeData: true });
plot.setState(state);
const json = plot.exportJSONState({ includeData: true });

Plot and Primitive Coverage

Implemented plot/primitive APIs include:

  • line, stream_line
  • lines
  • 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
  • candlestick, ohlc, quiver, contour, waterfall, spectrogram
  • 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_view_change(callback)
  • 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)
  • p.selection_bounds(selection)
  • p.highlight_selection(selection, series=None)
  • p.export_csv_selection(selection, series=None)
  • p.get_state(include_data=False) / p.set_state(state)
  • p.export_json_state(include_data=False)
  • p.export_html("plot.html")
  • p.set_theme("nbimplot")
  • p.set_linked_crosshair("group", axis="x|y|xy")
  • p.export_png(filename="nbimplot.png")
  • p.copy_png_to_clipboard()

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.13" "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
  • large line series maintain a reusable multiresolution LOD pyramid so pan/zoom recomputes buckets from cached summaries instead of rescanning every raw point

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.13.tar.gz (427.6 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.13-py3-none-any.whl (428.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nbimplot-0.1.13.tar.gz
  • Upload date:
  • Size: 427.6 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.13.tar.gz
Algorithm Hash digest
SHA256 06d32cfd9a86a27dc1484f268329d667f1d2f13f972bd5e6c88b0d68bf4f0eb6
MD5 20038acdfafd0298af66aa4085afe6aa
BLAKE2b-256 c4c70b3daf1d7204d98681335779dda21d06dcfeceace0184b1d7e58416d387d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: nbimplot-0.1.13-py3-none-any.whl
  • Upload date:
  • Size: 428.2 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.13-py3-none-any.whl
Algorithm Hash digest
SHA256 57e401cd71fef0784462112969c9dee35b6f6bedae779420d6cf02061e1a1176
MD5 073faa66be23458c7b3c60963d9de38f
BLAKE2b-256 caccede8548c5e689fccf4c7ff0c5d1b249e708403333e54e8e779fc30633c00

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.13 This release

2 files

0.1.11

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