Can I update a bokeh plot without callbacks from the server? - python

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

Related

Bokeh: Failed to trigger js_on_change on ColumnDataSource from doc.add_periodic_callback()

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

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

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

Bokeh: Update figure multiple times in a single Python callback

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)

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