Bokeh: major_label_overrides doesn't get updated after CustomJS callback

85 views Asked by At

I have written the code below to make a plot which can be updated when users apply some filters to the data. After selecting a value from the dropdown, the callback updates everything as expected, except for the yaxis.major_label_overrides (which writes the name (string) on top of the y-axis ticker (integer).

When I investigated the console, I have noticed the following exception in y_axis.major_label_overrides: Exception in browser console Here is the full exception: Exception: TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them at Function.invokeGetter (< anonymous > :3:28).

After some researches, I’m not sure how to solve this exception in this specific situation (and whether or not it is the cause of the problem).

What it looks like before filter is applied: Plot before filter is applied

What it looks like after filter is applied: Plot after filter is applied

It doesn’t through any error in the console, but the expected output after the filter is applied is a y-axis as below:

  • “Joe_updated” instead of “Bob”,
  • “Guy_updated” instead of “Joe”.

I’m using Google Chrome to open the output file. Bokeh version is 3.2.2 and Python is 3.11.4.

Below is my code:

### 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))

Thank you in advance for the time you might spend on this question :)

1

There are 1 answers

0
Jason On BEST ANSWER

A solution has been provided by Jonas_Grave_Kristens on the Bokeh Community Support (https://discourse.bokeh.org/t/customjs-major-label-overrides-isnt-updated-on-new-plot/10990/2).

The trick was to update the whole major_label_overrides with a new object instead of first clear and then set.

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;