================================= 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); }, // 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 ``
`` (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``. Navigated pages — one page that owns its data ============================================== A *navigated* figure is one where a navigator panel drives the others: move the crosshair over the scan and the signal panel shows that position's frame, its overlays follow, and a detector drawn on the signal panel reduces the whole dataset back onto the navigator. :func:`~anyplotlib.embed.navigated_html` exports that as a single file — the renderer, the figure state, the data and the bindings all inlined, no network and no Python at view time. The data travels as *blocks*. A **dense** block is a numpy array whose leading axes are the navigation axes; a :class:`~anyplotlib.embed.Ragged` block is a row-pointer array plus one value array per column, for a variable number of rows per position (diffraction spots, detected particles, peaks). :func:`~anyplotlib.embed.pack_blocks` concatenates them into one little-endian byte string, which the page decodes once into a single ``ArrayBuffer`` and reads through typed-array views — no per-block base64, no copy per frame. :: import numpy as np import anyplotlib as apl from anyplotlib.embed import navigated_html scan = np.load("scan.npy") # (32, 32, 128, 128) uint8 fig, axes = apl.subplots(1, 2, figsize=(760, 380)) navigator = axes[0].imshow(scan.sum(axis=(2, 3)), cmap="gray") signal = axes[1].imshow(scan[0, 0], cmap="gray") navigator.add_widget("crosshair", cx=0, cy=0) signal.add_widget("rectangle", x=48, y=48, w=32, h=32) # the detector html = navigated_html( fig, {"scan": scan}, [ {"panel_id": navigator._id, "role": "navigator"}, {"panel_id": signal._id, "role": "driven", "frame": {"block": "scan", "kind": "image"}, "reduce": {"block": "scan", "navigator_panel": navigator._id}}, ], title="Scan", caption="Drag the crosshair; drag the detector to re-map.", ) open("scan.html", "w", encoding="utf-8").write(html) Open ``scan.html`` in any browser, or point an Electron window or an ``