如何用Bokeh的DatetimeRangeSlider动态更新网络图?
Bokeh 时间滑块联动网络图实现方案
关于回调的疑问:不是只能用JavaScript
Bokeh 提供两种回调模式:
- 客户端回调(CustomJS):纯 JavaScript 编写,运行在浏览器,适合生成静态 HTML 文件的场景
- 服务器端回调:用 Python 编写,依赖 Bokeh Server 运行,逻辑实现更符合 Python 开发者习惯,处理复杂数据统计更便捷
方案一:服务器端 Python 回调(推荐,逻辑更直观)
核心思路
- 预处理节点/边数据,存储节点的时间范围、每条边的时间戳
- 用
ColumnDataSource管理节点的可见性、边的宽度字段 - 绑定滑块的
on_change事件,在 Python 回调中过滤时间范围,更新节点可见性和边宽
示例代码
from bokeh.io import curdoc from bokeh.models import GraphRenderer, DatetimeRangeSlider, ColumnDataSource, Circle, MultiLine from bokeh.layouts import column import pandas as pd # 示例数据 nodes_df = pd.DataFrame({ 'node_id': [0,1,2], 'x': [0,1,2], 'y': [0,1,0], 'start_time': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-01-03']), 'end_time': pd.to_datetime(['2023-01-05', '2023-01-06', '2023-01-07']) }) edges_df = pd.DataFrame({ 'source': [0,0,1,1,2], 'target': [1,2,0,2,0], 'timestamp': pd.to_datetime(['2023-01-01', '2023-01-03', '2023-01-02', '2023-01-04', '2023-01-05']) }) # 初始化数据源 node_source = ColumnDataSource(data={ 'x': nodes_df['x'], 'y': nodes_df['y'], 'visible': [True]*len(nodes_df), 'start': nodes_df['start_time'], 'end': nodes_df['end_time'] }) edge_source = ColumnDataSource(data={ 'start': edges_df['source'], 'end': edges_df['target'], 'line_width': [1]*len(edges_df) }) # 创建网络图渲染器 graph = GraphRenderer() graph.node_renderer.data_source = node_source graph.node_renderer.glyph = Circle(size=15, fill_color='blue', visible='visible') graph.edge_renderer.data_source = edge_source graph.edge_renderer.glyph = MultiLine(line_color='gray', line_width='line_width') # 创建时间滑块 time_slider = DatetimeRangeSlider( start=nodes_df['start_time'].min(), end=nodes_df['end_time'].max(), value=(nodes_df['start_time'].min(), nodes_df['end_time'].max()), title="时间范围" ) # 回调函数:更新节点可见性和边宽 def update_graph(attr, old, new): start, end = new # 更新节点可见性:判断节点时间范围与滑块范围是否重叠 node_source.data['visible'] = [ (s <= end) and (e >= start) for s, e in zip(node_source.data['start'], node_source.data['end']) ] # 统计时间范围内每条边的连接次数 filtered_edges = edges_df[(edges_df['timestamp'] >= start) & (edges_df['timestamp'] <= end)] edge_counts = filtered_edges.groupby(['source', 'target']).size().reset_index(name='count') # 映射边宽(无连接的边宽度设为0) new_line_width = [] for s, t in zip(edge_source.data['start'], edge_source.data['end']): count = edge_counts[(edge_counts['source'] == s) & (edge_counts['target'] == t)]['count'].values new_line_width.append(count[0] if len(count) > 0 else 0) edge_source.data['line_width'] = new_line_width # 触发视图更新 node_source.change.emit() edge_source.change.emit() time_slider.on_change('value', update_graph) # 布局并启动服务 curdoc().add_root(column(time_slider, graph))
运行方式:终端执行 bokeh serve --show your_script.py
方案二:客户端 CustomJS 回调(静态 HTML 场景)
核心逻辑
- 将原始边数据传入 JS 环境,手动遍历统计时间范围内的边连接次数
- 时间比较需转换为毫秒级时间戳(JS 中
Date.parse()处理) - 更新节点的
visible字段和边的line_width字段,触发数据源更新
示例代码(CustomJS 部分)
from bokeh.models import CustomJS # 给滑块绑定CustomJS回调 time_slider.js_on_change('value', CustomJS( args=dict(node_source=node_source, edge_source=edge_source, edges_raw=edges_df.to_dict('records')), code=""" const [start, end] = cb_obj.value; const node_data = node_source.data; const edge_data = edge_source.data; // 更新节点可见性 const new_visible = []; for (let i = 0; i < node_data.start.length; i++) { const node_start = Date.parse(node_data.start[i]); const node_end = Date.parse(node_data.end[i]); new_visible.push((node_start <= end) && (node_end >= start)); } node_source.data.visible = new_visible; // 统计时间范围内的边连接次数 const edgeCounts = {}; edges_raw.forEach(edge => { const ts = Date.parse(edge.timestamp); if (ts >= start && ts <= end) { const key = `${edge.source}-${edge.target}`; edgeCounts[key] = (edgeCounts[key] || 0) + 1; } }); // 更新边宽 const new_line_width = []; for (let i = 0; i < edge_data.start.length; i++) { const key = `${edge_data.start[i]}-${edge_data.end[i]}`; new_line_width.push(edgeCounts[key] || 0); } edge_source.data.line_width = new_line_width; // 触发视图更新 node_source.change.emit(); edge_source.change.emit(); """ ))
内容的提问来源于stack exchange,提问作者Laure D
相关产品推荐
相关产品推荐

