Skip to content

Component API

On this page

Main component

st_graph_workbench.component.component.graph_workbench

graph_workbench(elements: Elements, layout: Layout = 'cose', node_styles: Optional[List[Union[NodeStyle, StyleRule]]] = None, edge_styles: Optional[List[Union[EdgeStyle, StyleRule]]] = None, height: int = 500, key: Optional[str] = None, on_change: Optional[Callable[..., None]] = None, node_actions: Optional[List[NodeAction]] = None, crud_actions: Optional[List[CrudAction]] = None, edit_actions: Optional[List[EditAction]] = None, viewport_actions: Optional[List[ViewportAction]] = None, enable_node_actions: Optional[bool] = None, events: Optional[List[Event]] = None, validate: bool = True, selection_mode: SelectionMode = 'single', return_selection: bool = False, search: bool = False, analysis_actions: Optional[List[AnalysisAction]] = None, return_positions: bool = False, performance_profile: PerformanceProfile = 'default', min_zoom: Optional[float] = None, max_zoom: Optional[float] = None, wheel_sensitivity: Optional[float] = None, graph_commands: Optional[List[GraphCommand]] = None, elements_sync: ElementsSync = 'always', progressive_loading: Optional[ProgressiveLoadConfig] = None, expansion: Optional[dict[str, Any]] = None, show_selection_details: bool = True, connected_drag: Optional[ConnectedDragConfig] = None, toolbar: Optional[ToolbarConfig] = None) -> Optional[GraphEvent]

Renders an interactive graph workbench using Cytoscape.js in Streamlit.

Parameters:

  • elements (dict) –

    Graph elements data including nodes and edges. Each node should have an 'id', and 'label'. Each edge should have an 'id', 'source', 'target', and 'label'. Nodes can optionally include an 'expansion' object with 'state', 'next_count', 'total_count', 'depth', and 'collapse_count' to show expand/collapse badges and exact side-panel totals.

  • layout (Union[str, dict], default: 'cose' ) –

    Layout configuration for Cytoscape. A string selects a supported named layout, including "preset". A dictionary must contain a supported name and may override its Cytoscape options. Invalid names fail in Python before the browser mounts. Default is "cose". The supported layouts and defaults are available in st_graph_workbench.component.layouts.

  • node_styles (list[NodeStyle | StyleRule], default: [] ) –

    Node label styles and optional selector-based rules applied to nodes.

  • edge_styles (list[EdgeStyle | StyleRule], default: [] ) –

    Edge label styles and optional selector-based rules applied to edges.

  • height (int, default: 500 ) –

    Component height in positive integer pixels. The frontend updates the container height on rerun and observes size changes so Cytoscape can resize without forcing a remount.

  • key (str, default: None ) –

    A unique key for the component. If provided, this key allows multiple instances of the component to exist in the same Streamlit app without conflicts. Setting this parameter is also important to avoid unnecessary re-rendering of the component. The wrapper encodes this value into a Streamlit-safe internal component key, so similar values such as 'case__left' and 'case--left' remain separate instances without using reserved bidirectional-component ID delimiters.

  • on_change (callable, default: None ) –

    Optional Streamlit callback invoked when the component value changes. Read the latest event from the callback's component state or use the function's return value during ordinary reruns.

  • node_actions (list[NodeAction], default: [] ) –

    Enables node and neighborhood controls such as 'remove', 'expand', 'show_neighbors', 'show_incoming', 'show_outgoing', 'hide_unselected', and 'restore_hidden'. Expansion/collapse still emits the existing 'expand' action so callback behavior stays compatible.

  • crud_actions (list[CrudAction], default: [] ) –

    Enables browser-side CRUD intent buttons. The component emits an action == "crud" event with the requested operation and selected graph data; your Streamlit app remains responsible for changing elements, fetching external data, and persisting the graph.

  • edit_actions (list[EditAction], default: [] ) –

    Enables frontend-local editing tools such as add node, connect selected nodes, delete selected, lock/unlock selected nodes, make nodes grabbable/ungrabbable, snap to grid, undo, and redo. These actions emit action == "edit" events so Python can persist the changed graph. For large interactive editing, combine this with elements_sync='initial' and a stable key.

  • viewport_actions (list[ViewportAction], default: [] ) –

    Enables extra viewport tools such as toggling user zoom/pan, saving and restoring a browser-local viewport, and resetting pan/zoom.

  • enable_node_actions (bool, default: None (deprecated) ) –

    Compatibility parameter retained for older examples. Use node_actions instead to enable node actions. If enable_node_actions is set to True and node_actions is not provided, default actions ('remove', 'expand') will be enabled.

  • events (list[Event], default: [] ) –

    For advanced usage only. A list of events to listen to. When any of these events are triggered, the event information is sent back to the Streamlit app as the component's return value. The browser updates the listener set on rerun, so changing events does not require a new component key. Custom names cannot reuse built-in action names such as selection, search, analysis, crud, edit, viewport, or load_more.

  • validate (bool, default: True ) –

    Validates element IDs, edge references, compound parents, and expansion metadata in Python before rendering.

  • selection_mode (Literal['single', 'multiple', 'box'], default: 'single' ) –

    Controls Cytoscape selection behavior. Box mode displays a Pan/Box select control so blank-canvas dragging can switch between viewport navigation and rectangular node selection.

  • return_selection (bool, default: False ) –

    When True, selection changes are returned as Streamlit events without requiring a custom Event.

  • search (bool, default: False ) –

    Shows a frontend search panel that highlights/selects matches and emits matched element data without hiding graph context.

  • analysis_actions (list[AnalysisAction], default: [] ) –

    Enables built-in graph analysis actions in the toolbox.

  • return_positions (bool, default: False ) –

    Returns node positions after layout or drag updates. When connected_drag is configured, node-drag events also include a movement dictionary describing the operation, status, anchor, native and connected moved IDs, depth, candidate count, and limit.

  • performance_profile (Literal['default', 'large', 'dense'], default: 'default' ) –

    Applies frontend rendering defaults for larger graphs. Large and dense profiles reduce animation, hide labels during viewport movement, and enable Cytoscape texture rendering hints without hiding edges during zoom or pan.

  • min_zoom (float, default: None ) –

    Optional finite numeric lower bound for Cytoscape zoom. When both bounds are provided, min_zoom must be less than or equal to max_zoom.

  • max_zoom (float, default: None ) –

    Optional finite numeric upper bound for Cytoscape zoom. When both bounds are provided, max_zoom must be greater than or equal to min_zoom.

  • wheel_sensitivity (float, default: None ) –

    Optional finite numeric mouse-wheel zoom sensitivity passed to Cytoscape. Values must be greater than zero.

  • graph_commands (list[GraphCommand], default: [] ) –

    Incremental graph commands that the browser applies directly to the existing Cytoscape instance with cy.batch(...). Commands require a stable command_id and are applied once per mounted component.

  • elements_sync (Literal['always', 'initial'], default: 'always' ) –

    Controls when full elements are sent to the browser. Use 'always' for backward-compatible full reconciliation on every rerun. Use 'initial' with graph_commands for large graphs: full elements are sent on the first render, then only small commands are sent until the frontend requests a full sync or you remount/reset the component.

  • progressive_loading (ProgressiveLoadConfig, default: None ) –

    Shows an in-canvas progress indicator and Load more button. A click returns action == "load_more" with the configured cursor, page size, loaded and total counts, remaining count, and current browser node/edge counts. Python remains responsible for fetching the next batch and sending it with an idempotent graph command. The option updates on a live component without remounting. Each request includes a unique request_id. Echo it as acknowledged_request_id after handling a request, including a fetch failure, so the button becomes available again even if the cursor and counts did not change. Existing configurations can still complete a request by changing progress.

  • expansion (dict, default: None ) –

    Opt-in branch controls from ExpansionController.describe(provider). Requires a stable key. Explicit expansion events request expand, load-more, collapse, protection, scoped search/analysis or bounded bulk work. Source records remain Python-owned; legacy expand events are unchanged when this option is omitted. Configuration updates do not require a remount. Loaded records are sent only when this is enabled.

  • show_selection_details (bool, default: True ) –

    Initial selection-details preference, adjustable in the Selection menu. The checkbox hides details without clearing selected records or changing returned events. The panel's X clears selection without changing this preference. With a stable key, the browser preference survives reruns with the same argument. Changing this argument overrides that preference without remounting; a new key or browser reload starts with the supplied default. This is a display preference, not access control.

  • connected_drag (ConnectedDragConfig, default: None ) –

    Opts into rigid connected-record dragging. The Selection menu can enable the behavior and choose one to three visible, undirected hops. Automatic followers are bounded by max_nodes; explicitly selected nodes and compound descendants retain Cytoscape's native dragging behavior. The controls are hidden when this argument is omitted. With a stable key, unchanged Python defaults preserve browser choices; changed defaults override them without remounting.

  • toolbar (ToolbarConfig, default: None ) –

    Configures the controls anchored above the graph. Adaptive mode uses the expanded presentation when space permits and compact icon menus at narrower widths. Readers can minimize and restore the toolbar when collapsible is true. With sticky true, its measured height is reserved instead of covering graph nodes. A stable component key preserves the reader's minimized preference through ordinary reruns; changing the Python configuration resets that preference.

Returns:

  • GraphEvent or None –

    The latest JSON-compatible browser event dictionary, or None before an enabled interaction emits a value. Event fields depend on the action; the event and feature guides document every returned payload shape.

Raises:

  • TypeError –

    If an option, style, event, command, or element has the wrong Python type.

  • ValueError –

    If an option value, layout, viewport bound, ID, graph relationship, or command transition is invalid. Specialized validation errors subclass ValueError and identify the failing record or command.

Source code in st_graph_workbench/component/component.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
def graph_workbench(
    elements: Elements,
    layout: Layout = "cose",
    node_styles: Optional[List[Union[NodeStyle, StyleRule]]] = None,
    edge_styles: Optional[List[Union[EdgeStyle, StyleRule]]] = None,
    height: int = 500,
    key: Optional[str] = None,
    on_change: Optional[Callable[..., None]] = None,
    node_actions: Optional[List[NodeAction]] = None,
    crud_actions: Optional[List[CrudAction]] = None,
    edit_actions: Optional[List[EditAction]] = None,
    viewport_actions: Optional[List[ViewportAction]] = None,
    enable_node_actions: Optional[bool] = None,  # deprecated
    events: Optional[List[Event]] = None,
    validate: bool = True,
    selection_mode: SelectionMode = "single",
    return_selection: bool = False,
    search: bool = False,
    analysis_actions: Optional[List[AnalysisAction]] = None,
    return_positions: bool = False,
    performance_profile: PerformanceProfile = "default",
    min_zoom: Optional[float] = None,
    max_zoom: Optional[float] = None,
    wheel_sensitivity: Optional[float] = None,
    graph_commands: Optional[List[GraphCommand]] = None,
    elements_sync: ElementsSync = "always",
    progressive_loading: Optional[ProgressiveLoadConfig] = None,
    expansion: Optional[dict[str, Any]] = None,
    show_selection_details: bool = True,
    connected_drag: Optional[ConnectedDragConfig] = None,
    toolbar: Optional[ToolbarConfig] = None,
) -> Optional[GraphEvent]:
    """
    Renders an interactive graph workbench using Cytoscape.js in Streamlit.

    Parameters
    ----------
    elements : dict
        Graph elements data including nodes and edges. Each node should have
        an 'id', and 'label'. Each edge should have an 'id', 'source', 'target',
        and 'label'. Nodes can optionally include an 'expansion' object with
        'state', 'next_count', 'total_count', 'depth', and 'collapse_count' to
        show expand/collapse badges and exact side-panel totals.
    layout : Union[str, dict], default 'cose'
        Layout configuration for Cytoscape. A string selects a supported named
        layout, including ``"preset"``. A dictionary must contain a supported
        ``name`` and may override its Cytoscape options. Invalid names fail in
        Python before the browser mounts. Default is ``"cose"``. The supported
        layouts and defaults are available in
        ``st_graph_workbench.component.layouts``.
    node_styles : list[NodeStyle | StyleRule], default []
        Node label styles and optional selector-based rules applied to nodes.
    edge_styles : list[EdgeStyle | StyleRule], default []
        Edge label styles and optional selector-based rules applied to edges.
    height : int, default 500
        Component height in positive integer pixels. The frontend updates the
        container height on rerun and observes size changes so Cytoscape can
        resize without forcing a remount.
    key : str, default None
        A unique key for the component. If provided, this key allows multiple
        instances of the component to exist in the same Streamlit app without
        conflicts. Setting this parameter is also important to avoid unnecessary
        re-rendering of the component. The wrapper encodes this value into a
        Streamlit-safe internal component key, so similar values such as
        'case__left' and 'case--left' remain separate instances without using
        reserved bidirectional-component ID delimiters.
    on_change : callable, default None
        Optional Streamlit callback invoked when the component value changes.
        Read the latest event from the callback's component state or use the
        function's return value during ordinary reruns.
    node_actions : list[NodeAction], default []
        Enables node and neighborhood controls such as 'remove', 'expand',
        'show_neighbors', 'show_incoming', 'show_outgoing',
        'hide_unselected', and 'restore_hidden'. Expansion/collapse still emits
        the existing 'expand' action so callback behavior stays compatible.
    crud_actions : list[CrudAction], default []
        Enables browser-side CRUD intent buttons. The component emits an
        `action == "crud"` event with the requested operation and selected
        graph data; your Streamlit app remains responsible for changing
        `elements`, fetching external data, and persisting the graph.
    edit_actions : list[EditAction], default []
        Enables frontend-local editing tools such as add node, connect selected
        nodes, delete selected, lock/unlock selected nodes, make nodes
        grabbable/ungrabbable, snap to grid, undo, and redo. These actions emit
        `action == "edit"` events so Python can persist the changed graph. For
        large interactive editing, combine this with `elements_sync='initial'`
        and a stable `key`.
    viewport_actions : list[ViewportAction], default []
        Enables extra viewport tools such as toggling user zoom/pan, saving and
        restoring a browser-local viewport, and resetting pan/zoom.
    enable_node_actions : bool, default None (deprecated)
        Compatibility parameter retained for older examples. Use `node_actions`
        instead to enable node actions. If `enable_node_actions` is set to True
        and `node_actions` is not provided, default actions ('remove', 'expand')
        will be enabled.
    events : list[Event], default []
        For advanced usage only. A list of events to listen to.  When any of these
        events are triggered, the event information is sent back to the Streamlit
        app as the component's return value. The browser updates the listener
        set on rerun, so changing `events` does not require a new component key.
        Custom names cannot reuse built-in action names such as ``selection``,
        ``search``, ``analysis``, ``crud``, ``edit``, ``viewport``, or
        ``load_more``.
    validate : bool, default True
        Validates element IDs, edge references, compound parents, and expansion
        metadata in Python before rendering.
    selection_mode : Literal['single', 'multiple', 'box'], default 'single'
        Controls Cytoscape selection behavior. Box mode displays a Pan/Box
        select control so blank-canvas dragging can switch between viewport
        navigation and rectangular node selection.
    return_selection : bool, default False
        When True, selection changes are returned as Streamlit events without
        requiring a custom Event.
    search : bool, default False
        Shows a frontend search panel that highlights/selects matches and emits
        matched element data without hiding graph context.
    analysis_actions : list[AnalysisAction], default []
        Enables built-in graph analysis actions in the toolbox.
    return_positions : bool, default False
        Returns node positions after layout or drag updates. When
        ``connected_drag`` is configured, node-drag events also include a
        ``movement`` dictionary describing the operation, status, anchor,
        native and connected moved IDs, depth, candidate count, and limit.
    performance_profile : Literal['default', 'large', 'dense'], default 'default'
        Applies frontend rendering defaults for larger graphs. Large and dense
        profiles reduce animation, hide labels during viewport movement, and
        enable Cytoscape texture rendering hints without hiding edges during
        zoom or pan.
    min_zoom : float, default None
        Optional finite numeric lower bound for Cytoscape zoom. When both
        bounds are provided, `min_zoom` must be less than or equal to
        `max_zoom`.
    max_zoom : float, default None
        Optional finite numeric upper bound for Cytoscape zoom. When both
        bounds are provided, `max_zoom` must be greater than or equal to
        `min_zoom`.
    wheel_sensitivity : float, default None
        Optional finite numeric mouse-wheel zoom sensitivity passed to
        Cytoscape. Values must be greater than zero.
    graph_commands : list[GraphCommand], default []
        Incremental graph commands that the browser applies directly to the
        existing Cytoscape instance with `cy.batch(...)`. Commands require a
        stable `command_id` and are applied once per mounted component.
    elements_sync : Literal['always', 'initial'], default 'always'
        Controls when full `elements` are sent to the browser. Use `'always'`
        for backward-compatible full reconciliation on every rerun. Use
        `'initial'` with `graph_commands` for large graphs: full elements are
        sent on the first render, then only small commands are sent until the
        frontend requests a full sync or you remount/reset the component.
    progressive_loading : ProgressiveLoadConfig, default None
        Shows an in-canvas progress indicator and Load more button. A click
        returns ``action == "load_more"`` with the configured cursor, page
        size, loaded and total counts, remaining count, and current browser
        node/edge counts. Python remains responsible for fetching the next
        batch and sending it with an idempotent graph command. The option
        updates on a live component without remounting. Each request includes
        a unique ``request_id``. Echo it as ``acknowledged_request_id`` after
        handling a request, including a fetch failure, so the button becomes
        available again even if the cursor and counts did not change. Existing
        configurations can still complete a request by changing progress.

    expansion : dict, default None
        Opt-in branch controls from ``ExpansionController.describe(provider)``.
        Requires a stable key. Explicit ``expansion`` events request expand,
        load-more, collapse, protection, scoped search/analysis or bounded bulk
        work. Source records remain Python-owned; legacy expand events are
        unchanged when this option is omitted. Configuration updates do not
        require a remount. Loaded records are sent only when this is enabled.

    show_selection_details : bool, default True
        Initial selection-details preference, adjustable in the Selection menu.
        The checkbox hides details without clearing selected records or changing
        returned events. The panel's X clears selection without changing this
        preference. With a stable key, the browser preference
        survives reruns with the same argument. Changing this argument overrides
        that preference without remounting; a new key or browser reload starts
        with the supplied default. This is a display preference, not access control.

    connected_drag : ConnectedDragConfig, default None
        Opts into rigid connected-record dragging. The Selection menu can enable
        the behavior and choose one to three visible, undirected hops. Automatic
        followers are bounded by ``max_nodes``; explicitly selected nodes and
        compound descendants retain Cytoscape's native dragging behavior. The
        controls are hidden when this argument is omitted. With a stable key,
        unchanged Python defaults preserve browser choices; changed defaults
        override them without remounting.

    toolbar : ToolbarConfig, default None
        Configures the controls anchored above the graph. Adaptive mode uses the
        expanded presentation when space permits and compact icon menus at
        narrower widths. Readers can minimize and restore the toolbar when
        ``collapsible`` is true. With ``sticky`` true, its measured height is
        reserved instead of covering graph nodes. A stable component key
        preserves the reader's minimized preference through ordinary reruns;
        changing the Python configuration resets that preference.

    Returns
    -------
    GraphEvent or None
        The latest JSON-compatible browser event dictionary, or ``None`` before
        an enabled interaction emits a value. Event fields depend on the action;
        the event and feature guides document every returned payload shape.

    Raises
    ------
    TypeError
        If an option, style, event, command, or element has the wrong Python type.
    ValueError
        If an option value, layout, viewport bound, ID, graph relationship, or
        command transition is invalid. Specialized validation errors subclass
        ``ValueError`` and identify the failing record or command.
    """
    node_styles = [] if node_styles is None else node_styles
    if not isinstance(show_selection_details, bool):
        raise TypeError("show_selection_details must be a bool.")
    edge_styles = [] if edge_styles is None else edge_styles
    node_actions = _normalize_option_values("node_actions", node_actions)
    crud_actions = _normalize_option_values("crud_actions", crud_actions)
    edit_actions = _normalize_option_values("edit_actions", edit_actions)
    viewport_actions = _normalize_option_values("viewport_actions", viewport_actions)
    events = [] if events is None else events
    analysis_actions = _normalize_option_values(
        "analysis_actions",
        analysis_actions,
    )
    graph_commands = [] if graph_commands is None else graph_commands
    height = _normalize_height(height)
    min_zoom, max_zoom, wheel_sensitivity = _normalize_viewport_options(
        min_zoom,
        max_zoom,
        wheel_sensitivity,
    )
    component_key = _component_instance_key(key)
    progressive_loading = _normalize_progressive_loading(progressive_loading)
    connected_drag = _normalize_connected_drag(connected_drag)
    toolbar = _normalize_toolbar(toolbar)
    if expansion is not None:
        if not isinstance(expansion, dict) or not isinstance(
            expansion.get("nodes"), dict
        ):
            raise ValueError(
                "expansion must be configuration from ExpansionController.describe()."
            )
        if component_key is None:
            raise ValueError("A stable key is required for managed expansion.")
        for field, kind in {
            "controller_id": str,
            "source_id": str,
            "source_version": str,
            "view_revision": int,
            "limits": dict,
            "branches": list,
            "loaded": dict,
        }.items():
            if not isinstance(expansion.get(field), kind):
                raise ValueError(
                    f"Managed expansion requires a valid {field}; use controller.describe()."
                )
        ExpansionConfig(**expansion["limits"])
        validate_elements(expansion["loaded"])

    if validate:
        validate_elements(elements, strict=True)
        validate_graph_commands(graph_commands, elements=elements)
    else:
        validate_graph_commands(graph_commands)

    _validate_option_values(
        node_actions,
        crud_actions,
        edit_actions,
        viewport_actions,
        selection_mode,
        analysis_actions,
        performance_profile,
        elements_sync,
        component_key,
    )

    node_styles_dump = [n.dump() for n in node_styles]
    edge_styles_dump = [e.dump() for e in edge_styles]
    style = node_styles_dump + edge_styles_dump

    height_str = f"{height}px"

    layout_config = resolve_layout(layout)

    events_dump = [e.dump() for e in events]

    if enable_node_actions is not None:
        warnings.warn(
            "Parameter `enable_node_actions` is deprecated. Please use the "
            "`node_actions` parameter instead.",
            GraphWorkbenchDeprecationWarning,
        )
    if enable_node_actions and not node_actions:
        node_actions = ["remove", "expand"]

    send_elements = True
    sync_state_key = None
    if elements_sync == "initial" and component_key is not None:
        sync_state_key = _elements_sync_state_key(component_key)
        send_elements = not bool(st.session_state.get(sync_state_key, False))

    def handle_event_change() -> None:
        event = (
            _extract_event(st.session_state.get(component_key))
            if component_key is not None
            else None
        )
        if _is_sync_request(event):
            if sync_state_key is not None:
                st.session_state[sync_state_key] = False
            return
        if key is not None and component_key is not None:
            st.session_state[key] = event
        if on_change is not None:
            on_change()

    receipt_key = f"{component_key}:request-receipts"
    result_key = f"{component_key}:request-result"
    command_queue_key = f"{component_key}:pending-commands"
    command_receipt_key = f"{component_key}:command-receipts"
    if expansion is not None:
        pending = dict(st.session_state.get(command_queue_key, {}))
        completed = set(st.session_state.get(command_receipt_key, []))
        for command in graph_commands:
            identity = str(command["command_id"])
            if identity not in completed:
                pending[identity] = command
        st.session_state[command_queue_key] = pending
        graph_commands = list(pending.values())

    def handle_command_receipts_change() -> None:
        if expansion is None or component_key is None:
            return
        value = st.session_state.get(component_key, {})
        receipts = _component_field(value, "command_receipts")
        completed = set(st.session_state.get(command_receipt_key, []))
        pending = dict(st.session_state.get(command_queue_key, {}))
        for identity in receipts or []:
            if isinstance(identity, str):
                completed.add(identity)
                pending.pop(identity, None)
        st.session_state[command_queue_key] = pending
        st.session_state[command_receipt_key] = sorted(completed)

    def handle_requests_change() -> None:
        if component_key is None:
            return
        value = st.session_state.get(component_key, {})
        requests = _component_field(value, "requests")
        requests = requests if isinstance(requests, list) else []
        valid_requests = []
        for event in requests:
            if not isinstance(event, dict) or event.get("action") not in {
                "expansion",
                "load_more",
            }:
                continue
            data = event.get("data")
            identity = data.get("request_id") if isinstance(data, dict) else None
            if isinstance(identity, str) and identity:
                valid_requests.append((identity, event))
        receipts = list(st.session_state.get(receipt_key, []))
        # Retain receipts until the browser removes their queued requests.
        retained = {identity for identity, _ in valid_requests} | set(receipts[-256:])
        receipts = [identity for identity in receipts if identity in retained]
        seen = set(receipts)
        st.session_state[receipt_key] = receipts
        for request_id, event in valid_requests:
            if request_id in seen:
                continue
            if key is not None:
                st.session_state[key] = event
            if on_change is not None:
                on_change()
            receipts.append(request_id)
            seen.add(request_id)
            st.session_state[receipt_key] = receipts
            st.session_state[result_key] = event
            if on_change is None:
                # Return-only applications consume one request per rerun.
                break

    component_result = _component_func(
        data={
            "elements": elements if send_elements else None,
            "style": style,
            "layout": layout_config,
            "height": height_str,
            "nodeActions": node_actions,
            "crudActions": crud_actions,
            "editActions": edit_actions,
            "viewportActions": viewport_actions,
            "graphCommands": graph_commands,
            "elementsSync": elements_sync,
            "componentKey": component_key,
            "events": events_dump,
            "assetBasePath": f"./_stcore/bidi-components/{_COMPONENT_NAME}",
            "selectionMode": selection_mode,
            "returnSelection": return_selection,
            "showSelectionDetails": show_selection_details,
            "search": search,
            "analysisActions": analysis_actions,
            "returnPositions": return_positions,
            "performanceProfile": performance_profile,
            "viewportOptions": {
                "minZoom": min_zoom,
                "maxZoom": max_zoom,
                "wheelSensitivity": wheel_sensitivity,
            },
            "progressiveLoading": progressive_loading,
            "expansion": expansion,
            "connectedDrag": connected_drag,
            "toolbar": toolbar,
            "receivedRequestIds": list(st.session_state.get(receipt_key, [])),
        },
        height=height,
        key=component_key,
        on_event_change=handle_event_change,
        on_requests_change=handle_requests_change,
        on_command_receipts_change=handle_command_receipts_change,
    )

    if sync_state_key is not None and send_elements:
        st.session_state[sync_state_key] = True

    event = _extract_event(component_result)
    event = st.session_state.pop(result_key, None) or event
    if _is_sync_request(event):
        if sync_state_key is not None:
            st.session_state[sync_state_key] = False
        return None
    if key is not None and event is not None:
        st.session_state[key] = event
    return event

Action values

Type Accepted values
NodeAction remove, expand, show_neighbors, show_incoming, show_outgoing, hide_unselected, restore_hidden
CrudAction create_node, create_edge, read_selected, update_selected, delete_selected, request_node_data
EditAction add_node, connect_selected, delete_selected, lock_selected, unlock_selected, make_ungrabbable, make_grabbable, snap_to_grid, undo, redo
ViewportAction toggle_zoom, toggle_pan, save_viewport, restore_viewport, reset_viewport
AnalysisAction shortest_path, bfs, dfs, connected_components, degree
SelectionMode single, multiple, box
PerformanceProfile default, large, dense
ElementsSync always, initial

Progressive loading

Pass a validated ProgressiveLoadConfig to progressive_loading to show the in-canvas Load more control. It emits action == "load_more"; Python fetches the next page and returns it through graph_commands. See the progressive loading guide for every field and a runnable callback.

Loading events include a unique data.request_id. The optional acknowledged_request_id configuration field acknowledges a handled success or failure without requiring changed counts or a new component key.

Connected dragging

Pass ConnectedDragConfig to connected_drag to expose an opt-in Selection toolbar preference. enabled and depth define the initial browser choice; max_depth limits the available one-to-three-hop options; max_nodes places a positive bound on automatic followers. depth cannot exceed max_depth.

Omitting the argument hides the controls and preserves existing drag behavior. See selection and search for interaction semantics and events and exports for returned position metadata.

Adaptive toolbar

Pass ToolbarConfig to toolbar to choose adaptive, expanded, compact, or initially minimized presentation. collapsible exposes the reader's minimize/restore button. sticky=True reserves the measured top toolbar height instead of allowing controls to cover the graph canvas. Toolbar presentation is browser-local and emits no graph event.

See rendering and layouts for configuration examples and responsive behavior.

Update behavior

Height, selection mode, custom events, and zoom bounds update on a live instance. show_selection_details defaults to True and is a user-adjustable display preference. Repeating its value preserves the Selection menu checkbox across reruns; changing it overrides the checkbox without remounting. The checkbox hides the panel without deselecting; the panel's X clears selection without changing the checkbox. A new key or browser reload resets the preference to the supplied value. See selection and search. Connected-drag toolbar choices follow the same stable-key rule: unchanged Python defaults preserve browser choices, while changed defaults override them without remounting. Toolbar minimize and compact-search choices are also isolated by stable key; an explicitly changed Python toolbar configuration resets them. Wheel sensitivity is read during Cytoscape initialization. Stable keys preserve the intended component instance across reruns.