在 Python 的 Bokeh 中使用 Javascript 回调过滤数据

Filter data with Javascript callback in Python's Bokeh

提前为 unprecise/unappreciated 措辞道歉,因为这是我在这里的第一个问题。 欢迎指出我以后如何改进它。

我已经通读了 Bokeh 的所有用户指南和各种论坛,但我相信这个问题仍然没有得到足够的涵盖,因为它一遍又一遍地出现,没有一个可以普遍应用的答案。

我的任务是在 Python 的 Bokeh 中构建一个散点图,可以根据分类变量进行交互式过滤。我对 Javascript(以及数据的结构)的有限理解使我无法自己解决这个问题。

我发现,一种解决方案是附加满足条件的 x&y 值 (f.e。Filtering Bokeh LabelSet with Javascript)。但是,我也想保留所有其他变量,因为我使用它们来定义绘图中的图形参数/悬停信息。

因此我的问题是,如果其中一列满足 Javascript 中的特定条件,我如何将整行附加到新的输出数据?我也不确定我是否正确地调用了回调,这样情节实际上会对我的选择做出反应。所以这里也请大家指出错误。

在此处查看一些示例代码:

#Packages
import pandas as pd
import numpy as np
from bokeh.plotting import figure, output_file, show
import bokeh.events as bev
import bokeh.models as bmo
import bokeh.layouts as bla

#Data
data = pd.DataFrame(data = np.array([[1,1,'a',0.5],
                                     [2,2,'a',0.5],
                                     [3,3,'a',0.75],
                                     [4,4,'b',1],
                                     [5,5,'b',2]]),
                    columns = ['x', 'y', 'category', 'other information'])


#Setup
output_file('dashboard.html')

source = bmo.ColumnDataSource(data)

#Define dropdown options
dropdown_options = [('All', 'item_1'), None] + [(cat, str('item_' + str(i))) for i, cat in enumerate(sorted(data['category'].unique()), 2)]

#Generate dropdown widget
dropdown = bmo.Dropdown(label = 'Category', button_type = 'default', menu = dropdown_options)


#Callback
callback = bmo.CustomJS(args = dict(source = source),
                        code = """
                        
                        var data = source.data;
                        
                        var cat = cb_obj.value;
                        
                        if (cat = 'All'){
                                
                            data = source.data
                                
                        } else {
                            
                            var new_data = [];
                            
                            for (cat i = 0; i <= source.data['category'].length; i++){
                                    
                                    if (source.data['category'][i] == cat) {
                                            
                                            new_data.push(source.data[][i])
                                            
                                            }
                                    
                                    }
                            
                            data = new_data.data
                                                    
                        }
                            
                        source.data = data
                                                  
                        source.change.emit();
                        
                        """)


#Link actions
dropdown.js_on_event(bev.MenuItemClick, callback)

#Plot
p = figure(plot_width = 800, plot_height = 530, title = None)

p.scatter(x = 'x', y = 'y', source = source)


show(bla.column(dropdown, p))

毫不奇怪,过滤器不起作用。如前所述,非常感谢任何帮助,因为我不知道如何索引 Javascript 中的整行以及我做错了什么。

此致, 奥利弗

我为你的问题写了一个解决方案。我不是散景专家,所以我可能不是什么都知道,但希望这有助于理解正在发生的事情。一些解释:

  • 您开始时有一些语法错误:在您的 for 循环中您使用了 cat i,您的意思可能是 var i

  • 如果您将 All 分配给 cat,您需要进行比较:使用 cat == 'All'cat === 'All'

  • 您的 cb_obj.value 由于某种原因无法正常工作,返回未定义。您可以使用简单的 console.log(variableName) 检查您的变量,并在浏览器中打开开发控制台以查看正在运行的回调。我将您的列表理解更改为具有相同值的元组,而不是 (category_name, item_category_number)。现在 cb_obj.item returns category_name 你可以与之进行比较。

  • 您应该了解您的数据的格式,例如,您可以使用 console.log(source.data) 来了解。 source.data 这里是数组的对象(或者如果你要在 Python 中描述的话,就是列表的字典)。因此,您无法按照在 for 循环中所做的方式推送数据,并且出现语法错误:source.data[][i] - 您将无法使用空括号访问您想要的内容。我写了两个函数来处理这个功能。 generateNewDataObject 创建我们可以附加 addRowToAccumulator

    的空数组对象
  • 最后一件事是我需要两个 data_sources。首先,我们不会进行更改,其次,我们将修改并用于显示在绘图上。如果我们要修改第一个,那么在第一个过滤器之后,所有其他类别都将被删除,我们只能通过刷新页面来恢复它们。 'immutable' data_source 允许我们引用它并且不会在此过程中丢失过滤后的数据。

希望对您有所帮助。

# Packages

import bokeh.events as bev
import bokeh.layouts as bla
import bokeh.models as bmo
import numpy as np
import pandas as pd
from bokeh.plotting import figure, output_file, show

# Data
data = pd.DataFrame(
    data=np.array(
        [
            [1, 1, 'a', 0.5],
            [2, 2, 'a', 0.5],
            [3, 3, 'a', 0.75],
            [4, 4, 'b', 1],
            [5, 5, 'b', 2]
        ]
    ),
    columns=['x', 'y', 'category', 'other information']
)

# Setup
output_file('dashboard.html')

source = bmo.ColumnDataSource(data)

# Define dropdown options
dropdown_options = [
                       ('All', 'All'), None
                   ] + [(cat, cat)
                       for i, cat in enumerate(sorted(data['category'].unique()), 2)
                   ]
# Generate dropdown widget
dropdown = bmo.Dropdown(label='Category', button_type='default', menu=dropdown_options)

filtered_data = bmo.ColumnDataSource(data)
# Callback
callback = bmo.CustomJS(
    args=dict(unfiltered_data=source, filtered_data=filtered_data),
    code="""

var data = unfiltered_data.data;
var cat = cb_obj.item;

function generateNewDataObject(oldDataObject){
    var newDataObject = {}
    for (var key of Object.keys(oldDataObject)){
        newDataObject[key] = [];
    }
    return newDataObject

}

function addRowToAccumulator(accumulator, dataObject, index) {
    for (var key of Object.keys(dataObject)){
        accumulator[key][index] = dataObject[key][index];
    }
    return accumulator;
}

if (cat === 'All'){
    data = unfiltered_data.data;
} else {
    var new_data =  generateNewDataObject(data);
    for (var i = 0; i <= unfiltered_data.data['category'].length; i++){
        if (unfiltered_data.data['category'][i] == cat) {
            new_data = addRowToAccumulator(new_data, unfiltered_data.data, i);
        }
    }
    data = new_data;
}

filtered_data.data = data;
filtered_data.change.emit();
"""
)

# Link actions
dropdown.js_on_event(bev.MenuItemClick, callback)

# Plot
p1 = figure(plot_width=800, plot_height=530, title=None)

p1.scatter(x='x', y='y', source=filtered_data)

show(bla.column(dropdown, p1))