How do I make a bokeh DataTable respond to DoubleTap - python

I am trying to add a feature to recalculate the diff column.
Currently I using a button to trigger the callback but I would really like to trigger it by double clicking a row in the table.
I can only find solutions with single click implementation by using the code
source.selected.js_on_change('indices', callback).
Does anyone know how to get the DataTable to react to double clicks?
from bokeh.models import ColumnDataSource, TableColumn, DataTable, Div, CustomJS, Button
from bokeh.layouts import column
from bokeh.plotting import show
from bokeh import events
names = ['Alfa', 'Bravo', 'Charlie', 'Delta']
values = [150, 100, 125, 200]
difference = [0, 0, 0, 0]
data = dict(names=names, values=values, diff=difference)
source = ColumnDataSource(data)
columns = [TableColumn(field='names', title='Name', width=200),
TableColumn(field='values', title='Value (-)', width=200),
TableColumn(field='diff', title='Difference (%)', width=200)]
# create total table width value
table_width = 0
for col in columns:
table_width = table_width + col.width
header = Div(text=f'<b>Results<b>', style={'font-size': '150%'})
fig = DataTable(source=source, columns=columns, height=len(values) * 25 + 50, width=table_width, selectable=True)
# callback to change reference for (%) difference calculation
callback = CustomJS(args=dict(source=source), code="""
var idx = source.selected.indices[0]
if (typeof idx == "undefined") {
idx = 0
}
var ref_val = source.data['values'][idx]
console.log(ref_val)
var d = source.data['diff']
for (var i = 0; i < d.length; i++) {
value = source.data['values'][i]
source.data['diff'][i] = (100*(value/ref_val-1)).toFixed(2)
}
source.change.emit()
""")
button = Button()
button.label = 'Click HERE to change reference to selected row for Difference (%) calculation'
# source.selected.js_on_change('indices', callback)
source.selected.js_on_event(events.DoubleTap, callback)
button.js_on_event(events.ButtonClick, callback)
show(column([button, fig]))

My trick is to set 2 active rows for each row click.
lst_source_indices_old =[]
source = ColumnDataSource(df)
def callback(attr, old, new):
global lst_source_indices_old # declare it is a global variable
lst = source.selected.indices # selected row in DataTable
if(len(lst)==1):
nrow=lst[0]
if(lst==lst_source2_indices_old):
print("... DataTable : Double-click")
# Save
lst_source2_indices_old = [nrow]
source.selected.indices = [nrow, 9999] # s.t. every click will be triggered
source.selected.on_change('indices', callback)

Related

Interactively change a plot in Bokeh using sliders to select column

My question is very similar to this one, but I still cannot find how to adapt the answers to my problem.
I have a dataframe with 100 columns. I want to use two sliders in Bokeh to select one column to show in the plot. I want to do this with CDSView.
Say the columns are named as such: ["11", "12", .."99"]. Plus I have one column, "x", which is the x axis and does not change. The first slider, range [0-9], should select the first digit of the column name. The second slider should select the last two digits in the same way.
This would mean that if the user selects 2, 5 on the first and second sliders, Bokeh would show a plot using the column "25" from my dataframe.
How can I do this?
So I've found a solution, using some snippets from other questions.
Here is a working example (Bokeh 2+), I hope somebody will find it useful in the future.
import pandas as pd
from bokeh.plotting import figure, show, ColumnDataSource
from bokeh.layouts import column
from bokeh.models import CustomJS, Slider
df = pd.DataFrame([[1,2,3,4,5],[2,20,3,10,20]], columns = ['1','21','22','31','32'])
source_available = ColumnDataSource(df)
source_visible = ColumnDataSource(data = dict(x = df['1'], y = df['21']))
p = figure(title = 'SLIMe')
p.circle('x', 'y', source = source_visible)
slider1 = Slider(title = "SlideME", value = 2, start = 2, end = 3, step = 1)
slider2 = Slider(title = "SlideME2", value = 1, start = 1, end = 2, step = 1)
slider1.js_on_change('value', CustomJS(
args=dict(source_visible=source_visible,
source_available=source_available,
slider1 = slider1,
slider2 = slider2), code="""
var sli1 = slider1.value;
var sli2 = slider2.value;
var data_visible = source_visible.data;
var data_available = source_available.data;
data_visible.y = data_available[sli1.toString() + sli2.toString()];
source_visible.change.emit();
""") )
slider2.js_on_change('value', CustomJS(
args=dict(source_visible=source_visible,
source_available=source_available,
slider1 = slider1,
slider2 = slider2), code="""
var sli1 = slider1.value;
var sli2 = slider2.value;
var data_visible = source_visible.data;
var data_available = source_available.data;
data_visible.y = data_available[sli1.toString() + sli2.toString()];
source_visible.change.emit();
""") )
show(column(p, slider1, slider2))

How to synchronize ranges only when the user stops panning and zooming?

I have many plots and many samples per plot. I need to zoom and pan in all plots. Also, all ranges must be synchronized in real time. If I share range works well with a few plots, but with many plots it becomes laggy. Then, to solve this I would like to trigger the synchronization just when the pan or zoom action finishes.
There is a PanEnd event which is triggered when the the user stops panning. But I cannot do the same with the wheel zoom because there is no a MouseWheelEnd event, just a MouseWheel event, so I cannot detect when the user stops. Finally I added a periodic callback to update the ranges from time to time. But I do not like this solution.
I have also tried LODStart and LODEnd events (related with downsampling) and I had to force lod_threshold=1. But sometimes LODEnd is not triggered, only LODStart is always triggered.
from bokeh.plotting import figure
from bokeh.models.sources import ColumnDataSource, CDSView
from bokeh.models.filters import IndexFilter
from bokeh.models.markers import Scatter, Circle
from bokeh.models.tools import LassoSelectTool
from bokeh.models.ranges import DataRange1d
from bokeh.plotting import curdoc, gridplot
from bokeh.events import MouseWheel, PanEnd
import numpy as np
N = 3500
x = np.random.random(size=N) * 200
y = np.random.random(size=N) * 200
source = ColumnDataSource(data=dict(x=x, y=y))
plots = []
x_ranges = []
y_ranges = []
p_last_modified = -1
def render_plot(i, p_last_modified):
range_padding = 0.25
x_range = DataRange1d(
range_padding=range_padding,
renderers=[]
)
y_range = DataRange1d(
range_padding=range_padding,
renderers=[]
)
plot = figure(
width=500,
height=500,
x_range=x_range,
y_range=y_range,
toolbar_location='left',
tools='pan,wheel_zoom,tap,lasso_select',
output_backend='webgl',
)
c = plot.scatter(
x='x',
y='y',
size=3,
fill_color='blue',
line_color=None,
line_alpha=1.0,
source=source,
nonselection_fill_color='blue',
nonselection_line_color=None,
nonselection_fill_alpha=1.0,
)
c.selection_glyph = Scatter(
fill_color='yellow',
line_color='red',
line_alpha=1.0,
)
def mouse_wheel_event(event):
print('>> MOUSE WHEEL EVENT: PLOT NUMBER: {}'.format(i))
global p_last_modified
p_last_modified = i
plot.on_event(MouseWheel, mouse_wheel_event)
def pan_end_event(event):
print('>> PAN END: {}'.format(i))
for p in range(len(plots)):
if p != i:
plots[p].x_range.end = plots[i].x_range.end
plots[p].x_range.start = plots[i].x_range.start
plots[p].y_range.end = plots[i].y_range.end
plots[p].y_range.start = plots[i].y_range.start
plot.on_event(PanEnd, pan_end_event)
plots.append(plot)
x_ranges.append(x_range)
y_ranges.append(y_range)
for i in range(12):
render_plot(i, p_last_modified)
gp = gridplot(
children=plots,
ncols=4,
plot_width=300,
plot_height=300,
toolbar_location='left',
)
def callback():
global p_last_modified
print('-- CALLBACK: last_modified: {}'.format(p_last_modified))
if p_last_modified != -1:
for p in range(len(plots)):
if p != p_last_modified:
plots[p].x_range.end = plots[p_last_modified].x_range.end
plots[p].x_range.start = plots[p_last_modified].x_range.start
plots[p].y_range.end = plots[p_last_modified].y_range.end
plots[p].y_range.start = plots[p_last_modified].y_range.start
p_last_modified = -1
curdoc().add_periodic_callback(callback, 3000)
curdoc().add_root(gp)
Any other suggestion?
I got it working, although I don't like it so much.
It involves some JS and 3 'dummy' widgets, I'd expect there to be a more simple way, but anyhow that is one way.
dum_txt_timer is a textinput that will be used as a timer, its value is in seconds and will be updated with a desired timestep. When the value reaches a desired threshold the update on the ranges will be triggered. When the value is below the threshold it does nothing
dum_button is a button which does two things, a first click will start the timer in dum_txt_timer, a second click will stop the timer.
dum_txt_trigger is another textinput that is used to click dum_button and start/stop the timer.
The mouse_wheel_event function triggers on every single iteration of the mouse wheel. The value of the plot in which the mouse is is stored in mod_source, a data source that is passed to the dum_txt_timer callback.
It checks if the dum_txt_timer value is 0, if it is it updates the value in dum_txt_trigger, which clicks the button and starts the timer, and it updates dum_txt_timer so that other wheel events do nothing until the update. If it is different from 0 it does nothing.
The callback of the dum_txt_timer needs the dum_txt_trigger, the mod_source datasource that stores the plot ID and all the plot ranges.
The callback does nothing until the dum_txt_timer value is updated at the end of the timeout function. Otherwise it first updates the value of dum_txt_trigger which clicks dum_button a second time and stops the timer (resets it to 0. Then it updates the range of all the plots.
I this example the time before the update is set by the timeout function in the button callback.
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView, IndexFilter, Scatter, Circle, LassoSelectTool, DataRange1d, CustomJS, TextInput, Button
from bokeh.events import MouseWheel, PanEnd
from bokeh.layouts import widgetbox, gridplot
import numpy as np
N = 3500
x = np.random.random(size=N) * 200
y = np.random.random(size=N) * 200
source = ColumnDataSource(data=dict(x=x, y=y))
dum_txt_timer = TextInput(value='0',visible=False)
# javascript code for a dummy (invisible) button, it starts and stops a timer that will be written in dum_txt_timer
dum_button_code = """
if (cb_obj.button_type.includes('success')){
// start a timer in dum_txt by updating its value with a fixed timestep
var start = new Date();
var intervalID = setInterval(function(){var current = new Date(); var diff=((current-start)/1000.0).toFixed(4); dum_txt_timer.value=diff.toString(); }, 500)
cb_obj.button_type = 'warning';
} else {
// stop the timer and set the dum_txt_timer value back to 0
var noIntervals = setInterval(function(){});
for (var i = 0; i<noIntervals; i++) { window.clearInterval(i);}
dum_txt_timer.value='0';
cb_obj.button_type = 'success';
}
"""
dum_button = Button(label='dummy_button',button_type='success',visible=False) # the dummy button itself
dum_button.callback = CustomJS(args={'dum_txt_timer':dum_txt_timer},code=dum_button_code) # the callback of the button
# dummy textinput to click the dummy button
dum_txt_trigger = TextInput(value='0',visible=False)
dum_txt_trigger_code = """
// click the dummy button
var button_list = document.getElementsByTagName('button');
for(var i=0;i<button_list.length;i++){
if(button_list[i].textContent==="dummy_button"){button_list[i].click()}
}
"""
dum_txt_trigger.js_on_change('value',CustomJS(code=dum_txt_trigger_code))
dum_box = widgetbox(dum_txt_timer,dum_txt_trigger,dum_button,visible=False)
plots = []
x_ranges = []
y_ranges = []
mod_source = ColumnDataSource(data={'x':[]})
reference = None
def render_plot(i):
range_padding = 0.25
x_range = DataRange1d(range_padding=range_padding,renderers=[])
y_range = DataRange1d(range_padding=range_padding,renderers=[])
plot = figure(width=500,height=500,x_range=x_range,y_range=y_range,toolbar_location='left',tools='pan,wheel_zoom,tap,lasso_select',output_backend='webgl',)
c = plot.scatter(x='x',y='y',size=3,fill_color='blue',line_color=None,line_alpha=1.0,source=source,nonselection_fill_color='blue',nonselection_line_color=None,nonselection_fill_alpha=1.0,)
c.selection_glyph = Scatter(fill_color='yellow',line_color='red',line_alpha=1.0,)
def mouse_wheel_event(event):
if dum_txt_timer.value != '0':
return
# if the timer value is 0, start the timer
dum_txt_trigger.value = str(int(dum_txt_trigger.value)+1)
dum_txt_timer.value = '0.0001' # immediatly update the timer value for the check on 0 in the python callback to work immediatly
mod_source.data.update({'x':[i]})
plot.on_event(MouseWheel, mouse_wheel_event)
def pan_end_event(event):
print('>> PAN END: {}'.format(i))
for p in range(len(plots)):
if p != i:
plots[p].x_range.end = plots[i].x_range.end
plots[p].x_range.start = plots[i].x_range.start
plots[p].y_range.end = plots[i].y_range.end
plots[p].y_range.start = plots[i].y_range.start
plot.on_event(PanEnd, pan_end_event)
plots.append(plot)
x_ranges.append(x_range)
y_ranges.append(y_range)
for i in range(12):
render_plot(i)
dum_txt_timer_args = {'dum_txt_trigger':dum_txt_trigger,'mod_source':mod_source}
dum_txt_timer_args.update( {'xrange{}'.format(i):plot.x_range for i,plot in enumerate(plots)} )
dum_txt_timer_args.update( {'yrange{}'.format(i):plot.y_range for i,plot in enumerate(plots)} )
set_arg_list = "var xrange_list = [{}];".format(','.join(['xrange{}'.format(i) for i in range(len(plots))]))
set_arg_list += "var yrange_list = [{}];".format(','.join(['yrange{}'.format(i) for i in range(len(plots))]))
# code that triggers when the dum_txt_timer value is changed, so every 100 ms, but only clicks dum_button when the value is greater than 2 (seconds)
dum_txt_timer_code = set_arg_list + """
var timer = Number(cb_obj.value);
var trigger_val = Number(dum_txt_trigger.value);
// only do something when the value is greater than 2 (seconds)
if (timer>0.0001) {
trigger_val = trigger_val + 1;
dum_txt_trigger.value = trigger_val.toString(); // click button again to stop the timer
// update the plot ranges
var p_last_modified = mod_source.data['x'][0];
var nplots = xrange_list.length;
for (var i=0; i<nplots; i++){
if (i!=p_last_modified){
xrange_list[i].start = xrange_list[p_last_modified].start;
xrange_list[i].end = xrange_list[p_last_modified].end;
yrange_list[i].start = yrange_list[p_last_modified].start;
yrange_list[i].end = yrange_list[p_last_modified].end;
}
}
}
"""
dum_txt_timer.js_on_change('value',CustomJS(args=dum_txt_timer_args,code=dum_txt_timer_code))
gp = gridplot(children=plots,ncols=4,plot_width=300,plot_height=300,toolbar_location='left',)
grid = gridplot([[gp],[dum_box]],toolbar_location=None)
curdoc().add_root(grid)
One nice thing is that the same dummy widgets can be used to set a delay on range updates from different events, the event callback just needs to update dum_txt_trigger like in mouse_wheel_event

Highlighting multiple hex_tiles by hovering in bokeh

I try to visualize my data in a hex map. For this I use python bokeh and the corresponding hex_tile function in the figure class. My data belongs to one of 8 different classes, each having a different color. The image below shows the current visualization:
I would like to add the possibility to change the color of the element (and ideally all its class members) when the mouse hovers over it.
I know, that it is somewhat possible, as bokeh themselves provide the following example:
https://docs.bokeh.org/en/latest/docs/gallery/hexbin.html
However, I do not know how to implement this myself (as this seems to be a feature for the hexbin function and not the simple hex_tile function)
Currently I provide my data in a ColumnDataSource:
source = ColumnDataSource(data=dict(
r=x_row,
q=y_col,
color=colors_array,
ipc_class=ipc_array
))
where "ipc_class" describes one of the 8 classes the element belongs to.
For the mouse hover tooltip I used the following code:
TOOLTIPS = [
("index", "$index"),
("(r,q)", "(#r, #q)"),
("ipc_class", "#ipc_class")
]
and then I visualized everything with:
p = figure(plot_width=1600, plot_height=1000, title="Ipc to Hexes with colors", match_aspect=True,
tools="wheel_zoom,reset,pan", background_fill_color='#440154', tooltips=TOOLTIPS)
p.grid.visible = False
p.hex_tile('q', 'r', source=source, fill_color='color')
I would like the visualization to add a function, where hovering over one element will result in one of the following:
1. Highlight the current element by changing its color
2. Highlight multiple elements of the same class when one is hovered over by changing its color
3. Change the color of the outer line of the hex_tile element (or complete class) when the element is hovered over
Which of these features is possible with bokeh and how would I go about it?
EDIT:
After trying to reimplement the suggestion by Tony, all elements will turn pink as soon as my mouse hits the graph and the color won´t turn back. My code looks like this:
source = ColumnDataSource(data=dict(
x=x_row,
y=y_col,
color=colors_array,
ipc_class=ipc_array
))
p = figure(plot_width=800, plot_height=800, title="Ipc to Square with colors", match_aspect=True,
tools="wheel_zoom,reset,pan", background_fill_color='#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source=source, fill_color='color')
###################################
code = '''
for (i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
hovered_index = cb_data.index.indices[0];
hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
cb_data.renderer.data_source.data['color'][i] = 'pink';
}
}
cb_data.renderer.data_source.change.emit();
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(args=dict(colors=colors), code=code)
hover = HoverTool(tooltips=TOOLTIPS, callback=callback)
p.add_tools(hover)
########################################
output_file("hexbin.html")
show(p)
basically, I removed the tooltips from the figure function and put them down to the hover tool. As I already have red in my graph, I replaced the hover color to "pink". As I am not quite sure what each line in the "code" variable is supposed to do, I am quite helpless with this. I think one mistake may be, that my ColumnDataSource looks somewhat different from Tony's and I do not know what was done to "classifiy" the first and third element, as well as the second and fourth element together. For me, it would be perfect, if the classification would be done by the "ipc_class" variable.
Following the discussion from previous post here comes the solution targeted for the OP code (Bokeh v1.1.0). What I did is:
1) Added a HoverTool
2) Added a JS callback to the HoverTool which:
Resets the hex colors to the original ones (colors_array passed in the callback)
Inspects the index of currently hovered hex (hovered_index)
Gets the ip_class of currently hovered hex (hovered_ip_class)
Walks through the data_source.data['ip_class'] and finds all hexagons with the same ip_class as the hovered one and sets a new color for it (pink)
Send source.change.emit() signal to the BokehJS to update the model
The code:
from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']
source = ColumnDataSource(data = dict(
x = x_row,
y = y_col,
color = colors_array,
ipc_class = ipc_array
))
p = figure(plot_width = 800, plot_height = 800, title = "Ipc to Square with colors", match_aspect = True,
tools = "wheel_zoom,reset,pan", background_fill_color = '#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source = source, fill_color = 'color')
###################################
code = '''
for (let i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
const hovered_index = cb_data.index.indices[0];
const hovered_ipc_class = cb_data.renderer.data_source.data['ipc_class'][hovered_index];
for (let i = 0; i < cb_data.renderer.data_source.data['ipc_class'].length; i++) {
if (cb_data.renderer.data_source.data['ipc_class'][i] == hovered_ipc_class)
cb_data.renderer.data_source.data['color'][i] = 'pink';
}
}
cb_data.renderer.data_source.change.emit();
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(args = dict(ipc_array = ipc_array, colors = colors_array), code = code)
hover = HoverTool(tooltips = TOOLTIPS, callback = callback)
p.add_tools(hover)
########################################
output_file("hexbin.html")
show(p)
Result:
Maybe something like this to start with (Bokeh v1.1.0):
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors = ["green", "blue", "green", "blue"]
source = ColumnDataSource(dict(r = [0, 1, 2, 3], q = [1, 1, 1, 1], color = colors))
plot = figure(plot_width = 300, plot_height = 300, match_aspect = True)
plot.hex_tile('r', 'q', fill_color = 'color', source = source)
code = '''
for (i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
hovered_index = cb_data.index.indices[0];
hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
cb_data.renderer.data_source.data['color'][i] = 'red';
}
}
cb_data.renderer.data_source.change.emit();
'''
callback = CustomJS(args = dict(colors = colors), code = code)
hover = HoverTool(tooltips = [('R', '#r')], callback = callback)
plot.add_tools(hover)
show(plot)
Result:
Another approach is to update cb_data.index.indices to include all those indices that have ipc_class in common, and add hover_color="pink" to hex_tile. So in the CustomJS code one would loop the ipc_class column and get the indices that match the ipc_class of the currently hovered item.
In this setup there is not need to update the color column in the data source.
Code below tested used Bokeh version 3.0.2.
from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']
source = ColumnDataSource(data = dict(
x = x_row,
y = y_col,
color = colors_array,
ipc_class = ipc_array
))
plot = figure(
width = 800,
height = 800,
title = "Ipc to Square with colors",
match_aspect = True,
tools = "wheel_zoom,reset,pan",
background_fill_color = '#440154'
)
plot.grid.visible = False
plot.hex_tile(
'x', 'y',
source = source,
fill_color = 'color',
hover_color = 'pink' # Added!
)
code = '''
const hovered_index = cb_data.index.indices;
const src_data = cb_data.renderer.data_source.data;
if (hovered_index.length > 0) {
const hovered_ipc_class = src_data['ipc_class'][hovered_index];
var idx_common_ipc_class = hovered_index;
for (let i = 0; i < src_data['ipc_class'].length; i++) {
if (i === hovered_index[0]) {
continue;
}
if (src_data['ipc_class'][i] === hovered_ipc_class) {
idx_common_ipc_class.push(i);
}
}
cb_data.index.indices = idx_common_ipc_class;
cb_data.renderer.data_source.change.emit();
}
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(code = code)
hover = HoverTool(
tooltips = TOOLTIPS,
callback = callback
)
plot.add_tools(hover)
output_file("hexbin.html")
show(p)

Bokeh datatable filtering inconsistency

Apologies but I can't make any reproducible code for this question since it's pretty inconsistent.
So I have a bokeh data table, and I'm doing some filtering with it using 4 dropdown boxes. The data table updates based on dropdown box value, and the updates were written in JS. The filtering works as expected, but strangely enough for some very specific combinations of dropdown values it does not display anything in the data table. I was wondering if it was a problem with my data, but I coerced everything to strings and it still gave me the same problem.
The updates are written here:
combined_callback_code = """
var data = source.data;
var original_data = original_source.data;
var origin = origin_select_obj.value;
var classification = classification_select_obj.value;
var currency = currency_select_obj.value;
var grade = grade_select_obj.value;
for (var key in original_data) {
data[key] = [];
for (var i = 0; i < original_data['Origin'].length; ++i) {
if ((origin === "ALL" || original_data['Origin'][i] === origin) &&
(classification === "ALL" || original_data['Classification'][i] === classification) &&
(currency === "ALL" || original_data['Currency'][i] === currency) &&
(grade === "ALL" || original_data['BrokenPct'][i] === grade)){
data[key].push(original_data[key][i]);
}
}
}
target_obj.change.emit();
source.change.emit();
"""
# define the filter widgets, without callbacks for now
origin_list = ['ALL'] + df['Origin'].unique().tolist()
origin_select = Select(title="Origin:", value=origin_list[0], options=origin_list)
classification_list = ['ALL'] + df['Classification'].unique().tolist()
classification_select = Select(title="Classification:", value=classification_list[0], options=classification_list)
currency_list = ['ALL'] + df['Currency'].unique().tolist()
currency_select = Select(title = "Currency:", value=currency_list[0], options = currency_list)
grade_list = ["ALL"] + df['BrokenPct'].unique().tolist()
grade_select = Select(title = "Grade:", value = grade_list[0], options = grade_list)
# now define the callback objects now that the filter widgets exist
generic_callback = CustomJS(
args=dict(source=source,
original_source=original_source,
origin_select_obj=origin_select,
classification_select_obj=classification_select,
currency_select_obj = currency_select,
grade_select_obj = grade_select,
target_obj=data_table),
code=combined_callback_code
)
# finally, connect the callbacks to the filter widgets
origin_select.js_on_change('value', generic_callback)
classification_select.js_on_change('value', generic_callback)
currency_select.js_on_change('value', generic_callback)
grade_select.js_on_change('value', generic_callback)
The right way do update table data in your JS callback is this way:
var data = {};
//build your data here
source.data = data;
Where source is the Bokeh ColumnDataSource of you DataTable.
You don't need to use:
source.change.emit();
You do it only when you replace only a part of you data e.g. one table column.
And if data_table is your Bokeh DataTable object then also skip doing:
target_obj.change.emit();
The table date updates automatically when you update its ColumnDataSource.
See this simple example:
from bokeh.io import show
from bokeh.layouts import widgetbox
from bokeh.models import ColumnDataSource, Slider, DataTable, TableColumn, CustomJS
source = ColumnDataSource(dict(x = list(range(6)), y = [x ** 2 for x in range(6)]))
columns = [TableColumn(field = "x", title = "x"), TableColumn(field = "y", title = "x**2")]
table = DataTable(source = source, columns = columns, width = 320)
slider = Slider(start = 1, end = 20, value = 6, step = 1, title = "i", width = 300)
callback_code = """ i = slider.value;
new_data = {"x": [1, 2, 3, 4, 5], "y": [1, 4, 9, 16, 25]}
table.source.data = new_data
table.width = 320 + i * 25; """
callback = CustomJS(args = dict(slider = slider, table = table), code = callback_code)
slider.js_on_change('value', callback)
show(widgetbox(slider, table))

Bokeh Plot Update Using Slider

I am trying to use a slider to update my Bokeh Plot. I am finding it difficult to achieve it using pandas dataframe(did not find any examples so far).
The other way is to use the "columndatasource" (found some examples over forums) but still not able to achieve the functionality.
So I have two columns, X axis is date and the Y axis is Volume. I want to change my Y values based on slider input. I am able to see the plot but the slider functionality is not working
Any help will be very much appreciable.
source = ColumnDataSource(data=dict(x=df2['Date'],y=df2['Vol']))
S1 = figure(plot_width=400,plot_height=400,tools=TOOLS1,title="Volume Per Day",x_axis_type="datetime")
S1.line('x','y',source=source)
callback_test = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var s_val = cb_obj.value
x = data['x']
y = data['y']
console.log(cb_obj)
for (i = 0; i < s_val; i++) {
y[i] = y[i]
}
source.trigger('change');
""")
slider = Slider(start=0, end= max_Vol, value=1, step=100,title="Vol Per Day",callback=callback_test)
You are trying to update the range of data that is plotted using a slider.
When you do:
y = data['y']
for (i = 0; i < s_val; i++) {
y[i] = y[i]
}
the python equivalent would be, if y is some array with length>s_val:
for i in range(s_val):
y[i] = y[i]
This just replaces the elements from 0 to s_val-1 by themselves and doesn't change the rest of the list.
You can do two things:
update the displayed axis range directly
use an empty source that you will fill from your existing source based on the slider value
.
source = ColumnDataSource(data=dict(x=df2['Date'],y=df2['Vol']))
fill_source = ColumnDataSource(data=dict(x=[],y=[]))
S1 = figure(plot_width=400,plot_height=400,tools=TOOLS1,title="Volume Per Day",x_axis_type="datetime")
S1.line('x','y',source=fill_source)
callback_test = CustomJS(args=dict(source=source,fill_source=fill_source), code="""
var data = source.data;
var fill_data = fill_source.data;
var s_val = cb_obj.value;
fill_data['x']=[];
fill_data['y']=[];
for (i = 0; i < s_val; i++) {
fill_data['y'][i].push(data['y'][i]);
fill_data['x'][i].push(data['x'][i]);
}
fill_source.trigger('change');
""")
Here is the changes I have made to make it work with Bokeh last version
Some syntax error in the JavaScript part have been corrected, the method to trigger change is now change.emit, and the callback for a stand alone document is set after the Slider definition thanks to js_on_change
I have added all the import commands to give a complete example
I have also changed the visualization to show only data below the number of flight set by the slider (for more comprehension when moving the Slider towards lower values)
Below is the resulting code:
from bokeh.layouts import column, widgetbox
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Slider
from bokeh.plotting import Figure
import pandas as pd
from datetime import datetime, date, timedelta
from bokeh.plotting import show
from random import randint
today = date.today()
random_data = [[today + timedelta(days = i), randint(0, 10000)] for i in range(10)]
df2 = pd.DataFrame(random_data, columns = ['Date', 'Vol'])
source = ColumnDataSource(data = dict(x = df2['Date'], y = df2['Vol']))
fill_source = ColumnDataSource(data = dict(x = df2['Date'], y = df2['Vol'])) # set the graph to show all data at loading
TOOLS1 = []
S1 = Figure(plot_width = 400, plot_height = 400, tools = TOOLS1, title = "Volume Per Day", x_axis_type = "datetime")
S1.line('x', 'y', source = fill_source)
callback_test = CustomJS(args = dict(source = source, fill_source = fill_source), code = """
var data = source.data;
var fill_data = fill_source.data;
var s_val = cb_obj.value;
fill_data['x']=[];
fill_data['y']=[];
for (var i = 0; i <= data.x.length; i++) { // added "var" declaration of variable "i"
if (data['y'][i] <= s_val) { // more meaningful visualization: assuming you want to focuss on dates with less number of flights
fill_data['y'].push(data['y'][i]); // [i] index on left side of assignment removed
}
else {
fill_data['y'].push(0);
}
fill_data['x'].push(data['x'][i]);
}
fill_source.change.emit() ; // "trigger" method replaced by "change.emit"
""")
max_Vol = df2['Vol'].max()
slider = Slider(start = 0, end = max_Vol, value = max_Vol, step = 100, title = "Vol Per Day") # Remove attribute "callback = callback_test"
slider.js_on_change('value', callback_test) # new way of defining event listener
controls = widgetbox(slider)
layout = column(controls, S1)
show(layout)
Would be nice if I could embbed the resulting (HTML) visualization directly in this answer, let me now if it's possible ;)

Categories