我编写了下面的代码来制作一个图,当用户对数据应用一些过滤器时可以更新该图。从下拉列表中选择一个值后,回调会按预期更新所有内容,但 yaxis.major_label_overrides 除外(它将名称(字符串)写在 y 轴滚动条(整数)顶部。
)当我调查控制台时,我注意到 y_axis.major_label_overrides 中存在以下异常: 以下是完整的异常: Exception: TypeError: 'caller', 'callee', and 'arguments' 属性可能无法在严格模式函数上访问,或者无法在 Function.invokeGetter 调用它们的参数对象上访问 (< anonymous > :3:28 ).
经过一些研究,我不确定如何在这种特定情况下解决此异常(以及是否是问题的原因)。
应用过滤器控制台中没有出现任何错误,但应用过滤器后的预期输出是y轴,如下所示:
Google Chrome 打开输出文件。 Bokeh 版本是 3.2.2,Python 是3.11.4。
下面是我的代码:
### Packages ###
from math import pi
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
from bokeh.transform import factor_cmap
### Data ###
data = [
['medium', 'F', 'F', 108, 'Bob', 767.02, 'medium'],
['bad_medium_bad', 'F', 'DB_D_F', 202, 'Joe', 542.9, 'bad'],
['bad_medium_bad', 'DB', 'DB_D_F', 202, 'Joe', 5.8, 'bad'],
['bad_medium_bad', 'D', 'DB_D_F', 202, 'Joey', 0.0, 'medium'],
['medium', 'F', 'F', 810, 'Francis', 679.7, 'medium'],
['medium', 'F', 'F', 355, 'James', 354.6, 'medium'],
['medium', 'F', 'F', 288, 'Suze', 23.1, 'medium'],
['medium', 'F', 'F', 281, 'Anna', 36.5, 'medium'],
['medium_medium_medium', 'F', 'DB_D_F', 249, 'Guy', 673.1, 'medium'],
['medium_medium_medium', 'DB', 'DB_D_F', 249, 'Guy', 13.2, 'medium'],
['medium_medium_medium', 'D', 'DB_D_F', 249, 'Guy B', 99.1, 'medium']
]
data = pd.DataFrame(data, columns=['overall_status', 'group', 'overall_group', 'id', 'name', 'amount', 'status'])
data.insert(loc=0, column="y_ticker", value=0, allow_duplicates=True)
# replace id by a shorter number that can be used to place data on y_axis in a neat way.
for i in list(data.index.values):
if i == 0:
data.loc[i, "y_ticker"] = 0
else:
if data.loc[i-1, "id"] == data.loc[i, "id"]:
data.loc[i, "y_ticker"] = data.loc[i-1, "y_ticker"]
else:
data.loc[i, "y_ticker"] = data.loc[i-1, "y_ticker"] + 1
### Setup ###
output_file('test_dashboard.html')
source = bmo.ColumnDataSource(data.to_dict(orient='list'))
filtered_data = bmo.ColumnDataSource(data.to_dict(orient='list'))
### Plot ###
cmap = {
'good': '#006B3D',
'medium': '#FF980E',
'bad': '#D3212C',
}
TOOLS = "hover,save,ypan,reset,wheel_zoom"
p = figure(title='Dashboard for testing',
x_range=bmo.FactorRange(), x_axis_label = "Group",
y_range=bmo.Range1d(), y_axis_label = "Name",
x_axis_location="above", width=600, height=500,
tools=TOOLS, toolbar_location='below',
tooltips=[('Name', '@name'), ('Amount', '@amount')]
)
# create rectangles
p.rect(x="group", y="y_ticker", width=1, height=1, source=filtered_data, legend_field='status',
color=factor_cmap("status", palette=list(cmap.values()), factors=list(cmap.keys())),
line_color=None)
# create initial label_overrides
p.yaxis.major_label_overrides = dict(zip(filtered_data.data['y_ticker'], filtered_data.data['name']))
p.yaxis.ticker = list(range(0, len(p.yaxis.major_label_overrides)))
# create inital ranges
p.x_range.factors = list(np.unique(filtered_data.data['group']))
p.y_range.start = 0
# adapt y_range.end to the length of p.yaxis.major_label_overrides
# (I have thousands of rows in the real dataset)
max_y_range = 20
if(len(p.yaxis.major_label_overrides) < 20):
max_y_range = len(p.yaxis.major_label_overrides)
p.y_range.end = max_y_range
p.xgrid.grid_line_color = '#FFFFFF'
p.axis.axis_line_color = None
p.axis.major_tick_line_color = None
p.axis.major_label_text_font_size = "15px"
p.axis.major_label_standoff = 0
p.xaxis.major_label_orientation = pi / 3
p.x_range.range_padding = 1
### Define dropdown options ###
dropdown_group_options = [
'All'
] + [
cat for i, cat in enumerate(sorted(data['group'].unique()), 2) # à quoi sert le 2 ici ?
]
### Generate dropdown widget ###
dropdown_group = bmo.Select(title='group', value=dropdown_group_options[0], options=dropdown_group_options)
### Callback ###
callback = bmo.CustomJS(
args=dict(unfiltered_data=source,
filtered_data=filtered_data,
p=p,
y_axis=p.yaxis[0],
dropdown_group=dropdown_group),
code="""
// utils
function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
var source = unfiltered_data.data;
// create a variable for each column of the unfiltered_data
var group_or = source['group'] ;
var overall_group_or = source['overall_group'] ;
var id_or = source['id'] ;
var name_or = source['name'] ;
var amount_or = source['amount'] ;
var status_or = source['status'] ;
var overall_status_or = source['overall_status'] ;
var y_ticker_or = source['y_ticker'] ;
// init filtered_data
filtered_data.data['group'] = [] ;
filtered_data.data['overall_group'] = [] ;
filtered_data.data['id'] = [] ;
filtered_data.data['name'] = [] ;
filtered_data.data['amount'] = [] ;
filtered_data.data['overall_status'] = [] ;
filtered_data.data['status'] = [] ;
filtered_data.data['y_ticker'] = [] ;
// get value from dropdown
var f_sec = dropdown_group.value;
// push matching rows in filtered_data
var match = 0
for(var i=0; i < overall_status_or.length; i++){
if((overall_group_or[i].search(f_sec) != -1 || f_sec == 'All')){
filtered_data.data['group'].push(group_or[i]) ;
filtered_data.data['overall_group'].push(overall_group_or[i]) ;
filtered_data.data['id'].push(id_or[i]) ;
filtered_data.data['name'].push(name_or[i]) ;
filtered_data.data['amount'].push(amount_or[i]) ;
filtered_data.data['overall_status'].push(overall_status_or[i]) ;
filtered_data.data['status'].push(status_or[i]) ;
// I use the below if statement to 'reset' y_tickers value (start=0, by=1)
// it allows me to keep equally spread values on the y_axis
if(match == 0){
filtered_data.data['y_ticker'].push(match);
} else if(id_or[i] == id_or[i-1]){
filtered_data.data['y_ticker'].push(filtered_data.data['y_ticker'].at(-1));
} else{
var new_y_ticker = filtered_data.data['y_ticker'].at(-1) +1
filtered_data.data['y_ticker'].push(new_y_ticker)
}
++match
}
}
console.log(filtered_data.data)
// delete the initial mapping
y_axis.major_label_overrides.clear() ;
// create new y_tickers
const y_tickers = [...new Set(filtered_data.data['y_ticker'])]
console.log(y_tickers)
y_axis.ticker.ticks.length = y_tickers.length ;
y_axis.ticker.ticks = y_tickers ;
// update mapping in major_label_overrides
for(const element of y_tickers){
var key_name = getKeyByValue(filtered_data.data['y_ticker'], element) ;
y_axis.major_label_overrides.set(element, filtered_data.data['name'][key_name] + '_updated') ;
}
// get new x_range
var new_x_range = [...new Set(filtered_data.data['group'])];
// update existing x_range with new factors
p.x_range.factors.length = new_x_range.length
for(var i=0; i < new_x_range.length; i++){
p.x_range.factors[i] = new_x_range[i];
}
// adapt y_range to y_tickers length
var max_y_range = 20
if(y_tickers.length < 20){
max_y_range = y_tickers.length
}
p.y_range._reset_start = 0 ;
p.y_range._reset_end = max_y_range ;
console.log(y_axis.major_label_overrides)
// update everything
filtered_data.change.emit();
p.reset.emit();
p.change.emit();
y_axis.change.emit();
"""
)
### Link actions ###
dropdown_group.js_on_change('value', callback)
show(bla.column(dropdown_group, p))
提前感谢您在这个问题上花费的时间:)
https://discourse.bokeh.org/t/customjs-major-label-overrides-isnt-updated-on-new-plot/10990/2) .
技巧是用一个新对象更新整个major_label_overrides,而不是先清除然后设置。
const map1 = new Map();
for(const element of y_tickers){
var key_name = getKeyByValue(filtered_data.data['y_ticker'], element);
map1.set(element, filtered_data.data['name'][key_name] + '_updated');
// y_axis.major_label_overrides.set(element, filtered_data.data['name'][key_name] + '_updated');
}
y_axis.major_label_overrides = map1;