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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 20:50:33