Embedding outside Jupyter#
anyplotlib figures do not require Jupyter, ipywidgets, or the anywidget
runtime. The renderer is a single self-contained ES module
(figure_esm.js) that draws from a plain JSON state dict, so a figure can
live anywhere a browser engine runs: an Electron app, a Tauri/webview
app, an MDI-style multi-window workspace, a kiosk dashboard, or a static
web page.
There are three levels of integration, from zero-Python-at-runtime to a fully live Python backend.
Level 1 — self-contained HTML (no Python at view time)#
Export the figure as a single HTML file with the renderer and all data inlined:
import anyplotlib as apl
import numpy as np
fig, ax = apl.subplots(1, 1, figsize=(800, 500))
ax.imshow(np.load("frame.npy"), cmap="viridis")
fig.save_html("plot.html")
Load it in an Electron window — that’s the whole integration:
const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ width: 840, height: 560 });
win.loadFile('plot.html');
Pan, zoom, overlay widgets, markers, and keyboard shortcuts all work;
Python callbacks (obviously) do not. fig.to_html() returns the same
page as a string if you want to serve or template it yourself.
Level 2 — JS-driven: your app owns the data#
Bundle figure_esm.js into your app (anyplotlib.embed.esm_path()
tells you where to copy it from) and mount figures directly from
JavaScript:
import { mount } from './figure_esm.js';
const handle = mount(document.getElementById('plot-host'), state, {
onEvent: (ev) => {
// every interaction event: pointer_down/up/move, wheel, key_down …
if (ev.event_type === 'pointer_down')
console.log('clicked data coords', ev.xdata, ev.ydata);
},
});
// Live updates — replace one panel's state and it re-renders:
handle.setPanelState(panelId, newPanelState);
handle.resize(900, 600);
handle.dispose(); // remove the figure's DOM
state is the figure-state dict. Generate it from Python once (at build
time or via a one-shot script):
import json, anyplotlib as apl
from anyplotlib.embed import figure_state
fig, ax = apl.subplots(1, 1)
plot = ax.imshow(template_data)
json.dump(figure_state(fig), open("figure_state.json", "w"))
print("panel id:", plot._id) # key for setPanelState
Each mount() call is fully independent — mount as many figures as you
like into separate containers in one window. This is the natural fit for
MDI sub-windows: give every sub-window its own host <div> (or
<webview>/iframe for hard isolation) and call mount per window.
Call handle.resize(w, h) from your sub-window’s resize hook.
Level 3 — live Python backend (full callback support)#
Run Python next to your app (a sidecar process exposing a local WebSocket
is the common Electron pattern) and keep figures fully interactive —
@plot.add_event_handler(...) callbacks fire exactly as in Jupyter.
anyplotlib.embed.FigureBridge is transport-agnostic: you supply
the pipe, it supplies the (key, value) protocol.
Python sidecar (here with the websockets package):
import asyncio, json
import numpy as np
import websockets
import anyplotlib as apl
from anyplotlib.embed import FigureBridge
fig, ax = apl.subplots(1, 1, figsize=(700, 450))
plot = ax.imshow(np.random.rand(256, 256))
cross = plot.add_widget("crosshair", cx=128, cy=128)
async def serve(ws):
loop = asyncio.get_running_loop()
bridge = FigureBridge(fig, send=lambda key, value:
loop.create_task(ws.send(json.dumps({"key": key, "value": value}))))
await ws.send(json.dumps({"snapshot": bridge.snapshot()}))
@cross.add_event_handler("pointer_move") # fires from Electron!
def follow(event):
print("crosshair at", cross.cx, cross.cy)
async for message in ws:
m = json.loads(message)
bridge.receive(m["key"], m["value"]) # JS → Python
asyncio.run(websockets.serve(serve, "localhost", 8765))
Electron renderer:
import { mount } from './figure_esm.js';
const ws = new WebSocket('ws://localhost:8765');
let handle = null;
ws.onmessage = (msg) => {
const m = JSON.parse(msg.data);
if (m.snapshot) {
handle = mount(document.getElementById('plot-host'), m.snapshot, {
// forward every JS-side write (events, view changes) to Python
onSync: (key, value) => ws.send(JSON.stringify({ key, value })),
});
} else if (handle) {
handle.applyUpdate(m.key, m.value); // Python → JS, echo-free
}
};
Any Python-side mutation — plot.set_data(...), markers, titles, layout
changes — streams to the window automatically; drags, clicks, and keys
stream back into your Python callbacks. Echo is suppressed in both
directions by the bridge and applyUpdate.
API reference#
embed.py#
Use anyplotlib figures outside Jupyter — in Electron apps, MDI sub-windows, kiosk dashboards, or any plain web page. No kernel, no ipywidgets, no anywidget runtime in the page.
Three levels of integration#
1. Static / self-contained (no Python at runtime) — export a fully
self-contained HTML page (renderer + data inlined) and load it anywhere a
browser engine runs, e.g. an Electron BrowserWindow or <webview>:
import anyplotlib as apl
fig, ax = apl.subplots(1, 1)
ax.imshow(data)
fig.save_html("plot.html") # win.loadFile('plot.html')
All client-side interactivity (pan, zoom, widgets, markers) works; Python callbacks obviously do not.
2. JS-driven (your app owns the data) — ship figure_esm.js with your
app and mount figures from JavaScript using the exported mount():
import { mount } from './figure_esm.js';
const handle = mount(container, state, { onEvent: ev => ... });
handle.setPanelState(panelId, newPanelState); // live updates
handle.resize(w, h); handle.dispose();
state is the JSON dict produced by figure_state() — generate it
once from Python (build time, or a one-shot script) or construct it in JS.
Each mount() is fully self-contained, so one window can host many
figures (MDI-style) by mounting into separate containers.
3. Live Python backend — run Python alongside your app (sidecar process,
local WebSocket server, …) and keep figures fully interactive with Python
callbacks via FigureBridge, which is transport-agnostic:
# Python side (e.g. behind a websocket)
bridge = FigureBridge(fig, send=lambda key, value: ws.send(
json.dumps({"key": key, "value": value})))
ws.on_message = lambda m: bridge.receive(**json.loads(m))
// JS side
const handle = mount(el, snapshot, {
onSync: (key, value) => ws.send(JSON.stringify({key, value})),
});
ws.onmessage = (m) => { const u = JSON.parse(m.data);
handle.applyUpdate(u.key, u.value); };
See docs/embedding.rst for a complete Electron walkthrough.
- anyplotlib.embed.figure_state(fig)[source]#
Return the figure’s full serialised state as a plain JSON-safe dict.
The dict contains every synced trait —
layout_json,fig_width,fig_height,event_json, and onepanel_<id>_jsonentry per panel — and is exactly what the JSmount(el, state)entry point expects.
- anyplotlib.embed.to_html(fig, *, resizable=True)[source]#
Return a fully self-contained HTML page rendering fig.
The page inlines the renderer and all figure data; it needs no network, kernel, or Python at view time. Client-side interactivity (pan, zoom, overlay widgets) is preserved.
- anyplotlib.embed.save_html(fig, path, *, resizable=True)[source]#
Write
to_html()output to path and return it as aPath.
- anyplotlib.embed.esm_path()[source]#
Return the path to
figure_esm.jsfor bundling into a JS app.Copy (or import) this file into your Electron / web build; it exports
mountandcreateLocalModelalongside the anywidgetrender.- Return type:
- class anyplotlib.embed.FigureBridge(fig, send)[source]#
Bases:
objectTransport-agnostic two-way sync between a live
Figureand a remote JS view mounted withmount(el, state, {onSync}).You supply the pipe (WebSocket, Electron IPC via a sidecar, stdio, …); the bridge supplies the protocol: plain
(key, value)pairs.- Parameters:
Notes
Python → JS: any synced trait change triggers
send(key, value); deliver it tohandle.applyUpdate(key, value)in JS.JS → Python: deliver each JS
onSync(key, value)message toreceive(). Interaction events (event_json) are dispatched to the figure’s callback registries exactly as in Jupyter, so@plot.add_event_handler(...)handlers fire unchanged.Echo is suppressed in both directions.
JS handle reference#
mount(el, state, opts) → handle
|
Replace one panel’s state (dict or JSON string) and re-render it. |
|
Raw model write + sync flush. |
|
Read any model key. |
|
Apply a Python-originated update without
echoing it back through |
|
Resize the figure (CSS pixels). |
|
Remove the figure’s DOM and listeners. |
|
The underlying local model (advanced). |
opts.onEvent(ev) receives parsed interaction events (the same payloads
Python’s Event carries); opts.onSync(key, value)
receives every outbound model write for bridging to Python.
Notes and caveats#
The state dict is the wire format, not a stable public schema — treat panel-state internals as opaque where you can, and prefer regenerating states from Python when upgrading anyplotlib versions.
dispose()removes the figure’s DOM; for hard teardown of all window-level listeners, host each figure in its own iframe/webview and drop the frame (this is also the most robust MDI isolation).One renderer file, no build step:
figure_esm.jshas no imports, so it works with any bundler or directly as a<script type="module">.