I am building a simple HTML file to represent some data in the chart graph with dropdown but the callback for such interactivity doesn't work.
Here is my code:
from bokeh.models import CustomJS, ColumnDataSource, Select
from bokeh.plotting import figure, output_file, show
import pandas as pd
from bokeh.models import layouts
output_file("churn_graph.html", title="line_on_off.py example")
df_3_days = pd.read_excel("C:\\Users\\Admin\\Desktop\\data1.xlsx", sheet_name="3 days")
df_40_days = pd.read_excel("C:\\Users\\Admin\\Desktop\\data2.xlsx", sheet_name="40 days")
df_365_days = pd.read_excel("C:\\Users\\Admin\\Desktop\\data3.xlsx", sheet_name="365 days")
x = df_3_days['Month']
churn_3d_12m_all = df_3_days['churnRate_12m_all']
churn_3d_12m_b2b = df_3_days['churnRate_12m_B2B']
churn_3d_12m_b2c = df_3_days['churnRate_12m_B2C']
churn_3d_6m = df_3_days['churnRate_6m_all']
churn_40d_12m_all = df_40_days['churnRate_12m_all']
churn_40d_12m_b2b = df_40_days['churnRate_12m_B2B']
churn_40d_12m_b2c = df_40_days['churnRate_12m_B2C']
churn_40d_6m = df_40_days['churnRate_6m_all']
churn_365d_12m_all = df_365_days['churnRate_12m_all']
churn_365d_12m_b2b = df_365_days['churnRate_12m_B2B']
churn_365d_12m_b2c = df_365_days['churnRate_12m_B2C']
churn_365d_6m = df_365_days['churnRate_6m_all']
source = ColumnDataSource({
'x' : x, 'y3_12all' : churn_3d_12m_all,
'x' : x, 'y3_12B2B' : churn_3d_12m_b2b,
'x' : x, 'y3_12B2C' : churn_3d_12m_b2c,
'x' : x, 'y3_6m' : churn_3d_6m
})
p = figure(width=500, height=250)
y3_12all = p.line(x, churn_3d_12m_all, color='blue', legend_label="3 days, 12m all")
y3_12B2B = p.line(x, churn_3d_12m_b2b, color='brown', legend_label="3 days B2B")
y3_12B2C = p.line(x, churn_3d_12m_b2c, color='red', legend_label="3 days B2C")
y3_6m = p.line(x, churn_3d_6m, color='green', legend_label="3 days, 6m")
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
y3_12all = data['y3_12all']
y3_12B2B = data['y3_12B2B']
y3_12B2C = data['y3_12B2C']
y3_6m = data['y3_6m']
if (f == "y3_12all"){
for (i = 0; i < y3_12all.length; i++) {y3_12all[i] = y3_12all[i]}
for (i = 0; i < y3_12B2B.length; i++) {y3_12B2B[i] = 'nan'}
for (i = 0; i < y3_12B2C.length; i++) {y3_12B2C[i] = 'nan'}
for (i = 0; i < y3_6m.length; i++) {y3_6m[i] = 'nan'}
else {
for (i = 0; i < y3_12all.length; i++) {y3_12all[i] = y3_12all[i]}
for (i = 0; i < y3_12B2B.length; i++) {y3_12B2B[i] = y3_12B2B[i]}
for (i = 0; i < y3_12B2C.length; i++) {y3_12B2C[i] = y3_12B2C[i]}
for (i = 0; i < y3_6m.length; i++) {y3_6m[i] = y3_6m[i]}
}
source.trigger('change');
""")
buffers = ['12m all', '12m B2B', '12m B2C', '6m', 'all']
multi_select = Select(title="Select buffer: ", value=buffers[4], options=buffers, callback=callback)
output_file("churn_chart.html")
layout = layouts.VBox(multi_select, p)
show(layout)
After running I receive the message: "unexpected attribute 'callback' to Select, similar attributes are js_event_callbacks".
Any suggestions on how to fix it?
Thanks :)
From the changelog for 2.0.0 at https://docs.bokeh.org/en/latest/docs/releases.html#api-removals:
Additionally, the callback property is removed from all Bokeh models except CustomAction, HoverTool, TapTool, and OpenURL. Instead, the more general js_on_change or js_on_event methods should be used.
Try removing the callback argument to Select and adding something like
multi_select.js_on_change('value', callback)
Not related to your question, but you also might consider these changes:
Replace source.get('data') with source.data
Similarly, replace cb_obj.get('value') with cb_obj.value
Also, I'm not sure if 'nan' strings are a good approach. You can just use NaN if they cause any trouble.
Related
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.
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)
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))
I hope everyone is doing well.
I am trying to develop a Bokeh interaction whereby selecting a part of a scatter plot will update a table.
I am using a lot of the sample code from the Bokeh documentation. My workplace is running an older version of Bokeh (0.12.5) so I had to change the last line in the Custom JS (from s2.change.emit() to s2.trigger('change). I then added in a few lines to create a DataTable.
I naively thought that since sourcing 's1' in the Datatable works, sourcing 's2' will allow me to link the table to the lasso select. I even tried adding in an extra trigger to the table widget in the JS callback.
Does anyone know how to create a table from a lasso select in a graph?
Code
Thanks in advance.
from random import random
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.plotting import figure, output_file, show
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
output_file("callback.html")
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)
s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(plot_width=400, plot_height=400, x_range=(0, 1), y_range=(0, 1),
tools="", title="Watch Here")
p2.circle('x', 'y', source=s2, alpha=0.6)
###New code##
columns = [TableColumn(field ="x", title = "X axis"),
TableColumn(field ="y", title = "Y axis")]
table = DataTable(source =s2, columns = columns, width =400, height = 280)
##Added in table.trigger('change') hoping this would link to the lasso select.
s1.callback = CustomJS(args=dict(s2=s2), code="""
var inds = cb_obj.selected['1d'].indices;
var d1 = cb_obj.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.trigger('change');
table.trigger('change');
""")
##having 'table' in the layout, stops the callback from working, deleting table from the layout makes it work.
layout = row(p1, p2, table)
show(layout)
Here's a working version for bokeh 1.0.4
from random import random
import bokeh.io
from bokeh.io import output_notebook, show
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.plotting import figure
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.resources import INLINE
bokeh.io.output_notebook(INLINE)
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)
s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(plot_width=400, plot_height=400, x_range=(0, 1), y_range=(0, 1),
tools="", title="Watch Here")
p2.circle('x', 'y', source=s2, alpha=0.6)
columns = [TableColumn(field ="x", title = "X axis"),
TableColumn(field ="y", title = "Y axis")]
table = DataTable(source =s2, columns = columns, width =155, height = 380)
s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2, table=table), code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (var i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.change.emit();
table.change.emit();
""")
)
layout = row(p1, p2, table)
show(layout)
You can even select rows/values in the table and the points in the second plot will change opacity (and you can sort the table by clicking the headers)
Right now, your callback doesn't know, what "table" is. You need to pass it as an argument to your CustomJS function:
s1.callback = CustomJS(args=dict(s2=s2, table=table), code="""
var inds = cb_obj.selected['1d'].indices;
var d1 = cb_obj.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.trigger('change');
table.trigger('change');
""")
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 ;)