You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Bokeh的DatetimeRangeSlider动态更新网络图?

Bokeh 时间滑块联动网络图实现方案

关于回调的疑问:不是只能用JavaScript

Bokeh 提供两种回调模式:

  • 客户端回调(CustomJS):纯 JavaScript 编写,运行在浏览器,适合生成静态 HTML 文件的场景
  • 服务器端回调:用 Python 编写,依赖 Bokeh Server 运行,逻辑实现更符合 Python 开发者习惯,处理复杂数据统计更便捷

方案一:服务器端 Python 回调(推荐,逻辑更直观)

核心思路

  1. 预处理节点/边数据,存储节点的时间范围、每条边的时间戳
  2. 用 ColumnDataSource 管理节点的可见性、边的宽度字段
  3. 绑定滑块的 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 场景)

核心逻辑

  1. 将原始边数据传入 JS 环境,手动遍历统计时间范围内的边连接次数
  2. 时间比较需转换为毫秒级时间戳(JS 中 Date.parse() 处理)
  3. 更新节点的 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:35:19