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 one panel_<id>_json entry per panel — and is exactly what the JS mount(el, state) entry point expects.

Parameters:

fig (Figure)

Return type:

dict

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.

Parameters:
  • fig (Figure)

  • resizable (bool, optional) – Keep the figure’s drag-to-resize handle. Default True.

Return type:

str

anyplotlib.embed.save_html(fig, path, *, resizable=True)[source]#

Write to_html() output to path and return it as a Path.

Parameters:

resizable (bool)

Return type:

Path

anyplotlib.embed.esm_path()[source]#

Return the path to figure_esm.js for bundling into a JS app.

Copy (or import) this file into your Electron / web build; it exports mount and createLocalModel alongside the anywidget render.

Return type:

Path

class anyplotlib.embed.FigureBridge(fig, send)[source]#

Bases: object

Transport-agnostic two-way sync between a live Figure and a remote JS view mounted with mount(el, state, {onSync}).

You supply the pipe (WebSocket, Electron IPC via a sidecar, stdio, …); the bridge supplies the protocol: plain (key, value) pairs.

Parameters:
  • fig (Figure) – The live figure. All Python-side mutations (plot.set_data(...), marker/widget updates, layout changes) are forwarded automatically.

  • send (callable(key: str, value) -> None) – Called for every outbound state change. Wire it to your transport.

Notes

  • Python → JS: any synced trait change triggers send(key, value); deliver it to handle.applyUpdate(key, value) in JS.

  • JS → Python: deliver each JS onSync(key, value) message to receive(). 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.

snapshot()[source]#

Full state dict for the initial mount() on the JS side.

Return type:

dict

receive(key, value)[source]#

Apply one inbound (key, value) message from the JS view.

event_json messages are dispatched to plot/widget callbacks; other keys (e.g. a panel’s view state after a JS-side 3D rotate) are stored on the figure without echoing back.

Parameters:

key (str)

Return type:

None

close()[source]#

Stop forwarding (unobserve the figure).

Return type:

None

JS handle reference#

mount(el, state, opts) handle

handle.setPanelState(id, st)

Replace one panel’s state (dict or JSON string) and re-render it.

handle.set(key, value)

Raw model write + sync flush.

handle.get(key)

Read any model key.

handle.applyUpdate(key, v)

Apply a Python-originated update without echoing it back through onSync.

handle.resize(w, h)

Resize the figure (CSS pixels).

handle.dispose()

Remove the figure’s DOM and listeners.

handle.model

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.js has no imports, so it works with any bundler or directly as a <script type="module">.