Model explorer¶
A model explorer for Jupyter: tree navigator beside a diagram pane.
explore() builds the widget:
import longeron
from longeron.widgets import explore
model = longeron.load("examples/deepscout")
explore(model)
The LEFT pane is ModelTree – a small self-contained anywidget
tree over the model’s OWNING structure (packages, definitions, usages,
nested members; node ids are qualified names). Rows carry a kind badge
(part def / state / requirement …) colored by family, a dim
: Type suffix for typed usages, disclosure triangles with lazy child
rendering (only expanded rows reach the DOM, so the biggest shipped
example stays snappy), roving-focus keyboard navigation (arrows move
focus, Enter/Space selects), and the toolbar search idiom from
longeron.toolbar: a live substring filter over names and
qualified names with a matches/total count that prunes the tree to
matches plus their ancestors. RELATIONSHIPS are first-class rows too:
connections, bindings, flows, satisfies, allocations, interfaces,
dependencies, imports, exposes, element filters and aliases appear
under the element that OWNS them (kind='relationship' at the tree
seam) with a dim italic label in the satisfy idiom (satisfy X,
connect a to b, expose P::**) and a dashed kind chip; a compact
toggle button beside the search box shows/hides them (the
show_relationships trait, default ON; the matches/total counts
respect it). Documentation and comments stay out of the tree.
The RIGHT pane shows the selected element through the diagram kind
picked by a compact toggle switcher that only offers the APPLICABLE
kinds: structure for everything (
structure_diagram() scoped to the nearest owning package, so the element
appears among its siblings and relationship edges), state / action
for state and action definitions/usages, and requirements – the
structure view re-scoped to the containing package’s requirement
definitions/usages, satisfy usages and their satisfying elements (see
requirements_view()). Switching kinds preserves the selection;
every diagram keeps its own toolbar (fit / center / routing / search).
Selection links BOTH ways and is echo-free by idempotence: selecting a
tree row renders (or reuses – diagrams are cached per scope and kind)
the applicable diagram and highlights the element through the diagram’s
selection tool; clicking a diagram node (
on_select()) selects and reveals the element in the tree – ancestors
expand, the row scrolls into view – WITHOUT rebuilding the diagram that
was clicked. Every hop writes a trait only when the value actually
changes, so a selection echo dies at its first fixpoint instead of
ping-ponging. Relationships join the loop where they are DRAWN: a
relationship rendered as a diagram EDGE (an anonymous connect / bind /
flow / satisfy / allocate, a dependency, an alias) is selected through
its edge’s synthetic transport id – the widget’s _lgn_rel_edges
seam (longeron.diagrams.structure_diagram()) maps edge ids back
to model elements, so a tree click highlights the edge and an edge
click reveals the tree row. A relationship with no drawn edge (expose,
import, filter; unresolvable ends) still selects in the tree, shows its
OWNER’s diagram, and highlights the nearest drawn ancestor.
Composed strictly from public surfaces: the diagram constructors and
on_select from longeron.diagrams, the model vocabulary, and
the resolver. Needs the diagram toolchain (the vendored ipyelk) plus
anywidget, like longeron.widgets.replay.
The tree pane sits behind a SMALL EXPLICIT SEAM – the TreeView
protocol (set_nodes / selected / on_select / reveal /
filter) over TreeNode dicts – so the engine can be swapped
(say, for a react-arborist-based tree widget) without touching any
explorer logic: pass any conforming engine as Explorer(model,
tree=...). ModelTree is the built-in engine.
Layout is a second small seam: explore(model, layout=...) composes
the SAME panes either inline (a plain HBox, works everywhere) or
into a resizable JupyterLab split panel via ipylab (lab; the
explorer extra); auto picks lab only when ipylab is
installed and a Lab frontend is detected, else falls back inline.
Docking is a WELL-BEHAVED Lab citizen, twice over. First, the panel
docks with mode="tab-after" by default – its own full-width
main-area tab (not activated), so running a notebook never reshapes it;
pass mode="split-right" (or any Lab dock mode) to opt into a split.
Either way the panes FILL the panel: the header keeps its natural
height and the diagram box flex-grows to every remaining pixel (the
inline layout instead stays bounded at the height parameter – it
lives in a notebook cell). Because a background tab renders hidden,
the panel’s FIRST reveal triggers one diagram re-fit (the initial
auto-fit aimed at a zero-sized viewport). Everything else about
keeping the framing honest is the DIAGRAM WIDGET’S OWN job now: every
widget built by longeron.diagrams carries a hidden fit sentinel
inside its own DOM (see longeron.toolbar.AutoFitTool), so a
newly built diagram is fitted the moment its view actually renders
(the first-layout auto-fit can be dropped mid view construction: wide
diagrams used to come up unfitted, overflowing the pane), and a pane
resize re-fits the visible diagram unless the user has panned or
zoomed since the last auto-fit (their viewport is theirs). The
explorer adds only what the sentinel cannot know: a cached diagram
re-entering the box on a kind switch is re-fitted against its current
rendered size (built widgets stay in the box as persistent,
display-toggled children, so their live views hear the fit). Second,
docking is IDEMPOTENT per model: re-running the
cell – or restarting the kernel and running all cells – REPLACES the
model’s panel instead of stacking a new one. Two mechanisms cooperate,
keyed by _dock_key() (a slug of the model’s display name, so
explorers over different models coexist):
same kernel: a module-level registry (
_DOCKED_PANELS) closes the previous ipylab panel before adding the new one;fresh kernel (the restart case, where the old panel’s kernel is dead and no Python-side handle can reach it): every docked panel’s tab carries its identity as
data-lgxkey/data-lgxstampattributes (lumino renderstitle.datasetonto the tab viasetAttribute, which lowercases keys – hence the flat spelling; ipylab syncspanel.title.dataset), and a tiny hidden anywidget (_DockSweeper) rides inside each panel. When the NEW panel’s sweeper renders, it finds any main-area tab with the same key and an OLDER stamp and closes it through lumino’s own close path (synthetic pointer events on the tab’s close icon – exactly what a user click does), so the dock layout stays consistent; a MutationObserver re-sweeps whenever the dock’s tabs change, so panels that finish attaching late are still reconciled. ipylab 1.1 itself exposes no dispose-by-id surface (itsShellmodel only supportsadd/expandLeft/expandRight), which is why the orphan case is handled browser-side.
- longeron.widgets.explorer.DIAGRAM_KINDS: tuple[Literal['structure', 'state', 'action', 'requirements'], ...] = ('structure', 'state', 'action', 'requirements')¶
every diagram kind the switcher can offer, in display order (the same vocabulary a view persists:
longeron.views.ViewKind; derived from it, so the two cannot drift)
- class longeron.widgets.explorer.Explorer(model, *, tree=None, layout='auto', mode='tab-after', structure_scope='package', height='600px')[source]¶
Bases:
HBoxTree navigator (left) + applicable-kind diagram pane (right).
Build one with
explore(). The public knobs:tree– the tree engine, anyTreeView(defaultModelTree; itsselected/querytraits are the headless automation surface);kind_switcher– the toggle buttons offering the applicable diagram kinds for the current selection;diagram– the currently displayed diagram widget;select()– programmatic selection by qualified name or element;save_view()(and the header’ssave_button) – save the current diagram as a SysML v2 view usage plus sidecar entry (longeron.views);layout_strategy– the resolved layout ("inline"or"lab"; seeexplore());dock_mode– how thelablayout docks into the shell (default"tab-after"; seeexplore()).
The panes are built ONCE; the layout strategy only composes them:
inlineputs them side by side in this HBox (28%/72%),labdocks them as a resizable JupyterLab split panel (lab_panel) and leaves a small placeholder in the cell output.Diagrams are cached per (scope, kind): re-selecting inside the same package reuses the SAME widget (the browser keeps its layout), so a selection change costs one trait write, not a diagram rebuild.
- property kind: Literal['structure', 'state', 'action', 'requirements']¶
The active diagram kind (one of
DIAGRAM_KINDS).
- property diagram: Any¶
The diagram widget currently SHOWN in the right pane.
Built widgets stay in the diagram box as persistent children – their browser views must survive re-shows (see
_show()) – so the shown one is the child whosedisplayis notnone.
- save_view(name=None, *, sidecar=None)[source]¶
Save the current pane’s diagram as a SysML v2 view usage.
The chrome affordance behind the header’s save button. The currently shown diagram widget carries its own root and kind (the
longeron.viewsseam), so this is a thin capture:longeron.views.save_view()appends the typed view usage – with a recursive expose of the shown scope and the standardrenderreference – to the scope’s owning package, and the live presentation (direction, routing, collapse state) lands in the sidecar entry.sidecaris a path,Noneto auto-discover the workspace sidecar next to the model’s sources (skipped silently for in-memory models), orFalseto skip the sidecar write. The model text itself is NOT rewritten here: export the model (longeron.save()/to_sysml) or push it over the API to materialize the change. Returns the view usage element; the tree refreshes so the saved view appears.- Return type:
- refresh()[source]¶
Re-read the model after an edit: rebuild the tree, re-render.
The app calls this on its launched explorers after a
longeron.editrename or value edit (the tree’s labels, qualified-name node ids, and the diagrams’ drawn labels may all be stale). The tree payload rebuilds wholesale (_tree_data()+set_nodes– the cheapest correct hook); the resolver is rebuilt too, because renames invalidate its resolution caches. Every CACHED diagram is dropped and the current selection re-rendered through the normal_showpath; the previously built widgets stay in the diagram box as hidden children – removing them would zombie their live browser views (see_show()) – but the cache drop guarantees a stale diagram is never shown again. Selection is preserved by element IDENTITY (its qualified name may have changed); an element that left the tree falls back to the root.- Return type:
- longeron.widgets.explorer.LayoutChoice¶
the layout strategies
explore()(and the app’sopen) accept: detect the frontend, force the side-by-side HBox, or force the JupyterLab dock (which requires ipylab)alias of
Literal[‘auto’, ‘inline’, ‘lab’]
- class longeron.widgets.explorer.ModelTree(nodes=(), **kwargs)[source]¶
Bases:
AnyWidgetThe built-in
TreeViewengine (a self-contained anywidget).Disclosure rows, kind badges, filter, keyboard navigation. Pure presentation over
TreeNodedicts – it holds no model references, only ids (qualified names).selectedis the two-way selection trait (at most one id); setting it from Python reveals the node in the browser (ancestors expand, the row scrolls into view).querylive-filters the tree exactly like the diagram toolbar’s search (case-insensitive substring over label and qualified name);match_count/total_countmirror itsmatches/totalcounter and are computed kernel-side too, so headless tests see the same numbers the browser shows.show_relationships(default True) is the tree-toolbar toggle’s trait: False hides everykind='relationship'row – they drop out of the rendered tree AND out of both counts – so kernels and notebooks can drive the toggle programmatically.- selected¶
selected node ids; [] = no selection
- query¶
live filter text; empty shows the whole tree
- show_relationships¶
whether relationship rows are shown (and counted)
- match_count¶
how many visible nodes match the query
- total_count¶
how many nodes the tree shows
- set_nodes(nodes)[source]¶
Replace the tree’s contents (the browser re-indexes and re-renders).
- Return type:
- on_select(callback)[source]¶
Call
callbackwith the selected ids on every selection change.- Return type:
- longeron.widgets.explorer.ResolvedLayout¶
what
"auto"resolves to: the concrete strategy a built widget runs (Explorer.layout_strategy; the inspector accepts the same pair)alias of
Literal[‘inline’, ‘lab’]
- longeron.widgets.explorer.StructureScope¶
what the structure view scopes to: the selection’s owning package, or the selected element itself
alias of
Literal[‘package’, ‘element’]
- class longeron.widgets.explorer.TreeNode[source]¶
Bases:
_TreeNodeBaseOne tree node, as plain data.
childrennests the owned members (engines that lazy-load may key offhas_childreninstead of materializing them up front);suffixis an optional dim tail (: Typefor typed usages).kindvaluerelationshipmarks relationship rows (the built-in engine dims them and offers a show/hide toggle; an engine that does not style them may treat the value like any other family). Engines must ignore keys they do not understand.
- class longeron.widgets.explorer.TreeView(*args, **kwargs)[source]¶
Bases:
ProtocolThe seam between the explorer and its tree pane.
The explorer drives the tree ONLY through this surface, so the engine can be swapped – e.g. for a react-arborist-based widget – without touching any explorer logic. An engine that is also an ipywidget joins the explorer’s layout; a headless engine (like the test suite’s stub) simply is not displayed.
selectedholds the selected node ids (at most one for a single-selection engine) and must be plain-assignable; assignment from the explorer MAY re-enteron_select()callbacks – the explorer is idempotent against that echo.
- longeron.widgets.explorer.applicable_kinds(element)[source]¶
Which
DIAGRAM_KINDSapply toelement.structurealways applies;state/actionapply to state and action definitions/usages (usages typed by a definition expand its submachine, exactly likestate_diagram());requirementsapplies when the nearest owning package (or the model root) contains requirement definitions/usages or satisfies.
- longeron.widgets.explorer.explore(model, *, tree=None, layout='auto', mode='tab-after', structure_scope='package', height='600px')[source]¶
Explore
model: a tree navigator beside a diagram pane.Keyword arguments reach
Explorer(spelled out here so the vocabularies typecheck at the call site):layout–"auto"(the default: dock into JupyterLab when ipylab is installed and a Lab frontend is detected, else render inline),"inline"(a plain side-by-side HBox; works everywhere – nbclient, VS Code, docs), or"lab"(require the ipylab docking; raisesMissingExtraErrorunless theexplorerextra is installed);mode– how thelablayout docks into the shell, passed straight through to JupyterLab ("tab-after","tab-before","split-right","split-left","split-top","split-bottom", …). The default"tab-after"opens the explorer as its own full-width main-area tab WITHOUT stealing focus or width from the notebook; choose asplit-*mode (or drag the tab) to see both at once. Ignored by theinlinelayout. Re-running the cell – or restarting the kernel and running all cells – REPLACES the model’s docked panel instead of accumulating copies (see the module docstring);structure_scope–"package"(the default) scopes the structure view to the selection’s owning package so relationship edges to siblings stay visible;"element"scopes it to the selected namespace itself;height– the explorer’s inline CSS height: the tree pane and the diagram pane both honor it (default"600px"; the diagram area takes the pane minus the kind-switcher header). Thelablayout ignores it – the docked panel fills its tab, and the dock’s split handles own the sizing.
- Return type:
- longeron.widgets.explorer.requirements_view(scope, *, resolver=None, **kwargs)[source]¶
The requirements landscape of
scopeas a structure diagram.Collects the requirement definitions and usages under
scope, the satisfy usages, and each satisfy’s satisfying element (itsbytarget and satisfied requirements, resolved against the real model), then renders them throughstructure_diagram()under a syntheticrequirementspackage. The collected elements are listed in the synthetic package WITHOUT re-parenting (theirownerchains – and therefore their qualified names, the diagram node ids – stay exactly those of the real model), so the view is a pure read-only projection: satisfy keyword edges, reference subsetting into «requirement» boxes, and typing edges all draw from the same public structure view the explorer uses everywhere else.Elements whose ancestor is already collected are skipped – they are drawn nested inside that ancestor’s box, and a second top-level node would duplicate their qualified-name id.
Keyword arguments pass through to
structure_diagram()(direction,routing,membership, …) – the seam view restoration uses to re-apply persisted presentation.- Return type: