Skip to content

Persisted layouts and scale

On this page

Persist positions

Enable return_positions=True, deduplicate position events, and store model coordinates by stable node ID. Persist positions separately from business data when the layout is user-specific.

import copy
import streamlit as st

from st_graph_workbench import graph_workbench


def apply_positions(elements: dict, position_records: list[dict]) -> dict:
    positions = {
        str(item["id"]): item["position"]
        for item in position_records
        if "id" in item and "position" in item
    }
    updated = copy.deepcopy(elements)
    for node in updated["nodes"]:
        node_id = str(node["data"]["id"])
        if node_id in positions:
            node["position"] = positions[node_id]
    return updated


event = graph_workbench(
    st.session_state.elements,
    layout={"name": "preset", "fit": True, "padding": 40},
    return_positions=True,
    connected_drag={"enabled": False, "depth": 1, "max_nodes": 100},
    key="persisted-layout",
)

if isinstance(event, dict) and event.get("action") == "positions":
    st.session_state.elements = apply_positions(
        st.session_state.elements,
        event.get("data", {}).get("positions", []),
    )

Restore a layout

Attach each saved position to its node and use layout={"name": "preset", "fit": True, "padding": 40}. Missing positions are valid during gradual migration, but a complete saved layout produces the most predictable result.

Command-driven updates

import streamlit as st

from st_graph_workbench import (
    apply_graph_command,
    graph_workbench,
    upsert_elements_command,
)

if "elements" not in st.session_state:
    st.session_state.elements = {"nodes": [], "edges": []}
if "commands" not in st.session_state:
    st.session_state.commands = []

if st.button("Load sighting"):
    command = upsert_elements_command(
        "load-sighting-1",
        nodes=[
            {"data": {"id": "ABC123", "label": "VEHICLE", "name": "ABC123"}},
            {"data": {"id": "gate-4", "label": "LOCATION", "name": "Gate 4"}},
        ],
        edges={
            "data": {
                "id": "ABC123-gate-4",
                "label": "SEEN_AT",
                "source": "ABC123",
                "target": "gate-4",
            }
        },
    )
    st.session_state.elements = apply_graph_command(st.session_state.elements, command)
    st.session_state.commands = [command]

graph_workbench(
    st.session_state.elements,
    graph_commands=st.session_state.commands,
    elements_sync="initial",
    key="command-driven-graph",
)

elements_sync="initial" preserves the live Cytoscape viewport while commands add, update, or remove small batches. Mirror mutation commands in Python so a future remount starts from the same graph.

For a user-triggered cursor workflow, pass ProgressiveLoadConfig through the progressive_loading argument. The complete callback and event dictionary are shown in the progressive loading guide.

Large and dense graphs

Choose large when element count and layout cost dominate. Choose dense when edge count and visual clutter dominate. Combine profiles with progressive expansion, labels that disappear at low zoom, non-animated updates, and focused subgraph workflows.

Operational checks

Measure node/edge totals, layout duration, command size, expansion latency, browser memory, and drag/search responsiveness. Test repeated expansion, collapse, selection, deselection, and viewport restoration rather than only the initial render.

Conclusion

Persisted positions protect analytical context. Incremental commands protect responsiveness. Both depend on stable IDs and synchronized Python state.