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))
Related
I am taking my first steps at creating a bokeh interactive visualization app, and I am trying to create a few dropdown menus for data selection.
Before implementing it on my own data I've tried reproducing widgets from the Bokeh tutorial.
My code
from math import pi
import pandas as pd
from bokeh.palettes import Category20c
from bokeh.plotting import figure
from bokeh.transform import cumsum
from bokeh.io import show, output_file, output_notebook, curdoc
from bokeh.models import ColumnDataSource, Select
from bokeh.layouts import widgetbox
from bokeh.resources import INLINE
import bokeh.io
bokeh.io.output_notebook(INLINE)
# Create two dropdown Select widgets: select1, select2
select1 = Select(title="First", options=["A", "B"], value="A")
select2 = Select(title="Second", options=["1", "2", "3"], value="1")
# Define a callback function: callback
def callback(attr, old, new):
# If select1 is 'A'
if select1.value == "A":
# Set select2 options to ['1', '2', '3']
select2.options = ["1", "2", "3"]
# Set select2 value to '1'
select2.value = "1"
else:
# Set select2 options to ['100', '200', '300']
select2.options = ["100", "200", "300"]
# Set select2 value to '100'
select2.value = "100"
# Attach the callback to the 'value' property of select1
select1.on_change("value", callback)
# Create layout and add to current document
layout = widgetbox(select1, select2)
curdoc().add_root(layout)
After excepting the code all I get is a warning, but I see nothing.
What am I doing wrong?
Thank you
p.s.
I know I am calling a lot more then I need, I would use all packages later on, or just remove them.
You can replace WidgetBox with Column to avoid the warning, but the real problem is that you don't display anything.
Add "show(layout)" at the end of the notebook if you want to display it.
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))
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]))
import numpy as np
import pandas as pd
from bokeh.plotting import figure, gridplot
from bokeh.io import output_file, show, curdoc
from bokeh.models.widgets import FileInput, DataTable, DateFormatter, TableColumn
from pybase64 import b64decode
from bokeh.models import ColumnDataSource
import io
# output to static HTML file
output_file("./OUTPUT/001_koreksi.html", title='Koreksi Gayaberat')
def upload_fit_data(attr, old, new):
print("fit data upload succeeded")
decoded = b64decode(new)
f = io.BytesIO(decoded)
new_df = pd.read_excel(f, sheet_name='data')
# print(new_df)
data = dict(utmx=new_df['UTM X'],
utmy=new_df['UTM Y'],
elev=new_df['Elevasi'],
lat=new_df['Latitude'],
lon=new_df['Longitude'],
ta=new_df['Tinggi Alat'],
N=new_df['N'],
E=new_df['E'],
S=new_df['S'],
W=new_df['W'],
time=new_df['Time'],
kt=new_df['Koreksi Tide'],
rdgrav=new_df['Bacaan'])
source = ColumnDataSource(data)
columns = [TableColumn(field="utmx", title="UTM X"), TableColumn(field="utmy", title="UTM Y"),
TableColumn(field="elev", title="Elevasi"), TableColumn(field="lat", title="Latitude"),
TableColumn(field="lon", title="Longitude"), TableColumn(field="ta", title="Tinggi Alat"),
TableColumn(field="N", title="North"), TableColumn(field="E", title="East"),
TableColumn(field="S", title="South"), TableColumn(field="W", title="West"),
TableColumn(field="time", title="Waktu Pengukuran"), TableColumn(field="kt", title="Koreksi tide"),
TableColumn(field="rdgrav", title="Bacaan Alat")]
data_table = DataTable(source=source, columns=columns, width=500, height=300)
global data_table
# return data_table
# data_table = DataTable(source=source, columns=columns, width=500, height=300)
file_input = FileInput(accept=".xlsx")
file_input.on_change('value', upload_fit_data)
p = gridplot([file_input, data_table])
doc = curdoc()
doc.add_root(p)
Question: how to use and show "data_table" value in bokeh application? if i run this code, in bokeh interface the "data_table" not show
Apart from the issues mentioned in the comment to your question by Z4-tier, there are others:
output_file will not work with bokeh serve - and bokeh serve is exactly what you need given that you want to run Python code in response to users' actions in the web UI
gridplot accepts a list of tuples that specify not only the items themselves but also where to put each item. Since you have only two widgets, column should be sufficient
Consider this working code:
import io
from base64 import b64decode
import pandas as pd
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import FileInput, DataTable, TableColumn
column_map = dict(utmx='UTM X', utmy='UTM Y', elev='Elevasi',
lat='Latitude', lon='Longitude', ta='Tinggi Alat',
N='N', E='E', S='S', W='W', time='Time',
kt='Koreksi Tide', rdgrav='Bacaan')
source = ColumnDataSource(data={c: [] for c in column_map})
columns = [TableColumn(field="utmx", title="UTM X"), TableColumn(field="utmy", title="UTM Y"),
TableColumn(field="elev", title="Elevasi"), TableColumn(field="lat", title="Latitude"),
TableColumn(field="lon", title="Longitude"), TableColumn(field="ta", title="Tinggi Alat"),
TableColumn(field="N", title="North"), TableColumn(field="E", title="East"),
TableColumn(field="S", title="South"), TableColumn(field="W", title="West"),
TableColumn(field="time", title="Waktu Pengukuran"), TableColumn(field="kt", title="Koreksi tide"),
TableColumn(field="rdgrav", title="Bacaan Alat")]
data_table = DataTable(source=source, columns=columns, width=500, height=300)
def upload_fit_data(attr, old, new):
f = io.BytesIO(b64decode(new))
new_df = pd.read_excel(f, sheet_name='data')
source.data = {ds_c: new_df[df_c] for ds_c, df_c in column_map.items()}
file_input = FileInput(accept=".xlsx")
file_input.on_change('value', upload_fit_data)
curdoc().add_root(column(file_input, data_table))
Note that if you don't want to have an interactive web page and just want to have a static HTML file, then you cannot use any Python callbacks. You will have to specify the XLSX file via the CLI and process the data right when the script runs.
When I use a DataTable in my bokeh application, the plots on my page become decativated whenever I click inside the datatable. This happens with editable=True and editable=False.
The only way that I found to activate the plot again is to click the "Reset" tool button.
Questions:
Am I doing something wrong in my Bokeh app?
Has anyone else encountered this, and found a workaround? (a Javascript workaround, perhaps?)
Screen shot (notice the deactivated color in the plot):
The complete bokeh app is below, run with bokeh serve --show filename.py:
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import bokeh
from bokeh.layouts import widgetbox, row, layout
from bokeh.models import ColumnDataSource, Button, DataTable, TableColumn, Paragraph
from bokeh.models.widgets.tables import DateFormatter, StringEditor
from bokeh.plotting import curdoc, figure
def create_dataframe(number_of_series, number_of_values):
t0 = datetime(2017, 1, 1)
data = {
"timestamp": [t0 + (i * timedelta(seconds=3600*2)) for i in range(number_of_values)]
}
data.update({
"col{}".format(i): np.random.rand(number_of_values)
for i in range(number_of_series)
})
return pd.DataFrame(data).set_index("timestamp")
source = ColumnDataSource(create_dataframe(10, 1000))
btn = Button(label="Click Me")
my_figure = figure(x_axis_type="datetime")
my_figure.line(y="col0", x="timestamp", source=source)
data_table = DataTable(editable=True, source=source, columns=[
TableColumn(title="Timestamp", field="timestamp", formatter=DateFormatter(format="%Y-%m-%d %H:%M:%S"), editor=StringEditor()),
TableColumn(title="col0", field="col0")
])
page_layout = layout([
[widgetbox(Paragraph(text="Bokeh version: {}".format(bokeh.__version__)))],
[widgetbox(btn), my_figure],
[data_table]
])
curdoc().add_root(page_layout)
This is actually the intended behaviour behind using a bokeh ColumnDataSource. When you select a row in the table, that is registered in the data source selected attribute. see
https://docs.bokeh.org/en/latest/docs/user_guide/data.html#columndatasource
You cant notice this at first because there are a large number of data points on the plot - any one isnt visible when selected.Try shift click and select multiple rows, and you will see segments of the plot turn dark blue - corresponding to those rows.
The simplest way to prevent the behaviour is to use seperate data sources.
If you wanted them to be the same, you would have to restore the source.selected dictionary each time it is updated, which seems pointless.
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import bokeh
from bokeh.layouts import widgetbox, row, layout
from bokeh.models import ColumnDataSource, Button, DataTable, TableColumn, Paragraph
from bokeh.models.widgets.tables import DateFormatter, StringEditor
from bokeh.plotting import curdoc, figure
def create_dataframe(number_of_series, number_of_values):
t0 = datetime(2017, 1, 1)
data = {
"timestamp": [t0 + (i * timedelta(seconds=3600*2)) for i in range(number_of_values)]
}
data.update({
"col{}".format(i): np.random.rand(number_of_values)
for i in range(number_of_series)
})
return pd.DataFrame(data).set_index("timestamp")
source = ColumnDataSource(create_dataframe(10, 1000))
sourcetable = ColumnDataSource(create_dataframe(10, 1000))
btn = Button(label="Click Me")
my_figure = figure(x_axis_type="datetime")
my_figure.line(y="col0", x="timestamp", source=source)
data_table = DataTable(editable=True, reorderable=False, source=sourcetable, columns=[
TableColumn(title="Timestamp", field="timestamp", formatter=DateFormatter(format="%Y-%m-%d %H:%M:%S"), editor=StringEditor()),
TableColumn(title="col0", field="col0")
])
page_layout = layout([
[widgetbox(Paragraph(text="Bokeh version: {}".format(bokeh.__version__)))],
[widgetbox(btn), my_figure],
[data_table]
])
curdoc().add_root(page_layout)