How can I choose between graphs with bokeh? is this possible?

hi, I have a bokeh graph (two) and whant to choose them with this:

# selector de gráficas
LABELS = ["Graph A", "Graph B", "Graph C" ]

radio_button_group = RadioButtonGroup(labels=LABELS, active=0)
radio_button_group.js_on_click(CustomJS(code="""
    console.log('radio_button_group: active=' + this.active, this.toString())
"""))

show(radio_button_group)

I tried using if:
if radio_button_group == RadioButtonGroup(labels="Graph A", active=0):
blablabla and the same with B and C … but the broser appeared in blank. what can I do to show a graph using the selector?

Hi @javi_topo,

What I had done using Tabs which was using one figure, when I checked in the main.py code for the active state in a handler, then updated the figure with the data corresponding to which selection was done. The tabs in my case controlled the view of the controls for each plot.

Here is a basic example of allowing the RadioButtonGroup to change the data on a line plot. Basically using the .on_change('active', ...) allows you detect the change and code your specifics to your hearts content.

# app.py
from bokeh.models import (
    ColumnDataSource, RadioButtonGroup
)
from bokeh.plotting import figure, curdoc

from numpy import linspace, random
from pathlib import Path

LABELS = ["Graph A", "Graph B"]
data = {
    LABELS[0] : {
        'x' : linspace(0, 100),
        'y' : linspace(0, 100)**2,
    },
    LABELS[1] : {
        'x' : random.random(50)*20,
        'y' : random.random(50)*20,
    }
}

axes = {
    LABELS[0] : {
        'x' : 'Time (s)',
        'y' : 'Speed (m/s)',
    },
    LABELS[1] : {
        'x' : 'x-direction (cm)',
        'y' : 'y-direction (cm)',
    }
}

DEFAULT = 'Graph A'
source = ColumnDataSource(data=data[DEFAULT])

p = figure(title="Radio Select Demo")
p.xaxis.axis_label = axes[DEFAULT]['x']
p.yaxis.axis_label = axes[DEFAULT]['y']
p.line(x='x', y='y', source=source)

radio_btn_grp = RadioButtonGroup(labels=LABELS, active=0)

def radio_handler(attr: str, old, new) -> None:
    global p
    label = LABELS[new]
    source.data = data[label]
    p.xaxis.axis_label = axes[label]['x']
    p.yaxis.axis_label = axes[label]['y']

radio_btn_grp.on_change("active", radio_handler)
    
layout = column(p, radio_btn_grp)

curdoc().add_root(layout)

NOTE if your data is plotted in each graph using different glyphs, like lines and circles, it would be better to delete the data and replot using the corresponding function. Ideally you would have a wrapper function to handle that logic.

Hope this helps!

Edit: As @gmerritt123 mentioned, yes this is intended to be served via

bokeh serve app.py

and not rely on any Javascript.

1 Like

If you want to stay “standalone” i.e. not require a server to run python code as a callback)… you need to write actual javascript instead (which you’re attempting with the CustomJS). Javascript is the language of web browsers, which execute code/instructions on the data you have embedded in the html.

One workflow you could use to accomplish this, and keep things maybe a bit more simple is to just make two figures with all the formatting/data etc you want on each, and then just use CustomJS to toggle the visibility property of each:

  1. Make your two figures exactly how you want each to be, with visible = False for fig2.
  2. Make your RadioButton Group (rbg) with two options.
  3. Write out a CustomJS that will toggle the visibility property of the figures depending on which button is pressed by passing the two figures and the radiobutton group instance into the callback code as args:
cb = CustomJS(args=dict(fig1=fig1,fig2=fig2,rbg=rbg)
              ,code='''
              if (rbg.active == 0){
                fig1.visible = true
                fig2.visible = false}
              else if (rbg.active ==1){
                fig1.visible = false
                fig2.visible = true
                }
              ''')
  1. Tell that callback to execute whenever the “active” property of the radiobutton group changes:

rbg.js_on_change('active',cb)

  1. Wrap up all your items in a layout with fig1 and fig2 side by side and save/show that:

lo = layout([row([fig1,fig2]),rbg]

1 Like

@gmerritt123 Oh that is clever! I have been avoiding CustomJS as much as I can, but I see the power here means I shouldn’t shy away from it.

2 Likes

Hi, I think I’m going to try to solve this without js

I still cant do it. I’m traying to solve this still with an if condition …

I have selector, and dont know how to use with what you have written without

also, what is the radio_handler? can I use it without labels (I have my own labels)?

Hi @javi_topo,

So a lot of interactions with bokeh widegts follow similar principles of GUI coding. When an event is triggered the software captures that, and allows you to pass a function on how you would like to deal with the event.

For example, I have created a function called radio_handler(attr: str, old, new). These functions can be named whatever you want, but I prepend handler or callback so I know in code what they are used for.

Bokeh requires certain events to use that style of function call, such that,

# Used for on_change methods 
def my_func_name_callback(attr: str, old, new):
    # Do your stuff here, change plots etc...
    # attr : string of the event type to watch (see radio_handler e.g.)
    # old  : is the previous value (not always used)
    # new  : new value selected or interacted with

For simplicity you can pass global variables for widgets or models in bokeh that you need to change.

If you look at some of the documentation, you will see some widgets have methods like on_change or on_click. For example, the on_change can take events like,

  1. "active"e.g., RadioButtonGroup or Tabs
  2. "value"e.g., IntSlider, FloatSlider

Button widget uses the on_click, but it’s function follows a more simple format (no parameters passed),

btn = Button(label="Click Me")
def my_button_callback():
    # Do something interesting here
    print("This string is interesting")

btn_onclick(my_button_handler)

It is hard to give you a concrete example if I don’t have an example of what you are trying to change, how many data sets you have etc… but I came up with something that uses two different data sets, with different dictionary keys, and rescales the axes based on setting lines visible. It is similar to @gmerritt123 by changing visible lines, but does it all in python.

# -*- coding: utf-8 -*-
from bokeh.layouts import column
from bokeh.models import (
    ColumnDataSource, RadioButtonGroup
)
from bokeh.plotting import figure, curdoc

from numpy import linspace, min, max, random
from pathlib import Path

# This is handy for the radio_handler
LABELS = ["Graph A", "Graph B"]

data1 = {
        't' : linspace(0, 100),
        's' : linspace(0, 100)**2,
}

data2 = {
        'x' : random.random(50)*20,
        'y' : random.random(50)*20,
}

source1 = ColumnDataSource(data=data1)
source2 = ColumnDataSource(data=data2)

p = figure(title="Radio Select Demo")
p.xaxis.axis_label = "Time (s)" 
p.yaxis.axis_label = "Speed (m/s)"

line1 = p.line(x='t', y='s', source=source1)
line2 = p.line(x='x', y='y', source=source2)
line2.visible = False

# Create Radio Button Group
radio_btn_grp = RadioButtonGroup(labels=LABELS, active=0)


# User defined function to handle logic when Radio Button button changes
# Required for on_change(...). These functions require the prototype:
# def my_func_name(str, old, new)
def radio_handler(attr: str, old, new) -> None:
    # Use globals to access functions or lines to change data
    global p, line1, line2

    # new holds the index of the label (either 0 or 1 in this case)
    label = LABELS[new]
    if label == "Graph A":
        line1.visible = True
        line2.visible = False

        # Rescale plot
        p.x_range.start = min(data1['t'])
        p.x_range.end   = max(data1['t'])
        p.y_range.start = min(data1['s'])
        p.y_range.end   = max(data1['s'])
        
        p.xaxis.axis_label = "Time (s)"
        p.yaxis.axis_label = "Speed (m/s)"
    elif label == "Graph B":
        line1.visible = False
        line2.visible = True

        # Rescale plot
        p.x_range.start = min(data2['x'])
        p.x_range.end   = max(data2['x'])
        p.y_range.start = min(data2['y'])
        p.y_range.end   = max(data2['y'])
        
        p.xaxis.axis_label = "x-direction (cm)"
        p.yaxis.axis_label = "y-direction (cm)"
    else:
        print("Label does not exist")

# NOTE you need to tell bokeh RadioButtonGroup to execute the function when the
# EVENT: active button changes i.e., .on_change("active", myfunction)
radio_btn_grp.on_change("active", radio_handler)
    
layout = column(p, radio_btn_grp)

curdoc().add_root(layout)

Again this expects you to run,

bokeh serve app.py

Compared to the last code I gave in the previous post, you have to manually do more work. One neat thing you can do (like in the old code) is change the source.data dictionary to change the data which auto rescales the axes, as long as you use the same dictionary keys.

Hope this helps, but it might be worth stepping through the documentation on widgets. As you can do things in both python and javascript.

1 Like

hey, thanks! its cool I can use different data …

I will mess arround today with this … and after that I will ask q’s if I have some . if not, I will check as solved.

after having many problems … I’m going to see If I can make this work. thanks

how do I call fig 1? do I define fig1? with a class? how?

can I change layout (I’m better sleeping at my bed) for show? layout needs to be defined? (sleeping inside please)

You import figure from bokeh.plotting. while layout, column and row come from bokeh.layouts. This is all in the docs and countless examples. Here’s another one doing what you’re looking to do:

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

fig1= figure(title='Here is one graph',height=300,width=400)
fig1.line(x=[1,2,3],y=[2,1,4])

fig2 = figure(title='Look at thissss Graaappph!',visible=False,height=300,width=400)
fig2.image_url(url=['https://i.ytimg.com/vi/sIlNIVXpIns/mqdefault.jpg']
               ,x=0,y=0,w=100,h=100)

rbg = RadioButtonGroup(labels=['Graph 1', 'Graph2'],active=0)

cb = CustomJS(args=dict(rbg=rbg,fig1=fig1,fig2=fig2)
              ,code='''
                if (rbg.active == 0){
                  fig1.visible = true
                  fig2.visible = false}
                else if (rbg.active ==1){
                  fig1.visible = false
                  fig2.visible = true
                  }
                ''')
rbg.js_on_change('active',cb)

lo = column([row([fig1,fig2]),rbg])

show(lo)

graapph

1 Like

is this for static graphs?

I have a graph that uses data (and can select between data).

having trouble to mix up everythin;

I have two graphs

the fist one is this one:

''' A crossfilter plot map that uses the `Auto MPG dataset`_. This example
demonstrates the relationship of datasets together. A hover tooltip displays
information on each dot.
.. note::
    This example needs the Pandas package to run.
.. _Auto MPG dataset: https://archive.ics.uci.edu/ml/datasets/auto+mpg
'''
import pandas as pd

from bokeh.layouts import column, row
from bokeh.models import Select
from bokeh.palettes import Spectral5
from bokeh.plotting import curdoc, figure
from bokeh.sampledata.autompg import autompg_clean as df

df = df.copy()

SIZES = list(range(6, 22, 3))
COLORS = Spectral5
N_SIZES = len(SIZES)
N_COLORS = len(COLORS)

# data cleanup
df.cyl = df.cyl.astype(str)
df.yr = df.yr.astype(str)
del df['name']

columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]

def create_figure():
    xs = df[x.value].values
    ys = df[y.value].values
    x_title = x.value.title()
    y_title = y.value.title()

    kw = dict()
    if x.value in discrete:
        kw['x_range'] = sorted(set(xs))
    if y.value in discrete:
        kw['y_range'] = sorted(set(ys))
    kw['title'] = "%s vs %s" % (x_title, y_title)

    p = figure(height=600, width=800, tools='pan,box_zoom,hover,reset', **kw)
    p.xaxis.axis_label = x_title
    p.yaxis.axis_label = y_title

    if x.value in discrete:
        p.xaxis.major_label_orientation = pd.np.pi / 4

    sz = 9
    if size.value != 'None':
        if len(set(df[size.value])) > N_SIZES:
            groups = pd.qcut(df[size.value].values, N_SIZES, duplicates='drop')
        else:
            groups = pd.Categorical(df[size.value])
        sz = [SIZES[xx] for xx in groups.codes]

    c = "#31AADE"
    if color.value != 'None':
        if len(set(df[color.value])) > N_COLORS:
            groups = pd.qcut(df[color.value].values, N_COLORS, duplicates='drop')
        else:
            groups = pd.Categorical(df[color.value])
        c = [COLORS[xx] for xx in groups.codes]

    p.circle(x=xs, y=ys, color=c, size=sz, line_color="white", alpha=0.6, hover_color='white', hover_alpha=0.5)

    return p


def update(attr, old, new):
    layout.children[1] = create_figure()


x = Select(title='X-Axis', value='mpg', options=columns)
x.on_change('value', update)

y = Select(title='Y-Axis', value='hp', options=columns)
y.on_change('value', update)

size = Select(title='Size', value='None', options=['None'] + continuous)
size.on_change('value', update)

color = Select(title='Color', value='None', options=['None'] + continuous)
color.on_change('value', update)

controls = column(x, y, color, size, width=200)
layout = row(controls, create_figure())

curdoc().add_root(layout)
curdoc().title = "Crossfilter"

and the second one is this one:


import numpy as np
import scipy.special

from bokeh.layouts import gridplot
from bokeh.plotting import figure, show


def make_plot(title, hist, edges, x, pdf, cdf):
    p = figure(title=title, tools='', background_fill_color="#fafafa")
    p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
           fill_color="navy", line_color="white", alpha=0.5)
    p.line(x, pdf, line_color="#ff8888", line_width=4, alpha=0.7, legend_label="PDF")
    p.line(x, cdf, line_color="orange", line_width=2, alpha=0.7, legend_label="CDF")

    p.y_range.start = 0
    p.legend.location = "center_right"
    p.legend.background_fill_color = "#fefefe"
    p.xaxis.axis_label = 'x'
    p.yaxis.axis_label = 'Pr(x)'
    p.grid.grid_line_color="white"
    return p

# Normal Distribution

mu, sigma = 0, 0.5

measured = np.random.normal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(-2, 2, 1000)
pdf = 1/(sigma * np.sqrt(2*np.pi)) * np.exp(-(x-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((x-mu)/np.sqrt(2*sigma**2)))/2

p1 = make_plot("Normal Distribution (μ=0, σ=0.5)", hist, edges, x, pdf, cdf)

# Log-Normal Distribution

mu, sigma = 0, 0.5

measured = np.random.lognormal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 8.0, 1000)
pdf = 1/(x* sigma * np.sqrt(2*np.pi)) * np.exp(-(np.log(x)-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((np.log(x)-mu)/(np.sqrt(2)*sigma)))/2

p2 = make_plot("Log Normal Distribution (μ=0, σ=0.5)", hist, edges, x, pdf, cdf)

# Gamma Distribution

k, theta = 7.5, 1.0

measured = np.random.gamma(k, theta, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 20.0, 1000)
pdf = x**(k-1) * np.exp(-x/theta) / (theta**k * scipy.special.gamma(k))
cdf = scipy.special.gammainc(k, x/theta)

p3 = make_plot("Gamma Distribution (k=7.5, θ=1)", hist, edges, x, pdf, cdf)

# Weibull Distribution

lam, k = 1, 1.25
measured = lam*(-np.log(np.random.uniform(0, 1, 1000)))**(1/k)
hist, edges = np.histogram(measured, density=True, bins=50)

x = np.linspace(0.0001, 8, 1000)
pdf = (k/lam)*(x/lam)**(k-1) * np.exp(-(x/lam)**k)
cdf = 1 - np.exp(-(x/lam)**k)

p4 = make_plot("Weibull Distribution (λ=1, k=1.25)", hist, edges, x, pdf, cdf)

show(gridplot([p1,p2,p3,p4], ncols=2, width=400, height=400, toolbar_location=None))

I need both to be seen choosing with a button or radiobutton … dont mind if the transition to one or the other isnt ok or width si different. (whn one is done … I can add more graphs) … but dont really know how to do this.

also … it would be amazing to not press in the terminal bokeh server … any ideas?

Yes my example is with two static figures. If you have a layout with multiple figures, widgets etc, and want to toggle viewing that and another layout with multiple figures, widgets etc. You can either set up Tabs/Panels, or expand out my CustomJS example toggling visibility of an entire row/column etc. Please review Creating layouts — Bokeh 2.4.3 Documentation

If you want to not use a server, you need to replace your “update” function with the CustomJS equivalent so the web browser itself can execute the callback instructions. Then when assigning the conditions to execute the callback, use the js_on_change method instead of on_change. Please review Adding widgets — Bokeh 2.4.3 Documentation

1 Like

As @gmerritt123 said, you may need to use Tabs + Panels to do what you want. If you want to only stay in python, there are two ways you can approach it,

  1. Each tab + panel houses a figure and the controls (or a layout), and clicking tabs changes what you see (easiest solution).
  2. Each tab + panel only shows the controls for each graph, and clicking the tab has a callback function to do the exact same thing as I showed in the above example.

If you want to do (2), then the pseudo code for that would be,

...
from bokeh.models import Tabs, Panel

...
# fig, line1, line2, source1, source2 as above examples
# layout1 and layout2 are from columns, rows or grids

tabs_widget = Tab(
    tabs=[
        Panel(child=layout1, title="Tab 1"), 
        Panel(child=layout2, title="Tab2"),
    ]
)

def tabs_callback(attr: str, old, new) -> None:
    global fig, line1, line2, source1, source2
    global fig1_layout, fig2_layout

    tab = tabs_widget.tabs[new].title

    # Do stuff like my RadioButtonGroup example
    if tab == "Tab1":
        # Or if only one of them is a figure and the other is a layout 
        # then fig1.visible = True, and fig2.visible = False, etc...
        fig1_layout.visible = True
        fig2_layout.visible = False
    elif tab == "Tab2":
        fig1_layout.visible = False
        fig2_layout.visible = True
    else:
        print("Invalid Tab selected or not handled")

tabs_widget.on_change('active', tabs_callback)

...

However, based on the code you gave above, you have one that plots with 1 graph, and one that plots with 4 graphs. So if you were doing that, I would would then just change the if tab == ... to just make the layouts of the graphs invisible, which is included above.

But you should spend some time going through the documentation and creating plots as these provide a lot of help and understanding of the imports and widgets.

@gmerritt123 I had a good laugh at the Nickleback reference, haha.

1 Like