如何用Streamlit、NetworkX、Plotly实现带交互功能的图管理UI?
Streamlit + Plotly + NetworkX 交互式图管理UI实现方案
环境准备
先安装依赖包:
pip install streamlit networkx plotly
核心思路
用NetworkX维护图的拓扑结构(节点、边),Plotly渲染可交互的图可视化,Streamlit搭建UI控制面板并通过st.session_state持久化图数据和交互状态(比如选中的节点、节点位置)。
基础操作实现(增删节点/边)
通过Streamlit的输入组件和按钮,结合NetworkX的API完成基础编辑:
- 添加节点:输入节点ID,点击按钮后调用
G.add_node() - 删除节点:输入节点ID,点击按钮后调用
G.remove_node()(先判断节点是否存在) - 创建边:输入两个节点ID,点击按钮后调用
G.add_edge()(判断两个节点是否都存在)
核心交互功能实现
1. 拖拽节点调整位置
Plotly散点图支持拖拽交互,启用draggable=True后,监听拖拽事件并将更新后的节点位置同步到st.session_state.pos字典,确保下次渲染时使用最新位置。
2. 点击两个节点创建边
通过st.session_state记录第一次点击的节点ID,第二次点击时自动创建边:
- 监听Plotly的单击事件,获取点击的节点数据
- 无选中节点时,标记当前节点为选中状态;已有选中节点时,创建边并清空选中状态
3. 点击节点执行删除操作
利用Plotly的双击事件触发删除:
- 监听双击事件,获取目标节点ID
- 调用
G.remove_node()移除节点,同时删除对应位置数据并刷新页面
完整代码示例
import streamlit as st import networkx as nx import plotly.graph_objects as go # 初始化会话状态 if 'G' not in st.session_state: st.session_state.G = nx.Graph() st.session_state.G.add_nodes_from(['A', 'B', 'C']) st.session_state.pos = nx.spring_layout(st.session_state.G) if 'selected_node' not in st.session_state: st.session_state.selected_node = None # 页面布局:左侧控制面板,右侧可视化 col1, col2 = st.columns([1, 3]) with col1: st.subheader("图管理面板") # 添加节点 node_id = st.text_input("输入节点ID") if st.button("添加节点"): if node_id and node_id not in st.session_state.G.nodes: st.session_state.G.add_node(node_id) # 给新节点分配基础位置 base_pos = st.session_state.pos[list(st.session_state.G.nodes)[0]] st.session_state.pos[node_id] = (base_pos[0] + 0.1, base_pos[1] + 0.1) # 删除节点 del_node_id = st.text_input("输入要删除的节点ID") if st.button("删除节点"): if del_node_id in st.session_state.G.nodes: st.session_state.G.remove_node(del_node_id) del st.session_state.pos[del_node_id] # 创建边 edge_from = st.text_input("边的起点") edge_to = st.text_input("边的终点") if st.button("创建边"): if edge_from in st.session_state.G.nodes and edge_to in st.session_state.G.nodes: st.session_state.G.add_edge(edge_from, edge_to) with col2: st.subheader("交互式图可视化") # 构建Plotly图 fig = go.Figure() # 添加边 for edge in st.session_state.G.edges: x0, y0 = st.session_state.pos[edge[0]] x1, y1 = st.session_state.pos[edge[1]] fig.add_trace(go.Scatter( x=[x0, x1], y=[y0, y1], mode='lines', line=dict(width=2, color='#888'), hoverinfo='none' )) # 添加节点 node_x = [st.session_state.pos[node][0] for node in st.session_state.G.nodes] node_y = [st.session_state.pos[node][1] for node in st.session_state.G.nodes] node_text = list(st.session_state.G.nodes) fig.add_trace(go.Scatter( x=node_x, y=node_y, mode='markers+text', text=node_text, textposition='top center', marker=dict( size=20, color='#1f77b4', line=dict(width=2, color='#fff') ), hoverinfo='text', draggable=True # 启用节点拖拽 )) # 布局设置 fig.update_layout( showlegend=False, hovermode='closest', margin=dict(b=20, l=5, r=5, t=40), xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), dragmode='select' ) # 渲染图并监听交互事件 plotly_event = st.plotly_chart(fig, on_select='rerun', key='graph_plot') # 处理点击事件:创建边或删除节点 if plotly_event and plotly_event['selection']['points']: clicked_node = plotly_event['selection']['points'][0]['text'] # 双击删除节点 if plotly_event['event_type'] == 'plotly_doubleclick': if clicked_node in st.session_state.G.nodes: st.session_state.G.remove_node(clicked_node) del st.session_state.pos[clicked_node] st.rerun() # 单击选择节点创建边 elif plotly_event['event_type'] == 'plotly_click': if st.session_state.selected_node is None: st.session_state.selected_node = clicked_node st.success(f"已选中节点:{clicked_node},请点击第二个节点创建边") else: if st.session_state.selected_node != clicked_node: st.session_state.G.add_edge(st.session_state.selected_node, clicked_node) st.success(f"已创建边:{st.session_state.selected_node} -> {clicked_node}") st.session_state.selected_node = None st.rerun() # 处理拖拽事件:更新节点位置 if plotly_event and plotly_event['relayout']: for key, val in plotly_event['relayout'].items(): if key.startswith('xaxis.range') or key.startswith('yaxis.range'): continue if 'marker' in key: idx = int(key.split('[')[1].split(']')[0]) node = node_text[idx] if 'x' in key: st.session_state.pos[node] = (val, st.session_state.pos[node][1]) elif 'y' in key: st.session_state.pos[node] = (st.session_state.pos[node][0], val) st.rerun()
关键注意事项
- 会话状态管理:必须用
st.session_state保存图对象、节点位置和选中状态,否则Streamlit每次重运行都会重置数据 - 交互事件处理:Plotly的事件需要通过
st.plotly_chart的on_select='rerun'触发页面重运行,才能更新状态 - 节点位置持久化:拖拽后要及时更新
pos字典,确保下次渲染用最新位置 - 边界判断:所有操作前要检查节点是否存在,避免NetworkX抛出异常
内容的提问来源于stack exchange,提问作者evan54
相关产品推荐
相关产品推荐

