Layout with Drop Down Select Option without Bokeh Server

HI,
I have created 10 different layouts in Bokeh which are similar to each other with changes in numbers and content. I want to create a drop down which will help select one of the layouts. I am able to do it with Bokeh server but I want to save it as a single HTML file, which does not seem to be an option.

Any way around it?

Certainly, but the HTML file will have to contain everything all together, so it might be fairly large depending on what your data is. Your best bet is to combine all ten sub-layouts in one high level layout like a row or column and then use a CustomJS callback on the the drop down to control the visible property on the sub-layouts appropriately (e.g. make all of them invisible except the one you want to currently display). There are several questions on this forum that have touched on this pattern so I would suggest searching here for “visible” as a first step.

I want it as a dropdown. I know I can get it using the tabs but how do I get the dropdown? Thats only working on server, not as the show function.

@Vishal_Agarwal I am not sure what you are asking. My answer was above was explicitly for accomplishing what you want using a dropdown: Add everything (all the different layouts) up front as children of one top-level column, and use the JS callback on the dropdown to control the visibility of the children in the column. That does not require a Bokeh server.

That answers the question but I am not getting the right response from the callback. I am not sure if something else is required. Is this the right place to post the code for checking?


I don’t want to duplicate by uploading on stackoverflow. I am attaching the screenshot of the code which is set for JS Callback for the layouts. Can you please check and point out the error!

Bryan - If there is a sample code that I can refer, that will be great. I did not find a related one online.

@Vishal_Agarwal please do not ever post screenshots of code. Besides making it more difficult for people to use to help you (because it cannot be copy and pasted to investigate), it is also completely inaccessible to users that employ screen reader or other assistive technologies. Always paste code as actual text.

You CustomJS code should do the following:

  • First, loop over every item in layout.children and set .visible = false. for all of them This will hide all the layouts.
  • Second, based on the value of the Select widget, set the .visible = true for whichever one of the layouts you want to currently be visible.

Note, based on your description of the task, there is nothing to do with sources or data or change events on those.

I am trying to incorporate as you mentioned here. I am not familiar with JS scripts so some help on getting the right code will be helpful. Here is the code:

Code:

layouts = column([layout_single(customer_1),layout_single(customer_2)])

df = pd.DataFrame.from_dict(
    {
        'F company_green': {'value':layout_single(customer_1)},
        'F company_red': {'value': layout_single(customer_2)}
    }
)

d_map = {
    'F company': ['F company_green'],
    'I company': ['I company_red']
}
selection_list = list(d_map.keys()) 
initial_value = selection_list[0]

initial_col = [i for i in df.columns.tolist() if i.startswith(initial_value)]
source = ColumnDataSource(df)
opts = selection_list
select = Select(value = opts[0], options = opts)

callback = CustomJS(args = dict(graph=source, source= df.to_dict()), code =
                    
            """
            for (var c=0; c < layouts.children.length; c++){
                c.visible = false
                }
            
            graph.data = source[cb_obj.value];
            graph.change.emit();
            cb_obj.value.visible = true;
            
            """)
select.js_on_change('value', callback)
layout = column([select, layouts])
show(layout)

@Vishal_Agarwal please also make use of the code formatting options in the editor so that your code is intelligible (you can go back and edit your previous post). This is especially important for Python code, since blank space is significant. You can use either the </> icon on the editing toolbar, or add triple backtick ``` fences around the code blocks, to make them appear properly formatted.

Thank you. I have edited. Will await response on sorting the code issue. This current one does not change anything from the dropdown.

@Vishal_Agarwal This is what I meant:

from bokeh.layouts import row, column
from bokeh.models import CustomJS, Select
from bokeh.plotting import figure, show

p1 = figure()
p1.circle([1,2,3], [1,3,2], size=20)

p2 = figure(visible=False)
p2.line([1,2,3], [1,3,2], line_width=3)

col = column(p1, p2)

s = Select(value="circle", options=["circle", "line"])

s.js_on_change('value', CustomJS(args=dict(s=s, col=col), code="""
    for (const plot of col.children) {
        plot.visible = false
    }

    // Pick one of the column children to make visible. 
    //
    // *You* will have to provide the logic to decide which one of the
    // children to make visible, based on the select value. That is a 
    // policy decision that depends completely on your requirements. 
    if (s.value == "circle") {
        col.children[0].visible = true
    }
    else if (s.value == "line") {
        col.children[1].visible = true
    }
"""))

show(row(s, col))

This uses single plots for simplicity, but replace them with any more complicated layout and it is the same.

ScreenFlow

Thank you so much. This solves the issue.
One final question: If I have to just make the selected value visible, do I need to do a if-else for all the 10/20 different layouts or can I do something like:

if (s.value == "Cust1") {
        col.children[0].visible = true
    }
    else {
        index = options.indexof(s.value)
        col.children[index].visible = true
    }

You could set the name property of each child layout to match one of the options in the select, and then compare the current select value to the child.name. In fact you could accomplish this in one loop. Something like (untested)

    for (const child of col.children) {
        child.visible = (child.name == s.value)
    }

While choosing a value of the dropdown here, the components of the respective layout get overlapped into each other. Is there a way around this?

I don’t know what that means, you will need to provide a screenshot.

Here are the screenshots. The code is the same. The layout is created in such a way that blue is called banner 1 and red is called banner 2.

layout_single= row([banner1, banner2])

The rest of the code is the same. The numbers are different for the 2 layout_single for cust1 and 2 and each customer has one blue and one red.

Screenshot Default-

Screenshot after making a selection-

Note: THe layout is simplified to focus on the roots problem.

Could be a bug, or could be some usage issue. At this point we’d need a complete Minimal Reproducible Example that can actually be run to reproduce the situation for direct investigation.

Here is the reproducible sample code which clearly shows the issues:

import numpy as np
import pandas as pd
from bokeh.models.widgets import Div
from bokeh.layouts import row, column
from bokeh.models import CustomJS, Select
from bokeh.plotting import figure, show, ColumnDataSource

cust1 = {
    'industry' : 'Construction',
    'biz_type' : 'Manufacturing',
    'total_loan': '20,000',
    'location': 'Ohio'}

cust2 = {
    'industry' : 'Services',
    'biz_type' : 'Plumbing Services',
    'total_loan': '354,000',
    'location': 'San Jose'}

def layout_single(cust):
    div_leftpanel1 = Div(text = f'&nbsp;&nbsp;Customer Profile\
                <br><br><i class="fa fa-file"></i>&nbsp;&nbsp;Contact Us</a> \
                <br><br><i class="fa fa-file"></i>&nbsp;&nbsp;Blogs</a> \
                <br><br><br><br><br> \
                <p style="font-size:14px;font-family:verdana"><u>Customer Info</u> <br><br> Industry: {cust["industry"]}\
                <br> Type of Business: {cust["biz_type"]} <br> Location: {cust["location"]} </p> \
                <b>', 
          
                style={'width':'350px','height':'680px','border-radius': '15px',
                 'text-align': 'left','padding-left': '50px','font-size': '150%',
                 'color': 'green', 'background-color': '#91EBE8','font-family':'verdana',
                 'padding-top': '10px'})
    
    banner1 = Div(text = f'<b><p style="font-size:15px;">${cust["total_loan"]}</p>\
                    <hr>\
                    Loan Amount<b>',\
              style={'width':'250px','height':'90px','border-radius': '15px 15px 0px 0px',
                 'text-align': 'left','padding-left': '50px','padding-right': '50px',
                 'font-family':'verdana'})
    
    return row([div_leftpanel1, banner1])


col = column([layout_single(cust1), layout_single(cust2)])

s = Select(value="Customer1", options=["Customer1", "Customer2"])

s.js_on_change('value', CustomJS(args=dict(s=s, col=col), code="""
    for (const plot of col.children) {
        plot.visible = false
    }

    if (s.value == "Customer1") {
        col.children[0].visible = true
    }
    else if (s.value == "Customer2") {
        col.children[1].visible = true
    }
"""))

final_layout = column(s, col)
show(final_layout)

@Vishal_Agarwal Bokeh manages the layout and size of its own components. If you start setting CSS width, height, padding, etc. you will almost certainly interfere with that, and all bets are off. If you have need for this level of control you might be better off using a custom Jinja template and having the CustomJS manipulate real DOM elements directly.

Even if I remove the styling, the issue remains. So in that case, I cannot create layouts for drop-down menus. With the use of Jinja Template, I will need to use a server but I want to save it as a single HTML file. Is there a way around it?