Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/facett-embed-data-seam-design.md

SUPERSEDED — kept for the analysis, not for the instructions.

The facett-embed crate described here no longer exists (it was facett-streamlit before that). Its successor is facet-wrapped + the facet-wrapped-web wheel, and the current runbook is .nornir/python-packaging-publish-runbook.md; the rows and measurements are in .nornir/facet-wrapped-wheel.md.

What survives, and is why this file is still here: the analysis below — that a component falling back to its own demo fixture is indistinguishable from success, and that the fix is structural rather than careful. That is still the law the loader's refusal path implements.

facett-embed — the data seam (and why demo fixtures were the bug)

facett-embed (formerly facett-streamlit) embeds the real facett components into any web host as wasm. This document is the "perfect wrap" analysis: what went wrong, why it was structural rather than careless, and what changed in facett.

The failure, precisely

The Python Streamlit companion (facett/py/facett_demo_python) shipped a Sweden map built with pydeck, a graph built with networkx + plotly, and zero facett code. Rickard spotted it instantly — "the map looks a bit too good … is it really our map tech?" It wasn't.

The lookalike then drifted, immediately and visibly: pydeck defaults to map_provider=CARTO, which drew a third-party world basemap underneath our data, so a correctly-clipped Sweden appeared to include Africa. The data was never wrong — measured at the source, 0 of 55 526 way vertices fall outside the Sweden bbox. The second renderer was wrong.

Why it happened — the structural cause

It is tempting to file this as "someone took a shortcut". It was not. The bridge could not have rendered korp data, for two independent reasons:

  1. Every registry factory was a zero-argument demo fixture. GraphNav::demo(), TubeView::demo(), StatGridView::local(), … — 13 entries, all fixtures, and no data-injection API anywhere in the crate. A host holding a live korp scene had no way to hand it in. The bridge was a showcase, not an embed. Faced with "render this data", bypassing it was the only option.
  2. The map was excluded by construction. The dependency comment scoped entries to components "with a zero-arg local()/demo() Facet builder (pure egui/serde, no wgpu/gix/fs)" — precisely the predicate that leaves the OSM map out. The single most-wanted component was unwrappable, so it got hand-rolled.

The lesson generalises: a wrapper that cannot accept data will be bypassed, and every bypass becomes a second implementation that drifts. LAW #2 (both demos in lockstep) cannot be satisfied by discipline when the architecture forbids it; one implementation compiled to two targets satisfies it by construction.

What changed in facett

1. Entries are data-driven (registry.rs)

pub type DataFactory = fn(&serde_json::Value) -> Option<Box<dyn Facet>>;

Entry::new("osm2d", "…", || Box::new(OsmView::local()))   // fixture (showcase)
    .with_data(crate::data::osm2d_from_json)              // LIVE (host rows)

Entry::build() still returns the fixture. Entry::build_with(&payload) builds the same component from the host's data.

2. Refusal is typed and loud — never a silent fixture

pub enum EmbedError {
    UnknownComponent(String),
    NotDataDriven(&'static str),   // "…must not be shown as live data"
    EmptyPayload(&'static str),
}

There is deliberately no fixture fallback on the data path. A caller that asked for its own data and silently received canned demo data is the exact failure we are engineering against — a fixture rendered as if it were live is worse than an error, because it looks fine. The manifest carries data_driven: bool per component so a host can grey out or refuse up-front, and the frontend loader calls facett_check_data before mounting, showing the refusal text instead of a canvas.

3. The MAP wave (the gap that caused this)

name component payload
osm2d facett_geomap::OsmView — the OSM 2D navigator points + ways (+ raw OSM tags)
geomap facett_geomap::GeoMap — geo points/hotspots points + hotspots
map facett_map::MapView — point/line viewer points
graphnav facett_graphnav::GraphNav — Graph-DB IDE nodes + edges

All four build for wasm32 on the default path (their wgpu fast-paths stay off, so the pure-egui CPU painter runs in the browser canvas).

Crucially, ways are reconstructed with facett_map::Way::from_lonlat(osm_id, lonlat, tags, theme) — korp's raw OSM tag JSON is passed straight through, so the component performs classification and styling. The host never picks a colour. This is what makes the embedded map identical to the native one rather than similar to it.

4. Payload shapes (mirror the source system's own reply)

// osm2d / geomap / map  — mirrors korp MapQueryReply
{ "title": "Sweden",
  "points": [[lon, lat], …],
  "ways":   [{ "osm_id": 12, "coords": [lon,lat,lon,lat,…], "tags": {"highway":"primary"} }] }

// graphnav — mirrors korp GraphReply
{ "title": "korp graph",
  "nodes": [{ "id": 1, "label": "Person", "key": "Person/1" }],
  "edges": [{ "src": 1, "dst": 2, "rel": "OWNS" }] }

coords is korp's flat [lon,lat,…] run exactly as it arrives on the wire; tags accepts korp's raw JSON string or a real object. Adapters are total — a malformed or empty payload yields EmptyPayload, never a panic inside the canvas.

5. Embedding fix in facett-geomap

OsmView fits its camera to the data on first paint, then latches fitted = true. An embedded canvas is laid out asynchronously — the first frames can arrive with a 0-sized or still-growing rect, and committing the one-shot fit to one of those strands the camera at the wrong zoom for the whole session. The fit is now only persisted once the pane exceeds MIN_FIT_RECT_PX; until then it re-fits the local copy each frame (correct paint, no cost). This is a fit-gating change, not a rendering change.

The rename

facett-streamlit → facett-embed. The crate is not Streamlit-specific: it embeds real facett components into any web host. Streamlit is one consumer. Updated in lockstep: workspace members, facett-py's path dep + use facett_embed::Registry, the xtask (release-embed, with release-streamlit kept as a back-compat alias), the Python companion package (facett_streamlit → facett_embed), and the wasm exports (facett_embed_state / facett_embed_mounted / facett_embed_error).

What the Python demo looks like now

pane renders via why
🗺 Map facett_component("osm2d", data=…) a facett component exists — use it
🕸 Graph facett_component("graphnav", data=…) a facett component exists — use it
📊 Population pandas + plotly no facett component is duplicated — Iceberg aggregates are pure data analytics, which is what Python is genuinely good for

Deleted as they were replaced (never leaving :8501 broken): the pydeck map body, the networkx/plotly graph body, and theme.py's OSM classification/colour tables (that logic belongs to facett_map, and a second copy is exactly the drift we removed). pydeck and networkx are gone from requirements.txt.

Guards (so this cannot regress)

Rust (cargo test -p facett-embed, 27 tests):

Python (pytest, 18 tests):

Render proof

Driven headless (Playwright/Chromium) against the shipped app on :8501:

mounted   : osm2d          ← the REAL facett component, not a lookalike
hasCanvas : True [1140, 620]
refusal   : ''             ← no fixture substitution
state     : center [17.146, 63.108]   ← central Sweden
            lod_sizes {country: 401, region: 10764, city: 16219}
            fps 42, gpu_enabled false (CPU painter)

The component paints its own status bar — sweden — korp MapQuery · 4000 pts / 27384 ways · zoom 5517px/u · 63.1079,17.1461 · 40 fps — which is facett chrome, not Streamlit's. No third-party basemap: the canvas is our geometry on our background. graphnav likewise mounts with korp's Company/Vessel nodes.

Build + run

cargo xtask wrap-wasm        # the REAL components -> wasm32 + wasm-bindgen + manifest
cargo xtask publish-pip      # -> the wheel, built IN A CONTAINER, verified back
cargo xtask publish-holger   # -> holger's PEP 503 index (znippy as a library)
cargo xtask streamlit        # -> the demo, in a container, on the INSTALLED wheel
cargo xtask test-demo        # -> its python suite, in the same container

On oden it runs as the systemd `--user` unit `facett-demo-python.service` (port 8501).