CustomJS callbacks

Hello, I’m trying to build an interactive pie chart that displays data from a csv file depending on the year specified by the user.
This is a picture of the data frame im working with

This is an auxiliary function I’ve defined to help with the plotting of the pie chart

# Create custom data to be displayed onto the chart i.e gen_data(1990) = CDS({"Disease: [" Meningitis ", " Alzheimer's disease and other dementias ",...], "Deaths":[2159, 1116...],...})
def gen_data2(year):
    idx = year - 1990
    data = {
    "Disease": df.columns[1:].to_list(),
    "Deaths": df.loc[idx][1:].reset_index(drop=True).to_list(),
    "Percentage": (df.loc[idx][1:] / sum(df.loc[idx][1:]) * 100).reset_index(drop=True).to_list(),
    "Angle": (df.loc[idx][1:] / sum(df.loc[idx][1:]) * 2*pi).reset_index(drop=True).to_list(),
    "Colour": colours}
    return ColumnDataSource(data=data)
# Not important but here is the code for the colours list
from bokeh.palettes import Turbo256
split = 0
for i in range(len(df.columns[1:])):
    colours.append(Turbo256[split])
    split += int(len(Turbo256) / (len(df.columns)) - 1)
# source_dict = {"1990": CDS(gen_data2(1990)), 1991: CDS(gen_data2(1991)), ...}
# rend_dict, creates a pie chart for every year's data
for j in df["Year"]:
    source_dict[str(j)] = gen_data2(j)
    rend_dict[str(j)] = p.wedge(x=0, y=0, radius=0.5, start_angle=cumsum('Angle', include_zero=True), end_angle=cumsum('Angle'), line_color="white", fill_color='Colour', source=source_dict[str(j)]) 

So the idea of the JS callback is to effectively “remove” (empty) the CDS data for a specific year in source_dict if the year is not selected by the slider widget and hence this won’t be displayed on the final plot. Or vice versa start with an empty source_dict and add data for years selected by the slider widget, same idea.

This is the code for my master source pretty much, so its similar to source_dict but instead the format is CDS(“Disease”:[list all names once, twice, three times etc etc], ‘Deaths’: [death numbers for disease 1, disease 2, disease 3 etc])
all values are stored in a single list

source = ColumnDataSource({'Disease': [], 'Deaths': [], 'Percentage': [], 'Angle': [], 'Colour': []})
for i in source.data.keys():
    for j in df["Year"]:
        for k in gen_data2(j).data[i]:
            source.data[i].append(k)
cjs = CustomJS(args=dict(sl=slider, source=source, source_dict=source_dict),code=''' 
    var year = cb_obj.value;
    var upd_source = {};
    var multiplier = year - 1990;

    for (const [k,v] of Object.entries(source_dict)){
          upd_source[k] = [];
        }

    for (const [k,v] of Object.entries(source.data)) {
        if (v >= multiplier * 31 && v < multiplier * 32) {
            upd_source[k].push(source.data[k][v]);
            }
        }
    // Loops through the source_dict (i.e {"1990": CDS({Disease: [...]...,})})
    
    /* Find whether the year k is the same as the slider value, if so set the CDS data for that year
    as the value in upd_source["Disease"/"Deaths"...] (for e.g) */
    
    for (const [k,v] of Object.entries(source_dict)){
        if (k == year) {
            for (const [a,b] of Object.entries(source.data)) {
                v.data = upd_source[a];
                }
            } else {
                for (let key in v.data) {
                    v.data[key] = [];
                    }
                }
        }
    
''')

Above is my js code to do what I mentioned above about removing values from source_dict however I keep getting an invalid value message. Any help on why this js code is not working would be greatly appreciated

data from Causes of Death - Our World in Data

Hello @AlZ11, your code is relatively long, so the chance that a community member takes time to go through it is rather low.
I encourage you to reduce your code to the smallest possible portion of code that reproduces your problem, in a form that can be easily run by the reader, see MRE.

At the same time, I recommend to start from a JavaScript callback example from the documentation (JavaScript callbacks — Bokeh 2.4.2 Documentation) and progressively modify it to match your needs.
This will take you some time but if you proceed this way, the probability is high that you find the problem yourself.

no worries, thanks for letting me know I’ll make a shorter post

Please do not open another topic for the same issue.

Unfortunately your code still cannot be easily tested.
A complete code starts with the import section and contains all that is needed to run it (ideally with self contained reduced data), only keep what is necessary to reproduce the error.
The code usually ends with show(layout), or curdoc().add_root(layout) if there are Python callbacks.
That way, the reader can run the code quickly and the probability you get an answer is higher.

Also, I would recommend to use more specific title to your topic, for instance:
“CustomJS callbacks to modify plot source”.

If I had to solve your problem, I would write the following code:

import pandas as pd
from math import pi
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import CustomJS, Slider, HoverTool, ColumnDataSource
from bokeh.layouts import column
from bokeh.transform import cumsum
import colorcet

# Provide some data (is not the same as in your example):
data = {"Year": [1990, 1991, 1992],
        "Meningitis": [2159, 1592, 3914],
        "Alzheimer": [1116, 842, 534],
        "Parkinson": [371, 2376, 1675],
        "Malaria": [93, 189, 231]    
}
df = pd.DataFrame(data).set_index("Year")

df_transposed = df.transpose()

# Create one data frame per year and save them to a list of data frames.
# I find it easier to keep data frames at first, because operations can
# be applied at a higher level, so with less code:
year_dfs = []
for year in df_transposed.columns:
    year_df = df_transposed[year].to_frame(name="Deaths")
    year_df.index.name = "Disease"
    year_df["Percent"] = 100 * year_df["Deaths"]/year_df["Deaths"].sum()
    year_df["angle"] = year_df["Deaths"]/year_df["Deaths"].sum() * 2 * pi
    # Compute start and end angle for each disease, so that the first disease starts
    # at the top of the pie and the next ones appear in clockwise direction:
    year_df["start_angle"] = pi/2 - (year_df["angle"].cumsum() - year_df["angle"])
    year_df["end_angle"] = pi/2 - year_df["angle"].cumsum()
    # Set the color column. Feel free to use other palettes:
    year_df['color'] = list(map(lambda x: colorcet.b_glasbey_category10[x], range(len(year_df))))
    year_dfs.append(year_df)

# Convert the dataframes to dictionaries in the ColumnDataSource data format to hand over to Javascript:
year_dicts = []
for df in year_dfs:
    year_dicts.append(dict(ColumnDataSource(df).data))

# Setup pie chart:
height = 550
width = 650
plot = figure(height=height, width=width, title="Diseases", x_range=(-0.6, 1.0))

plot_source = ColumnDataSource(year_dicts[0])

plot.wedge(x=0, y=1, radius=0.6, source=plot_source,
         start_angle="start_angle", end_angle="end_angle", direction="clock",
        line_color="white", legend_field='Disease', fill_color='color')

# Setup hover inspection:
hover = HoverTool()
hover.tooltips = [("Disease", "@{Disease}"),
                  ("Deaths", "@Deaths"),
                  ("Percent", "@Percent{f0.0} %")]
plot.add_tools(hover)

plot.axis.axis_label = None
plot.axis.visible = False
plot.grid.grid_line_color = None
plot.outline_line_color = None

plot.title.text_font_size = '16pt'

plot.toolbar.active_drag = None
plot.toolbar_location = None

# Setup slider and callback:
slider = Slider(start=1990, end=1992, value=1990, step=1, title="Select year")

callback = CustomJS(args=dict(plot_source=plot_source, sources=year_dicts), code="""
    const year_select = cb_obj.value
    const source_list = sources
    plot_source.data = source_list[year_select - 1990]
""")

slider.js_on_change('value', callback)

layout = column(slider, plot)

show(layout)

In this code, I hand over a list of all the ColumnDataSource data dictionaries to Javascript, so Javascript just has to pick the correct one and assign it to the source of the plot.

There are surely other ways to obtain the same behavior.
You could also have look here:
How to change data columns in CustomJS callback

Also, keep in mind that with CustomJS, all the data is contained in the html file.
With my code, all disease deaths for all years, together with the percents, angles and colors. Here, lightweighting could start with letting Javascript compute percents, angles and colors only for the selected year.
But if you have huge data, CustomJS ist not the best solution. An alternative could be the use of Python callbacks, which require the use of a Bokeh server to update the changes to the app.

Hi sorry apologies for that first of all, no worries I adapted your code for my problem and was able to get it working, thank you again!

1 Like

@AlZ11 glad to hear that this solved your problem.
Could you mark the post as solution so readers can find it easier?

2 Likes

@AlZ11 If instead of a list you want to use a dictionary with the years as keys to hand over to Javascript, it is also possible. Have a look here:

1 Like