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 `