Population Graph — the Sverige · Population showcase (design)
The Swedish 11-million-person population graph (the canonical
knut-popsimmodel) rendered in facett-demo as three beautiful, linked, interactive graphs. Native AND the wasm web demo. THE showstopper graph tab.
Where it lives
facett-demo/src/pop_model.rs— the query / LOD / colour layer overknut-popsim.facett-demo/src/pop_tab.rs— theSverigeFacet (the showcase tab).facett-demo/src/lib.rs— registers theSverigetab in the deck + LEGEND, and drives thegraph3dtab off the sameknut-popsimpopulation sample.- Engine extension:
facett-graphviewNodeDecoration { scale }+l0::lower(domain-agnostic node sizing) — see "Engine extensions" below.
The data source — knut-popsim (NOT re-invented)
knut-popsim (knut/crates/knut-popsim, lib knut_popsim) is the canonical, pure-Rust generator of the synthetic Swedish population graph: 12 labels (Person + 11 register entities), deterministic LCG, no falkordb / arrow / network / fs deps (optional serde only) → it compiles for wasm32-unknown-unknown. The demo depends on it by path (../../knut/crates/knut-popsim) and calls:
simulate(SimParams { n_persons, n_kommun, seed }) -> PopGraph— a sampled instance graph.schema() -> PopGraph— the 12-label meta-graph (the data model).LABELS,PERSON_TIE_PROB,PERSON_REL,geo::CITIES(14 kommuner w/ lon/lat).
PopGraph is the "graph-mem database that works in wasm". pop_model builds a thin query/filter/LOD layer over it — it never invents a second model.
Honest about scale (the LAW)
You cannot draw 100M nodes (≈400M with entities — OOMs; wasm has a hard memory ceiling). So:
- STATS are computed analytically at the requested scale (1M → 100M) from the per-label tie rates (
PopStats::at_scale) — we report the honest 11M / 100M numbers without materialising the graph. - Generation is clamped to
GEN_CAP = 6_000people (memory-safe, native + wasm). - Rendering is clamped to
RENDER_CAP = 1_400nodes per instance view (the LOD budget), and at full scale the kommun aggregation (14 nodes) is shown instead of dots. - The visible-vs-total split is surfaced on the tab AND in
state_json("rendering N of 11,000,000 — aggregated by kommun").
Scale field: a logarithmic slider 1M→100M + an 11M button. Default = the canonical 11,000,000.
The three linked views (one population, three lenses)
| view | what | layout |
| Full | the population as a force-clustered hub-and-spoke web | people on a coarse per-kommun grid, entities orbiting each person; LOD-sampled to RENDER_CAP |
| Debt | the enforcement sub-graph: Person → OWES → Debt → ESCALATES_TO → KfmCase | three columns (Person | Debt | KfmCase), escalated rows first; KfmCase nodes ringed red |
| Kommun | the per-kommun aggregation | one node per kommun on its real lon/lat, sized by population (proportional-symbol map of Sweden), tied to nearest neighbours |
A 3D view (graph3d tab) renders the same knut-popsim sample as a spun 3D force cloud (depth-shaded), via facett-graph3d.
Toggles (drive the render, observable in state_json)
- 12 label toggles (Person, Address, Company, Debt, Loan, TaxRecord, KfmCase, PoliceRecord, FkBenefit, AfRecord, Vehicle, Property) — a disabled label's nodes + edges drop from every view.
- 12 relation toggles (the 11 Person→entity rels + ESCALATES_TO).
- colour-by:
by-label(12-colour register palette) /by-kommun(hue wheel) /debt-heat(cool teal → hot red by per-person debt degree — the "follow the money" lens).
Beauty (the whole point)
Each view renders through the shared facett_core::render L0 SDF kernel (the same kernel the maps + graphview draw through), lowered with a glow lane: a soft RingInstance halo behind every node marker, sized to the marker. Pure CPU SDF → it lights up identically native AND in wasm with no GPU dependency and no l1-vello feature. Nodes read as luminous orbs over a dark ground. Colours from the 12-label palette / kommun wheel / debt-heat scale. Kommun orbs are population-sized (engine NodeDecoration.scale).
Engine extensions (domain-agnostic, kept in the engine)
facett-graphview::NodeDecorationgained an optionalscale: Option<f32>— a per-node marker-radius multiplier the caller drives by any importance metric (population, degree).l0::lowerreads it (default1.0). Tested:facett-graphviewnode_scale_decoration_enlarges_the_marker. The population specifics (what the size means) stay infacett-demo.
Observability (the LAW)
PopGraphTab::state_json() carries: n_persons, scale_min/max, generated_persons, gen_cap, render_cap, the analytic stats (total_nodes/edges + per_label), color_mode, active_labels, active_relations, and per-view {visible_nodes, total_nodes, edges, lit_px, zoom}. Plus a #[cfg(testmatrix)] functional_status emit from ui. Every view renders to a straight-RGBA Frame a test reads back (pop_graph_blob.rs) → PNG blobs at $TMPDIR/facett-demo/pop_*.png.
Tests (inject-assert, never no-crash)
pop_modelunit: stats scale to 11M without materialising; generation capped but scale honoured; full-model LOD cap + determinism; label/relation toggles drop nodes/edges; debt model is the OWES/ESCALATES story; kommun aggregates to 14; colour modes recolour; schema is the 12-label model.pop_tabunit: three linked non-blank views through L0+glow; scaling drives stats but caps generation; label toggle changes render + state; colour mode recolours the frame.tests/pop_graph_blob.rs: each view emits a non-blank, contained PNG (read back + asserted); debt-heat blob differs from by-label blob.facett-graphviewnode_scale_decoration_enlarges_the_marker(the engine ext).tests/mega_matrix.rs: aSverigecell (+ aVelo L1cell) in the coverage matrix.lib.rsdemo_state_has_every_component_populated: the Sverige tab in the full deck.
wasm
cargo build -p facett-demo --target wasm32-unknown-unknown --no-default-features builds green — knut-popsim is wasm-clean and the L0+glow lane is CPU-only. The same GEN_CAP/RENDER_CAP caps apply in the browser. Deploys via cargo xtask deploy-pages (unchanged).
Future (→ korp)
This lives in facett-demo now; it will move to korp later (the surveillance / back- office tool). pop_model + pop_tab are self-contained over knut-popsim + facett-graphview, so the move is a copy of two modules + the dep.