Looping an animation using Bokeh - python

I'm new to Bokeh and Python, and this is my first Stack Overflow question as well.
I'm using Bokeh to plot trajectory profiles of particles diffusing in the brain, but have it be animated. I have been able to successfully create a program that plots the points, but once all the points are plotted, it stops. I want to be able to loop the animation so that once all the points are plotted, it clears itself and starts over.
I am still very unfamiliar with coding terms, and I wasn't able to find something that could do this. I thought I was on the right track with importing using the reset function inside an if statement, but it doesn't seem to work. I have looked at the following as well for reference:
How to animate a circle using bokeh
Here is my code so far plotting a random trajectory:
import numpy as np
from bokeh.plotting import figure, show, gridplot, vplot, hplot, curdoc
from bokeh.io import output_notebook
from bokeh.client import push_session
from bokeh.core.state import State as new
# This is where the actual coding begins.
b = np.random.rand(300, 3)
xlist = b[:, 1]
ylist = b[:, 2]
# create a plot and style its properties. Change chart title here.
p = figure(title='PEG_PLGA15k_F68_R2_P81', title_text_font_size='13pt',
x_range=(min(xlist), max(xlist)), y_range=(min(ylist), max(ylist)),)
# add a text renderer to out plot (no data yet)
r = p.line(x=[], y=[], line_width=3, color='navy')
session = push_session(curdoc())
i = 0
ds = r.data_source
# create a callback that will add a number in a random location
def callback():
global i
ds.data['x'].append(xlist[i])
ds.data['y'].append(ylist[i])
ds.trigger('data', ds.data, ds.data)
if i < xlist.shape[0] - 1:
i = i + 1
else:
new.reset()
# Adds a new data point every 67 ms. Change at user's discretion.
curdoc().add_periodic_callback(callback, 67)
session.show()
session.loop_until_closed()

If all you want is to restart the animation once you reach some condition (like "all points have been plotted") you can just reset the DataSource. So, for instance, on your example you should have:
else:
i = 0
ds.data['x'] = []
ds.data['y'] = []
instead of:
else:
new.reset()
and that should do the trick. Just use your datasource... State is a more general component that should be used on different level and not to manage plot glyphs and datasources.
A couple of quick notes here:
On your question you've mentioned a link to the 0.10 version documentation but from your code I can tell you are not using a newer version (0.11.x). Always be sure to use the right docs for the version of Bokeh you are using since there might be a few changes between one version and another before the project reach 1.0.
You don't need to call ds.trigger('data', ds.data, ds.data) since bokeh property system will automatically detect your changes to the datasource fields inside your callback
You are designing/running your script as a bokeh script that uses a client session to the server (so you'll have a running instance of bokeh server somewhere and your script communicates with it). I'd suggest you to consider running your code as a Bokeh App instead, so your session and your code run inside the bokeh server instance. You can see more details about the difference at the bokeh server section on the official docs.

Related

How do I use `add_next_tick_callback` for a Panel-wrapped Bokeh figure?

I have a Bokeh figure in a notebook (VS Code), which I would like to update when the x_range is changed. To enable callbacks from JS to Python, I wrap the figure in a Panel panel:
from bokeh.plotting import figure, curdoc
import panel as pn
pn.extension(comms='vscode')
fig = figure()
fig.circle([1, 2, 3], [4, 5, 6])
tai = pn.widgets.input.TextAreaInput(sizing_mode='stretch_both')
panel = pn.Row(pn.pane.Bokeh(fig), tai)
def change_callback(attr, old, new):
tai.value += f'{fig.x_range.start}, {fig.x_range.end}\n'
fig.x_range.on_change('start', change_callback)
fig.x_range.on_change('end', change_callback)
panel
In this example, instead of actually updating the figure, I have an additional TextAreaInput to log callback events. This is what it looks like:
The problem is that every change of x_range leads to four events, and the figure update might take a second. (Two events make sense because I have two callbacks, but why is every event sent twice?) I would therefore like to prevent the update from being performed four times.
Looking at the Bokeh documentation, there is a method add_next_tick_callback. The idea would be to add this callback whenever an event occurs, but also remove an old callback if it exists. Something like
ntc = None
def tick_callback():
tai.value += f'{fig.x_range.start}, {fig.x_range.end}\n'
def change_callback(attr, old, new):
global ntc
doc = curdoc()
if ntc is not None:
doc.remove_next_tick_callback(ntc)
ntc = doc.add_next_tick_callback(tick_callback)
tai.value += f'{ntc}\n'
fig.x_range.on_change('start', change_callback)
fig.x_range.on_change('end', change_callback)
The figure update would then be performed in tick_callback.
This doesn't work. change_callback still gets called and creates a series of NextTickCallback objects, but tick_callback never gets called.
I thought this might be due to curdoc() not returning the correct Bokeh document in the context of Panel. There is also fig.document, which however gives the same document as curdoc(). And there is pn.state which has an add_periodic_callback method, but not add_next_tick_callback.
How do I use add_next_tick_callback for a Panel-wrapped Bokeh figure?

Bokeh Select Widget to Update Plot

I am trying to build a grid plot that updates based on value selected from 'Select' widget using Bokeh.
The graph works but there is no interaction between the widget and the graph. I am not sure how to do this. The goal is to use the 'Select' to update dfPlot then follow the remaining steps.
Here is what i have so far:
output_file('layout.html')
select = Select(title="Option:", options= list(dfExpense['Ident'].unique()), value= "VALUE")
def update_plot(attr, old, new):
dfPlot = dfExpense[dfExpense['Ident'] == select.value]
select.on_change('value', update_plot)
d = []
for x in dfPlot['Account'].unique():
d.append(f's_{x}')
plt = []
for i, x in enumerate(dfPlot['Account'].unique()):
dftemp = dfPlot[dfPlot['Account']==gl]
source1 = ColumnDataSource(dftemp)
d[i] = figure(plot_width = 250, plot_height = 250)
d[i].circle('X', 'Amount', source = source1)
plt.append(d[i])
grid= gridplot([i for i in plt], ncols = 6)
l = row(grid, select)
show(l)
curdoc().add_root(l)
Thanks!
Someone else will probably give you a better answer. I'll just say, I think you might be doing things completely wrong for what you are trying to do (I did the same thing when starting to work with Bokeh).
My understanding after a bit of experience with Bokeh, as it relates to your problem, is as follows:
Using curdoc to make an interactive widget based Bokeh plot means you are using Python to interact with the plot, meaning that you must use a Bokeh server, not just use a .html file. (as a corollary, you won't be using show or output file) https://docs.bokeh.org/en/latest/docs/user_guide/server.html
You can still make a standalone .html file and make it have interactive widgets like sliders, but you will have to write some Javascript. You'll most likely want to do this by utilizing CustomJS within Bokeh, which makes it relatively easy.
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
I had a similar problem, wanting interactivity without using a Python Bokeh server. CustomJS ended up serving my needs quite well, and even though I'm a novice at Javascript, they make it pretty easy (well, especially if your problem is similar to the examples, it can get tricky otherwise but still not very hard).

Updating bokeh plot with output of another script

My goal is to create a bokeh script that shows the processor and memory usage of several machines in my network. The first script pulls the cpu and memory usage, and the bokeh script shows a time series plot of these stats over the last few seconds.
I have copy pasted some bokeh code (thanks internet) that updates two plots in a line based on random numbers every 500 seconds:
import numpy as np
from bokeh.plotting import figure, curdoc
from bokeh.driving import linear
from bokeh.layouts import layout
import random
tools = 'pan'
#linear()
def update(step):
# Instead of random numbers, fetch stats from another script
ds1.data['x'].append(step)
ds1.data['y'].append(random.randint(0,100))
ds2.data['x'].append(step)
ds2.data['y'].append(random.randint(0,100))
ds1.trigger('data', ds1.data, ds1.data)
ds2.trigger('data', ds2.data, ds2.data)
# don't get this yet, but #linear is a decorator. Instead of having #linear(), we could also have
# update = linear(update)
p = figure(plot_width=400, plot_height=400)
r1 = p.line([], [], color="firebrick", line_width=2, legend='line1')
r2 = p.line([], [], color="navy", line_width=2, legend='line2')
ds1 = r1.data_source
ds2 = r2.data_source
curdoc().add_root( p )
# Add a periodic callback to be run every 500 milliseconds
curdoc().add_periodic_callback(update, 500)
Let's assume the first script pulls tuples of memory/cpu usage every 500 ms, how would I get the bokeh script to fetch that information? Once I have that information, I'll be set.
The only idea I have right now is to write the output of the first script to a dict/json/h5 file, and have the bokeh script read it and append that data to the plot. I'm wondering if there is a better way.
Since the update script is just grabbing some numbers, your best bet is probably wrapping that thing inside an update function, processing those numbers so they fit a ColumnDataSource, then update the main CDS, which will then update the plot. Something like:
def update():
# Do stuff to get new data
numbers = get_usage()
# Process new data into a CDS friendly format
data1, data2 = process_numbers(numbers)
# Update plot_source, this being whatever source is
r1_source.data = data1
r2_source.data = data2
curdoc().add_periodic_callback(update, 500)

Bokeh real time update x_axis after refresh

From the last days, I have been trying to use Bokeh to plot real-time data and display on a .html in order to be embeed in a webpage. I have sucessuly adapted one of the bokeh examples to my needs. I am using a buffer of 50 elements on the plot and I am noting the following behaviour:
1) In case I run the script and go to the browser the x_range fully adapts to incomming data and everything works correctly
2) If I click on "Refresh" on the browser the x_range stops to adapt to incoming data and freezes to the last value.
I tried to force the x_axis to initial and end values but the visualization behaves poorly.
I think I am not correctly understanding what does the "Refresh" hit impacts my code and how I can workaround this issue.
""" To view this example, first start a Bokeh server:
bokeh serve --allow-websocket-origin=localhost:8000
And then load the example into the Bokeh server by
running the script:
python animated.py
in this directory. Finally, start a simple web server
by running:
python -m SimpleHTTPServer (python 2)
or
python -m http.server (python 3)
in this directory. Navigate to
http://localhost:8000/animated.html
"""
from __future__ import print_function
import io
from numpy import pi, cos, sin, linspace, roll
from bokeh.client import push_session
from bokeh.embed import server_session
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource
fa = open('Accelerometer.txt', 'r')
source = ColumnDataSource(data=dict(x=[], y=[]))
fg = figure(width=250, plot_height=250, title="RT-Test")
fg.line(x='x', y='y', color="olive", source=source)
fg.x_range.follow = "end"
# Visualization scale and aesthetics
fg.xgrid.grid_line_color = None
fg.ygrid.grid_line_color = None
fg.background_fill_color = "snow"
# add the plot to curdoc
curdoc().add_root(fg)
# open a session which will keep our local doc in sync with server
session = push_session(curdoc())
html = """
<html>
<head></head>
<body>
%s
</body>
</html>
""" % server_session(fg, session_id=session.id, relative_urls=False)
with io.open("animated.html", mode='w+', encoding='utf-8') as f:
f.write(html)
print(__doc__)
def update():
line = fa.readline().split(',')
x = float(line[0])
y = float(line[1])
print(x, y)
# construct the new values for all columns, and pass to stream
new_data = dict(x=[x], y=[y])
source.stream(new_data, rollover=50)
curdoc().add_periodic_callback(update, 100)
session.loop_until_closed() # run forever
This kind of usage of the Bokeh server, with the actual code running in a separate process and calling session.loop_until_closed, is discouraged in the strongest terms. In the next release, all of the examples of this sort will be deleted, and mentions of this approach removed from the docs. This usage is inherently inferior in many ways, as outlined here, and I would say that demonstrating it so prominently for so long was a mistake on our part. It is occasionally useful for testing, but nothing else.
So what is the good/intended way to use the Bokeh server? The answer is to have a Bokeh app run in the Bokeh server itself, unlike the code above. This can be done in a variety of ways, but one common way os to wirte a simple script, then execute that script with
bokeh serve -show myapp.py
I don't have access to your "Accelerate.py" dataset, but a rough pass at updating your code would look like:
# myapp.py
from numpy import pi, cos, sin, linspace, roll
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource
fa = open('Accelerometer.txt', 'r')
source = ColumnDataSource(data=dict(x=[], y=[]))
fg = figure(width=250, plot_height=250, title="RT-Test")
fg.line(x='x', y='y', color="olive", source=source)
fg.x_range.follow = "end"
fg.xgrid.grid_line_color = None
fg.ygrid.grid_line_color = None
fg.background_fill_color = "snow"
curdoc().add_root(fg)
def update():
line = fa.readline().split(',')
x = float(line[0])
y = float(line[1])
# construct the new values for all columns, and pass to stream
new_data = dict(x=[x], y=[y])
source.stream(new_data, rollover=50)
curdoc().add_periodic_callback(update, 100)
Now if you run this script with the bokeh serve command, then any refresh will get you a brand new session of this app. It's also worth nothing that code written in this way is considerably simpler and shorter.
These kinds of apps can be embedded in Jupyter notebooks, Flask and other web-apps, or made into "regular" python scripts run with python instead of bokeh serve. For more information see Running a Bokeh Server in the User's Guide.

Selecting Glyphs in Python Bokeh plots

I have a Python Bokeh plot containing multiple lines, Is there a way I can interactively switch some of these lines on and off?
p1.line(Time,Temp0,size=12,color=getcolor())
p1.line(Time,Temp1,size=12,color=getcolor())
p1.line(Time,Temp2,size=12,color=getcolor())
p1.line(Time,Temp3,size=12,color=getcolor())
....
show(p1)
I just came across this problem myself in a similar scenario. In my case, I also wanted to do other operations on it.
There are 2 possible approaches:
1.) Client-server approach
2.) Client only approach
1.) Client Server Approach ak Bokeh Server
One way how you can achieve this interactivity is by using the bokeh server which you can read more about here. I will describe this way in more detail since at this point, I am a bit more familiar with it.
Going by your example above, if I were to use the bokeh serve, I would first setup a ColumnDataSource like so:
source = ColumnDataSource(data = dict(
time = Time,
temp0 = [],
temp1 = [],
temp2 = [],
temp3 = [],
)
Next I would setup a widget that allows you to toggle what temperatures to show:
multi_select = MultiSelect(title="Option:", value=["Temp1"],
options=["Temp1", "Temp2", "Temp3"])
# Add an event listener on the python side.
multi_select.on_change('value', lambda attr, old, new: update())
Then I would define the update function like below. The purpose of the update function is to update the ColumnDataSource (which was previously empty) with values you want to populate in the graph now.
def update():
"""This function will syncronize the server data object with
your browser data object. """
# Here I retrieve the value of selected elements from multi-select
selection_options = multi_select.options
selections = multi_select.value
for option in selection_options:
if option not in selections:
source.data[option] = []
else:
# I am assuming your temperatures are in a dataframe.
source.data[option] = df[option]
The last thing to do is to redefine how you plot your glyphs. Instead of drawing from lists, or dataframes, we will draw our data from a ColumnDataSource like so:
p1.line("time","temp0", source=source, size=12,color=getcolor())
p1.line("time","temp1", source=source, size=12,color=getcolor())
p1.line("time","temp2", source=source, size=12,color=getcolor())
p1.line(Time,Temp3, source=source, size=12,color=getcolor())
So basically by controlling the content of the ColumnDataSource which is synchronized with the browser object, I can toggle whether data points are shown or not. You may or may not need to define multiple ColumnDataSources. Try it out this way first.
2.) Client only approach ak Callbacks
The approach above uses a client-server architecture. Another possible approach would be to do this all on the front-end. This link shows how some simple interactions can be done completely on the browser side via various forms of callbacks.
Anyway, I hope this is helpful. Cheers!
The question is from some time back but Bokeh now has the interactive legend functionality - you can just specify
your_figure.legend.click_policy = 'hide'
And this makes legend while listing your lines interactive and you can switch each line on/off

Categories

Resources