I want to update a figure multiple times within a single Python callback. As a simple example, say after clicking on a button, I want to change the coordinates of a line 10 times, each time displaying the changed line for a short time. You can call that an animation if you like.
In the Bokeh documentation, I only found this.
Here is a non-working example, illustrating what I want:
from time import sleep
from bokeh.plotting import curdoc, figure
from bokeh.models import ColumnDataSource, Button
from bokeh.layouts import column
from bokeh.events import ButtonClick
source = ColumnDataSource(data=dict(x=[0, 1], y=[0, 0]))
doc = curdoc()
def button_pushed():
for i in range(10):
source.data = dict(x=[0, 1], y=[i, i])
sleep(0.5)
p = figure(plot_width=600, plot_height=300)
p.line(source=source, x='x', y='y')
button = Button(label='Draw')
button.on_event(ButtonClick, lambda: button_pushed())
doc.add_root(column(button, p))
With the above code as a Bokeh app, the line is updated only once after the callback is executed completely.
you can use asyncio to do this. Do your calculation in loop() and then use
doc.add_next_tick_callback() to update the data source.
from functools import partial
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Button
from bokeh.layouts import column
from bokeh.events import ButtonClick
from tornado.ioloop import IOLoop
import asyncio
def view(doc):
source = ColumnDataSource(data=dict(x=[0, 1], y=[0, 0]))
def update_source(new_data):
source.data = new_data
async def loop():
for i in range(10):
doc.add_next_tick_callback(partial(update_source, dict(x=[0, 1], y=[i, i**2])))
await asyncio.sleep(0.5)
def button_pushed():
IOLoop.current().spawn_callback(loop)
p = figure(plot_width=600, plot_height=300)
p.line(source=source, x='x', y='y')
button = Button(label='Draw')
button.on_event(ButtonClick, button_pushed)
doc.add_root(column(button, p))
show(view)
Related
I'm trying to update Slope y_intercept from another thread in bokeh server. But I can not trigger js_on_change, it seems the javascript code is not even generated. Could someone let me what's wrong here? This is the whole code (updated to use add_periodic_callback()):
#!/usr/bin/env python3
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slope, CustomJS
from bokeh.plotting import figure
from bokeh.server.server import Server
import pandas as pd
source = ColumnDataSource()
n = 0
def update():
global n
source.data = pd.DataFrame({"y": [n]})
print(f"{source.data=}")
n += 1
def bkapp(doc):
plot = figure(y_range=(-10, 10))
plot.line([-10, 10], [-10, 10])
slope = Slope(gradient=0, y_intercept=0, line_color='black')
plot.add_layout(slope)
source.js_on_change('data', CustomJS(
args=dict(ds=source, slopes=slope), code="""
console.log("whereismycode");
slope.y_intercept = ds.data["y"][0];
"""
))
doc.add_root(plot)
doc.add_periodic_callback(update, 5000)
server = Server({'/': bkapp}, port=5103, num_procs=1)
server.start()
if __name__ == '__main__':
print(f'address = {server.address}, port = {server.port}')
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
Purely based on the question, exactly as presented above, no CDS is necessary at all in order to update the slope, and adding one is an over-complication. Here is the code I would write:
import pandas as pd
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CustomJS, Slope
from bokeh.plotting import figure
from bokeh.server.server import Server
n = 0
def bkapp(doc):
plot = figure(y_range=(-10, 10))
plot.line([-10, 10], [-10, 10])
slope = Slope(gradient=0, y_intercept=0, line_color='black')
plot.add_layout(slope)
def update():
global n
slope.y_intercept = n
n += 1
doc.add_root(plot)
doc.add_periodic_callback(update, 5000)
server = Server({'/': bkapp}, port=5103, num_procs=1)
server.start()
if __name__ == '__main__':
print(f'address = {server.address}, port = {server.port}')
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
As an aside, note that if you expect at all to have more than one user/session at a time, then there cannot be any "global" Bokeh models (e.g. like the global CDS in your case). The bkapp function must return an entirely unique and new set of objects for each session.
This is the working code. as Tim Roberts pointed out, the issue is that bokeh doc doesn't know about the data source. A hacky solution is to attached it to a hidden glyph. #bigreddot, maybe there's a special method just for this purpose ?
#!/usr/bin/env python3
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slope, CustomJS, DataTable
from bokeh.plotting import figure
from bokeh.server.server import Server
from bokeh.layouts import column
import pandas as pd
source = ColumnDataSource()
n = 0
def update():
global n
source.data = pd.DataFrame({"y": [n]})
print(f"{source.data=}")
n += 1
def bkapp(doc):
plot = figure(y_range=(-10, 10))
plot.line([-10, 10], [-10, 10])
slope = Slope(gradient=0, y_intercept=0, line_color='black')
plot.add_layout(slope)
table = DataTable(source=source, visible=False)
source.js_on_change('data', CustomJS(
args=dict(ds=source, slope=slope), code="""
console.log("whereismycode");
slope.y_intercept = ds.data["y"][0];
"""
))
doc.add_root(column([plot, table]))
doc.add_periodic_callback(update, 5000)
server = Server({'/': bkapp}, port=5103, num_procs=1)
server.start()
if __name__ == '__main__':
print(f'address = {server.address}, port = {server.port}')
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
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]))
I want Bokeh to update periodically and arbitrarily when the results from a separate algorithm running in python returns results, not based on any input from the Bokeh interface.
I've tried various solutions but they all depend on a callback to a some UI event or a periodic callback as in the code below.
import numpy as np
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Plot, LinearAxis, Grid
from bokeh.models.glyphs import MultiLine
from time import sleep
from random import randint
def getData(): # simulate data acquisition
# run slow algorith
sleep(randint(2,7)) #simulate slowness of algorithm
return dict(xs=np.random.rand(50, 2).tolist(), ys=np.random.rand(50, 2).tolist())
# init plot
source = ColumnDataSource(data=getData())
plot = Plot(
title=None, plot_width=600, plot_height=600,
min_border=0, toolbar_location=None)
glyph = MultiLine(xs="xs", ys="ys", line_color="#8073ac", line_width=0.1)
plot.add_glyph(source, glyph)
xaxis = LinearAxis()
plot.add_layout(xaxis, 'below')
yaxis = LinearAxis()
plot.add_layout(yaxis, 'left')
plot.add_layout(Grid(dimension=0, ticker=xaxis.ticker))
plot.add_layout(Grid(dimension=1, ticker=yaxis.ticker))
curdoc().add_root(plot)
# update plot
def update():
bokeh_source = getData()
source.stream(bokeh_source, rollover=50)
curdoc().add_periodic_callback(update, 100)
This does seem to work, but is this the best way to go about things? Rather than having Bokeh try to update every 100 milliseconds can I just push new data to it when it becomes available?
Thanks
You can use zmq and asyncio to do it. Here is the code for the bokeh server, it wait data in an async coroutine:
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, curdoc
from functools import partial
from tornado.ioloop import IOLoop
import zmq.asyncio
doc = curdoc()
context = zmq.asyncio.Context.instance()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:1234")
socket.setsockopt(zmq.SUBSCRIBE, b"")
def update(new_data):
source.stream(new_data, rollover=50)
async def loop():
while True:
new_data = await socket.recv_pyobj()
doc.add_next_tick_callback(partial(update, new_data))
source = ColumnDataSource(data=dict(x=[0], y=[0]))
plot = figure(height=300)
plot.line(x='x', y='y', source=source)
doc.add_root(plot)
IOLoop.current().spawn_callback(loop)
to send the data just run following code in another python process:
import time
import random
import zmq
context = zmq.Context.instance()
pub_socket = context.socket(zmq.PUB)
pub_socket.bind("tcp://127.0.0.1:1234")
t = 0
y = 0
while True:
time.sleep(1.0)
t += 1
y += random.normalvariate(0, 1)
pub_socket.send_pyobj(dict(x=[t], y=[y]))
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:
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)