Python Bokeh CustomJS: Debugging a JavaScript callback for the Taping-Tool - javascript

I am working with Python 3.6.2 and Bokeh 1.0.4 to create a custom JavaScript callback in my plot.
By tapping on one of the points in the plot, I'd like all points sharing the same attribute in the id-column to be highlighted.
Iterating over all datapoints with JavaScript and manipulating the respective 'selected'-attribute in the ColumnDataSource-object should do the trick.
Unfortunately I can not figure out how to correct this code.
# Import packages
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
# Create the data for the points
x = [0, 1, 2, 3]
y = [0, 1, 0, 1]
ids = ['A','B','A','B']
data = {'x':x, 'y':y, 'id':ids}
source = ColumnDataSource(data)
# Add tools to the plot
tap = TapTool()
hover = HoverTool(tooltips=[("X", "#x"),
("Y", "#y"),
("ID", "#id")])
# Create a plotting figure
p = figure(plot_width=400, plot_height=400, tools=[tap,hover])
# Code for the callback
code = """
// Set column name to select similar glyphs
var column = 'id';
// Get data from ColumnDataSource
var data = source.data;
// Get indices array of all selected items
var selected = source.selected.indices;
// Array to store glyph-indices to highlight
var select_inds = [];
// Check if only a single glyph is selected
if(selected.length==1){
// Get the value of the column to find similar attributes/glyphs
attribute_value = data[column][selected[0]];
// Iterate over all entries in the ColumnDataSource
for (var i=0; i<data[column].length; ++i){
// Check if items have the same attribute
if(data[column][i]==attribute_value){
// Add index to selected list
select_inds.push(i);
}
}
}
// Set selected glyphs in ColumnDataSource
source.selected.indices = select_inds;
// Save changes to ColumnDataSource
source.change.emit();
"""
# Create a CustomJS callback with the code and the data
callback = CustomJS(args={'source':source}, code=code)
# Add the callback to the ColumnDataSource
source.callback=callback
# Plots circles
p.circle('x', 'y', source=source, size=25, color='blue', alpha=1, hover_color='black', hover_alpha=1)
# Show plot
show(p)
An older version of this problem with Bokeh 0.13.0 could not solve my problem.

You were almost there! The callback has to be added to the TapTool instead of the ColumnDataSource.
# Import packages
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
# Create the data for the points
x = [0, 1, 2, 3]
y = [0, 1, 0, 1]
ids = ['A','B','A','B']
# Generate data source for the visualization
data = {'x':x, 'y':y, 'id':ids}
source = ColumnDataSource(data)
# Add tools to the plot
tap = TapTool()
hover = HoverTool(tooltips=[("X", "#x"),
("Y", "#y"),
("ID", "#id")])
# Create a plotting figure
p = figure(plot_width=400, plot_height=400, tools=[tap,hover])
# Code for the callback
code = """
// Set column name to select similar glyphs
var column = 'id';
// Get data from ColumnDataSource
var data = source.data;
// Get indices array of all selected items
var selected = source.selected.indices;
// Array to store glyph-indices to highlight
var select_inds = [];
// Check if only a single glyph is selected
if(selected.length==1){
// Get the value of the column to find similar attributes/glyphs
var attribute_value = data[column][selected[0]];
// Iterate over all entries in the ColumnDataSource
for (var i=0; i<data[column].length; ++i){
// Check if items have the same attribute
if(data[column][i]==attribute_value){
// Add index to selected list
select_inds.push(i);
}
}
}
// Set selected glyphs in ColumnDataSource
source.selected.indices = select_inds;
// Save changes to ColumnDataSource
source.change.emit();
"""
# Create a CustomJS callback with the code and the data
callback = CustomJS(args={'source':source}, code=code)
# Add the callback to the taptool
tap.callback=callback
# Plots circles
p.circle('x', 'y', source=source, size=25, color='blue', alpha=1, hover_color='black', hover_alpha=1)
# Show plot
show(p)

Related

Implementing JavaScript callback for checkboxes in bokeh?

Short version: bokeh checkboxes with JS callbacks to plot subsets of a dataframe?
Longer version: The answer here gives a good explanation for multiselect, but I want to do the same thing for checkboxes. All my data is in one pandas dataframe, and I'm using the checkboxes to select bits of that dataframe for plotting. I believe that with checkboxes it is something to do with cb_obj.active but I'm very unsure how to get it to work. In particular my plot includes colored rectangles and text, all information for which: position, text, color, is taken from my dataframe. And I don't know how much plotting must be done in the callback, and how much outside.
As far as I can tell, the callback calls a function to do the actual plotting.
I know I should give a minimal example, but I can't think how to simplify my code enough... so all I really want at the moment is an example of the use of checkboxes, with a JavaScript callback, to plot a subset of a dataframe. Somebody must have done this, but I haven't found an example yet! Thanks.
Here is an example:
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import Slider, CheckboxGroup, CustomJS, ColumnDataSource, CDSView
from bokeh.models.filters import CustomJSFilter
from bokeh.layouts import row
from bokeh.transform import factor_cmap
from bokeh.palettes import Category10_10
output_notebook()
You can use CustomJSFilter to calculate the indice of rows to show:
from bokeh.sampledata import iris
source = ColumnDataSource(data=iris.flowers)
species = iris.flowers.species.unique().tolist()
checkboxes = CheckboxGroup(labels=species, active=list(range(len(species))))
fig = figure()
filter = CustomJSFilter(code="""
let selected = checkboxes.active.map(i=>checkboxes.labels[i]);
let indices = [];
let column = source.data.species;
for(let i=0; i<column.length; i++){
if(selected.includes(column[i])){
indices.push(i);
}
}
return indices;
""", args=dict(checkboxes=checkboxes, source=source))
checkboxes.js_on_change("active", CustomJS(code="source.change.emit();", args=dict(source=source)))
fig.scatter("sepal_length", "sepal_width",
color=factor_cmap("species", Category10_10, species),
source=source, view=CDSView(source=source, filters=[filter]))
show(row(checkboxes, fig))
Here's an adapted version of my answer for MultiSelect that you referred to:
from bokeh.models import CustomJS, ColumnDataSource, CheckboxGroup, Column
from bokeh.plotting import figure, show
import pandas as pd
data = dict(letter = ['A','A','B','C','B','B','A','C','C','B'],
x = [1, 2, 1, 2, 3, 2, 2, 3, 2, 3],
y = ['10','20','10','30','10','40','10','30','10','40'])
data = pd.DataFrame(data)
data_source = ColumnDataSource(data)
source = ColumnDataSource(dict(x = [], y = []))
plot = figure()
plot.circle('x', 'y', line_width = 2, source = source)
callback = CustomJS(args = {'source': source, 'data_source': data_source},
code = """
var data = data_source.data;
var s_data = source.data;
var letter = data['letter'];
var select_vals = cb_obj.active.map(x => cb_obj.labels[x]);
console.log(select_vals);
var x_data = data['x'];
var y_data = data['y'];
var x = s_data['x'];
x.length = 0;
var y = s_data['y'];
y.length = 0;
for (var i = 0; i < x_data.length; i++) {
if (select_vals.indexOf(letter[i]) >= 0) {
x.push(x_data[i]);
y.push(y_data[i]);
}
}
source.change.emit();
console.log("callback completed");
""")
chkbxgrp = CheckboxGroup(labels = ['A', 'B', 'C'], active=[])
chkbxgrp.js_on_change('active', callback)
layout = Column(chkbxgrp, plot)
show(layout)
Remarks:
The callback won't work in Internet Explorer because it uses arrow functions, which IE does not support. If that is an issue, you need
do the mapping using something other than arrow functions
as user bigreddot commented in the answer you referred to, this could also be done using CDSView using a custom Filter, as GroupFilter does not support multiple values

Showing cropped image in bokeh

I am showing a picture in a figure in bokeh and am using the BoxSelectTool in order to draw a rectangle.
box_select = BoxSelectTool(callback=callback)
p2 = figure(x_range=(0,700), y_range=(0,500),plot_width=1100,plot_height=1100,tools=[box_select])
p2.image_url( url='url',
x=1, y=1, w=700, h=500, anchor="bottom_left",source=im_src)
rect_source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))
callback = CustomJS(args=dict(rect_source=rect_source), code="""
// get data source from Callback args
var data = rect_source.data;
/// get BoxSelectTool dimensions from cb_data parameter of Callback
var geometry = cb_data['geometry'];
/// calculate Rect attributes
var width = geometry['x1'] - geometry['x0'];
var height = geometry['y1'] - geometry['y0'];
var x = geometry['x0'] + width/2;
var y = geometry['y0'] + height/2;
/// update data source with new Rect attributes
data['x'].push(x);
data['y'].push(y);
data['width'].push(width);
data['height'].push(height);
rect_source.data = data;
rect_source.change.emit();
'''
Now I want to show that image region as cropped in a different, smaller figure, after the rectangle is drawn, without clicking a button or anything:
d2 = figure(x_range=(0,200), y_range=(0,100),plot_width=200,plot_height=100)
d2.image( image='image',
x=1, y=1, dw=100, dh=100, source=img)
img = ColumnDataSource( data=dict(image=[]))
So I need something like this in JS:
tmp_im = cv2.imread('static/' + str(im_nr) + '.jpg')
tmp_im = tmp_im[geometry['y0']:geometry['y1'],geometry['x0']:geometry['x1']]
tmp_im = cv2.cvtColor(tmp_im, cv2.COLOR_BGR2GRAY)
img.data = dict(image=[tmp_im])
How can I do that in JS + bokeh?
I suggest to use the module holoviews (part of the pyviz ecosystem) for this task, which provides a high-level access to bokeh.
Holoviews provides so called streams, which can be used together with DynamicMaps to generate dynamic figures based on the (changing) values of the stream.
The module panel (also part of the pyviz ecosystem) can be used to define layouts for visualization.
import numpy as np
import holoviews as hv
from holoviews import opts
from holoviews.streams import BoundsXY
import panel as pn
pn.extension() # loading the panel extension for use with notebook
opts.defaults(opts.Image(tools=['box_select'])) # making sure, that box_select is available
minval, maxval = 0, 200
# x-y data
ls = np.linspace(minval, 10, maxval)
xx, yy = np.meshgrid(ls, ls)
# z-data, e.g. intensity
zz = xx*yy
# min and max, later used to recalibrate the colormapping
zzmin = zz.min()
zzmax = zz.max()
bounds=(0,0, 1,1) # bounds used for the image
im = hv.Image(zz, bounds=bounds)
# stream, xy-data are provided by the box_select-tool
# As start values the same bounds as for the image are used.
box = BoundsXY(bounds=bounds)
# The box-stream is used to draw a rectangle dynamically
# based on the current selection using the box_select-tool.
rect = hv.DynamicMap(
lambda bounds: hv.Bounds(bounds),
streams=[box])
# The box-stream is used to draw an image dynamically
# based on the current selection using the box_select-tool.
im_select = hv.DynamicMap(
lambda bounds: im[bounds[0]:bounds[2],bounds[1]:bounds[3]],
streams=[box])
# Arranging the layout.
# With redim.range we make sure the colormapping uses the original min- and max-values as in 'im',
# and not the min- and max-values from 'im_select'.
layout = pn.Row(im * rect \
+\
im_select.redim.range(z=(zzmin, zzmax)))
layout.app()

Adding widgets to toggle lines in all Bokeh subplots

So I'm trying to create a 6x6 grid of of plots where each plot has multiple lines. I want to add Bokeh's CheckboxGroup widget to be able to toggle lines on and off for all of the plots. I'm having trouble figuring out how to link the widget to all plots. When I run this code the widget & all plots I am able to only toggle for the last plot. Any suggestions?
# create a list of subplots to iterate over
ps = [figure(background_fill_color='#DFDFE5', plot_width=200,
plot_height=200) for i in range(36)]
for i in range(len(ps)):
# link the range of first plot with every other plot
ps[i].x_range = ps[0].x_range
ps[i].y_range = ps[0].y_range
# axes labels
ps[i].yaxis.axis_label = 'amplitude'
ps[i].xaxis.axis_label = 'age'
# plot data -- xaxis_arr & na are my data arrays
a = ps[i].line(xaxis_arr, na[0][i][2], line_width=2, color='#1f77b4')
b = ps[i].line(xaxis_arr, na[1][i][2], line_width=2, color='#ff7f0e')
c = ps[i].line(xaxis_arr, na[2][i][2], line_width=2, color='#2ca02c')
ps[i].title.text = i
customJScode = """
console.log(cb_obj.active);
line0.visible = false;
line1.visible = false;
line2.visible = false;
for (i in cb_obj.active) {
//console.log(cb_obj.active[i]);
if (cb_obj.active[i] == 0) {
line0.visible = true;
} else if (cb_obj.active[i] == 1) {
line1.visible = true;
} else if (cb_obj.active[i] == 2) {
line2.visible = true;
}
}
"""
callback = CustomJS(code=customJScode, args={} )
checkbox = CheckboxGroup(labels=["toggleLine1", "toggleLine2", "toggleLine3"],
active=[0,1,2], callback=callback)
callback.args = dict(line0=a, line1=b, line2=c, checkbox=checkbox)
myplots = gridplot(ps, ncols=6)
layout = column(myplots, widgetbox(checkbox))
show(layout)
At each step in your for loop, you are overwriting the callback attribute of your checkbox with a new code, and new arguments, that's why it works only for your last line.
In your for loop, you should fill lists of lines so that you can pass them all as arguments to your CustomJS callback, after the loop. Then you need your customJS code to handle all the lines at once.
Here is an example:
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import CheckboxGroup, CustomJS
from bokeh.layouts import gridplot
import numpy as np
figlist = [figure(title='Figure '+str(i),plot_width=200,plot_height=200) for i in range(6)]
x=np.arange(10)
linelist=[]
for fig in figlist:
line0 = fig.line(x,x,color='blue')
line1 = fig.line(x,x[::-1],color='red')
line2 = fig.line(x,x**2,color='green')
linelist+=[[line0,line1,line2]]
checkbox = CheckboxGroup(labels=['line0','line1','line2'],active=range(3))
iterable = [elem for part in [[('_'.join(['line',str(figid),str(lineid)]),line) for lineid,line in enumerate(elem)] for figid,elem in enumerate(linelist)] for elem in part]
checkbox_code = ''.join([elem[0]+'.visible=checkbox.active.includes('+elem[0].split('_')[-1]+');' for elem in iterable])
checkbox.callback = CustomJS(args={key:value for key,value in iterable+[('checkbox',checkbox)]}, code=checkbox_code)
grid = gridplot([figlist[:3]+[checkbox],figlist[3:]])
show(grid)
I also have more complex example code that use the line visibility toggle a lot, on my bitbucket repository
You do not need to pass the checkbox as an argument to its own callback (you can just use cb_obj), it's just that I often reuse the same "iterable" and "checkbox_code" for other widgets that can need it.

Interactive Slider using Bokeh

I'm trying to use a bokeh interactive slider to modify the contents of a plot, similar the example here. I have a two nested lists x and y.
I simply want the slider to change the index of the lists to plot. i.e. If the slider index = 0, then plot x[0] vs y[0], if the slider index is 1, plot x[1] vs y[1], etc...
The documentation example computes the new data on the fly, which is not feasible for the data that I need to work with.
When I run the code below, nothing shows up in the plot... I don't know javascript, so I'm guessing this is where I'm going wrong.
I'm running Python 3.5 and Bokeh 0.12. This is all run within a jupyter-notebook.
import numpy as np
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, show
from bokeh.io import output_notebook
from bokeh.resources import INLINE
output_notebook(INLINE)
x = [[x*0.05 for x in range(0, 500)],
[x*0.05 for x in range(0, 500)]]
y = [np.sin(x[0]),
np.cos(x[1])]
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=400, plot_height=400)
plot.line('x'[0], 'y'[0], source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value');
x = data['x'][f];
y = data['y'][f];
source.trigger('change');
""")
slider = Slider(start=0, end=1, value=0, step=1, title="index", callback=callback)
layout = row(plot, slider)
show(layout)
Instead of having a slider changing the index of the data to be plotted, you could define two ColumnDataSources: source_visible and source_available where the first one holds the data that is currently being shown in the plot and the second one acts as a data repository from where we can sample data in CustomJS callback based on user selection on the web page:
import numpy as np
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, Slider, CustomJS
from bokeh.plotting import Figure, show
# Define data
x = [x*0.05 for x in range(0, 500)]
trigonometric_functions = {
'0': np.sin(x),
'1': np.cos(x),
'2': np.tan(x),
'3': np.arctan(x)}
initial_function = '0'
# Wrap the data in two ColumnDataSources
source_visible = ColumnDataSource(data=dict(
x=x, y=trigonometric_functions[initial_function]))
source_available = ColumnDataSource(data=trigonometric_functions)
# Define plot elements
plot = Figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source_visible, line_width=3, line_alpha=0.6)
slider = Slider(title='Trigonometric function',
value=int(initial_function),
start=np.min([int(i) for i in trigonometric_functions.keys()]),
end=np.max([int(i) for i in trigonometric_functions.keys()]),
step=1)
# Define CustomJS callback, which updates the plot based on selected function
# by updating the source_visible ColumnDataSource.
slider.callback = CustomJS(
args=dict(source_visible=source_visible,
source_available=source_available), code="""
var selected_function = cb_obj.value;
// Get the data from the data sources
var data_visible = source_visible.data;
var data_available = source_available.data;
// Change y-axis data according to the selected value
data_visible.y = data_available[selected_function];
// Update the plot
source_visible.change.emit();
""")
layout = row(plot, slider)
show(layout)
Keep in mind that if your data is large, it might take a while to send it all at once to the client's browser.

Dependent Sliders with Bokeh, how to write the callbacks

I want to be able to slide through many plots which are the result of simulations accross 3+ dimensions. I am using the Bokeh package via Python.
For simplicity, let's assume I have two dimensions : d, and nc. But nc depends on d in the following way:
if d=100, nc=56,57
if d=20, nc=5,6
And I have 4 pictures:
d_100_nc_56.png,
d_100_nc_57.png,
d_20_nc_5.png,
d_20_nc_6.png
So I want two sliders, one for d, and one for nc, to cycle the .png images through the image_url function of Bokeh.plotting.Figure . However, the value of the nc slider should update itself as I change the slider in d
from bokeh.io import vform
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, output_file, show
output_file('image.html')
source = ColumnDataSource(data=dict(url=['d_100_nc_55.png']))
p = Figure(x_range=(0,1), y_range=(0,1))
callback_nc = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
old = data['url'][0]
to_replace=old.substring(old.lastIndexOf("nc_")+3,old.lastIndexOf(".png"))
data['url'][0] = old.replace(to_replace,f.toString(10))
source.trigger('change');
""")
callback_d = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
old = data['url'][0]
to_replace=old.substring(old.lastIndexOf("d_")+2,old.lastIndexOf("_nc_"))
data['url'][0] = old.replace(to_replace,f.toString(10))
source.trigger('change');
""")
p.image_url('url',source=source, x=0, y=1,w=1,h=1)
p.text(x=0,y=0,text=source.data['url'])
slider_nc = Slider(start=55, end=65, value=1, step=1, title="nc", callback=callback_nc)
slider_d = Slider(start=20, end=100, value=100, step=80, title="density", callback=callback_d)
layout = vform(slider_nc,slider_d, p)
show(layout)
However, I do not know how to pass the d slider as an argument to the nc slider to fetch its properties and update them on the fly. Is this possible ? Otherwise it limits the use of multiple sliders through bokeh quite substantially.
Edit: updated for more recent versions
You pass the slider the same way you pass source, as an item in the args dictionary. Any Python-side Bokeh model you pass there is automatically made available to the callback. Then, BokehJS model properties exactly match the python properties described in the reference guide Here is an example that updates one slider based off another:
# Example from Bokeh 0.12.x
from bokeh.plotting import show, output_file
from bokeh.layouts import column
from bokeh.models import CustomJS, Slider
s1 = Slider(start=1, end=10, value=1, step=1)
s2 = Slider(start=0, end=1, value=0, step=1)
s1.callback = CustomJS(args=dict(s1=s1, s2=s2), code="""
s2.end = s1.value;
""")
output_file("foo.html")
show(column(s1,s2))

Categories

Resources