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);
},
// 2-D hover readout (position + pixel value) for your own status bar —
// see "Owning the hover readout" below.
onReadout: (info) => { statusEl.textContent = info ? info.text : ''; },
});
// 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.
- class anyplotlib.embed.Ragged(offsets, columns, nav_shape=())[source]#
Bases:
objectA block with a variable number of rows per navigation position.
offsetsis the row-pointer array: positioniowns rowsoffsets[i]up tooffsets[i + 1], so it hasn_positions + 1entries.columnsmaps a name to one value per row.nav_shapegives the navigation grid when it has more than one axis, so a two-dimensional index resolves to the right row span.
- anyplotlib.embed.pack_blocks(blocks)[source]#
Pack arrays into one little-endian byte string plus a manifest.
blocks maps a name to a numpy array (a dense block whose leading axes are the navigation axes) or to a
Ragged. The return is(payload, manifest): the page base64-decodes payload once into a singleArrayBufferand takes a typed-array view per manifest entry, so no block is encoded or copied on its own.
Return a self-contained page whose navigator drives its other panels.
fig_or_state is a live
Figureor the dictfigure_state()returns. blocks is the data the page navigates, in the formpack_blocks()takes. bindings says what each panel does:{panel_id, role: "navigator" | "driven" | "static", widgets: [...], frame: {block, kind: "image" | "disks" | "points3d", radius?, combine?, levels?, width?, height?, colors?}, views: [{label, block, colors?}], overlays: [{block, kind, style, columns?, face_camera?}], reduce: {block, navigator_panel, x?, y?, value?}, readout: {block, names, units}}viewsare a committed result’s alternative frames (a strain map’s epsilon_xx, epsilon_yy, epsilon_xy, omega): the page renders a segmented control that swaps which block the panel’s frame is read from, at whatever position the navigator is already on. Withviews,frame.blockmay be omitted and the first entry is the one shown first. A navigator binding may carryinitial_indexto open somewhere other than the origin.A 3-D panel takes
kind: "points3d"(a dense(M, 3)float32 cloud plus acolorsblock) and a"highlight"overlay marking one point per navigation position;face_cameraon that overlay turns the panel to face the marked point.The renderer, the figure state, the packed data and the bindings are all inlined, so the page needs no network and no Python at view time.
JS handle reference#
mount(el, state, opts) → handle
|
Replace one panel’s state (dict or JSON string) and re-render it. |
|
Merge partial into one panel’s state and re-render.
Values are stored verbatim — markers, |
|
Replace a 2-D panel’s image with raw pixel bytes
( |
|
Paint pending |
|
The panel ids in layout order. |
|
Raw model write + sync flush. |
|
Read any model key. |
|
Apply a Python-originated update without echoing it back
through |
|
Resize the figure (CSS pixels). |
|
Composite to a PNG data URL. Resolves to |
|
The same, synchronously, returning |
|
Add an entry to the right-click export menu; returns an unregister function. |
|
Remove one by id. |
|
Remove the figure’s DOM and listeners. |
|
The underlying local model (advanced). |
Export options (all optional), shared by exportPNG and exportCanvas:
|
Extra multiplier over |
|
Draw overlay widgets, without their drag handles. |
|
Export just this panel instead of the whole figure. |
|
|
|
|
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.
Owning the hover readout#
2-D panels show the cursor’s position and pixel value in a small pill drawn on the
image (see Hover readout (2-D)). In a desktop app you usually want that in your own
chrome instead — a status line pinned to the bottom-right of the window, where it
never covers data. Hide the pill from Python and take the payload from
opts.onReadout:
plot.set_readout_visible(False) # before figure_state(fig)
const statusEl = document.getElementById('status-bar'); // your own chrome
mount(host, state, {
onReadout: (info) => {
// info === null when the cursor leaves the image
statusEl.textContent = info ? info.text : '';
},
});
The same payload is also dispatched as an apl:readout CustomEvent that
bubbles off the mount container, which is handy when the listener lives somewhere
other than the mount() call site:
host.addEventListener('apl:readout', (e) => render(e.detail));
info fields:
|
Which panel the cursor is over. |
|
Fractional position in image pixels; integer i is the centre of pixel i. |
|
Integer pixel index ( |
|
Physical position in |
|
Axis units string ( |
|
Pixel value, or |
|
|
|
|
|
The formatted one-line string the built-in pill uses. |
Updates are deduplicated by text, so a cursor moving inside one pixel does not
call back repeatedly. exact flips to true for the same pixel when a value
probe resolves, which fires one more callback — render it, don’t ignore it.
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">.An exported page inlines its state and data into a
<script>block, and an HTML parser ends that block at the first</in its text whether or not it sits inside a JavaScript string. Every JSON literal the page embeds therefore goes throughanyplotlib._repr_utils.script_json, which escapes</and<!--through the<, so a title or an axis label read from file metadata cannot close the block and run what follows. Titles and captions are HTML-escaped on their way into markup. If you template your own page aroundfigure_state, do the same.