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
Related
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)
Look at flowing code:
from kivy.app import App;
from kivy.uix.widget import Widget;
from kivy.animation import Animation;
from kivy.uix.button import Button;
from time import time;
import json;
but = Button();
anim = Animation(size_hint = (.75 , .75), duration = 1);
anim += Animation(size_hint = (.5 , .5), duration = 1);
anim += Animation(size_hint = (.25 , .25), duration = 1);
anim += Animation(size_hint = (.0 , .0), duration = 1);
progress_array = [];
time_array = [];
start_time = time();
def progr_fun(*args):
global time_array, progress_array;
time_array.append((time() - start_time));
print((time() - start_time));
progress_array.append(args[2]);
print(args[2]);
anim.bind(on_progress = progr_fun);
anim.start(but);
class testApp(App):
def build(self):
return but;
if __name__ == '__main__':
testApp().run();
f_obj = open('hello', 'w');
json.dump([progress_array, time_array], f_obj);
f_obj.close();
Its program, that makes easy animation for button. Animation consists of several parts (its important). There is callback on_progress that collects time data and progression. This data saved at the end of program.
I'm using another script to build plot by time and progression and get something like that:
enter image description here
As you can see in different parts of animation progression growing unevenly.
Why is this happening? How to fix it?
It is odd behavior, but I think the cause is a different time interval used to step through the different parts of the animation. I believe those intervals, while changing for the different phases, are within the resolution of the kivy clock.
I'm not sure why those intervals change. I believe that is done in the C-code underneath the kivy Clock.
The + operator for Animation objects produces a Sequence subclass of Animation, and that seems to trigger the difference. A workaround is to construct your own sequence, Something like this:
self.anim1 = Animation(size_hint=(.75, .75))
self.anim2 = Animation(size_hint=(.5, .5))
self.anim3 = Animation(size_hint=(.25, .25))
self.anim4 = Animation(size_hint=(.0, .0))
self.anim1.bind(on_progress=self.progr_fun, on_complete=self.start2)
self.anim2.bind(on_progress=self.progr_fun, on_complete=self.start3)
self.anim3.bind(on_progress=self.progr_fun, on_complete=self.start4)
self.anim4.bind(on_progress=self.progr_fun)
self.anim1.start(self.but)
def start2(self, *args):
self.anim2.start(self.but)
def start3(self, *args):
self.anim3.start(self.but)
def start4(self, *args):
self.anim4.start(self.but)
One more possible solution - make your own progress rate using time.time(). Its can looks like that:
but = Button();
anim = Animation(size_hint = (.75 , .75), duration = 1);
anim += Animation(size_hint = (.5 , .5), duration = 1);
anim += Animation(size_hint = (.25 , .25), duration = 1);
anim += Animation(size_hint = (.0 , .0), duration = 1);
def start_fun(*args):
global start_time;
start_time = time();
def progr_fun(*args):
global time_array, progress_array, start_time , anim;
my_progress = (time() - start_time) / anim.duration;
print(my_progress);
anim.bind( on_progress = progr_fun,
on_start = start_fun);
anim.start(but);
But this variant not so accurate. You can get something like that at the end of animation:
1.0635101795196533
1.0677716732025146
1.0720353722572327
When you hover over the legend, all lines except the corresponding line will be hidden. How to achieve this via bokeh, python or javascript. I have no idea what to do to achieve this function. It would be great if we could provide a simple example with three lines.Thanks for your help.My code example is as follows:
import bokeh.palettes as bp
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import LinearAxis, Range1d, NumeralTickFormatter, Legend
from bokeh.layouts import column
import numpy as np
if __name__ == '__main__':
num = 5
color_list2 = bp.magma(num)
color_list1 = bp.viridis(num)
plotTools = 'box_zoom, wheel_zoom, pan, tap, crosshair, hover, reset, save'
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
p.title.text = 'Hover and Hide'
items_c1 = []
i = 0
pictures = []
labels = ['a', 'b', 'c', 'd', 'e']
for label in labels:
n = np.random.randint(low=3, high=6)
xs = np.random.random(n) * 1000
y1s = np.random.random(n) - 2.5
temp_line = p.line(xs, y1s, line_width=2, color=color_list1[i % num],
alpha=0.3, hover_color='red', hover_alpha=0.9) # , legend_label=label
items_c1.append((label + '_BER', [temp_line]))
i = i + 1
if i % num == 0:
legend_1 = Legend(items=items_c1)
p.add_layout(legend_1, 'left')
p.xaxis.axis_label = 'run_time'
p.yaxis[0].axis_label = 'BER'
p.legend[0].orientation = 'vertical'
p.legend[0].location = 'bottom_center'
p.legend[0].click_policy = 'hide'
pictures.append(p)
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
items_c1 = []
file = "test_ask_5"
file_path = file + '.html'
output_file(file_path)
show(column(pictures))
The solution below hides all lines but the one that is being clicked (not hovered). This code works for Bokeh v1.3.4
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import CustomJS
colors = ['orange', 'cyan', 'lightgreen']
p = figure()
lines = [p.line(np.arange(10), np.random.random(10), line_color = colors[i], line_width = 3, legend=colors[i], name=colors[i]) for i in range(3)]
code = '''if(Bokeh != 'undefined' && (Bokeh.active_line === 'undefined' || Bokeh.active_line == null))
{
Bokeh.active_line = cb_obj.name;
}'''
[line.js_on_change('visible', CustomJS(code = code)) for line in lines]
code = ''' for(i = 0; i < lines.length; i++) {
if (lines[i].name == Bokeh.active_line) {
lines[i].visible = true
}
else {
lines[i].visible = false
}
}
Bokeh.active_line = null'''
p.js_on_event('tap', CustomJS(args = {'lines' : lines}, code = code))
code = ''' for(i = 0; i < lines.length; i++) {
lines[i].visible = true
}
Bokeh.active_line = null'''
p.js_on_event('reset', CustomJS(args = dict(lines = lines), code = code))
p.legend.click_policy = 'hide'
show(p)
The first callback is applied to all glyph renderers (lines) and is being triggered when the line must be hidden, that is when a user clicks a legend item. The callback just sets the global variable Bokeh.active_line which remembers the renderer (line) name, e.g. "orange" or "cyan"
The second callback is attached to the plot canvas and is triggered every time the user clicks anywhere on the plot. What is basically does is inverting the glyphs (lines) visibility. It only shows the line specified by
Bokeh.active_line
The third callback is attached to the plot and is triggered when user clicks on the reset tool in the toolbar. It restores visibility of all lines.
Some context:
I was looking into the vispy module to plot in realtime (or as close as possible to) data coming from an instrument. My attempt follow.
from vispy.plot import Fig
from vispy import app,scene
from vispy.visuals import TextVisual
import numpy as np
import Queue
FONT_SIZE = 14
MIN = 0
MAX = 1.1
w_size = 100
N = 5000
M = 2500
color_map = 'cubehelix'
q_size = 1000
Nb = 5
#generate (empty) initial data to fill the plot
data = np.zeros(N*M)
data = np.reshape(data, (N,M))
#setup the plot
fig = Fig(show = False,size = (16*w_size,9*w_size),bgcolor='black')
fig.title = 'my plot'
main_plot = fig[0,0].image(data = data,fg_color='w',cmap=color_map,clim=(MIN,MAX))
fig[0,0].view.camera.aspect = N/float(M) * 16./9.
title = scene.Label("someoutput", font_size=FONT_SIZE, color = 'w')
fig[0,0].grid.add_widget(title, row=0, col=4)
fig[0,0].grid[2,4].border_color = 'black'
fig[0,0].grid[2,4].bgcolor = 'black'
xlabel_title = scene.Label("x_axis [unit]", font_size=FONT_SIZE, color = 'w')
fig[0,0].grid.add_widget(xlabel_title, row=4, col=4)
ylabel_title = scene.Label("y_axis [unit]", font_size=FONT_SIZE,rotation=-90, color='w')
fig[0,0].grid.add_widget(ylabel_title, row=2, col=2)
scale = scene.ColorBarWidget(orientation='left',
cmap=color_map,
label='Some value',
clim=(MIN,MAX),
border_color = 'w',
border_width = 1,
label_color = 'w'
)
fig[0,0].grid.add_widget(scale, row=2, col=6)
fig[0,0].cbar_right.width_max = \
fig[0,0].cbar_right.width_min = 50
#fill a queue so to excude the generation time from the plotting time
q = Queue.Queue()
for i in range(q_size):
new_data = (np.abs(0.5*np.random.randn(Nb*M)[:])).astype('float32')
new_data = np.reshape(new_data, (Nb,M))
q.put(new_data[:])
#update function
def update(ev):
global main_plot, q, data, Nb,M,fig,index
#acquire
new_data = q.get()
#roll the plot data
data[Nb:, :] = data[:-Nb, :]
data[:Nb,:] = new_data
#recycle the new data
q.put(new_data)
#update the plot
main_plot.set_data(data)
main_plot.update()
# setup timer
interv = 0.01
timer = app.Timer(interval = interv)
timer.connect(update)
timer.start(interval = interv)
if __name__ == '__main__':
fig.show(run=True)
app.run()
This code currently works but it's much slower than the data rate. In the vispy gallery, as well as in some examples, I saw much more points being plotted and updated. I think that the main problem is that I completely set each time all the data of the plot instead of shifting them and inserting new points.
I also had a look at this example:
https://github.com/vispy/vispy/blob/master/examples/demo/scene/oscilloscope.py
However I don't know how to generalize the update function that rolls the data (I have no knowledge of OpenGL) and I cannot use the example as is because I need a quantitative color scale (that seems well implemented in vispy.plot).
The question:
Is there a way to write a function that rolls the data of a plot generated with the vispy.plot class?
Thanks.
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 ;)