Skip to main content

st-graph-workbench

st-graph-workbench logo

st-graph-workbench is a general-purpose graph component for Streamlit, powered by Cytoscape.js and integrated with Streamlit Components v2. It helps you turn Python records into explorable networks without building and maintaining a separate JavaScript application. Users can search relationships, select records, expand connected data, run graph analysis, edit structures, and return useful results to Python.

Why st-graph-workbench

Build interactive applications around connected data. Users can explore relationships, search and select nodes and edges, inspect records, run graph analysis, or request edits, while your Python application handles the results.

Use it for knowledge graphs, dependency maps, workflows, network topologies, or other connected datasets. You define what the nodes and relationships represent; the library provides reusable exploration, styling, analysis, editing, and event handling. Your application keeps control of its data and logic.

On this page

What the library provides

  • Turn validated Python dictionaries and dataframes into interactive graphs.
  • Find relevant records with text, label, property, and selector search.
  • Select individual records or groups and receive analysis-ready dictionaries.
  • Keep graph controls readable with an adaptive, compact, or minimized toolbar that can reserve space above the canvas.
  • Rigidly move a bounded one-to-three-hop connected neighborhood without changing selection or record data.
  • Reveal connections progressively through expansion and neighborhood tools.
  • Trace shortest paths and inspect traversals, components, and node degree.
  • Add application-owned CRUD workflows without surrendering data control.
  • Choose from ten layouts, preserve positions, and control the viewport.
  • Apply domain styling and familiar icons to make complex networks readable.
  • Export graph data, positions, and images for downstream data workflows.
  • Scale interaction using incremental commands and performance profiles.
  • Load large result sets in cursor-aware batches without resetting the viewport.

Python remains responsible for source records, authorization, persistence, and business rules. The component supplies the interactive graph surface and sends structured user actions back to the application.

See it in action

Find a relationship, then use the result in Python. Select two records and run shortest path: the component highlights the route and returns its node IDs, edge IDs, distance, and analysis scope. Use those results to filter a dataframe, prepare a report, or drive the next step in your application.

Shortest-path analysis highlights the four-edge route between ALPHA and BETA, leaving a separate connected group unchanged.

Real output from the analysis tutorial: five nodes on the route, four edges, and a separate group outside the result. Icons, colors, and labels come from the application's styles. The fictional sample records can be replaced with dependencies, documents, infrastructure, or any other connected data.

Continue through the screenshots below:

All five screenshots come from runnable tutorials. The annotated screenshot gallery also links each interaction to its tutorial and API guide.

Example data: All datasets and records used in the examples, documentation, and screenshots are synthetically generated. They do not describe real people, vehicles, locations, organizations, claims, or incidents.

Install

With uv:

uv add st-graph-workbench

Or with pip:

python -m pip install st-graph-workbench

For development from a source checkout:

uv sync --extra dev --extra docs

The supported minimums are Python 3.10, pandas 2.2.2, and Streamlit 1.57.

A small workflow, from records to results

The four examples below build on each other in one Streamlit script. They use a small fictional vehicle-observation dataset only to illustrate the APIs; the library is not specific to vehicles or any particular domain. Replace the sample records, labels, and relationships with those from your own application. Run the complete script with streamlit run app.py; the examples app also runs these same snippets and displays their results.

Create a graph from dictionaries

Use graph_workbench(...) to render a dictionary with nodes and edges. Here, vehicle ABC123 was observed at Harbour camera 7 at 08:14. The table appears before the graph, making the source of each element clear.

import streamlit as st

from st_graph_workbench import graph_workbench, records_to_dataframe

# Each node needs a unique ID; edges refer to those IDs.
elements = {
    "nodes": [
        {"data": {"id": "ABC123", "label": "VEHICLE", "name": "ABC123"}},
        {"data": {"id": "camera-7", "label": "LOCATION", "name": "Harbour camera 7"}},
    ],
    "edges": [
        {
            "data": {
                "id": "sighting-1",
                "source": "ABC123",
                "target": "camera-7",
                "label": "SEEN_AT",
                "observed_at": "2024-09-02 08:14",
            }
        }
    ],
}

# Inspect the source records before exploring the graph.
st.dataframe(records_to_dataframe(elements), hide_index=True)
event = graph_workbench(
    elements,
    layout="cose",
    selection_mode="multiple",
    return_selection=True,
    search=True,
    key="readme_quick_start_output",
    height=420,
)

Result: two nodes joined by one edge, plus text search and multiple selection. The edge keeps the observation time as data, available for inspection. event is None before an interaction; afterwards it contains an action name, a data dictionary, and a timestamp. A stable key identifies the component across Streamlit reruns.

Style nodes and edges

Use NodeStyle(...) to assign a color, display field, size, and packaged icon to a node type. Use EdgeStyle(...) for relationship labels and arrows.

from st_graph_workbench import EdgeStyle, NodeStyle, graph_workbench

# Match the label in each node's data; display its name and a packaged icon.
node_styles = [
    NodeStyle("VEHICLE", "#2A629A", "name", "directions_car", size=40),
    NodeStyle("LOCATION", "#2D936C", "name", "place", size=40),
]
edge_styles = [
    EdgeStyle("SEEN_AT", "#2D936C", "label", directed=True),
]

# Reuse elements from the previous example with a separate component key.
event = graph_workbench(
    elements,
    layout="cose",
    node_styles=node_styles,
    edge_styles=edge_styles,
    selection_mode="multiple",
    return_selection=True,
    search=True,
    key="readme_styled_output",
    height=420,
)

Result: a blue vehicle with a car icon, a green location with a place icon, and a directed SEEN_AT relationship. Styles match the label values in your data, so they apply to every element of that type. For conditional formatting such as highlighting high-confidence observations, use StyleRule(...).

Prepare dataframes and graph records

Use records_to_dataframe(...) to turn dictionaries, grouped records, graph elements, or returned element records into a pandas dataframe. Use dataframe_to_records(...) to convert table rows back into JSON-friendly dictionaries, including normalization of dates and missing values.

from st_graph_workbench import dataframe_to_records, records_to_dataframe

# Named groups become a record_group column in the resulting dataframe.
sightings = {
    "sightings": [
        {"vehicle_id": "ABC123", "location": "Harbour camera 7", "time": "08:14"},
        {"vehicle_id": "ABC123", "location": "Depot gate", "time": "08:41"},
        {"vehicle_id": "12VEC", "location": "Depot gate", "time": "09:10"},
    ]
}
sightings_df = records_to_dataframe(sightings)

# Convert dataframe rows back into JSON-friendly dictionaries.
records = dataframe_to_records(sightings_df.drop(columns="record_group"))

# Decide which columns identify nodes; the helper does not infer relationships.
vehicle_df = sightings_df[["vehicle_id"]].drop_duplicates()
vehicle_df = vehicle_df.rename(columns={"vehicle_id": "id"})
vehicle_df["label"] = "VEHICLE"
vehicle_df["name"] = vehicle_df["id"]
vehicle_nodes = [{"data": row} for row in dataframe_to_records(vehicle_df)]

Result: sightings_df has three rows and a record_group column containing "sightings"; records is a list of the original evidence dictionaries. vehicle_nodes contains two distinct vehicle nodes, each wrapped in {"data": ...}, ready for a graph's nodes list.

These helpers convert data representations; they do not infer graph topology. Your application chooses unique IDs and creates edges with explicit source and target IDs. See the dataframe-to-graph recipe for a complete relationship mapping.

Turn selections into useful results

Use the returned selection to filter evidence, build a detail view, or prepare IDs for a database query. This example uses event from the styled graph and sightings_df from the previous section.

import streamlit as st

from st_graph_workbench import records_to_dataframe

# Use the event from the styled graph and the source table above.
selection = event["data"] if event and event["action"] == "selection" else {}
selected_ids = selection.get("selected_node_ids", [])
selected_rows = records_to_dataframe(selection.get("selected_elements", []))

# Join graph IDs back to the source evidence, without changing that evidence.
matching_sightings = sightings_df[sightings_df["vehicle_id"].isin(selected_ids)]
st.metric("Matching sightings", len(matching_sightings))
st.dataframe(matching_sightings, hide_index=True)

Result: selecting ABC123 in the styled graph returns its two sightings, at 08:14 and 08:41. With no vehicle selected, the result is an empty table and a zero count. Clearing the selection clears the filtered result; the source records remain unchanged.

In the returned dictionary, action="selection" identifies the interaction, data.selected_node_ids supplies IDs for filtering, and data.selected_elements supplies complete element records. selected_rows flattens those records into a dataframe for a detail panel. Search and analysis produce different payloads, so check action before reading action-specific fields.

Documentation

The MkDocs site summarizes what the library can do and provides feature guides, practical recipes, troubleshooting help, and detailed API documentation for the public functions, parameters, return values, and errors.

Read the published manual at ayhaidar.github.io/st-graph-workbench.

Run it on any unused local port. This example uses 8000:

uv sync --extra docs
uv run mkdocs serve -f mkdocs.local.yml --dev-addr 127.0.0.1:8000

Start with:

Build the static documentation locally:

uv run mkdocs build --strict

Interactive Examples

Run the separate Streamlit demonstration app on port 8502:

uv run streamlit run examples/app.py --server.port 8502

Open http://localhost:8502 to Build your first graph. Follow sixteen independent tutorials from creating and styling records through selection, analysis, editing, exports, commands, and progressive loading. Each lesson shows source data, executable code, a live graph, and explained results.

Switch to Feature Lab for the comprehensive playgrounds and searchable icon browser. The learning path explains the sequence and state model; MkDocs provides the full manual and API reference. The original live README walkthrough remains available at http://localhost:8502/readme.

Core Capabilities

  • Validated Cytoscape-style nodes, edges, compound parents, and expansion data.
  • Dataframe/record conversion and pure Python graph element helpers.
  • Node, edge, selector, and packaged Material Symbols-style icon styling.
  • Ten layouts including preset, cose, fcose, dagre, and cola.
  • Search, single/multiple/box selection, neighborhood and visibility tools.
  • An adaptive, compact, or minimized toolbar that can reserve its measured height so controls do not cover the graph.
  • Opt-in bounded connected dragging with returned movement and position metadata.
  • Shortest path, BFS, DFS, connected components, and degree analysis.
  • Python-owned CRUD intents and idempotent incremental browser commands.
  • Browser-local editing, position return, viewport control, and undo/redo.
  • Visible/full/selected JSON, PNG, JPG, and node-position exports.
  • Default, large, and dense performance profiles.
  • Cursor-aware progressive loading with visible progress and incremental batches.
  • Opt-in reversible branches, shared-record retention, bounded expand-all, cached search, scoped analysis, and exploration snapshots through ExpansionController.

The expansion guide explains independent branches and provides the corresponding tutorial and Feature Lab workflows.

Add records only when they are useful

The in-graph Load more button is useful when a source dataset is larger than the first question a user needs to answer. It requests the next bounded page; Python remains responsible for authorization, filtering, pagination, validation, and persistence. Add it by passing progressive_loading and handling the returned load_more event with an incremental add_elements_command:

progress = {
    "page_size": 100,
    "loaded_count": len(loaded_records),
    "total_count": total_records,
    "has_more": next_cursor is not None,
    "cursor": next_cursor,
}

value = graph_workbench(
    elements,
    progressive_loading=progress,
    graph_commands=[next_page_command],
    elements_sync="initial",
    key="records-graph",
    on_change=handle_graph_event,
)

Each accepted batch is appended to the existing browser graph, preserving positions, pan, and zoom. A stable command ID prevents duplicate pages, and a failed request can be retried without advancing the cursor. The local Feature Lab includes Progressive Loading and a separate Progressive Loading Scale Test for comparing 100 through 10,000 vehicle records in the browser.

The complete list of public exports and signatures is maintained in the API reference. The capability matrix maps every browser module to its supported Python surface and demonstration page. Developers can continue to the frontend architecture and development guide for the component lifecycle, source layout, tests, builds, and packaging.

Advanced workflows

Search records while keeping context

Find starting points using text, node or edge labels, properties, or Cytoscape selectors. Search returns matching IDs and complete records, so the same result can populate a Python table or drive a follow-up query.

Node-label search for PLACE reports three matches and highlights Location 1, Location 2, and Location 3 while the surrounding records remain visible.

Three locations match PLACE. The camera has been reframed after searching to show the surrounding graph; other records have not been deleted. Selection details are hidden, but selected records still return to Python.

Use incoming/outgoing neighborhood tools to explore those matches. See selection and search for search modes, box selection, and returned dictionaries.

Collapse a branch without losing shared context

ExpansionController distinguishes source records, the loaded cache, and the displayed graph. Open independent branches, load more neighbors, and collapse only the branch you are finished with. Shared records stay visible when another active branch still needs them; reopening restores cached exploration.

Before collapse: both locations have been expanded and their remaining pages loaded. Each branch contributes a connection to the Shared report.

Both location branches expanded: eight nodes and eight edges include Camera 1, 08:14, the shared report, Camera 2, and AB123.

Eight nodes and eight edges are displayed. The selected Shared report belongs to both active branches, rather than being owned exclusively by either location.

After collapse: right-click Location 1 and choose Collapse connections.

After collapsing Location 1, the graph retains both locations, Location 2's connections, and the shared report; Location 1 has a badge for its hidden records.

The displayed graph falls from eight nodes to six. Camera 1 and 08:14 are hidden, not deleted. The selected Shared report remains connected through Location 2. Node and edge counts are reported separately, and the viewport stays in place.

Bounded expand-all, pagination, filters, retry/cancel, protected records, displayed/loaded/source analysis scopes, and exploration snapshots support longer workflows. See the expansion guide and the before/after screenshots.

Keep validation and persistence in Python

A CRUD button emits an intent. Your Streamlit app can open a dialog, check permissions and fields, then accept the change and send an incremental command. The graph does not write directly to your database.

The CRUD tutorial's Graph record dialog prepares a Camera node with ID camera-8 and name Camera 8, with Confirm and Cancel actions.

This is the tutorial's real Streamlit dialog, populated but not yet submitted. The node type choices belong to the application, not a fixed library schema. Confirm validates and updates its Python checkpoint; Cancel leaves it unchanged.

Use this approval-first workflow for governed records. For drafting, enable browser-local editing with add/connect/delete, undo/redo, locking, and grid snapping, then explicitly reconcile accepted edits with Python. See CRUD workflows and browser editing.

Development

uv run ruff format --check .
uv run ruff check .
uv run mypy st_graph_workbench/
uv run pytest --reruns 0 -q
uv run mkdocs build --strict
uv build

Frontend checks:

cd st_graph_workbench/frontend
npm ci
npm run format
npm run lint
npm run build

Webpack enforces a 512 KB entry and asset budget and keeps optional layout engines in asynchronous chunks. The wheel ships the Components v2 manifest, compiled frontend, and icons; the source distribution also contains examples, tests, documentation, screenshots, and frontend source. Distributions also include typing information and third-party license notices.

Before contributing, read CONTRIBUTING.md. Report security issues privately as described in SECURITY.md.

License

Original work in st-graph-workbench is licensed under the Apache License 2.0. Copyright 2026 Ali Haidar. See the NOTICE for project attribution. Third-party code and assets retain their respective licenses, including the MIT-licensed st-link-analysis foundation.

Acknowledgements

It all started with st-link-analysis, whose original ideas and examples laid the foundation for st-graph-workbench and its broader graph exploration, analysis, and editing capabilities.

See the third-party notices for upstream attribution and bundled dependency licenses.

Built with Cytoscape.js, Streamlit, Pandas, Material Symbols, and Material for MkDocs.

Metadata

Release files for st-graph-workbench 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for st-graph-workbench 0.1.0
File Size Uploaded
st_graph_workbench-0.1.0.tar.gz 5.9 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for st-graph-workbench 0.1.0
File Interpreter ABI Platform
st_graph_workbench-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 6.3 MB

Release files / st_graph_workbench-0.1.0.tar.gz

Download URL st_graph_workbench-0.1.0.tar.gz
Size 5.9 MB
Tags Source
SHA-256 checksum
How to use checksums
3c72b876948a786da7f65be0f7540a27ac824a490ab5da5f248611a5dffff2ce
BLAKE2b-256 checksum
How to use checksums
531d0af10f3ca3f6fe3c35539a82e9ce2f842a27f5b5a12c4d36a940ff282299
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / st_graph_workbench-0.1.0-py3-none-any.whl

Download URL st_graph_workbench-0.1.0-py3-none-any.whl
Size 357.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0fdff3b2d158edc907da3fe51a74da6da40dc3e0f089815f66ffcd7436462c86
BLAKE2b-256 checksum
How to use checksums
e889817a0d9bda8460ca7f88f5dd617eca9f250e57569e19ecfd3a39181efda0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page