CustomJS callback to modify layout (organize report)

Hi,

I’m still getting used to bokeh, I’ve found it to be a great tool. I recently hit a roadblock trying to implement a CustomJS callback. I’m generating a report which uses tabs, sometimes I can get a lot of these tabs and navigating the document becomes really cumbersome. Luckily the plots have some attributes which could be used to group some plots together. So I was trying to implement a way of filtering the number of visible tabs based on such attribute. I was pretty much successful on sketching a solution with bokeh server but my end solution would need to implement a CustomJS callback since I need to distribute the html report. I’m kind of lost since I’m not familiar with how to implement CustomJS callbacks, or even if what I’m trying to achieve is even possible without bokeh server.

My main objective would be to substitute the ‘change_plot’ callback with a CustomJS callback, if anyone has a pointer of how this could be possible I’d greatly appreciate some help.

I’ve tried implementing with no luck a CustomJS which modifies the children properties, On the snippet below ‘col[i]’ is the current column layout, figcol is a column layout storing all the figures, group_dd[i] is a dropdown menu.

cjs[i] = CustomJS(args=dict(col=col[i], select=group_dd[i], allfigs=figcol), code="""
// Split the index
var dd_val = (select.value)
var valARR = dd_val.split(',')
var index = parseInt(valARR[0])

// replace with appropiate figure?
col.children[1] = allfigs.children[index]
""")    

After investigating with console.log(col) it seems that the action is being done correctly however the new column layout is not rendered after the change takes place.

I’m attaching a minimal example of my script below.

Thanks
Javier

FILTERED REPORT USING BOKEH SERVER:

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Tabs, Panel, Dropdown, PreText
from bokeh.plotting import figure, curdoc


#Initialize variables
nplots = 6 # Number of plots (actual report has way more plots)
ngroup = 4 # Number of plots assigned to first group

# Definition of report structure
groups = [f'Quad' if i < ngroup else f'Linear' for i in range(nplots)] # Arbitrary grouping of plots
tabnames = [f'Title_{i}' for i in range(nplots)] # Individual plot names

# Creates list of unique groups without modifying first appearance order
cnt = 0
unq_grp = []
original_groups = groups[:]
while len(groups):
    cnt = cnt + 1
    unq_grp.append(groups[0])
    groups = list(filter(lambda group: group != groups[0], groups))
    if cnt > len(groups):
        break
    
# Data Variables
x = [None]*nplots
y = [None]*nplots

# Plot Variables
fig = [None]*nplots
source = [None]*nplots

# Generates figures with plots from data with custom process
for i in range(nplots):
    x[i] = [x[i] for x[i] in range(0, 10)]
    if i < ngroup:        
        y[i] = [(i*n)**2 for n in x[i]]
    else:
        y[i] = [(i*n) for n in x[i]]
    source[i] = ColumnDataSource(data=dict(x=x[i], y=y[i]))
    fig[i] = figure()
    fig[i].line('x', 'y', source=source[i], line_width=3, line_alpha=0.6)
    
# Callback to change Plot and Plot Title
def change_plot(attr, old, new):
    if new is not 'None':
        index = int(new.split(',')[0])
        group = int(new.split(',')[1])
        title[group].text = f'Plot: {subgroup[group][index][0]}'
        display[group].children[2] = fig[index]

subgroup = [None]*len(unq_grp) #List of tuples ('plot_name', ['tabname_index','unique_group_index'])
menu = [None]*len(unq_grp) #List that populates dropdown menu
group_dd = [None]*len(unq_grp) #Placeholder for dropdown GUI elements
tab = [None]*len(unq_grp) #Placeholder for tab GUI elements
title = [None]*len(unq_grp) #Placeholder for title GUI elements
display = [None]*len(unq_grp) #Placeholder for column GUI elements

# Cycle through each unique group
for i, group in enumerate(unq_grp):
    # Filter the figures corresponding to current group
    subgroup[i] = [(tabnames[j],str(f'{j},{i}')) if original_group == group else None for j, original_group in enumerate(original_groups)]
    # Populates the dropdown menu
    menu[i] = list(filter(None,subgroup[i]))
    # Reference default figure index (first in the menu)
    default = int(menu[i][0][1].split(',')[0])
    # Creates GUI/Report elements
    group_dd[i] = Dropdown(label = "Select Group", button_type = "default", menu=menu[i])
    title[i] = PreText(text=f'Plot: {menu[i][0][0]}', width=200)
    display[i] = column([group_dd[i],title[i],fig[default]])
    # Listens to callback event
    group_dd[i].on_change('value', change_plot)
    # Creates tabs
    tab[i] = Panel(child = display[i], title = group)
out_tabs = Tabs(tabs = tab)

curdoc().title = "Plotting Tool"
curdoc().add_root(out_tabs)

WHAT I HAVE SO FAR

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Tabs, Panel, Dropdown, PreText, CustomJS
from bokeh.plotting import figure, output_file, show


#Initialize variables
nplots = 6 # Number of plots
ngroup = 4 # Number of plots assigned to first group

# Definition of report structure
groups = [f'Quad' if i < ngroup else f'Linear' for i in range(nplots)] # Arbitrary grouping of plots
tabnames = [f'Title_{i}' for i in range(nplots)] # Individual plot names
output_file("tabs.html")

# Creates list of unique groups without modifying first appearance order
cnt = 0
unq_grp = []
original_groups = groups[:]
while len(groups):
    cnt = cnt + 1
    unq_grp.append(groups[0])
    groups = list(filter(lambda group: group != groups[0], groups))
    if cnt > len(groups):
        break
    
# Data Variables
x = [None]*nplots
y = [None]*nplots

# Plot Variables
fig = [None]*nplots
source = [None]*nplots

# Generates figures with plots from data with custom process
for i in range(nplots):
    x[i] = [x[i] for x[i] in range(0, 10)]
    if i < ngroup:        
        y[i] = [(i*n)**2 for n in x[i]]
    else:
        y[i] = [(i*n) for n in x[i]]
    source[i] = ColumnDataSource(data=dict(x=x[i], y=y[i]))
    fig[i] = figure()
    fig[i].line('x', 'y', source=source[i], line_width=3, line_alpha=0.6)
figcol = column(fig)


output_file("tabs.html")
subgroup = [None]*len(unq_grp) #List of tuples ('plot_name', ['tabname_index','unique_group_index'])
menu = [None]*len(unq_grp) #List that populates dropdown menu
group_dd = [None]*len(unq_grp) #Placeholder for dropdown GUI elements
tab = [None]*len(unq_grp) #Placeholder for tab GUI elements
title = [None]*len(unq_grp) #Placeholder for title GUI elements
col = [None]*len(unq_grp) #Placeholder for column GUI elements
cjs = [None]*len(unq_grp) #Placeholder for column GUI elements

# Cycle through each unique group
for i, group in enumerate(unq_grp):
    # Filter the figures correspondig to current group
    subgroup[i] = [(tabnames[j],str(f'{j},{i}')) if original_group == group else None for j, original_group in enumerate(original_groups)]
    # Populates the dropdown menu
    menu[i] = list(filter(None,subgroup[i]))
    # Reference default figure index (first in the menu)
    default = int(menu[i][0][1].split(',')[0])
    # Creates GUI/Report elements
    group_dd[i] = Dropdown(label = "Select Group", button_type = "default", menu=menu[i])
    col[i] = column([group_dd[i],fig[default]])
    cjs[i] = CustomJS(args=dict(col=col[i], select=group_dd[i], allfigs=figcol), code="""
    // Split the index
    var dd_val = (select.value)
    var valARR = dd_val.split(',')
    var index = parseInt(valARR[0])
    
    // replace with appropiate figure?
    col.children[1] = allfigs.children[index]
    """)    

    # Listens to callback event
    group_dd[i].js_on_change('value',cjs[i])
    # Creates tabs
    tab[i] = Panel(child = col[i], title = group)
out_tabs = Tabs(tabs = tab)

show(out_tabs)

ORIGINAL REPORT WITHOUT FILTERING

from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Tabs, Panel, PreText
from bokeh.plotting import figure, output_file, show

output_file("tabs.html")

nplots = 6
ngroup = 4
x = [None]*nplots
y = [None]*nplots
source = [None]*nplots
rtab = [None]*nplots
fig = [None]*nplots
title = [None]*nplots
cols = [None]*nplots

groups = [f'Quad' if i < ngroup else f'Linear' for i in range(nplots)]
tabnames = [f'Title_{i}' for i in range(nplots)]

for i in range(nplots):
    x[i] = [x[i] for x[i] in range(0, 200)]
    if i < ngroup:        
        y[i] = [(i*n)**2 for n in x[i]]
    else:
        y[i] = [(i*n) for n in x[i]]
    source[i] = ColumnDataSource(data=dict(x=x[i], y=y[i]))
    fig[i] = figure()
    fig[i].line('x', 'y', source=source[i], line_width=3, line_alpha=0.6)
    title[i] = PreText(text=f'Plot: {tabnames[i]}', width=200)
    cols[i] = column(children = [title[i],fig[i]], sizing_mode='fixed')    
    rtab[i] = Panel(child = cols[i], title = tabnames[i])
out_tabs = Tabs(tabs = rtab)
show(out_tabs)

You can’t update containers like this:

col.children[1] = allfigs.children[index]

in the browser. This is possible in Python (to some extent), because we wrap built-in types. This isn’t done on JS side, so you have to update entire objects instead of their items, e.g.:

var children = [...col.children] // or use .slice()
children[1] = allfigs.children[index]
col.children = children

With this change updating layout should just work. Note that there are some properties that aren’t hooked up with change listeners, so other things may not work without a little more push (e.g. #7357).

2 Likes