Bokeh与CustomJS:实现HoverTool与TapTool的交互集成
解决方案
核心思路
- 借助TapTool的CustomJS回调,新增一个数据源记录选中点的索引,实现点击后保留同组线段,再次点击其他点或空白区域时更新/清除线段
- 复用现有
links字典的关联逻辑,同时保留HoverTool的原有功能,并且让两者逻辑互不冲突
修改后的完整代码
import pandas as pd from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool from bokeh.plotting import figure, output_file, show # 读取数据集 data = pd.read_csv("sanitized_data.csv") x = list(data["X"]) y = list(data["Y"]) # 生成同组点的关联字典:每个点对应同组其他所有点的索引 links = { data["Col2"].index[index]: list(data[(data["Col1"] == data["Col1"][index]) & (data["Col2"] != data["Col2"][index])].index) for index in data["Col2"].index } # 线段数据源:存储线段的起点和终点坐标 segment_source = ColumnDataSource({'x0': [], 'y0': [], 'x1': [], 'y1': []}) # 主数据点数据源:存储所有点的坐标和分组信息 point_source = ColumnDataSource({"x": x, "y": y, 'Col2': list(data['Col2']), 'Col1': list(data['Col1'])}) # 选中状态数据源:记录当前选中点的索引,用于hover和tap逻辑互斥 selected_source = ColumnDataSource({'selected_index': [None]}) # 配置绘图参数 output_file("sanitized_example.html") p = figure(width=1900, height=700) # 绘制线段和圆点 sr = p.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=0.4, line_width=.5, source=segment_source) cr = p.circle(x='x', y='y', color='blue', size=5, alpha=1, hover_color='orange', hover_alpha=1.0, source=point_source) # HoverTool回调逻辑:仅在无选中点时,hover显示同组线段 hover_code = """ const links = %s const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []} const indices = cb_data.index.indices // 有选中点时,hover不覆盖已显示的线段 if (selected.data.selected_index[0] === null) { for (let i = 0; i < indices.length; i++) { const start = indices[i] for (let j = 0; j < links[start].length; j++) { const end = links[start][j] data['x0'].push(point.data.x[start]) data['y0'].push(point.data.y[start]) data['x1'].push(point.data.x[end]) data['y1'].push(point.data.y[end]) } } segment.data = data } """ % links # TapTool回调逻辑:点击点时显示同组线段,点击空白处清除线段 tap_code = """ const links = %s const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []} const indices = cb_data.source.selected.indices let selectedIndex = null if (indices.length > 0) { selectedIndex = indices[0] const start = selectedIndex // 生成当前点与同组其他点的线段数据 for (let j = 0; j < links[start].length; j++) { const end = links[start][j] data['x0'].push(point.data.x[start]) data['y0'].push(point.data.y[start]) data['x1'].push(point.data.x[end]) data['y1'].push(point.data.y[end]) } } // 更新选中状态和线段数据 selected.data.selected_index = [selectedIndex] segment.data = data // 清除点的选中样式,避免视觉残留 cb_data.source.selected.indices = [] """ % links # 配置提示框 TOOLTIPS = [("Col2", "@Col2"), ("Col1", "@Col1")] # 创建回调实例 hover_callback = CustomJS(args={'point': point_source, 'segment': segment_source, 'selected': selected_source}, code=hover_code) tap_callback = CustomJS(args={'point': point_source, 'segment': segment_source, 'selected': selected_source}, code=tap_code) # 添加工具到绘图 p.add_tools(HoverTool(tooltips=TOOLTIPS, callback=hover_callback, renderers=[cr])) tap_tool = TapTool(renderers=[cr], callback=tap_callback) p.add_tools(tap_tool) # 展示绘图 show(p)
关键修改说明
- 新增选中状态数据源:用
selected_source记录当前选中点的索引,实现hover和tap功能的逻辑互斥——有选中点时,hover不会覆盖已显示的线段 - TapTool回调优化:点击点时生成对应同组线段,点击空白区域时清除线段并重置选中状态,同时清除点的选中样式避免视觉干扰
- HoverTool回调逻辑调整:仅在无选中点的情况下,hover才会触发线段显示,确保点击后的线段不会被hover覆盖
内容的提问来源于stack exchange,提问作者John Doe
相关产品推荐
相关产品推荐

