From 608229c8e99e6c9f6355bab829cec19dccf9c4f0 Mon Sep 17 00:00:00 2001 From: Teon L Brooks Date: Sun, 15 Mar 2026 16:18:44 -0400 Subject: [PATCH] Add SVG and PNG buttons --- docs/pyodide-in-electron-vite.md | 135 ++++++++++++++++++ src/main/index.ts | 21 ++- src/preload/index.ts | 11 +- src/renderer/actions/pyodideActions.ts | 1 + .../components/HomeComponent/index.tsx | 6 +- src/renderer/components/PyodidePlotWidget.tsx | 63 ++++++-- src/renderer/epics/pyodideEpics.ts | 1 + src/renderer/reducers/pyodideReducer.ts | 5 + src/renderer/utils/filesystem/storage.ts | 10 +- src/renderer/utils/webworker/index.ts | 1 + 10 files changed, 236 insertions(+), 18 deletions(-) diff --git a/docs/pyodide-in-electron-vite.md b/docs/pyodide-in-electron-vite.md index 34c5934..dc8ef5f 100644 --- a/docs/pyodide-in-electron-vite.md +++ b/docs/pyodide-in-electron-vite.md @@ -163,6 +163,138 @@ Current PyPI packages: `mne`, `pooch`, `tqdm`, `platformdirs`, `lazy-loader` --- +## Plot Pipeline + +### matplotlib Backend in Web Workers + +Use `agg`, not `webagg`. Set it before any Python imports run: + +```js +await pyodide.runPythonAsync('import os; os.environ["MPLBACKEND"] = "agg"'); +``` + +WebAgg (`webagg`) fails in web workers because it tries to inject CSS via `js.document` during initialisation — and `js.document` does not exist in worker scope. The error looks like: + +``` +ImportError: cannot import name 'document' from 'js' +``` + +`agg` is a non-interactive raster backend that writes to a buffer, which is exactly what we need. + +--- + +### plotKey Correlation Pattern (Fire-and-Forget Messaging) + +`worker.postMessage()` returns `undefined` — there is no return channel. Redux-Observable plot load epics cannot receive the worker's result directly. + +**Solution:** attach a `plotKey` string to every outgoing message; the worker echoes it back in the response object. `pyodideMessageEpic` routes by `plotKey` to the correct Redux action. + +```js +// webworker.js — echo plotKey back in every response +const { data, plotKey, ...context } = event.data; +self.postMessage({ results: await pyodide.runPythonAsync(data), plotKey }); +``` + +```ts +// pyodideMessageEpic — route by plotKey +switch (plotKey) { + case 'ready': return of(PyodideActions.SetWorkerReady()); + case 'topo': return of(PyodideActions.SetTopoPlot(mimeBundle)); + case 'psd': return of(PyodideActions.SetPSDPlot(mimeBundle)); + case 'erp': return of(PyodideActions.SetERPPlot(mimeBundle)); + default: return of(PyodideActions.ReceiveMessage(e.data)); +} +``` + +Plot load epics become fire-and-forget — they call `worker.postMessage()` as a side effect and emit nothing: + +```ts +// loadTopoEpic +action$.pipe( + filter(isActionOf(PyodideActions.LoadTopo)), + tap(() => plotTestPlot(state$.value.pyodide.worker!)), + mergeMap(() => EMPTY) +); +``` + +--- + +### Worker Readiness Gating + +`loadUtils` posts `plotKey: 'ready'` when `utils.py` finishes loading. This drives an `isWorkerReady` flag in Redux state that gates any UI that depends on Python being initialised. + +```ts +export const loadUtils = async (worker: Worker) => + worker.postMessage({ data: utilsPy, plotKey: 'ready' }); +``` + +`pyodideMessageEpic` dispatches `PyodideActions.SetWorkerReady()` on receiving `plotKey === 'ready'`. + +--- + +### SVG Output from matplotlib + +Produce SVG in Python — no base64 encoding needed: + +```python +import io +import matplotlib.pyplot as plt + +_fig, _ax = plt.subplots() +_ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) +_buf = io.BytesIO() +_fig.savefig(_buf, format="svg", bbox_inches="tight") +plt.close(_fig) +_buf.getvalue().decode() # SVG string is the Python return value +``` + +The SVG string flows through `pyodide.runPythonAsync()` → worker `postMessage` → Redux state as `{ 'image/svg+xml': string }`. + +--- + +### Rendering SVG Safely in the Renderer + +Use a data URI on an `` tag — sandboxed, no script execution: + +```tsx + +``` + +Prefer this over `dangerouslySetInnerHTML` — inline SVG executes `