0

I’m generating a Bokeh report which uses tabs, sometimes I can get a lot of these 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 these attributes. 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. I tried to implement a CustomJS based on other people posts but so far I've been unsuccesfull.

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’m providing a minimal example of my script below. Any help or pointers would be much appreciated.

Bokeh Server version of what I'm trying to achieve:

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
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):
    index = int(new.split(',')[0])
    group = int(new.split(',')[1])
    title[group].text = f'Plot: {subgroup[group][index][0]}'
    col[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
col = [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])
    title[i] = PreText(text=f'Plot: {menu[i][0][0]}', width=200)
    col[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 = col[i], title = group)
out_tabs = Tabs(tabs = tab)

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

Standalone Report (my code 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]

    // send new column, maybe?
    col.change.emit();
    """)    

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

1 Answers1

0

You can show all the figures in a column layout, and show/hide the figure by setting the visible property. Here is an example:

import numpy as np
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Tabs, Panel, Select, PreText, CustomJS
from bokeh.plotting import figure, output_file, show

output_file("tabs.html")

x = np.linspace(0, 10)

def create_figure(x, y, label):
    source = ColumnDataSource(data=dict(x=x, y=y))
    fig = figure(name=label)
    fig.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
    return fig

def create_select(figs, title="Select Group"):
    names = [fig.name for fig in figs]
    drop = Select(title=title, value=names[0], options=names)
    for fig in figs[1:]:
        fig.visible = False

    callback = CustomJS(args=dict(figs=figs), code="""
        let selected = cb_obj.value;
        for(let fig of figs){
            fig.visible = fig.name == selected;
        }
    """)

    drop.js_on_change("value", callback)
    return [drop] + figs

quad_figs = [create_figure(x, (i * x)**2, f"Quad {i}") for i in range(3)]
linear_figs = [create_figure(x, (i * x), f"Linear {i}") for i in range(3)]

tabs = Tabs(tabs=[
    Panel(child=column(create_select(quad_figs)), title="Quad"),
    Panel(child=column(create_select(linear_figs)), title="Linear")
])

show(tabs)
HYRY
  • 94,853
  • 25
  • 187
  • 187
  • I tried implementing the code but I got an attribute error, it seems that "Figure" does not has 'visible' attribute. `AttributeError: unexpected attribute 'visible' to Figure, similar attributes are disabled` – Javier Nava Jul 23 '19 at 12:47