Bokeh widget not showing - python

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.

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

Bokeh - How to have the same widget (or duplicate a widget) in two different tabs?

I'm trying to create a widget filter (made up of TextInput and MultiSelect) that is replicated on two different Bokeh Tabs. The desired functionality is that filtering results should be preserved between tabs, regardless of which filter receives the text to filter off of.
The code below(it is working code) builds the Filter widget which is instantiated as filter1 and filter2. The callback is the update function which does the actual filtering and updates the MultiSelect part of the filter.
from bokeh.io import curdoc
from bokeh.layouts import column, widgetbox, row, layout, gridplot
from bokeh.models import Slider, Select, TextInput, MultiSelect
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
multiselect = None
input_box = None
def update(widget, attr, old, new):
print("df['fruits']: {}".format(list(df['fruits'])))
print("{} : {} changed: Old [ {} ] -> New [ {} ]".format(widget, attr, old, new))
if widget == 'input':
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
print("col_date: {}".format(col_data))
multiselect.update(options = sorted(list(col_data)))
def init():
global multiselect
multiselect = MultiSelect(title = 'multiselect',
name = 'multiselect',
value = [],
options = list(df["fruits"]))
multiselect.on_change('value', partial(update, multiselect.name))
global input_box
input_box = TextInput(title = 'input',
name ='input',
value='Enter you choice')
input_box.on_change('value', partial(update, input_box.name))
class Filter:
def __init__(self):
self.multiselect = multiselect
self.input_box = input_box
self.widget = widgetbox(self.input_box, self.multiselect)
init()
filter1 = Filter().widget
filter2 = Filter().widget
curdoc().add_root(row(filter1, filter2))
The code above produces/assembles the widget as shown here:
Also, the functionality of the two mirrored filters is as desired; when text is entered in one of the boxes, the results are displayed on both filters.
Now, and here is where I need help, I want the same filters with the same functionality but I need them in two different tabs; one filter in one tab and the other filter in the other tab.
The code used to build the two tabs structure is:
p1 = Panel(child = filter1, title = "Panel1")
p2 = Panel(child = filter2, title = "Panel2")
tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))
On the results side, the code preserves the desired functionality but filters are displayed on the same page. More than that, panels/tabs are not even being built.
Any idea what's missing? (If you want to play with the code it should work right off the bat if you have bokeh installed.)
I do not think your example should even build a document, both your textinputs and multiselect models have the same id, which may be why the display of tabs gets messed up.
My solution is similar to HYRY's, but with a more general function to share attributes using two different things:
model.properties_with_values()
Can be used with any bokeh model and returns a dictionary of all the attribute:value pairs of the model. It's mostly useful in ipython to explore bokeh objects and debug
Document.select({'type':model_type})
Generator of all the widgets of the desired type in the document
Then I just filter out the widgets that do not share the same tags as the input widget, which would avoid "syncing" other inputs/multiselect not generated with box_maker(). I use tags because different models cannot have the same name.
When you change a TextInput value, it will change the associated Multiselect in the update function, but it will also change all the other TextInputs and trigger their update in the same way too. So each Input triggers update once and changes the options of their respective multiselect (and not multiplte times each because it's a "on_change" callback, if you give the same value for the new input it does not trigger).
For the Multiselect the first trigger of update will do the job, but since it changed the values of the other Multiselect it still triggers as many times as there are Multiselect widgets.
from bokeh.io import curdoc
from bokeh.layouts import widgetbox
from bokeh.models import TextInput, MultiSelect
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
def sync_attr(widget):
prop = widget.properties_with_values() # dictionary of attr:val pairs of the input widget
for elem in curdoc().select({'type':type(widget)}): # loop over widgets of the same type
if (elem!=widget) and (elem.tags==widget.tags): # filter out input widget and those with different tags
for key in prop: # loop over attributes
setattr(elem,key,prop[key]) # copy input properties
def update(attr,old,new,widget,other_widget):
print("\ndf['fruits']: {}".format(list(df['fruits'])))
print("{} : {} changed: Old [ {} ] -> New [ {} ]".format(str(widget),attr, old, new))
if type(widget)==TextInput:
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
print("col_date: {}".format(col_data))
other_widget.update(options = sorted(list(col_data)))
sync_attr(widget)
def box_maker():
multiselect = multiselect = MultiSelect(title = 'multiselect',tags=['in_box'],value = [],options = list(df["fruits"]))
input_box = TextInput(title = 'input',tags=['in_box'],value='Enter you choice')
multiselect.on_change('value',partial(update,widget=multiselect,other_widget=input_box))
input_box.on_change('value',partial(update,widget=input_box,other_widget=multiselect))
return widgetbox(input_box, multiselect)
box_list = [box_maker() for i in range(2)]
tabs = [Panel(child=box,title="Panel{}".format(i)) for i,box in enumerate(box_list)]
tabs = Tabs(tabs=tabs)
curdoc().add_root(tabs)
Note that the highlighting of the options in multiselect may not look consistent, but that just seems to be visual as the values/options of each of them are changing correctly.
But unless you are particularly attached to the layout look when you put the widgets inside the panels, you could just put one input and multiselect outside, and write their callbacks to deal with what will be in the different panels.
You can't use the same widget model to create multiple views. You can create new widgets in every tabs and link the value:
from bokeh.io import curdoc
from bokeh.layouts import column, widgetbox, row, layout, gridplot
from bokeh.models import Slider, Select, TextInput, MultiSelect, CustomJS
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
class Filter:
def __init__(self):
self.multiselect = MultiSelect(title = 'multiselect',
name = 'multiselect',
value = [],
options = list(df["fruits"]))
self.multiselect.on_change('value', self.selection_changed)
self.input_box = TextInput(title = 'input',
name ='input',
value='Enter you choice')
self.input_box.on_change('value', self.input_box_updated)
self.widget = widgetbox(self.input_box, self.multiselect)
def input_box_updated(self, attr, old, new):
print(attr, old, new)
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
self.multiselect.update(options = sorted(list(col_data)))
def selection_changed(self, attr, old, new):
print(new)
filter1 = Filter()
filter2 = Filter()
def link_property(property_name, *widgets):
wb = widgetbox(*widgets)
wb.tags = [property_name, 0]
def callback(widgets=wb):
if widgets.tags[1] != 0:
return
widgets.tags[1] = 1
for widget in widgets.children:
widget[widgets.tags[0]] = cb_obj.value
widgets.tags[1] = 0
jscallback = CustomJS.from_py_func(callback)
for widget in widgets:
widget.js_on_change(property_name, jscallback)
link_property("value", filter1.input_box, filter2.input_box)
link_property("value", filter1.multiselect, filter2.multiselect)
p1 = Panel(child = filter1.widget, title = "Panel1")
p2 = Panel(child = filter2.widget, title = "Panel2")
tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))
It seems that there is a bug in MultiSelect that doesn't deselect previous items.

Bokeh plot is deactivated when user clicks in DataTable, and there's no proper way to activate the plot

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)

Categories

Resources