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
Related
I am new to python and want to make an interactive chart with a dataset given in class. I want to be able to simply display the data associated with the point I select with the TapTool in Bokeh. The data is too dense to jsut simply use the hovertool though. It seems the hovertool did not require javascript, but apparently getting the tap tool requires knowing a whole other coding language. I am not familiar with python as it is, so adding a javascript callback is making things a bit complicated.
Right now I have this figure plotted with my dataframe using data from the California Housing dataset from sci-kit. I also have the US_states sampledata from bokeh to plot too.
from bokeh.layouts import layout
from bokeh.plotting import figure, show, output_file
from bokeh.models import Circle, Div, ColumnDataSource, CustomJS, MultiChoice, HoverTool, LinearColorMapper, ColorBar
from bokeh.io import output_notebook
from bokeh.sampledata import us_states
from bokeh.transform import transform
from bokeh.models.tools import *
#US state sample data
us_states = us_states.data.copy()
us_states = us_states["CA"]
#Creating figure and adding california basemap
color = LinearColorMapper(palette = 'Viridis256',
low = df.MedInc.min(),
high = df.MedInc.max())
color2 = LinearColorMapper(palette = 'Inferno256',
low = df.price.min(),
high = df.price.max())
cal = figure(title = "California Housing Data Geographic Distribution",
plot_width = 1000)
cal.circle('Longitude','Latitude',source = geo_source,
color= transform('MedInc', color),size =2,
alpha = 0.2,legend_label = "Median Income")
cal.circle('Longitude','Latitude',source = geo_source,
color= transform('price', color2),size =1,
alpha = 0.2,legend_label = "House Price")
color_bar = ColorBar(color_mapper = color,
label_standoff = 14,
location = (0,0),
title = 'Median Income')
color_bar2 = ColorBar(color_mapper = color2,
label_standoff = 14,
location = (0,0),
title = 'House Price')
cal.add_layout(color_bar,'right')
cal.add_layout(color_bar2,'right')
cal.legend.location = "top_right"
cal.legend.click_policy="hide"
I have no clue what to write for the JS callback for the TapTool though since I'm not really sure how it works, how the variables transfer, etc.
callback = CustomJS(args=dict(source=geo_source), code="""
//Insert JS code here
""")
taptool = TapTool(callback=callback)
geo_source.selected.js_on_change('indices', callback)
cal.add_tools(taptool)
you should add figure to Tap event
plot.on_event(Tap, function)
and in function you can use indices to update your data/graph
def function():
print(source.selected.indices)
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.
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.
I've tried around using Bokeh, and now I want to search a word and change the color of its glyph. My code looks like that:
import bokeh.plotting as bp
from bokeh.models import HoverTool, CustomJS
from bokeh.models.widgets import TextInput
from bokeh.io import vform
words = ["werner", "herbert", "klaus"]
x=[1,2,3]
y=[1,2,3]
color = ['green', 'blue', 'red']
word_input= TextInput(value="word", title="Point out a word")
source = bp.ColumnDataSource(data= dict(x=x,y=y,words=words, color='color'))
hover= HoverTool(tooltips=[("word", "#words")])
# output to static HTML file (with CDN resources)
bp.output_file("plot.html", mode="cdn")
# create a new plot with the tools above, and explicit ranges
p = bp.figure(plot_height = 600, plot_width = 800, title="word2vec", tools=[hover], logo =None)
# add a circle renderer with vectorized colors and sizes
p.circle('x','y', radius= 0.1, color = color, source=source, line_color=None)
callback= CustomJS(args=dict(source=source), code ="""
var data = source.get('data');
var glyph = cb_obj.get('value')
words = data['words']
colors=data['color']
for (i=0; i< words.length;i++){
if(glyph==words[i]){colors[i]='yellow'}
}
source.trigger('change');
""")
layout = vform(word_input, p)
# show the results
bp.show(layout)
This code just doesn't work, and I can't figure out why not.
What am I doing wrong? I've posted an other question earlier that day and this is kind of a first step solving it.
there are a couple of problems you have:
you create the CallbackJS but never set is as the callback property of the TextInput
you set the color key of data dict to the string "color" not to the list of colors
you passed the actual list of colors as the color argument to figure (it should be the string name of the data source column you want to use, e.g. "color")
Here is a version that works:
from bokeh.io import vform
from bokeh.models import HoverTool, CustomJS
from bokeh.models.widgets import TextInput
from bokeh.plotting import output_file, figure, show, ColumnDataSource
output_file("plot.html")
words = ["werner", "herbert", "klaus"]
x, y = [1,2,3], [1,2,3]
color = ['green', 'blue', 'red']
source = ColumnDataSource(data=dict(x=x, y=y, words=words, color=color))
hover = HoverTool(tooltips=[("word", "#words")])
p = figure(plot_height=600, plot_width=800, title="word2vec", tools=[hover])
p.circle('x','y', radius=0.1, fill_color='color', source=source, line_color=None)
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data')
var value = cb_obj.get('value')
var words = data['words']
for (i=0; i < words.length; i++) {
if ( words[i]==value ) { data.color[i]='yellow' }
}
source.trigger('change')
""")
word_input = TextInput(value="word", title="Point out a word", callback=callback)
layout = vform(word_input, p)
show(layout)
Updated #bigreddot's solution to work under Bokeh 2.2.3 if others come along similar tasks.
from bokeh.layouts import column
from bokeh.models import HoverTool, CustomJS
from bokeh.models.widgets import TextInput
from bokeh.plotting import output_file, figure, show, ColumnDataSource
output_file("plot.html")
words = ["werner", "herbert", "klaus"]
x, y = [1,2,3], [1,2,3]
color = ['green', 'blue', 'red']
source = ColumnDataSource(data=dict(x=x, y=y, words=words, color=color))
hover = HoverTool(tooltips=[("word", "#words")])
p = figure(plot_height=600, plot_width=800, title="word2vec", tools=[hover])
p.circle('x','y', radius=0.1, fill_color='color', source=source, line_color=None)
callback = CustomJS(args=dict(source=source), code="""
var data = source.data;
var value = cb_obj.value;
var words = data['words'];
for (var i=0; i < words.length; i++) {
if ( words[i]==value ) { data.color[i]='yellow' }
}
source.change.emit();
""")
word_input = TextInput(value="word", title="Point out a word")
word_input.js_on_change('value', callback)
layout = column(word_input, p)
show(layout)
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))