Callback function for CheckboxGroup and Button - python

I want to create a visualization with CheckboxGroup, which shows the line of the currency in the graph if the checkbox of this currency is activated. In addition i want one Button 'Select all' and one 'Select none' to select all or none currency at once.
By now, I have this code but I get the following error:
unexpected attribute 'checkbox' to CustomJS, possible attributes are args, code, js_event_callbacks, js_property_callbacks, name, subscribed_events or tags
I would appreciate a check of my code and some help. Thank you
from bokeh.io import output_file, show, save
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool, FactorRange, CheckboxGroup, CustomJS, Button
from bokeh.layouts import row, column
...
args = []
code = "active = cb_obj.active;"
for c in range(len(currencies)):
line = p.line(x='dates', y=currencies[c], line_width=2, alpha=1, name=currencies[c], legend_label=currencies[c], source=source)
args += [('line'+str(c), line)]
code += "line{}.visible = active.includes({});".format(c, c)
...
checkbox_group = CheckboxGroup(labels=currencies, active=list(range(len(currencies))))
checkbox_group.callback = CustomJS(args={key:value for key,value in args}, checkbox=checkbox_group, code=code)
def callback_button_on():
checkbox_group.active = list(range(len(currencies)))
def callback_button_off():
checkbox_group.active = []
select_all = Button(label='Select all')
select_all.on_click(callback_button_on)
select_none = Button(label='Select none')
select_none.on_click(callback_button_off)
group = column(select_all, select_none, checkbox_group)
show(row(group, p))
output_file("Daily_Returns.html")

A CustomJS callback does not accept arbitrary arguments, but all bokeh models that should be available in the javascript code must be part of the args dictionary. Simply add the checkbox group into it:
args = []
args.append(("checkbox", checkboxgroup))
code = "active = cb_obj.active;"
...
checkbox_group.callback = CustomJS(args={key:value for key,value in args}, code=code)
or if your code has no explicit mention of the checkboxgroup (i tonly uses cb_obj) you can also just not pass the checkbox to the CustomJS callback.

This code works for Boekh v2.1.1
There were a few issues with your code:
Callback data is only allowed inside args dictionary
In JS you need to always use var for variable declaration => var active = cb_obj.active
Your Python callbacks for buttons won't work in a stand-alone HTML page. Python callbacks only work in a server app. For your stand-alone HTML page use only JS callbacks => js_on_click.
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CheckboxGroup, CustomJS, Button
from bokeh.layouts import row, column
from datetime import datetime, timedelta
currencies = ['EUR', 'GBP']
data = {'dates': [datetime(2017,1,1) + timedelta(days=x) for x in range(0,3)], 'EUR': [1.3, 1.1, 1.3], 'GBP': [1.6, 1.7, 1.6]}
source = ColumnDataSource(data)
p = figure(x_axis_type="datetime")
checkbox_group = CheckboxGroup(labels=currencies, active=list(range(len(currencies))))
select_all = Button(label='Select all')
select_none = Button(label='Select none')
args = [('checkbox', checkbox_group)]
code = "var active = cb_obj.active;"
for c in range(len(currencies)):
line = p.line(x='dates', y=currencies[c], line_width=2, alpha=1, name=currencies[c], legend_label=currencies[c], source=source)
args += [('line'+str(c), line)]
code += "line{}.visible = active.includes({});".format(c, c)
checkbox_group.js_on_change('active', CustomJS(args={key:value for key,value in args}, code=code))
select_all.js_on_click(CustomJS(args={'checkbox_group': checkbox_group, 'currencies': currencies}, code="checkbox_group.active = Array.from(currencies, (x, i) => i);"))
select_none.js_on_click(CustomJS(args={'checkbox_group': checkbox_group}, code="checkbox_group.active = [];"))
group = column(select_all, select_none, checkbox_group)
show(row(group, p))

Related

Get the name of the top most image with hover tool and show the tooltip at a fixed position

I need to assign a name to each plot in the same figure. I want to get this name of the top most plot upon hovering or tapping in the figure. For now I use a TextInput to show the name. Inside the CustomJS, what is the correct method to access the name of the plot? I googled around and couldn't find a document for what is inside the cb_obj or cb_data. Thank you for any help.
Sample code:
from bokeh.server.server import Server
from bokeh.plotting import figure, ColumnDataSource, show
from bokeh.layouts import column
from bokeh.models import Button, HoverTool, TapTool, TextInput, CustomJS
import numpy as np
def make_document(doc):
p = figure(match_aspect=True)
img1 = np.random.rand(9, 9)
img2= np.random.rand(9, 9)
p.image(image=[img1], x=0, y=0,
dw=img1.shape[0], dh=img1.shape[1],
palette="Greys256", name='image1')
p.image(image=[img2], x=5.5, y=5.5,
dw=img2.shape[0], dh=img2.shape[1],
palette="Greys256", name='image2')
text_hover = TextInput(title='', value='', disabled=True)
callback_hover = CustomJS(args=dict(text_hover=text_hover), code="""
text_hover.value = cb_obj['geometry']['name'];
""") # what should be used here?
hover_tool = HoverTool(callback=callback_hover, tooltips=None)
p.add_tools(hover_tool)
doc.add_root(column([p, text_hover], sizing_mode='stretch_both'))
apps = {'/': make_document}
server = Server(apps)
server.start()
server.io_loop.add_callback(server.show, "/")
try:
server.io_loop.start()
except KeyboardInterrupt:
print('keyboard interruption')
print('Done')
I noticed there exists a tags argument, it can be accessed in CustomJS, but how?
tags (List ( Any )) –
An optional list of arbitrary, user-supplied values to attach to this
model.
This data can be useful when querying the document to retrieve
specific Bokeh models
Or simply a convenient way to attach any necessary metadata to a model
that can be accessed by CustomJS callbacks, etc.
By consulting multiple sources, got a temporary solution:
from bokeh.plotting import figure, ColumnDataSource, show
from bokeh.models import HoverTool, CustomJS
import numpy as np
img1 = np.random.rand(9, 9)
img2= np.random.rand(9, 9)
source = ColumnDataSource(dict(image=[img1, img2],
name=['image1', 'image2'],
x=[0, 5.5],
y=[0, 5.5],
dw=[img1.shape[0], img2.shape[0]],
dh=[img1.shape[1], img2.shape[0]]))
p = figure(match_aspect=True)
render =p.image(source=source, image='image', x='x', y='y', dw='dw', dh='dh', name='name', palette="Greys256")
callback = CustomJS(code="""
var tooltips = document.getElementsByClassName("bk-tooltip");
for (var i = 0, len = tooltips.length; i < len; i ++) {
tooltips[i].style.top = ""; // unset what bokeh.js sets
tooltips[i].style.left = "";
tooltips[i].style.top = "0vh";
tooltips[i].style.left = "4vh";
}
""")
hover = HoverTool(renderers=[render], callback=callback)
hover.tooltips = """
<style>
.bk-tooltip>div:not(:last-child) {display:none;}
</style>
#name
"""
p.add_tools(hover)
show(p)
You just need to access p.title.text:
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import TextInput, CustomJS, HoverTool
from bokeh.plotting import figure
p = figure(title="Hello there")
p.circle(0, 0)
text_hover = TextInput(title='', value='', disabled=True)
callback_hover = CustomJS(args=dict(text_hover=text_hover, plot=p),
code="text_hover.value = plot.title.text;")
p.add_tools(HoverTool(callback=callback_hover, tooltips=None))
show(column([p, text_hover]))

Python Bokeh: after the button callback function, refreshing my figure

Can you please help me to refresh my figure? I have added new "p" variable within the callback function to reset my figure but it does not work. It just shows me an empty figure. Every time I press the button, it overlaps the new plot on the top of the old one. I have tried to use reset.emit() method but it says 'Figure' object has no attribute 'rest'. I also want to add the title in the figure, but it contains a variable. item_input, but I don't know where to start...
bokeh server version 2.0.2
Python 3.8.1
Tornado 6.0.3
from pandas import read_csv
from pandas import to_datetime
from bokeh.layouts import column
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, HoverTool, Title, TextInput, Button
source_data = 'somewhere'
def call_back():
try:
item_input = item.value
df = read_csv(source_data)
df1 = df[df['item'] == int(item_input)]
title = str(item_input) + ' ' + df1.iloc[0]['name']
source = ColumnDataSource(data=dict(
system_qty = df1['system_qty'],
man_date = to_datetime(df1['man_date']),
))
p.circle(
x='man_date', y='system_qty'
)
hover = HoverTool(
tooltips = [
("Manufacturing Date", "#man_date{%Y-%m-%d}"),
("Reserved Qty", "#reserved_qty"),
],
formatters = {
'#man_date': 'datetime'
},
)
p.add_tools(hover)
p.add_layout(Title(text="Manufacturing Date", align="center"), "below")
p.add_layout(Title(text="Quantity", align="center"), "left")
except ValueError:
raise
p = figure(x_axis_type='datetime')
item = TextInput(value='', title="Item:")
button = Button(label='Submit')
button.on_click(call_back)
curdoc().add_root(column(item, button, p))
Call methods of p only once, do not call them in a callback. Also, in general you should also create instances of Bokeh models just once, especially of ColumnDataSource. Create it once and then just reassign its data property in the callback.

linking JS bokeh widget

I have two simple bokeh widgets: a Select and a Slider - I can get the two widgets working separately but I can't find a way to get the two widgets linked together - so that the js_on_change action on one of them will update the status of the other.
My attempt below fails at the linking stage:
from bokeh.models.widgets import Select, Slider
from bokeh.io import output_notebook, show
from bokeh.resources import INLINE
from bokeh.models import ColumnDataSource, CustomJS, Select
from bokeh.layouts import column
output_notebook(INLINE)
options = ['a', 'b', 'c']
indexes = [0, 1, 2]
s1 = ColumnDataSource(data=dict(options=options))
s2 = ColumnDataSource(data=dict(indexes=indexes))
select = Select(title="Option:", options=options)
slider = Slider(title="Index", value=0, start=0, end=len(indexes) -1, step=1)
select_callback = CustomJS(args=dict(options=s1, indexes=s2), code="""
var opt = options.data;
console.log(cb_obj.value,
Object.values(options.data)[0].indexOf(cb_obj.value));""")
slider_callback = CustomJS(args=dict(options=s1, indexes=s2, select=select), code="""
var opt = options.data;
console.log(Object.values(opt)[0][cb_obj.value],
cb_obj.value);""")
select.js_on_change('value', select_callback)
slider.js_on_change('value', slider_callback)
# the following will not work as I am not using it properly
# slider.js_link('value', select, 'value')
show(column(select, slider))
I need to have this behavior running on the JS code,
as for my use case, I need to embed the resulting widgets in a static HTML page (no bokeh-server).
Thanks for any advice!
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import CustomJS, Select
from bokeh.models.widgets import Slider
options = ['a', 'b', 'c']
init_idx = 0
select = Select(title="Option:", options=options, value=options[init_idx])
slider = Slider(title="Index", value=init_idx, start=0, end=len(options) - 1, step=1)
select.js_on_change('value', CustomJS(args=dict(slider=slider),
code="slider.value = cb_obj.options.indexOf(cb_obj.value);"))
slider.js_on_change('value', CustomJS(args=dict(select=select),
code="select.value = select.options[cb_obj.value];"))
show(column(select, slider))

plotting multiple lines of streaming data in a bokeh server application

I'm trying to build a bokeh application with streaming data that tracks multiple "strategies" as they are generated in a prisoners-dilemma agent based model. I've run into a problem trying to get my line plots NOT to connect all the data points in one line. I put together this little demo script that replicates the issue. I've read lots of documentation on line and multi_line rendering in bokeh plots, but I just haven't found something that seems to match my simple case. You can run this code & it will automatically open a bokeh server at localhost:5004 ...
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button
from bokeh.layouts import column
import random
def make_document(doc):
# Create a data source
data_source = ColumnDataSource({'step': [], 'strategy': [], 'ncount': []})
# make a list of groups
strategies = ['DD', 'DC', 'CD', 'CCDD']
# Create a figure
fig = figure(title='Streaming Line Plot',
plot_width=800, plot_height=400)
fig.line(x='step', y='ncount', source=data_source)
global step
step = 0
def button1_run():
global callback_obj
if button1.label == 'Run':
button1.label = 'Stop'
button1.button_type='danger'
callback_obj = doc.add_periodic_callback(button2_step, 100)
else:
button1.label = 'Run'
button1.button_type = 'success'
doc.remove_periodic_callback(callback_obj)
def button2_step():
global step
step = step+1
for i in range(len(strategies)):
new = {'step': [step],
'strategy': [strategies[i]],
'ncount': [random.choice(range(1,100))]}
fig.line(x='step', y='ncount', source=new)
data_source.stream(new)
# add on_click callback for button widget
button1 = Button(label="Run", button_type='success', width=390)
button1.on_click(button1_run)
button2 = Button(label="Step", button_type='primary', width=390)
button2.on_click(button2_step)
doc.add_root(column(fig, button1, button2))
doc.title = "Now with live updating!"
apps = {'/': Application(FunctionHandler(make_document))}
server = Server(apps, port=5004)
server.start()
if __name__ == '__main__':
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
My hope was that by looping thru the 4 "strategies" in the example (after clicking button2), I could stream the new data coming out of the simulation into a line plot for that one strategy and step only. But what I get is one line with all four values connected vertically, then one of them connected to the first one at the next step. Here's what it looks like after a few steps:
I noticed that if I move data_source.stream(new) out of the for loop, I get a nice single line plot, but of course it is only for the last strategy coming out of the loop.
In all the bokeh multiple line plotting examples I've studied (not the multi_line glyph, which I can't figure out and which seems to have some issues with the Hover tool), the instructions seem pretty clear: if you want to render a second line, you add another fig.line renderer to an existing figure, and it draws a line with the data provided in source=data_source for this line. But even though my for-loop collects and adds data separately for each strategy, I don't get 4 line plots, I get only one.
Hoping I'm missing something obvious! Thanks in advance.
Seems like you need a line per strategy, not a line per step. If so, here's how I would do it:
import random
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.palettes import Dark2
from bokeh.plotting import figure, ColumnDataSource
from bokeh.server.server import Server
STRATEGIES = ['DD', 'DC', 'CD', 'CCDD']
def make_document(doc):
step = 0
def new_step_data():
nonlocal step
result = [dict(step=[step],
ncount=[random.choice(range(1, 100))])
for _ in STRATEGIES]
step += 1
return result
fig = figure(title='Streaming Line Plot', plot_width=800, plot_height=400)
sources = []
for s, d, c in zip(STRATEGIES, new_step_data(), Dark2[4]):
# Generate the very first step right away
# to avoid having a completely empty plot.
ds = ColumnDataSource(d)
sources.append(ds)
fig.line(x='step', y='ncount', source=ds, color=c)
callback_obj = None
def button1_run():
nonlocal callback_obj
if callback_obj is None:
button1.label = 'Stop'
button1.button_type = 'danger'
callback_obj = doc.add_periodic_callback(button2_step, 100)
else:
button1.label = 'Run'
button1.button_type = 'success'
doc.remove_periodic_callback(callback_obj)
def button2_step():
for src, data in zip(sources, new_step_data()):
src.stream(data)
# add on_click callback for button widget
button1 = Button(label="Run", button_type='success', width=390)
button1.on_click(button1_run)
button2 = Button(label="Step", button_type='primary', width=390)
button2.on_click(button2_step)
doc.add_root(column(fig, button1, button2))
doc.title = "Now with live updating!"
apps = {'/': Application(FunctionHandler(make_document))}
server = Server(apps, port=5004)
if __name__ == '__main__':
server.io_loop.add_callback(server.show, "/")
server.start()
server.io_loop.start()
Thank you, Eugene. Your solution got me back on the right track. I played around with it a bit more and ended up with the following:
import colorcet as cc
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button
from bokeh.layouts import column
import random
def make_document(doc):
# make a list of groups
strategies = ['DD', 'DC', 'CD', 'CCDD']
# initialize some vars
step = 0
callback_obj = None
colors = cc.glasbey_dark
# create a list to hold all CDSs for active strategies in next step
sources = []
# Create a figure container
fig = figure(title='Streaming Line Plot - Step 0', plot_width=800, plot_height=400)
# get step 0 data for initial strategies
for i in range(len(strategies)):
step_data = dict(step=[step],
strategy = [strategies[i]],
ncount=[random.choice(range(1, 100))])
data_source = ColumnDataSource(step_data)
color = colors[i]
# this will create one fig.line renderer for each strategy & its data for this step
fig.line(x='step', y='ncount', source=data_source, color=color, line_width=2)
# add this CDS to the sources list
sources.append(data_source)
def button1_run():
nonlocal callback_obj
if button1.label == 'Run':
button1.label = 'Stop'
button1.button_type='danger'
callback_obj = doc.add_periodic_callback(button2_step, 100)
else:
button1.label = 'Run'
button1.button_type = 'success'
doc.remove_periodic_callback(callback_obj)
def button2_step():
nonlocal step
data = []
step += 1
fig.title.text = 'Streaming Line Plot - Step '+str(step)
for i in range(len(strategies)):
step_data = dict(step=[step],
strategy = [strategies[i]],
ncount=[random.choice(range(1, 100))])
data.append(step_data)
for source, data in zip(sources, data):
source.stream(data)
# add on_click callback for button widget
button1 = Button(label="Run", button_type='success', width=390)
button1.on_click(button1_run)
button2 = Button(label="Step", button_type='primary', width=390)
button2.on_click(button2_step)
doc.add_root(column(fig, button1, button2))
doc.title = "Now with live updating!"
apps = {'/': Application(FunctionHandler(make_document))}
server = Server(apps, port=5004)
server.start()
if __name__ == '__main__':
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
Result is just what I was looking for ...

Is there a way to use a MultiSelect in Bokeh to choose which channel of streaming data is plotted?

I'm putting together a bokeh server to collect multiple streams of data, and provide a live plot of whichever channel the user selects in a MultiSelect menu. I have the streaming bit working, but I'm not sure how to select which stream is displayed in the figure that I've added to the layout.
I've tried using curdoc().remove_root() to remove the current layout and then add a new one, but that just kills the app and the new layout doesn't show up. I've also tried to simply update the figure, but that also just kills the app.
from bokeh.layouts import column
from bokeh.plotting import figure,curdoc
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import MultiSelect
def change_plot(attr,old,new):
global model,selector,p,source
curdoc().remove_root(mode)
p = figure()
p.circle(x=new+'_x',y=new+'_y',source=source)
model = column(selector,p)
curdoc().add_root(model)
def update_plot():
newdata = {}
for i in range(10):
# the following two lines would nominally provide real data
newdata[str(i)+'_x'] = 1
newdata[str(i)+'_y'] = 1
source.stream(newdata,100)
selector = MultiSelect(title='Options',value=[str(i) for i in range(10)])
selector.on_change('value',change_plot)
data = {}
for i in range(10):
data[str(i)+'_x'] = 0
data[str(i)+'_y'] = 0
source = ColumnDataSource(data=data)
p = figure()
p.circle(x='0_x',y='0_y',source=source)
curdoc().add_root(model)
curdoc().add_periodic_callback(update_plot,100)
I run this code using bokeh serve --show app.py, and I would've expected it to create a new plot every time the MultiSelect is updated, but instead, it just crashes somewhere in the change_plot callback.
In this code selecting a line in MultiSelect adds a new line if it was not in the canvas and starts streaming or just toggles streaming if the line already was in the canvas. Code works for Bokeh v1.0.4. Run with bokeh serve --show app.py
from bokeh.models import ColumnDataSource, MultiSelect, Column
from bokeh.plotting import figure, curdoc
from datetime import datetime
from random import randint
from bokeh.palettes import Category10
lines = ['line_{}'.format(i) for i in range(10)]
data = [{'time':[], item:[]} for item in lines]
sources = [ColumnDataSource(item) for item in data]
plot = figure(plot_width = 1200, x_axis_type = 'datetime')
def add_line(attr, old, new):
for line in new:
if not plot.select_one({"name": line}):
index = lines.index(line)
plot.line(x = 'time', y = line, color = Category10[10][index], name = line, source = sources[index])
multiselect = MultiSelect(title = 'Options', options = [(i, i) for i in lines], value = [''])
multiselect.on_change('value', add_line)
def update():
for line in lines:
if line in multiselect.value:
if plot.select({"name": line}):
sources[lines.index(line)].stream(eval('dict(time = [datetime.now()], ' + line + ' = [randint(5, 10)])'))
curdoc().add_root(Column(plot, multiselect))
curdoc().add_periodic_callback(update, 1000)
Result:

Categories

Resources