================================= 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: .. code-block:: 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 ``
`` (or ````/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. :class:`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**: .. code-block:: javascript 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 ============= .. automodule:: anyplotlib.embed :members: :undoc-members: 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 :class:`~anyplotlib.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 ``