Filter DataTable Bokeh - python

I have a minimal sample below to support my question.
I have a collection of dataframes in a dictionary that I used to generate DataTable(s) and storage them in an array with the def fDP, then storage in dictionaries the panels and tabs in order to filter them later with the Select widget as in the example. It works fine still I would like to use another Select widget to filter the tabs even further, in that case by product. I tried few different approaches and none worked out, if someone could help me out please.
I added the second Select widget at the end of the code, to give a better idea what I have in mind.
d1 = {'table1' : [['Product1', 0], ['Product2',0]],
'table2': [['Product1', 1], ['Product2',1]],
'table3': [['Product1', 0], ['Product2',3]]}
dfs = {k:pd.DataFrame(v, columns=['Product', 'value']) for k, v in zip(d1.keys(), [d1[t] for t in d1.keys()])}
def fDP(df):
tables = []
for s in df.keys():
src = ColumnDataSource(df[s])
cls = [TableColumn(field=c, title=c) for c in df[s].columns]
dt = DataTable(source=src,columns=cls, width=600, height=200,editable=True)
tables.append(dt)
return tables
plist = list(dfs['table1']['Product'].unique())
tables1 = fDP(dfs)
panels1 = {t: Panel(child=p, title='') for t, p in zip(dfs.keys(), tables1)}
tabs1 = Tabs(tabs=[x for x in panels1.values()], align='start', width=10)
ls = [x for x in dfs.keys()]
sel1 = Select(title='Select Check:', align='start', value=ls[0], options=ls, width=195, margin = (15, 5, 0, 0))
colT = column([sel1, tabs1], spacing=-26)
sel1.js_on_change('value', CustomJS(args={'sel':sel1, 'tab':tabs1, 'diPanel':panels1}
,code='''
var sv = sel.value
tab.tabs = [diPanel[sv]]
'''))
show(colT)
selP = Select(title='Select Product:', align='start', value=plist[0], options=plist, width=195, margin = (15, 5, 0, 0))

The code below is an example how you could solve it (made for bokeh v2.1.1)
The basic idea is to pass the original dataframe to the callback, then filter that data based on current dropdown values and create a new data dictionary that you then assign to the table data source.
For more practical cases (more data) I would recommend using the dataframe-js JS library.
In that case you need to use Bokeh preamble or postamble template approach like explained in this post to be able to attach this library to your generated HTML. Having dataframe-js in your code allows you to use many basic Python Pandas functions transferred to JS domain and this is more than enough for what you need.
import pandas as pd
from bokeh.models import ColumnDataSource, DataTable, TableColumn, Tabs, Select, Panel, Row, Column, CustomJS
from bokeh.plotting import show
d1 = {'table1' : [['Product1', 11], ['Product2',12]],
'table2': [['Product1', 21], ['Product2',22]],
'table3': [['Product1', 31], ['Product2',32]]}
dfs = {k:pd.DataFrame(v, columns=['Product', 'value']) for k, v in zip(d1.keys(), [d1[t] for t in d1.keys()])}
def fDP(df):
tables = []
for s in df.keys():
src = ColumnDataSource(df[s])
cls = [TableColumn(field=c, title=c) for c in df[s].columns]
dt = DataTable(source=src,columns=cls, width=600, height=200,editable=True)
tables.append(dt)
return tables
plist = list(dfs['table1']['Product'].unique())
plist.insert(0, ' ')
tables1 = fDP(dfs)
panels1 = {t: Panel(child=p, title='') for t, p in zip(dfs.keys(), tables1)}
tabs1 = Tabs(tabs=[x for x in panels1.values()], align='start', width=10)
ls = [x for x in dfs.keys()]
sel1 = Select(title='Select Check:', align='start', value=ls[0], options=ls, width=195, margin = (15, 5, 0, 0))
selP = Select(title='Select Product:', align='start', value=plist[0], options=plist, width=195, margin = (15, 5, 0, 0))
colT = Column(Row(sel1, selP), tabs1, spacing=-26)
sel1.js_on_change('value', CustomJS(args={'sel':sel1, 'selP':selP, 'tab':tabs1, 'diPanel':panels1}
,code='''
var sv = sel.value
tab.tabs = [diPanel[sv]]
selP.value = ' '
'''))
ds = ColumnDataSource(d1)
selP.js_on_change('value', CustomJS(args={'selT':sel1, 'selP':selP, 'tab':tabs1, 'diPanel':panels1, 'ds': ds}
,code='''
var tb_name = selT.value
var p_name = selP.value
var c_tb_ds = diPanel[tb_name].child.source // table datasource to update
var c_tb_ds_data = ds.data[tb_name] // original data to work with
var new_data = {}
var p_index = null
var keys = Object.keys(c_tb_ds.data)
if (index > -1) {
keys.splice(index, 1); // 1 means remove one item only => index
}
for (var k in keys){
new_data[keys[k]] = []
}
for (var i in c_tb_ds_data) {
if(p_index == null) {
for (var j in keys) {
if(c_tb_ds_data[i][j] == p_name) {
if(c_tb_ds_data[i][j].includes('Product')) {
p_index = i
}
}
}
}
}
if(p_index != null) {
for (var j in keys) {
new_data[keys[j]].push(c_tb_ds_data[p_index][j])
}
}
c_tb_ds.data = new_data // update table data source'''))
show(colT)
Result:

Related

How do I make a bokeh DataTable respond to DoubleTap

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)

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))

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))

Multiple identical keys from Python dict to JSON

I am trying to create JSON object in Python, and it works just fine despite the fact that I can't get multiple keys with the same name - but I need to do it.
Here's a function:
findings = AutoTree()
findings['report']['numberOfConditions'] = num_cond
if r == 'Mammography':
f_temp = df['Relevant findings'].values.tolist()[0:8]
f_list = [x for i, x in enumerate(f_temp) if i == f_temp.index(x)]
f_num_total = len(f_list)
f_rand = random.randrange(1, f_num_total + 1)
iter_params_mass = ['shape', 'margin', 'density']
for i in range(num_cond):
br = get_birad(row, 2, 7)
cond = camelCase(get_cond_name())
findings[cond]['biRad'] = br
for k in range(f_rand + 1):
f = camelCase(random.choice(f_list))
#f = 'mass'
if f == 'mass':
rep_temp = create_rep(iter_params_mass, row, f, r)
findings[cond][f] = rep_temp
"""I also have a lot elif conditions, and it just grabs parameters."""
report = json.dumps(findings)
print(report)
Output:
{
"report":{
"id":85,
"name":"Lydia",
"age":39,
"relevantModality":"Mammography",
"numberOfConditions":2
},
"ductEctasia":{
"biRad":"birad[1]",
"calcifications":[
{
"typicallyBenign":"Vascular",
"suspiciousMorphology":"Coarse heterogeneous",
"distribution":"Diffuse"
}
],
"lymphNodes":[
{
"lymphNodes":"Lymph nodes \u2013 axillary"
}
]
}
}
And I want to have multiple "lymphNodes" and "calcifications" objects. Is it possible? Maybe, you can suggest another way to create JSON object, not nested dictionaries? The problem is that I need to create object respectively to random parameter chosen from the database.

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