Expansion and visibility¶
On this page¶
- Managed branches
- Source data and example
- Requests and returned dictionaries
- Limits, search, and analysis
- Node actions
- Right-click workflow
- Expansion metadata
- Python expansion
- Neighborhood and visibility
- Position stability
Managed branches¶
Use ExpansionController for independent, reversible exploration. Source data,
loaded records, and displayed records have different roles. Collapse removes
records only from the displayed graph, retaining cached edits and positions.
It does not delete database rows or dataframe records.
Expand ABC123 to discover its locations, then expand each location separately.
Collapsing Location 1 retains the vehicle and both locations; the shared report
remains visible through Location 2. Deeper branches are remembered and return
when Location 1 reopens. Cycles do not retain an unanchored branch.
Right-click offers Expand connections, Load more, Collapse connections,
and Keep visible. Keeping a record visible is separate from locking its position.
The Python protect method also accepts edges, keeping their endpoints available
as anchors for later exploration and search.
When calling collapse, an explicit branch_id closes only that branch, even
when its starting node is also selected. Without branch_id, the selected nodes'
branches are closed together.
The connections dialog contains direction, relationship, exact-match property,
and ISO-8601 time filters, configurable limits, and individual cached branches.
An exact None attribute matches an explicit null value, not a missing property.
Time ranges are validated before scheduling: the start cannot follow the end,
and timestamps without an offset are interpreted as UTC.
Collapse all closes every branch, retaining starting/protected records.
Source data and example¶
The shared example has 12 nodes and 13 edges: two vehicles, three locations, two cameras, two times, and three reports/attachments. The first view contains only ABC123. Location 1 and Location 2 connect to the same report; Camera 1 and its sighting form a cycle. These sample entities illustrate general graph exploration, not a domain restriction.
"""Run from the checkout: uv run python -m docs.snippets.managed_expansion."""
from examples.expansion_data import expansion_source, expansion_start
from st_graph_workbench import (
ExpansionController,
InMemoryExpansionProvider,
records_to_dataframe,
)
source = expansion_source()
provider = InMemoryExpansionProvider(source)
controller = ExpansionController(expansion_start(), source_version="2026-01")
# Load the two locations, then their independent connections.
for node_id in ("ABC123", "location-1", "location-2", "camera-1"):
controller.expand(node_id, provider)
# Location 1 and the vehicle remain. The shared report is retained by Location 2.
controller.collapse(["location-1"])
print(records_to_dataframe(controller.view()))
print(records_to_dataframe(controller.changes))
# Reopen from the cache, including the previously opened camera branch.
controller.expand("location-1", provider)
snapshot = controller.snapshot()
restored = ExpansionController.restore(snapshot, source_version="2026-01")
assert restored.view() == controller.view()
assert len(source["nodes"]) == 12 # Collapse never edited the source.
The output dataframe retains the shared report after Location 1 collapses. The change dataframe explains each added, hidden, or retained record. The loaded cache is unchanged by collapse, and the snapshot assertion verifies restoration.
Run /learn_expansion or /branch_expansion in the local examples app. They
execute the same workflow, showing source records before code and graph.
Requests and returned dictionaries¶
Pass controller.describe(provider) to graph_workbench(expansion=...), a stable
key, and controller.commands(previous_view) for incremental updates with
elements_sync="initial". In the callback, pass the event to handle_event and
retain returned positions using update_records.
An event's data.operation identifies the intent; data.node_ids identifies its
anchors; data.request_id deduplicates delivery; data.controller_id rejects
events from an obsolete controller. Provider requests additionally include
branch_id, query, cursor, limit, source_id, source_version, and revision.
Responses contain request_id, elements, has_more, cursor, total_nodes,
and total_edges. Totals count distinct records separately; None means unknown.
A continuing page must advance its cursor. Edges can refer to loaded endpoints.
Overlapping records are deduplicated and do not overwrite explicit cached edits.
Badges show node and edge changes separately, such as +2n/2e. Counts describe
the next displayed change, not degree. A shared record excluded from a collapse
is excluded from that collapse count. Edge-only expansion remains available.
Remote providers without a cheap local preview show unknown next counts.
controller.changes is dataframe-ready: id identifies the record, kind
distinguishes node/edge, change is added/hidden/retained, and reason explains
starting, protected, shared, active-branch, or not-in-view status.
retained_by lists the starting nodes of branches that currently require that record.
Limits, search, and analysis¶
The managed-expansion toolbar compacts earlier than a simple graph toolbar because it carries source-scope and branch controls. Select the search icon to open its dedicated row. Closing that row returns the measured space to the canvas without changing loaded records, branch state, selection, or search results.
Defaults are 50 neighbors per page, depth 3, 500 additional displayed nodes, and 2,000 additional displayed edges. The interactive example uses pages of two to make pagination observable. Bulk execution processes one batch per continuation; cancel prevents future batches and invalidates late responses. It cannot abort an application's already-running network call. Oversized batches are rejected whole, so a limit can stop below its numerical maximum.
Failures preserve the last valid graph and cursor. Retry uses a new request ID. Errors from filtered branches also appear in the graph status area; reopen the connections dialog and retry with the same filters. Receipt acknowledgment is separate from provider completion; persistent request state survives Components v2 reruns. Source-version invalidation rejects old data. Successful requests remain acknowledged if a later callback fails. Receipts for requests still queued in the browser are retained until that queue drains. Hiding or filtering a pending request's starting node invalidates its response, even if the node is subsequently restored before that response arrives. Managed incremental commands also remain pending until the browser acknowledges them. An unchanged view produces no new commands, avoiding selection/rerun loops.
For asynchronous providers, call request, schedule the returned request in your
application, then call apply_response on completion. Keep credentials, timeouts,
authorization, and worker scheduling in the application. The synchronous
expand and handle_event helpers intentionally do not create background workers.
Apply controller state changes on one owner thread; workers should return
responses for that thread to validate and apply.
Search offers displayed, loaded, and provider-backed source scopes. Revealing a cached result reopens its recorded path. Source results must contain actual context paths; disconnected results are reported, never connected artificially. Revealing a source match respects cached edge edits and deletions. A path that no longer exists is rejected without changing the current graph; repeat the search against the updated source instead.
Analysis defaults to Displayed graph. Loaded records uses a separate
headless Cytoscape graph. Source dataset requires a provider implementing
analyze(request); unsupported analyses must fail explicitly, not fall back.
The example provider implements source degree. Result dictionaries state scope,
source/version, view revision, graph counts, and completeness; collapsed result
IDs are listed separately from highlighted visible records.
Browser results use scope="visible" or scope="loaded", source_dataset_id,
source_version, and view_revision. complete=True means complete for the
chosen graph, while source_complete=False prevents interpreting a partial cache
as the full source. Shortest-path source_id still identifies its starting node.
Snapshots include versioned cached records, branches, independent masks,
protected IDs, positions, and viewport, but no provider or credentials. Application
properties may still contain sensitive data: treat snapshot files like source data.
Restore requires the same source version; pass the expected source_id too when
accepting uploaded snapshots. Use commands(previous, restore_positions=True)
and a set_viewport command when explicitly restoring saved coordinates and view.
Use invalidate explicitly for a new
checkpoint. Keep actual CRUD deletion (delete_records) separate from collapse.
Edge updates through update_records include id, source, and target, as
required by upsert_elements. Snapshots resolve starting records against current
cached edits and retain required edge endpoints and compound parents.
Cached records and positions must be JSON-compatible; invalid coordinates or
malformed batches fail before replacing the last valid graph.
Node actions¶
node_actions accepts these values:
removeexpandshow_neighborsshow_incomingshow_outgoinghide_unselectedrestore_hidden
The toolbox exposes enabled actions. A node context menu provides the most direct expand, collapse, and remove workflow.
Right-click workflow¶

Right-click a node to request expansion or collapse; the badge comes only from explicit metadata.
The browser emits an intent containing the relevant node IDs. Python then queries related data, validates it, and updates the authoritative graph.
Expansion metadata¶
A collapsed node may report next_count, total_count, and depth; an
expanded node may report collapse_count. Metadata controls only the badge and
summary. It does not contain or automatically fetch the related records.
Omit data.expansion from nodes with no expansion. This avoids misleading
badges on terminal records.
Python expansion¶
import streamlit as st
from st_graph_workbench import graph_workbench, upsert_elements
RELATED = {
"ABC123": {
"nodes": [
{
"data": {
"id": "person-maya",
"label": "PERSON",
"name": "Maya Reed",
}
}
],
"edges": [
{
"data": {
"id": "ABC123-person-maya",
"label": "REGISTERED_TO",
"source": "ABC123",
"target": "person-maya",
}
}
],
}
}
if "graph" not in st.session_state:
st.session_state.graph = {
"nodes": [
{
"data": {
"id": "ABC123",
"label": "VEHICLE",
"name": "ABC123",
"expansion": {
"state": "collapsed",
"next_count": 1,
"total_count": 2,
"depth": 1,
},
}
}
],
"edges": [],
}
def handle_event() -> None:
event = st.session_state.get("expansion-graph")
if not isinstance(event, dict) or event.get("action") != "expand":
return
for node_id in event.get("data", {}).get("node_ids", []):
related = RELATED.get(node_id)
if related:
st.session_state.graph = upsert_elements(
st.session_state.graph,
nodes=related["nodes"],
edges=related["edges"],
)
graph_workbench(
st.session_state.graph,
node_actions=["expand"],
on_change=handle_event,
key="expansion-graph",
)
Production handlers should also deduplicate timestamps, persist positions, and support collapse by tracking which nodes and edges each expansion introduced.
Neighborhood and visibility¶
show_neighbors, show_incoming, and show_outgoing temporarily filter the
visible graph around the current selection. hide_unselected keeps selected
elements and their immediate connective context. restore_hidden shows all
elements again. These operations do not delete Python records.
Visibility events return the operation and visible node/edge IDs, which can be used to synchronize a table or describe the current analytical view.
Position stability¶
Expansion should preserve the existing viewport and positions. Place newly loaded nodes near the expanded node, reuse returned positions, and avoid rerunning a full layout after every small update. For command-driven graphs, add elements incrementally and run a layout only when the analyst requests it.
Conclusion¶
Expansion is an application query initiated from the graph. Accurate metadata, idempotent handlers, and stable positions make it predictable at increasing node counts.