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);
valueonly exists withliterals=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.
hierarchypairs 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);linksadds undirected edges that only influence the within-layer ordering. Each layer becomes a ring around the y axis:sweepsalternating 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 insideradiuslikespring_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.
- 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_graphis aModel(projected vialongeron.rdf.to_graph()) or an already-builtrdflib.Graph. Nodes are the typed subjects (every projected element, anonymous ones included); edges are the object-valued relationship triples, grouped into theEDGE_STYLESfamilies. Literal triples fold into each node’sinfohover lines. Filters:namespaceskeeps only elements under the named top-level namespaces,familieskeeps only the named edge families,isolated=Falsedrops nodes left edgeless by the filters.literals=Trueadds value literals as leaf nodes (edge familyvalue);external=Trueadds 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), andnamespaces(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.
- 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_graphis aModelor a graph already built withlongeron.rdf.to_graph()(pass the latter to keepevaluated=Trueliterals in the hover payloads).namespaces/families/literals/external/isolatedselect the initial view exactly as ingraph_view(); the in-scene panel (orwidget.filter(...)) changes them later, re-layouting kernel-side on every change.seedanditerationssteer the deterministicspring_layout()embedding; the layereddag_layout()embedding ships alongside it and the in-scene slider morphs between the two without kernel round trips.Views larger than
node_capnodes keep thenode_caphighest-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
selectedtrait (qualified names, two-way) pluson_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), andcounts/layout_secondsreport the current view’s size and layout cost.focus(id, k=...)/unfocus()isolate a neighborhood kernel-side, andexport_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).
countnodes andedges(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 overiterationssteps andseedfixes the starting positions, so the result is reproducible. The returned positions are centered and scaled to fit a sphere ofradius.