nbimplot
Jupyter-native, ImPlot-powered plotting for very large arrays.
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:
- Dear ImGui: https://github.com/ocornut/imgui
- ImPlot: https://github.com/epezent/implot
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:
xandymust be 1D, finite, and equal length.xmust 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"andy="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/xAxisselects 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_linelinesscatter,bubbles,stairs,stems,digitalbars,bar_groups,bars_h,shadederror_bars,error_bars_hinf_lines,vlines,hlineshistogram,histogram2d,heatmap,image,pie_chartcandlestick,ohlc,quiver,contour,waterfall,spectrogramtext,annotation,dummytag_x,tag_y,colormap_slider,colormap_button,colormap_selectordrag_line_x,drag_line_y,drag_point,drag_rectdrag_drop_plot,drag_drop_axis,drag_drop_legend
For a broader cookbook, see docs/EXAMPLES.md.
Search-focused guides:
docs/FAST_JUPYTER_PLOTTING.mddocs/MILLION_POINT_NOTEBOOK_PLOTTING.mddocs/WEBAPP_INTEGRATION.mddocs/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.ipynbnotebooks/nbimplot_api_gallery.ipynbnotebooks/nbimplot_benchmarks.ipynbnotebooks/nbimplot_colab_complete.ipynb(Colab-ready complete examples)
Open the Colab notebook directly:
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:
- Emscripten SDK (
emcmake,emcc) - CMake >= 3.20
- Dear ImGui sources (
imgui): https://github.com/ocornut/imgui - ImPlot sources (
implot): https://github.com/epezent/implot
Build:
scripts/build_wasm.sh
Expected outputs in nbimplot/wasm/:
nbimplot_wasm.jsnbimplot_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06d32cfd9a86a27dc1484f268329d667f1d2f13f972bd5e6c88b0d68bf4f0eb6
|
|
| MD5 |
20038acdfafd0298af66aa4085afe6aa
|
|
| BLAKE2b-256 |
c4c70b3daf1d7204d98681335779dda21d06dcfeceace0184b1d7e58416d387d
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57e401cd71fef0784462112969c9dee35b6f6bedae779420d6cf02061e1a1176
|
|
| MD5 |
073faa66be23458c7b3c60963d9de38f
|
|
| BLAKE2b-256 |
caccede8548c5e689fccf4c7ff0c5d1b249e708403333e54e8e779fc30633c00
|