RDF graph in 3D

Requires the rdf extra for the projection and the viz extra for the widget (pip install "longeron[rdf,viz]").

Every payload ships two deterministic embeddings of the same view: the force layout and a layered hierarchy. The in-scene slider morphs between them in the browser, with no kernel round trip. The panel searches qualified names, budgets the billboard labels, and toggles namespaces and edge families with pills. Select a node and press f to isolate its k-hop neighborhood. The in-scene breadcrumb chip steps back out. widget.export_html(path) writes the current view as a self-contained page that opens without a kernel.

Explore the RDF projection as a 3D force-directed graph (anywidget).

graph_viewer() turns the longeron.rdf projection of a model into an interactive three.js scene: every element is an instanced sphere, every relationship triple a line segment, and a deterministic force-directed embedding (computed kernel-side, in numpy) gives the graph its shape. Instancing keeps the whole default view at a handful of draw calls, so orbiting a five-figure graph stays smooth.

The default view is deliberately smaller than the triple count. A projected model is mostly literals – names, kinds, flags, attribute values – and drawing them as nodes would drown the structure (the DeepScout program projects to ~8.6k triples but only ~1.1k elements). So the view shows element nodes and relationship edges only: containment (ownedMember), specialization (specializes / subsets / redefines), typing (definedBy / references / crosses), connections (connects), satisfaction (satisfiedBy), and the reference family (imports, aliases, dependencies, metadata). Every literal folds into its element’s hover payload instead; literals=True opts value literals (sysml:value / evaluatedValue / valueExpression) back in as leaf nodes when the full picture is wanted.

Nodes speak the explorer’s kind-color language: the same family colors the tree rows use as chips (structure blue, behavior purple, data green, connector amber, requirement red, package gray) color the spheres, and node size grows with degree, so hubs – the shared MultiRotor airframe abstraction, the package roots – are visible at a glance. Edges color by predicate family (EDGE_STYLES), with the reference family dashed.

Layout runs in the kernel, not the browser, and every payload ships two embeddings of the same view. spring_layout() is a ~40-line Fruchterman-Reingold simulation in 3D with a seeded generator, so the same model always lands in the same shape and tests can assert on coordinates. dag_layout() is its layered counterpart: the hierarchy edges (membership plus specialization) assign each node a layer by longest path, a barycenter pass orders each layer to shorten edges, and the layers stack as rings on the y axis – packages on top, the specialization fringe at the bottom. A prominent in-scene slider morphs between the two by pure front-end interpolation: dragging it never touches the kernel, and edges, labels, and picking follow at frame rate. Repulsion in the force layout is exact O(n^2) (vectorized and chunked); that is trivial at the default view’s size and the honest ceiling for very large graphs, which is why graph_viewer() caps the view at node_cap nodes (highest degree first, with an in-scene notice) instead of degrading silently.

Interaction follows longeron.widgets.viewer3d: drag to orbit, shift-drag or right-drag to pan, scroll to zoom, double-click to re-fit. Hovering a sphere names it (qualified name plus the folded literals) and lights its k=1 neighborhood; clicking selects it – the selected node pops in the JupyterLab accent, neighbors keep their color, everything else recedes toward the canvas color, base edges drop to a fraction of their opacity, and the incident edges re-draw on an accent overlay. Billboard labels name the highest-degree nodes (a panel slider budgets the density, camera distance fades them) and always name the selection and its neighbors. A legend chip names the node kinds, edge families, and the degree-size cue in-scene.

The control surface is the house widget chrome (longeron.widgets._chrome): a veiled, collapsible panel with a type-ahead search over qualified names, toggle pills (real checkboxes under the styling) for namespaces, edge families, and unlinked nodes, and slim filled-track sliders. Every filter change re-extracts and re-layouts kernel-side, exactly like the widget.filter(...) seam. The chrome and the scene are theme-aware end to end: JupyterLab theme tokens drive the panel, the text, and the canvas clear color, and a theme flip re-reads them live.

Focus mode isolates a neighborhood: select a node and press f (or click the focus chip) and the kernel re-extracts the k-hop neighborhood (k = 1 or 2 from the panel), re-layouts both embeddings – instant at sub-graph size – and a breadcrumb chip steps back out. The camera flies to selections and search hits with a 600ms eased move, and an optional idle orbit (a panel pill) spins the scene slowly until any interaction cancels it.

Linked views: the widget exposes the explorer’s selection contract – a two-way selected trait of qualified names plus on_select(callback) – so a graph click can drive the same consumers a tree or diagram selection drives, and kernel code can select programmatically by assigning widget.selected. Focus mode leaves the contract untouched.

widget.export_html(path) writes the current view as a self-contained standalone page: the payload, the front-end module, and a tiny model shim are inlined, so the file opens in any browser with no kernel and no anywidget. Kernel-backed controls (filters, focus) hide themselves in the standalone page; the morph slider, search, labels, legend, and selection emphasis all work.

Offline tradeoff: the front-end imports three.js from the jsDelivr CDN at view time, exactly like longeron.widgets.viewer3d; offline front-ends (and the exported page, offline) get a printed notice instead of a scene.

Requires the rdf extra for rdflib and the viz extra for anywidget and numpy: pip install "longeron[rdf,viz]".

longeron.widgets.graph3d.EDGE_STYLES: dict[str, tuple[str, bool, float]] = {'connection': ('#b07a26', False, 0.85), 'membership': ('#c9ccd1', False, 0.3), 'reference': ('#9a9fa8', True, 0.6), 'satisfy': ('#b0413e', False, 0.9), 'specialization': ('#3d6fb4', False, 0.75), 'typing': ('#7b4bab', False, 0.75), 'value': ('#8fbf6f', False, 0.4)}

edge family -> (color, dashed, opacity); value only exists with literals=True. The dense membership skeleton stays faint so the colored cross-cutting families read on top of it.

longeron.widgets.graph3d.NODE_COLORS: dict[str, str] = {'behavior': '#7b4bab', 'connector': '#b07a26', 'data': '#3f7a1f', 'external': '#c0c4cb', 'literal': '#8fbf6f', 'package': '#6d6d6d', 'relationship': '#9a9fa8', 'requirement': '#b0413e', 'structure': '#3d6fb4'}

node color per family – the explorer tree’s chip palette, so the graph and the explorer name kinds in the same colors

longeron.widgets.graph3d.dag_layout(count, hierarchy, links=(), *, radius=10.0, sweeps=4)[source]

A deterministic layered (“hierarchy”) 3D embedding.

hierarchy pairs are directed (above, below) constraints – in the graph view, membership parents sit above their children and specializations sit below their generals. Longest-path layering assigns each node a layer (roots at layer 0, strictly monotone along every hierarchy edge); links adds undirected edges that only influence the within-layer ordering. Each layer becomes a ring around the y axis: sweeps alternating barycenter passes order every ring against its neighbor layers to shorten edges, and the ring radius grows with the layer’s population so spacing stays readable. Everything is scaled to sit inside radius like spring_layout(), so a front-end can interpolate between the two embeddings without re-fitting.

Pure Python and seed-free: the same input always returns the same positions, regardless of edge iteration order.

Return type:

list[list[float]]

longeron.widgets.graph3d.graph_view(model_or_graph, *, namespaces=None, families=None, literals=False, external=False, isolated=True)[source]

Extract the drawable node/edge view from a projected graph.

model_or_graph is a Model (projected via longeron.rdf.to_graph()) or an already-built rdflib.Graph. Nodes are the typed subjects (every projected element, anonymous ones included); edges are the object-valued relationship triples, grouped into the EDGE_STYLES families. Literal triples fold into each node’s info hover lines. Filters: namespaces keeps only elements under the named top-level namespaces, families keeps only the named edge families, isolated=False drops nodes left edgeless by the filters. literals=True adds value literals as leaf nodes (edge family value); external=True adds reference targets that resolve outside the projection (standard-library types, dangling names).

Returns a dict with nodes (id, label, kind, family, color, ns, deg, r, info), edges ([source, target, family] index triples), families (the style table the family indices point into), and namespaces (every top-level namespace in the graph, for filter options). Node ids are qualified names; anonymous elements get stable ~-prefixed ids. Ordering is deterministic, so a view built twice from the same model is identical.

Return type:

dict[str, Any]

longeron.widgets.graph3d.graph_viewer(model_or_graph, *, namespaces=None, families=None, literals=False, external=False, isolated=True, seed=7, iterations=60, node_cap=5000, width_px=760, height_px=520)[source]

Explore a model’s RDF projection as an interactive 3D graph.

model_or_graph is a Model or a graph already built with longeron.rdf.to_graph() (pass the latter to keep evaluated=True literals in the hover payloads). namespaces / families / literals / external / isolated select the initial view exactly as in graph_view(); the in-scene panel (or widget.filter(...)) changes them later, re-layouting kernel-side on every change. seed and iterations steer the deterministic spring_layout() embedding; the layered dag_layout() embedding ships alongside it and the in-scene slider morphs between the two without kernel round trips.

Views larger than node_cap nodes keep the node_cap highest-degree nodes and say so in an in-scene notice: rendering is instanced and stays fluid into five figures, but the exact O(n^2) layout is the honest ceiling, so the cap protects the kernel rather than the GPU.

The widget’s selected trait (qualified names, two-way) plus on_select(callback) form the same selection contract the explorer’s tree exposes: clicks land in the kernel, kernel assignments drive the in-scene emphasis (and an eased camera fly-to), and counts / layout_seconds report the current view’s size and layout cost. focus(id, k=...) / unfocus() isolate a neighborhood kernel-side, and export_html(path) writes the current view as a self-contained standalone page.

Return type:

AnyWidget

longeron.widgets.graph3d.spring_layout(count, edges, *, seed=7, iterations=60, radius=10.0)[source]

A deterministic 3D Fruchterman-Reingold embedding (numpy).

count nodes and edges (index pairs; duplicates and self-loops are ignored) settle under the classic forces: k^2/d pairwise repulsion, d^2/k attraction along edges, plus a mild pull toward the origin so disconnected components stay in frame. The temperature cools linearly over iterations steps and seed fixes the starting positions, so the result is reproducible. The returned positions are centered and scaled to fit a sphere of radius.

Return type:

list[list[float]]