viewers — the graph-navigator / 3D-tube / Streamlit / lineage subsystem — design
Class:
design· Function axis:f0(the four new capabilities as one) ·f1helix 3D data-tube (facett-helix::TubeView) ·f2the GRAPH NAVIGATOR (facett-graphview::analysis+facett-graphnav) ·f3facet-wrapped (embed registry + wasm canvas mount + PyPI xtask) ·f4facett-lineage (skadelineage_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 afacett_core::Facetunder 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:
wall— atheta_bins × z_slicesgrid, row-majorwall[z*theta_bins + theta], one value per(θ, z)cell smeared over the pipe's outer surface (θ around the circle, z along the axis).core— one value perzslice, running straight down the centre axis.
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]:
- the cylinder WALL — a
theta_bins × z_slicesvertex grid atradius, θ wrapping columni → (i+1)%tb; each quad coloured by the mean of its four corner wall values, with outward radial normals so it lights around the pipe; - the CORE axis tube — a slender hexagonal-ish cylinder down the centre (
x=z=0, radiusCORE_RADIUS_FRAC = 0.12of the wall,CORE_SEGMENTS = 8;mesh.rs:24), each ring coloured by the core value at that slice.
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.
- Msg (
tubeview.rs:32):Tick(dt)/SetClock(s)/SetAzimuth/ToggleSpin/Preset(ViewPreset)— the robot-/CLI channel (updateattubeview.rs:94). - Injected clock (
with_clock): auto-spin isazimuth + t·SPIN_RATE, a pure function of the clock ⇒ determinism (FC-7). Live mode falls back toui.input().time. state_json(tubeview.rs:264): resolution, wall/core min-max, mesh vertex/triangle counts, camera pose, spin, clock — the picture asserted as DATA.
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:
- query bar —
SetQuery/RunQuery; the engine (query.rs:90) runs a tolerant subset of Cypher (MATCH (n:Person {name:"Alice"}) RETURN n,MATCH ()-[r:KNOWS]->() RETURN r) and Gremlin (g.V().hasLabel('Person'),g.E().hasLabel('KNOWS')), falling back to free-text search (query.rs:108) over id / caption / label / property values; - selection + inspector —
SelectNode/SelectEdge/ClearSelection; the inspector shows the element's label + properties; - layout switcher —
SetLayout(LayoutKind); five deterministic layouts (layout.rs:47):Force(Fruchterman–Reingold, circular-seeded),Hierarchical(BFS-layered),Radial(layout.rs— BFS depth → concentric ring, the gdotv concentric-tree for provenance/dependency DAGs),Circular,Grid; - styling —
SetStyle(StyleBy); colour nodes bylabelor any property key from a fixed brand-neutral 8-hue wheel (style.rs:74compute,style.rs:10WHEEL); - expand / collapse —
ExpandNeighborhood/CollapseNeighborhoodgrow or prune thevisibleset; - search + filter —
SetSearch(recomputes the match set) /SetFilterLabel; - path highlight —
SetPath(Some((from,to)))runsfacett_graphview::analysis::pathfindingover the builtAdjacency; - history & saved queries —
SaveQuery/RecallQuery/ClearHistory; distinct run queries are recorded most-recent-last (capQUERY_HISTORY_CAP = 50), and a name→text book pins queries for one-click recall (the gdotv "recent queries" recall); - subgraph extraction —
ExtractSubgraph(radius)induces the ego-network withinradiusundirected hops of the selected node as its own graph (subgraph,lib.rs:360; surfaced instate_json["subgraph"]),ClearSubgraphclears it; - export —
Export(ExportFormat);export()(lib.rs:463) emits JSON (prettyPropGraph), DOT (digraph), or CSV (nodes + edges blocks).
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")]:
- discovery (
src/wasm_api.rs) — cheap, canvas-freewasm-bindgenexports:facett_manifest()(:16),facett_component_count(),facett_component_state(name)(:31). Enumerate components and read initial state without booting eframe. - the eframe canvas mount (
src/wasm_app.rs) — the real interactive surface.facett_mount(canvas_id, component)(wasm_app.rs:71) resolves the component (falling back to the first built-in so the canvas is never blank), then boots eframe'sWebRunner(glow/WebGL — the same pathfacett-demoships on the web) driving aMountApp(wasm_app.rs:51) whoseuiis the facet's ownui— pan / click / drive, live in the Streamlit iframe. Every frame it republishes the live facet'sstate_jsoninto a process-global string thatfacett_streamlit_state()(wasm_app.rs:129) returns, andfacett_streamlit_mounted()(wasm_app.rs:143) reports which component booted — the "verify the SHIPPED wasm artifact headlessly" seam.
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().
LineageRow(lib.rs:37) +lineage_events_schema()(lib.rs:70) — the 13 reserved columns:event_id, actor, operation, input_table, input_snapshot, output_table, output_snapshot, source_system, source_instance, target_system, target_instance, ts_micros, commit_seq(the non-null five match skade's nullability).read_rows(batch)(lib.rs:98) — the by-name, null-tolerant reader (the inverse of skade'sto_arrow_row); a missing required column is a loud error.to_record_batch(lib.rs:151) is its round-trip partner.build_graph(rows)(lib.rs:201) — the adapter: datasets are nodes (labelled by their system kind —"skade"when local — so styling-by-label colours by system; system-qualified ids so asparkeventsand skademain.eventsnever collide), runs are edges (input → output, edge type = the uppercased operation, carrying actor / event_id / ts / commit_seq). Input-less events (create / external ingest) still contribute their output node.LineageView(lib.rs:241) — aFacetwrapping aGraphNavover thatPropGraph.uirenders the whole graph navigator IDE unchanged;update(Msg)drives the inner navigator;state_json(lib.rs:350) is the navigator's plus a"lineage"summary (lineage_json,lib.rs:298— event / dataset / run counts, the operation histogram, distinct systems + actors, max commit cursor).demo()is a multi-system, multi-hop DAG (a Spark ingest → clean/metrics fan-out → gold promotion → atomic release).
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.