Trigger a JavaScript callback upon ColumnDataSource change in a plot - javascript

Is there a way in Bokeh to trigger a JavaScript callback when a ColumnDataSource is updated with new data?
I'm looking for something similar to this...
cds1 = ColumnDataSource(some_pandas_dataframe)
cds1.js_onchange('', callback) # attach some kind of callback here to do something when cds1.data changes.
cds2 = ColumnDataSource(some_pandas_dataframe)
cds1.data.update(cds2.data)

One way to do it in Bokeh v1.1.0 is to attach a callback to the data attribute of ColumnDataSource:
import numpy as np
from bokeh.plotting import Figure, show, curdoc
from bokeh.models import CustomJS, ColumnDataSource, Button, Column
from bokeh.events import Tap
cds = ColumnDataSource(data = dict(x = [0.1, 0.9], y = [0.1, 0.9]))
p = Figure(x_range = (0, 1), y_range = (0, 1), tools = "")
p.scatter(x = 'x', y = 'y', source = cds)
data = dict(x = [0.5], y = [0.5])
callback = CustomJS(args = {'cds': cds, 'data': data}, code = "cds.data = data")
button = Button(label = 'click me')
button.js_on_event('button_click', callback)
cds.js_on_change('data', CustomJS(code = "alert('data_change_detected');"))
show(Column(p, button))

Related

js_on_change not working for network graph on Bokeh

Following this example, I am trying to build a network graph using Bokeh where I can use a select widget to filter my data. My data will look something like this:
source target var1 var2
a c 1.0 0.0
b g 2.0 0.0
c e 3.0 0.0
e a 0.0 1.0
f h 0.0 2.0
And can be recreated using this code:
d = {'weight': [1, 2,3,1,2], 'var': ["var1","var1","var1","var2", "var2"], 'source': ["a", "b","c","e","f"], 'target': ["c","g","e","a","h"]}
df1 = pd.DataFrame(data=d)
df2 = df1.pivot( index= ["source","target"], values = "weight", columns = "var").reset_index()
df2 = df2.fillna(0)
Basically, I want to create the network graph where I can filter the columns (var1, var2) and they will become the weight attribute in my graph. (Filtering for when this weight value is greater than 0.)
To accomplish this I tried the following. But even though the graph renders, when I change the selected value nothing happens. I don't see any errors in the console either. I am not sure what I am doing wrong, probably something in the JS call because I am new to this, but I am trying to reproduce the example as closely as possible and still not sure where im going wrong. Please help!
from bokeh.plotting import from_networkx
from bokeh.plotting import figure, output_file, show
from bokeh.models import Plot, Range1d, MultiLine, Circle, HoverTool,NodesAndLinkedEdges,EdgesAndLinkedNodes, TapTool, BoxSelectTool,ColumnDataSource
from bokeh.models import CustomJS, ColumnDataSource, Select, Column
HOVER_TOOLTIPS = [("Search Term", "#index")]
title = "my title"
plot = figure(tooltips = HOVER_TOOLTIPS,
tools="pan,wheel_zoom",
active_scroll='wheel_zoom',
x_range=Range1d(-10.1, 10.1),
y_range=Range1d(-10.1, 10.1),
title=title, plot_width=1000
)
category_default = "var1"
unique_categories = ["var1","var2"]
subset = df2
subset_data = subset[["var1","var2"]].to_dict("list")
source = ColumnDataSource({
"weight": subset[category_default],
"source": subset.source,
"target": subset.target
})
a = pd.DataFrame(source.data)
G = nx.from_pandas_edgelist(a[a["weight"] >0], edge_attr = "weight")
network_graph = from_networkx(G, networkx.spring_layout, scale=10, center=(0, 0), )
network_graph.edge_renderer.glyph = MultiLine(line_alpha=0.5, line_width="weight" )
select = Select(title='Category Selection', value=category_default, options=unique_categories)
callback = CustomJS(
args={"subset_data": subset_data, "source": source},
code="""
source.data['weight'] = subset_data[cb_obj.value];
source.change.emit();
""")
plot.renderers.append(network_graph)
select.js_on_change("value", callback)
show(Column(plot, select))

How to use Javascript in Bokeh map?

I want to change the name of a variable as I will use it later to specify exactly what I need to plot. However, Javascript is giving me more trouble than I thought. I am using a dropdown menu and as I select a value , the variable should also change, but that is not happening. Any suggestions? I am still very new to Javascript, so any advice would be appreciated
column="justastring"
selecthandler = CustomJS(args=dict(column=column), code="""
var col=column.value;
if (cb_obj.value=="Ozone"){
col='OZONE';
}
if (cb_obj.value=="O2"){
col='O_2';
}
if(cb_obj.value=="DO2"){
col='DO2';
}
column.change.emit();
""")
select.js_on_change('value',selecthandler)
You didn't provide a runnable code so I've created a minimal example from scratch:
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Select, CustomJS
from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis3
ds = ColumnDataSource(dict(x=[0, 1, 2],
a=[0, 0, 1],
b=[3, 2, 1]))
p = figure()
renderer = p.rect(x='x', y=0, width=1, height=1,
fill_color=linear_cmap('a', Viridis3, 0, 1), source=ds)
s = Select(value='a', options=['a', 'b'])
s.js_on_change('value',
CustomJS(args=dict(r=renderer, ds=ds),
code="""
const c = cb_obj.value;
const fc = r.glyph.fill_color;
fc.field = c;
fc.transform.low = Math.min(...ds.data[c]);
fc.transform.high = Math.max(...ds.data[c]);
// If you don't change `low` and `high` fields above, you will
// have to uncomment this line to trigger value change signals
// so that the glyphs are re-rendered.
//r.glyph.fill_color = fc;
"""))
show(column(s, p))

How do I create a loading indicator in bokeh?

I am using a radio button with Bokeh. I want to be able to show a loading indicator when a radio button is clicked and then show that it has finished after the python callback has completed. How do you do this with Bokeh?
I've tried using combinations of js_onchange with onchange for the radio buttons. I just can't come up with a way to notify the JS side of things when the Python callback is completed.
callback = CustomJS(args={}, code="""
document.getElementById('message_display').innerHTML = 'loading...';
""")
radio_button.js_on_change('active', callback)
radio_button.on_change('active', some_other_callback)
When I run it the innerHTML gets set to loading and the on_change Python callback runs and the graph updates but I have no way to trigger a change on the JS side of things change the innerHTML to say done.
Assuming the user already have a plot in view one option would be to set a callback on the start attribute of the plot's range so it will be triggered when the plot gets updated.
from bokeh.models import CustomJS
p = figure()
def python_callback()
p.y_range = Range1d(None, None)
# get your data here and update the plot
code = "document.getElementById('message_display').innerHTML = 'loading finished';"
callback = CustomJS(args = dict(), code = code)
p.y_range.js_on_change('start', callback)
See working example below:
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import CustomJS, ColumnDataSource
points = np.random.rand(50, 2)
cds = ColumnDataSource(data = dict(x = points[:, 0], y = points[:, 1]))
p = figure(x_range = (0, 1), y_range = (0, 1))
p.scatter(x = 'x', y = 'y', source = cds)
cb_to_make_selection = CustomJS(args = {'cds': cds}, code = """
function getRandomInt(max){return Math.floor(Math.random() * Math.floor(max));}
cds.selected.indices = [getRandomInt(cds.get_length()-1)]
""")
p.x_range.js_on_change('start', cb_to_make_selection)
show(p)

Update selection indices of bokeh circle with TapTool

I'm having trouble with a CustomJS callback on the TapTool. I would like to force the selection of the 50 points after the one clicked. Therefore I have made a javascript callback that modify the list of indices selected in the datasource and should update the plot. I can see, with the console, that the datasource is updated but the plot is not.
I have made a test version from the documentation example
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
but it doesn't work neither. Is it because there is a different way to update the plot when the selection is changed ?
Here is the test version I have made :
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, output_file, show
output_notebook()
x = [x*0.005 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=400, plot_height=400)
plot.circle('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
var l_selected=source.selected;
var indices = l_selected['1d'].indices;
if(indices.length <= 1) {
var new_indices = Array.from(new Array(50), (x,i) => i + indices[0]);
l_selected['1d'].indices=new_indices;
}
source.selected=l_selected;
console.log(source.selected)
source.change.emit();
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)
plot.add_tools(TapTool(callback=callback))
layout = column(slider, plot)
show(layout)
I do not know if it can be useful, but I am using the 0.12.16 version of Bokeh and I am trying to make it work in a Jupyter notebook
Seb gave the answer in comments. Since bokeh 0.12.15 source.selected['1d'] became source.selected.indices

How to set properties of selected/unselected glyphs in bokeh

I have a dataset consisting of timeseries of a few observables, and I'd like to use bokeh to look at the phase diagram at different points in the time series. What I want to know is how to change the properties of the selected or unselected glyphs (in this case I'd like to reduce the alpha of the unselected points or change the color of the selected ones).
The code below creates the interface I want in an ipython notebook, and is based on the example in the users guidehttp://docs.bokeh.org/en/latest/docs/user_guide/interaction/linking.html. I can't find any obvious options to set and I'd really rather not have to learn javascript for this one thing.
import numpy as np
from pandas import DataFrame
from bokeh.plotting import figure, output_notebook, show, gridplot
from bokeh.models import ColumnDataSource, widgets
def znzt_ts():#, plot_antisym=False, **kwargs):
t = np.arange(1000)
Eu = np.sin(t * np.pi/10) + np.random.random(1000)
Ez = np.cos(t * np.pi/10) + np.random.random(1000)
ts = DataFrame({'t': t, 'Eu': Eu, 'Ez': Ez})
tools = 'box_zoom,pan,reset,save,box_select'
source = ColumnDataSource(data=ts)
original_source = ColumnDataSource(data=ts)
p1 = figure(plot_width=300, plot_height=300,
tools=tools)
p2 = figure(plot_width=300, plot_height=300, tools=tools,)
p1.circle('t', 'Eu', source=source, size=1)
p2.circle('Eu', 'Ez', source=source, size=1)
return gridplot([[p1, p2]])
gp = znzt_ts()
output_notebook()
show(gp)
This section shows you how to do this:
https://docs.bokeh.org/en/latest/docs/user_guide/styling.html#selected-and-unselected-glyphs
You need to set the selection_glyph and nonselection_glyph of your circle renderers to glyphs with the desired properties.
In the manual, the renderer is accessed by assigning a name in the p.circle(..., name="mycircle") call and then using renderer = p.select(name="mycircle"). You could also save the reference to the renderer when it is returned by the p.circle(...) function: renderer = p.circle('t', 'Eu', source=source, line_color=None).
Once you have the reference to the renderer, you can assign the glyphs:
renderer.selection_glyph = Circle(fill_color='firebrick', line_color=None)
renderer.nonselection_glyph = Circle(fill_color='#1f77b4', fill_alpha=0.1, line_color=None)
import numpy as np
from pandas import DataFrame
from bokeh.plotting import figure, output_notebook, show, gridplot
from bokeh.models import ColumnDataSource, widgets
from bokeh.models.glyphs import Circle
def znzt_ts():#, plot_antisym=False, **kwargs):
t = np.arange(1000)
Eu = np.sin(t * np.pi/10) + np.random.random(1000)
Ez = np.cos(t * np.pi/10) + np.random.random(1000)
ts = DataFrame({'t': t, 'Eu': Eu, 'Ez': Ez})
tools = 'box_zoom,pan,reset,save,box_select'
source = ColumnDataSource(data=ts)
original_source = ColumnDataSource(data=ts)
selection_glyph = Circle(fill_color='firebrick', line_color=None)
nonselection_glyph = Circle(fill_color='#1f77b4', fill_alpha=0.1, line_color=None)
p1 = figure(plot_width=300, plot_height=300, tools=tools)
r1 = p1.circle('t', 'Eu', source=source, line_color=None)
r1.selection_glyph = selection_glyph
r1.nonselection_glyph = nonselection_glyph
p2 = figure(plot_width=300, plot_height=300, tools=tools)
r2 = p2.circle('Eu', 'Ez', source=source, line_color=None)
r2.selection_glyph = selection_glyph
r2.nonselection_glyph = nonselection_glyph
return gridplot([[p1, p2]])
gp = znzt_ts()
output_notebook()
show(gp)
To remove completely the unselected glyphs, you can use:
fig = Figure()
c = fig.circle(x='x', y='y', source=source)
c.nonselection_glyph.visible = False

Categories