如何在Bokeh中用CustomJS动态联动更新下拉菜单选项
实现Bokeh下拉菜单联动:先选物种再选对应样方
我来帮你搞定这个联动需求!核心是要在选中物种后动态过滤出该物种对应的可用样方(quadrat),然后更新第二个下拉菜单的选项,同时最终的绘图要结合两个选择器的条件来过滤数据。下面是完整的修正方案,我会先给你可运行的代码,再解释关键细节:
完整可运行代码
from bokeh.models import CustomJS, ColumnDataSource, MultiSelect, Column from bokeh.plotting import figure, show from bokeh.layouts import row, column import pandas as pd # 数据准备 data = dict( common_name=['A','A','B','C','B','B','A','C','C','B','A','B','C'], length=[10,20,10,20,30,20,20,30,20,30,30,20,30], weight=[100,200,100,300,100,400,100,300,100,400,500,600,450], quadrat=['N','N','N','N','M','M','M','M','O','O','M','N','O'] ) df = pd.DataFrame(data) data_source = ColumnDataSource(df) # 空数据源,初始绘图为空 plot_source = ColumnDataSource(dict(length=[], weight=[])) # 创建绘图 plot = figure(title="Length vs Weight", x_axis_label='Length', y_axis_label='Weight') plot.circle(x='length', y='weight', source=plot_source, size=10) # 物种选择器 available_species = sorted(df['common_name'].unique()) multiselect_species = MultiSelect(title='Select Species:', value=[], options=available_species, width=240) # 预处理:建立物种到对应可用样方的映射(后端提前处理,前端直接用) species_quadrat_map = {sp: sorted(df[df['common_name'] == sp]['quadrat'].unique()) for sp in available_species} # 样方选择器(初始为空,等待物种选择后更新) multiselect_quadrat = MultiSelect(title='Select Quadrat:', value=[], options=[], width=240) # 物种选择变化的回调:更新样方选项 + 绘制选中物种的所有数据 species_callback = CustomJS( args={ 'data_source': data_source, 'plot_source': plot_source, 'quadrat_select': multiselect_quadrat, 'species_quadrat_map': species_quadrat_map }, code=""" // 获取当前选中的物种数组 const selected_species = cb_obj.value; // 收集所有选中物种对应的可用样方,去重后排序 let available_quadrats = []; selected_species.forEach(sp => { available_quadrats = available_quadrats.concat(species_quadrat_map[sp]); }); available_quadrats = [...new Set(available_quadrats)].sort(); // 更新样方选择器的选项,同时清空选中值(避免无效选项残留) quadrat_select.options = available_quadrats; quadrat_select.value = []; // 更新绘图:显示选中物种的所有数据(此时样方未选) const data = data_source.data; const plot_data = plot_source.data; plot_data.length = []; plot_data.weight = []; for (let i = 0; i < data.common_name.length; i++) { if (selected_species.includes(data.common_name[i])) { plot_data.length.push(data.length[i]); plot_data.weight.push(data.weight[i]); } } plot_source.change.emit(); """ ) # 样方选择变化的回调:结合物种和样方的选择过滤数据 quadrat_callback = CustomJS( args={ 'data_source': data_source, 'plot_source': plot_source, 'species_select': multiselect_species }, code=""" const selected_species = species_select.value; const selected_quadrats = cb_obj.value; const data = data_source.data; const plot_data = plot_source.data; plot_data.length = []; plot_data.weight = []; // 同时满足物种和样方的条件才加入绘图数据 for (let i = 0; i < data.common_name.length; i++) { if (selected_species.includes(data.common_name[i]) && selected_quadrats.includes(data.quadrat[i])) { plot_data.length.push(data.length[i]); plot_data.weight.push(data.weight[i]); } } plot_source.change.emit(); """ ) # 绑定回调函数 multiselect_species.js_on_change('value', species_callback) multiselect_quadrat.js_on_change('value', quadrat_callback) # 页面布局 widgets_layout = column(multiselect_species, multiselect_quadrat) page_layout = row(widgets_layout, plot) show(page_layout)
关键细节解释
1. 预处理物种-样方映射
我在Python端提前生成了每个物种对应的可用样方列表species_quadrat_map,这样前端JS可以直接快速获取对应选项,不用在前端做复杂的数据过滤,提升性能和代码简洁性。
2. 物种选择回调的核心逻辑
- 当你选中/取消选中物种时,会自动收集所有选中物种对应的样方,去重排序后更新第二个下拉菜单的
options。 - 同时清空样方选择器的选中值,避免出现已经不存在的无效选项。
- 此时绘图会显示选中物种的所有数据(因为还没选样方)。
3. 样方选择回调的逻辑
当你选择样方时,回调会同时获取当前选中的物种和样方,只有同时满足两个条件的数据才会被过滤出来更新绘图,实现了双条件筛选。
原代码的问题修正
- 原代码中更新样方选项的JS语法错误:
multiselect_quadrat.options = "available_quadrat[cb_obj.value]"这种字符串赋值完全不符合JS语法,正确的做法是直接赋值数组。 - 原代码没有根据选中物种过滤样方,只是用了所有样方选项,不符合你的需求。
- 原代码的两个回调是独立的,没有结合双条件筛选,导致选样方时会覆盖物种的过滤逻辑,现在改成了双条件叠加筛选。
内容的提问来源于stack exchange,提问作者user11516234
相关产品推荐
相关产品推荐

