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

nordisk/facett

.nornir/viewers-design.md

viewers — the graph-navigator / 3D-tube / Streamlit / lineage subsystem — design

Class: design · Function axis: f0 (the four new capabilities as one) · f1 helix 3D data-tube (facett-helix::TubeView) · f2 the GRAPH NAVIGATOR (facett-graphview::analysis + facett-graphnav) · f3 facet-wrapped (embed registry + wasm canvas mount + PyPI xtask) · f4 facett-lineage (skade lineage_events → graph navigator). Strict: this file states only the shape that is built. Workflow → viewers-guide.md; zero-knowledge walkthrough → viewers-for-idiots.md. Every component here is a facett_core::Facet under the same FC-1..FC-9 contract as the rest of facett (facet-design.md).

f0 — the whole (four capabilities, one contract)

The branch adds two new viewer Facets, one shared algorithm toolbox, and one embedding path — every piece obeys the canonical Facet split (a serialisable Model, a Msg mutation alphabet, a pure &self view, and a state_json() that asserts the picture as DATA so a headless robot can drive and read it):

# crate / module what it is Facet reuses
f1 facett-helix (tube / mesh / tubeview) a real 3D tube/pipe with data smeared on the cylinder wall + a data core down the centre axis TubeView (facett-helix/src/tubeview.rs:47) facett_map3d::{Mesh3, OrbitCamera, ViewPreset} (CPU project → depth-sort → Lambert)
f2 facett-graphview::analysis + facett-graphnav a graph-DB IDE (V.G = G.V()): query bar, canvas, inspector, layouts, styling, paths, schema, export — over a pure analysis toolbox GraphNav (facett-graphnav/src/lib.rs:105) facett_graphview::analysis (centrality / community / paths / cycles / components / k-core / similarity)
f3 facet-wrapped (the wasm bundle) / facet-wrapped-web (the wheel) embed any facett Facet into Streamlit: a per-component registry, a wasm eframe-canvas mount, a Python package, and a pure-Rust PyPI release xtask — (host crate; a cdylib) the whole registry of Facets; eframe::WebRunner
f4 facett-lineage the data-lineage viewer: reads skade's lineage_events Arrow shape → a property graph → the graph navigator LineageView (facett-lineage/src/lib.rs:241) facett_graphnav::GraphNav unchanged (this crate is only the adapter)

The portability seam recurs throughout: the data cores (facett-helix::tube, facett-graphnav::model) depend on nothing but serde + std — no egui, no GPU — so they round-trip through serde and are unit-testable headless; only the *View files bind egui/eframe.


f1 — the helix 3D data-tube

A cylindrical scalar field rendered as a lit, orbiting, translucent 3D pipe. Two independent fields share one cylindrical frame; the translucent wall lets the opaque core show through the pipe.

f1.1 — TubeField: the wall + core data (facett-helix/src/tube.rs:20)

Dependency-free (serde + std only). Two scalar fields over one (θ, z) frame:

Constructors: TubeField::new(theta_bins, z_slices) (zeroed; floors of θ≥3, z≥2), from_fn(tb, zs, wall(θ,z), core(z)) (samples closures over θ∈[0,1), z∈[0,1] — deterministic), and demo() (tube.rs:128 — a helical hot-band on the wall + a swelling pulse down the core). is_valid() (tube.rs:66) is the shape invariant a deserialised field must satisfy before it can be meshed. Accessors wall_at/core_at wrap θ / clamp z; wall_range/core_range give the (min,max) used for colour normalisation.

f1.2 — tube_mesh: geometry (facett-helix/src/mesh.rs:61)

tube_mesh(field, radius, height) -> facett_map3d::Mesh3 builds two parts sharing the +Y axis (the normalised design frame facett_map3d uses), spanning y ∈ [-height/2, +height/2]:

Colour is ramp(t) (mesh.rs:32) — a perceptual turbo-ish ramp deep-blue→cyan→green→ yellow→red — applied to each field normalised against its own range so both always span the full ramp. An invalid field yields an empty mesh. Pure, deterministic, no RNG.

f1.3 — TubeView: the Facet (facett-helix/src/tubeview.rs:47)

Orbits, lights, and paints the tube mesh. The pure render core is faces(w, h) -> Vec<Face> (tubeview.rs:151): builds the mesh, projects it with a facett_map3d::OrbitCamera, culls off-pane triangles, computes a two-sided Lambert shade (shade = 0.35 + 0.65·|n·light|), gamma-multiplies the wall faces by WALL_ALPHA = 0.5 (tubeview.rs:28) for translucency (the core stays opaque), and depth-sorts far→near (painter's algorithm). Face is public so this whole path is headless-testable without egui/GPU.


f2 — the GRAPH NAVIGATOR

A full graph-DB IDE surface (gdotv-style V.G = G.V()) built in two layers: a pure-algorithm toolbox (facett-graphview::analysis) and the IDE Facet (facett-graphnav).

f2.1 — the analysis toolbox (facett-graphview/src/analysis/)

Every algorithm works on the index-based shape (n, edges: &[(usize,usize)]) and returns plain Vecs keyed by node index — no egui, no GPU, no RNG, deterministic, headless-testable (analysis/mod.rs:1). A caller maps its String-id graph to indices once, builds an Adjacency (analysis/mod.rs:31 — directed out/in + undirected views, weighted; self-loops & out-of-range dropped, parallel edges accumulate), then runs the whole toolbox:

group file functions
centrality analysis/centrality.rs degree (:11), in_out_degree (:18), closeness (:29, Wasserman–Faust), betweenness (:56, Brandes), pagerank (:107, damped power-iteration), eigenvector (:150, (A+I) power-iteration — the identity shift cures bipartite oscillation)
community analysis/community.rs louvain (re-exported), label_propagation (:19, deterministic tie-break) → a Communities with modularity as quality DATA
pathfinding analysis/pathfinding.rs bfs (:13), dfs (:37), shortest_path (:63, BFS), dijkstra (:93, weighted), all_simple_paths (:124, max_len-bounded)
cycles analysis/cycles.rs has_cycle (:12, iterative 3-colour DFS), topological_sort (:48, Kahn), find_cycles (:84, Johnson-style, limit-bounded, canonicalised)
components analysis/components.rs connected (:34, union-find), strongly_connected (:49, iterative Tarjan) → a compact Components labelling
k-core analysis/kcore.rs core_numbers (:10, Batagelj–Zaveršnik peeling), degeneracy (:46), k_core (:52)
similarity analysis/similarity.rs common_neighbours (:17), jaccard (:28), adamic_adar (:41), cosine (:56), most_similar (:70, top-k)

f2.2 — the property-graph model (facett-graphnav/src/model.rs)

The graph-DB data model, dependency-free (serde + std): Node (model.rs:16 — an id, a label = its type, a bag of string props), Edge (model.rs:42 — from/to + relationship type + props), and PropGraph (model.rs:62) with index_of, neighbours, edge_indices (the (usize,usize) shape the analysis toolbox eats), and schema() (model.rs:123 — the entity-relationship overview: node labels, edge types, and from-[type]->to triples). PropGraph::demo() (model.rs:147) is a two-cluster social graph (Person/Company, KNOWS/WORKS_AT).

f2.3 — the IDE Facet GraphNav (facett-graphnav/src/lib.rs:105)

One Facet composes every IDE panel; every field round-trips through serde and mutation flows only through update (lib.rs:214). The Msg surface (lib.rs:62) is the whole IDE as a robot-drivable alphabet:

state_json (lib.rs:553) is the full IDE surface as DATA: query (text / kind / result counts / error), canvas (layout + laid-out, styled, path-/match-flagged nodes), layout options, inspector, style categories, search, history (recent + saved), subgraph, filter, path (resolved node sequence + length), schema, minimap bounds, and export formats. Layout positions land in the normalised [-1,1]² box (layout.rs); bounds (layout.rs:248) drives the minimap.

The Facet / state_json / robot contract. GraphNav is its own Elm Model; render (lib.rs:658) is a pure &self → Vec<Msg>; ui drains those Msgs back into update. A test (or a CLI robot, or a Streamlit host) never touches egui — it calls update(Msg::…) and asserts on state_json, which is exactly what facett-graphnav/tests/robot.rs does.


f3 — facet-wrapped

Embed any facett Facet into a Streamlit page. Three parts ship together (facet-wrapped/src/lib.rs:1): a cdylib compiled to wasm32, a per-component registry, and a Python package + release xtask. The registry is plain host Rust and fully unit-tested; the wasm layer is a thin wrapper over it, so the whole surface is verifiable without a browser.

f3.1 — the component registry (facet-wrapped/src/registry.rs)

An Entry (registry.rs:13) pairs a stable name (the Streamlit key) + a description with a factory fn() -> Box<dyn Facet>. Registry::with_builtins() (registry.rs:43) registers one embed wrapper per embeddable facett *-view — 13 today: graphnav, helix-tube, stat-grid, console, timeline, table, log, matrix, tabbar, graphpan, plandag, security, lineage. Add a Facet here and it becomes instantiable from Streamlit by name with zero Streamlit-side glue. manifest_json (registry.rs:158) is the [{name, description, title}] a sidebar enumerates; component_state_json (registry.rs:173) is a component's initial state_json a host reads back.

f3.2 — the wasm boundary + eframe canvas mount

Two wasm layers, both #[cfg(target_arch = "wasm32")]:

f3.3 — the Python package + the release xtask

The python/ package (facet-wrapped/python/) is a Streamlit custom component: facett_component(name, height=…, **props) (__init__.py:55) renders the named component in an iframe; components() / manifest() read frontend/manifest.json. The frontend is a wasm-bindgen bundle not built by pip — it is produced and shipped inside the wheel by cargo xtask wrap-wasm + cargo xtask publish-pip (xtask/src/main.rs:186), pure std::process orchestration (no shell scripts): cargo build --target wasm32-unknown-unknown --lib -p facet-wrapped → wasm-bindgen --target web (pinned to the Cargo.lock version) into frontend/ → write manifest.json (from the manifest example) + the loader index.html (write_streamlit_index, xtask/src/main.rs:280) → python -m build (wheel + sdist) → with --publish, twine upload to PyPI. The wheel is facet-wrapped (the wasm bundle) / facet-wrapped-web (the wheel) (pyproject.toml).


f4 — facett-lineage

The data-lineage viewer (facett-lineage/src/lib.rs:1). facett is a leaf peer of skade, so it does not depend on skade; instead the reserved lineage_events column shape is mirrored locally, byte-for-byte against skade's lineage_events_schema().

This is the "facett viewer adapter" the skade lineage design defers to facett: a skade warehouse's lineage_events RecordBatch drops straight into read_rows and nothing skade-side has to change.