Notebook panel
Meratyn can run a Jupyter notebook beside the map, in a resizable,
collapsible panel (open it from Processing → Jupyter Notebook). It works in
both the web and desktop builds, with a different Python runtime behind the same
<iframe> panel:
- Web build → JupyterLite. A full
Jupyter UI running entirely in the browser on a Pyodide
(WebAssembly) kernel — no server. Install extra packages on demand from a cell
with
%pip install geopandas. - Desktop build → a real JupyterLab server. The app launches
jupyter labfrom a uv-managed Python environment (the same mechanism as the FastAPI sidecar) on127.0.0.1:8766, token-authenticated, and embeds it. This gives full CPython with the native geospatial stack (geopandas, rasterio, GDAL, …).
This is distinct from the meratyn Python package, which does the
inverse — embedding the whole Meratyn app inside a notebook cell.
Scripting the map from a cell
A lightweight meratyn client is preloaded into the notebook kernel — on web
it is bundled into JupyterLite’s filesystem, so no pip install is needed;
on desktop it is placed on the kernel’s import path. Just import it:
import meratyn
m = meratyn.connect() # or meratyn.Map()
m.fly_to(-74.1982, 11.2408, zoom=11) # animate the live map in the left pane
m.add_geojson(gdf, name="My layer") # GeoDataFrame, dict, or JSON string
m.fit_bounds([-123, 37, -122, 38])
m.set_basemap("https://…/style.json")
m.set_visibility(layer_id, False)
m.remove_layer(layer_id)
Most calls are fire-and-forget: they post a command to the host app over the
shared scripting protocol (the same createScriptingHandlers surface used by the
in-app Python console and the Jupyter widget) and return immediately, so they
behave identically on the in-browser kernel and a real server. Canonical client
source: services/server/notebook_client.py.
The bundled desktop client also uses bounded correlated relay calls for these operations when it is connected to Meratyn Desktop’s Jupyter server:
m.add_geojson(...)returns the new layer id as a string. It returnsNonein JupyterLite or when it must fall back to fire-and-forget delivery; a relay timeout likewise returnsNonewith a warning because the layer may still be created.m.list_layers()returns public layer-summary dictionaries.m.get_layer(layer_id)returns that layer’s GeoJSON feature dictionaries, excluding source paths and metadata.
Other read-back queries (for example get_center) are not exposed by the
bundled client.
Driving the map from an external client (VS Code, …)
The desktop app’s JupyterLab server is a normal, token-authenticated Jupyter server, so you can attach any Jupyter client to it and keep your own editor — and the map commands above still work. Open the Notebook panel once (that is what starts the server), then use its link button in the panel header to copy the connection URL and paste it into your client (in VS Code: Jupyter: Specify Jupyter Server for Connections → Existing).
This works because commands travel over a relay on the Jupyter server rather than depending on how the notebook is being displayed:
services/server/src/sidecar/jupyter_relay.pyis a Jupyter Server extension (enabled fromjupyter_server_config.py) exposingPOST …/mappview/relay/command, a…/mappview/relay/socketWebSocket, andGET …/mappview/relay/status. At load it publishesMAPPVIEW_RELAY_URLandMAPPVIEW_RELAY_TOKENinto the server’s environment, which kernels inherit — that is howimport meratynfinds the map with no configuration.- The app subscribes to that socket for its whole lifetime
(
integrations/jupyter/relay/runtime.ts), reconnecting with backoff, and runs each command against the samecreateScriptingHandlerssurface. integrations/jupyter/notebook/bridge.tsremains the postMessage path for the embedded panel and for web (JupyterLite), where the notebook page really is the app’s iframe.
The POST answers with how many app windows received the command, so a disconnected session is reported instead of silently doing nothing:
meratyn.is_connected() # False -> no Meratyn window is listening
The bundled client sends fire-and-forget commands. Other trusted relay callers may include a request id for a correlated reply: the server broadcasts a fresh dispatch id to every live app window, returns the first valid reply under the caller’s original id, and ignores late replies. Since each window has its own map state, use one connected window when chaining a returned layer id into a later read-back request.
When a command cannot be delivered the client raises a
MappViewNotConnectedWarning pointing at your own line. Promote it to an error
with:
import warnings, meratyn
warnings.simplefilter("error", meratyn.MappViewNotConnectedWarning)
Only the desktop server has the relay: on web (JupyterLite) there is no server to attach an external client to.
Theme
On the web build the notebook follows the app’s light/dark theme live (no
reload). JupyterLite is built with exposeAppInBrowser (see
apps/mappview/jupyterlite/jupyter-lite.json), which puts its app object
on window.jupyterapp; since the JupyterLite iframe is same-origin,
integrations/jupyter/notebook/theme.ts reaches in and runs the built-in apputils:change-theme
command whenever the app theme changes. On the desktop build the JupyterLab
server is a different origin, so this cross-origin call is a no-op and the
notebook keeps its own theme (syncing it there would need a small JupyterLab
extension — a future enhancement).
Building the web JupyterLite site
The web build embeds a self-hosted JupyterLite site under
apps/mappview/public/jupyterlite/. It is not committed (≈70 MB) and
is generated by a best-effort prebuild step that needs the jupyter lite CLI:
pip install -r apps/mappview/jupyterlite/requirements.txt
bun run build:jupyterlite
Both bun run dev and bun run build run this automatically:
bun run devbuilds it once on first run via apredevhook (--if-missing), then is instant on subsequent runs.bun run buildalways rebuilds it (viaprebuild) so a changed client/config is picked up.
Both skip gracefully when jupyter lite is not installed — a Node-only build
still succeeds and the web Notebook panel shows a “not built” message until the
site is generated (install the deps above and re-run). The desktop (Tauri) dev
and build paths skip it entirely (they use the real JupyterLab server), so the
static site never bloats the installer.
The production Docker image is different: it installs these build-only Python dependencies and marks JupyterLite as required. A container build fails if the site cannot be generated, and nginx returns a scoped 404 rather than the app fallback if a notebook asset is ever missing.
Build config lives in apps/mappview/jupyterlite/ (a
jupyter_lite_config.json, the build requirements.txt, and a starter
files/Welcome.ipynb). The generated directory is excluded from the PWA
precache (see pwaPlugin in apps/mappview/vite.config.ts).
Desktop server
- Backend extra:
notebookinservices/server/pyproject.toml(jupyterlab,jupyter-server), synced into its own uv project environment so it never disturbs the sidecar’s env. - Launcher / lifecycle:
start_jupyter_server/stop_jupyter_serverTauri commands inapps/mappview/src-tauri/src/lib.rs(mirrors the sidecar launcher), with the TS wrapper inapps/mappview/src/integrations/jupyter/relay/server.ts. - Framing:
services/server/jupyter_server_config.pyrelaxesContent-Security-Policy: frame-ancestorsto the Tauri webview / loopback origins so the app can embed the server; the Tauri CSP (tauri.conf.json) adds the loopback origins toframe-src/child-src. - Map-command relay: the same config enables the
sidecar.jupyter_relayserver extension (see above). Its WebSocket accepts only the app’s own origins and every endpoint requires the server’s per-launch token, so a command can only come from something that already has kernel-execution rights there.