CustomJS: major_label_overrides isn't updated on new plot

Hello there,

I’ve 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.

What it looks like before filter is applied:

What it looks like 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”.

One last thing. When I investigated the console, I have noticed the following exception in y_axis.major_label_overrides:

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 would make the major_label_overrides behave as expected).

Any help would be greatly appreciated :slight_smile:

I’m using Google Chrome to open the output file. Bokeh version is 3.2.2 and Python is 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))

A somewhat complicated MRE :wink: What I would do is update the whole major_label_overrides with a new object instead of first clear and then set.
Eg

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;

Also, I am very sure you do not need

y_axis.change.emit();

Why do you do this? I do not think this is a property but I can be wrong

y_axis.ticker.ticks.length = y_tickers.length ; 
2 Likes

Your solution works perfectly! Thank you so much!

Also, you are right about:

I first added this step as a precaution, but the code works fine without it. So I removed it.

Again, thank you for your help. Have a great day!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.