Streamlit中Bokeh散点图点击回调无法获取点击值问题
问题:Streamlit 结合 Bokeh 点击散点图无法获取点击数据
使用 Streamlit 框架搭配 Bokeh 2.4.3 版本时,点击散点图中的点后,后端定义的 clicked_data 字典始终无法获取到点击的数值,仅能正常显示图表。相关代码如下:
import streamlit as st from bokeh.plotting import figure, show from bokeh.models import ColumnDataSource, CustomJS # Create a Streamlit web app st.title("Interactive Scatter Plot") # Create some example data data = { "x": [1, 2, 3, 4, 5], "y": [5, 4, 3, 2, 1], } # Create a Bokeh figure and add a scatter plot p = figure(title="Click on a dot", tools="tap,reset") source = ColumnDataSource(data=data) scatter = p.circle("x", "y", size=20, source=source) # Create a JavaScript callback function callback_code = """ const selected_indices = cb_obj.indices; if (selected_indices.length > 0) { const selected_index = selected_indices[0]; const x_value = source.data['x'][selected_index]; const y_value = source.data['y'][selected_index]; clicked_data.x = x_value; clicked_data.y = y_value; } """ clicked_data = {"x": None, "y": None} callback = CustomJS( args={"source": source, "clicked_data": clicked_data}, code=callback_code ) # Attach the JavaScript callback to the scatter plot scatter.js_on_event("tap", callback) # Display the Bokeh plot in Streamlit st.bokeh_chart(p, use_container_width=True) st.write(clicked_data)
问题原因
核心矛盾在于前端 JavaScript 与后端 Python 的运行环境完全分离:
CustomJS是在浏览器端执行的代码,传入的clicked_data会被转换为前端的 JavaScript 对象,修改的只是前端对象的值,无法直接同步回后端的 Python 字典。- Streamlit 采用会话驱动的运行模式,每次页面交互都会重新执行整个 Python 脚本,后端的
clicked_data会被重新初始化为{"x": None, "y": None},自然无法保留前端修改的值。
解决方案
方案1:前端直接显示点击结果(无需同步到后端)
如果只需要在页面上展示点击数据,可借助 Bokeh 的 Div 组件在前端直接更新内容,无需同步到 Python 后端:
import streamlit as st from bokeh.plotting import figure from bokeh.models import ColumnDataSource, CustomJS, Div st.title("Interactive Scatter Plot") data = { "x": [1, 2, 3, 4, 5], "y": [5, 4, 3, 2, 1], } p = figure(title="Click on a dot", tools="tap,reset") source = ColumnDataSource(data=data) scatter = p.circle("x", "y", size=20, source=source) # 创建用于展示结果的Div组件 result_div = Div(text="点击一个点查看数据") callback_code = """ const selected_indices = cb_obj.indices; if (selected_indices.length > 0) { const selected_index = selected_indices[0]; const x_value = source.data['x'][selected_index]; const y_value = source.data['y'][selected_index]; // 更新Div的文本内容 result_div.text = `点击的数据:x=${x_value}, y=${y_value}`; } else { result_div.text = '点击一个点查看数据'; } """ callback = CustomJS( args={"source": source, "result_div": result_div}, code=callback_code ) scatter.js_on_event("tap", callback) # 在Streamlit中展示图表和结果 st.bokeh_chart(p, use_container_width=True) st.bokeh_chart(result_div)
方案2:同步点击数据到 Python 后端
若需要将点击数据同步到后端用于后续逻辑处理,可通过 Streamlit 的组件消息机制,将前端数据传递回后端并更新会话状态:
import streamlit as st from bokeh.plotting import figure from bokeh.models import ColumnDataSource, CustomJS from bokeh.embed import components st.title("Interactive Scatter Plot") # 初始化会话状态,用于存储点击数据 if 'clicked_data' not in st.session_state: st.session_state.clicked_data = {"x": None, "y": None} data = { "x": [1, 2, 3, 4, 5], "y": [5, 4, 3, 2, 1], } p = figure(title="Click on a dot", tools="tap,reset") source = ColumnDataSource(data=data) scatter = p.circle("x", "y", size=20, source=source) # 编写CustomJS,通过消息传递将数据发送到Streamlit后端 callback_code = """ const selected_indices = cb_obj.indices; if (selected_indices.length > 0) { const selected_index = selected_indices[0]; const x_value = source.data['x'][selected_index]; const y_value = source.data['y'][selected_index]; // 发送消息到Streamlit组件 window.parent.postMessage({ type: 'streamlit:setComponentValue', value: {x: x_value, y: y_value} }, '*'); } """ callback = CustomJS(args={"source": source}, code=callback_code) scatter.js_on_event("tap", callback) # 将Bokeh图表转换为HTML组件 script, div = components(p) # 自定义HTML,添加消息监听以接收并传递数据到Streamlit html_content = f""" {div} {script} <script> window.addEventListener('message', (event) => {{ if (event.data.type === 'streamlit:setComponentValue') {{ // 将数据传递给Streamlit组件 Streamlit.setComponentValue(event.data.value); }} }}); </script> """ # 嵌入HTML组件并获取返回的点击数据 returned_data = st.components.v1.html( html_content, height=400, width=700, key="bokeh_interactive_plot" ) # 更新会话状态中的点击数据 if returned_data: st.session_state.clicked_data = returned_data # 展示后端获取到的点击数据 st.write("后端获取的点击数据:", st.session_state.clicked_data)
内容的提问来源于stack exchange,提问作者Omar
相关产品推荐
相关产品推荐

