Given the following bokeh chart (this code must be run in a jupyter notebook):
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[0, 1])
show(column([p, checkbox_button_group])) # show the results
Which produces a chart like this:
How can I connect up the checkbox buttons so that they show/hide the relevant series on the plot?
Note:
I know that I can click the legend to achieve this effect. However, I want to plot more series than the legend can show (e.g. it only shows 13 series in the screenshot). Obviously, people will only have maybe 10 series shown at any one time otherwise it becomes hard to see information.
Here is my attempt. It feels clunky though, is there a better solution? Also, how can I call my callback automatically when the plot has loaded, so that series [0,1,2,3] only are made active?
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
series = []
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
series.append(p.line(x, y, line_width=1, legend=str(n), color=next(colors)))
p.legend.location = "top_left"
p.legend.click_policy="hide"
js = ""
for n in range(nseries):
js_ = """
if (checkbox.active.indexOf({n}) >-1) {{
l{n}.visible = true
}} else {{
l{n}.visible = false
}} """
js += js_.format(n=n)
callback = CustomJS(code=js, args={})
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[0,1,2,3], callback=callback)
callback.args = dict([('l{}'.format(n), series[n]) for n in range(nseries)])
callback.args['checkbox'] = checkbox_button_group
show(column([p, checkbox_button_group])) # show the results
Your solution is fine.
Here is a more compact js callback that relies on the line being numbered with their "name" attribute
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors), name=str(n))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[])
code = """
active = cb_obj.active;
rend_list = fig.renderers;
for (rend of rend_list) {
if (rend.name!==null) {
rend.visible = !active.includes(Number(rend.name));
}
}
"""
checkbox_button_group.callback = CustomJS(args={'fig':p},code=code)
show(column([p, checkbox_button_group])) # show the results
It's also useful if you want to hide groups of lines via keywords by having them share those in their "name" attribute
And here is how you can do it with the bokeh server:
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
line_list = []
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
line_list += [p.line(x, y, line_width=1, legend=str(n), color=next(colors))]
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[])
def update(attr,old,new):
for lineID,line in enumerate(line_list):
line.visible = lineID in new
checkbox_button_group.on_change('active',update)
curdoc().add_root(column([p, checkbox_button_group]))
def init_active():
checkbox_button_group.active = range(3)
curdoc().add_timeout_callback(init_active,1000)
Related
I'm trying to make a dropdown menu with Bokeh that highlights the points in clusters I found. I have the dropdown menu working, but now I want to be able to visualize another categorical variable by color: Noun Class with levels of Masc, Fem, and Neuter. The problem is that the legend won't update when I switch which cluster I'm visualizing. Furthermore, if the first cluster I visualize doesn't have all 3 noun classes in it, the code starts treating all the other clusters I try to look at as (incorrectly) having that first cluster's noun class. For example, if Cluster 0 is the default and only has Masc points, all other clusters I look at using the dropdown menu are treated as only having Masc points even if they have Fem or Neuter in the actual DF.
My main question is this: how can I update the legend such that it's only attending to the respective noun classes of 'Curr'
Here's some reproducible code:
import pandas as pd
from bokeh.io import output_file, show, output_notebook, save, push_notebook
from bokeh.models import ColumnDataSource, Select, DateRangeSlider, CustomJS
from bokeh.plotting import figure, Figure, show
from bokeh.models import CustomJS
from bokeh.layouts import row,column,layout
import random
import numpy as np
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper for noun classes
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .5, source=Overall) #plot all other points in gray
bokeh_p.circle(x='x_coord', y='y_coord', color=mapper, line_width = 1, source=Curr, legend_group = 'noun class') # plotting the desired cluster using glyph circle and colormapper
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)
Thanks in advance and I hope you have a nice day :)
Imma keep it real with y'all... The code works how I want now and I'm not entirely sure what I did. What I think I did was reset the noun classes in the Curr data source and then update the legend label field after selecting a new cluster to visualize and updating the xy coords. If anyone can confirm or correct me for posterity's sake I would appreciate it :)
Best!
import pandas as pd
import random
import numpy as np
from bokeh.plotting import figure, Figure, show
from bokeh.io import output_notebook, push_notebook, show, output_file, save
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
from bokeh.layouts import layout, gridplot, column, row
from bokeh.models import ColumnDataSource, Slider, CustomJS, Select, DateRangeSlider, Legend, LegendItem
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
sc.data['noun class'] =[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
bokeh_p.legend.label.field = sc.data['noun class'];
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper- sorry this was a thing that carried over from og code (fixed now)
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .05, source=Overall)
bokeh_p.circle(x = 'x_coord', y = 'y_coord', fill_color = mapper, line_color = mapper, source = Curr, legend_field = 'noun class')
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)
This sample is altered from the bokeh example, sliders can control the bars in the figure. (sliders.py)
This sample is altered from the bokeh example, sliders can control the bars in the figure. (sliders.py)
In my situation, there are more than 30 sliders on the left. It seems a little messy, so I am trying to use bokeh select element to connect sliders. The aim is the slider area will show only one slider when I select.
I read the document, and there are two ways to use:
One is disabled. If True, the widget will be greyed-out and not responsive to UI events. But it would not hide. Another is visible, but I got an attribute error:
AttributeError("unexpected attribute 'visible' to Slider, similar attributes are disabled")
Is it possible to make bokeh sliders hide or invisible? Or is there any other way to make sliders (more than 30) distinguish more clearly?
Here is the code which can run in Jupyter notebook
import bokeh.plotting.figure as bk_figure
from bokeh.io import curdoc, show
from bokeh.layouts import row, widgetbox
from bokeh.models import ColumnDataSource, Select
from bokeh.models.widgets import Slider, TextInput
from bokeh.io import output_notebook # enables plot interface in J notebook
import numpy as np
# init bokeh
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
output_notebook()
# Set up data
data = {
'line_x' : [1,2,3,4],
'line_y' : [4,3,2,1],
'bar_x':[1, 2, 3, 4],
'bar_bottom':[1,1,1,1],
'bar_top':[0.2, 2.5, 3.7, 4],
}
#bar_color
determine_top = data['bar_top']
determine_bottom = data['bar_bottom']
determine_color = []
for i in range(0,4):
if (determine_top[i] > determine_bottom[i]):
determine_color.append('#B3DE69') #green
else:
determine_color.append('firebrick')
i+=1
data['determine_colors'] = determine_color
source = ColumnDataSource(data=data)
# Set up plot
plot = bk_figure(plot_height=400, plot_width=400, title="test",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 10], y_range=[-5, 5])
plot.vbar(x='bar_x', width=0.5, top='bar_top',bottom='bar_bottom',
source=source, color='determine_colors')
# Set up widgets
select = Select(title="days_select:",
options=["d1_select", "d2_select", "d3_select", "d4_select"])
d1 = Slider(title="d1", value=0.0, start=-5.0, end=5.0, step=0.1)
d2 = Slider(title="d2", value=1.0, start=-5.0, end=5.0, step=0.1)
d3 = Slider(title="d3", value=0.0, start=0.0, end=2*np.pi)
d4 = Slider(title="d4", value=1.0, start=0.1, end=5.1, step=0.1)
# Set up callbacks
def update_data(attrname, old, new):
# Get the current slider values
d1_value = d1.value
d2_value = d2.value
d3_value = d3.value
d4_value = d4.value
##select
#if select.value == "d2_select":
# d3.disabled = True
# d4.visible = False
# Generate the new curve
new_data = {
'line_x' : [1,2,3,4],
'line_y' : [4,3,2,1],
'bar_x': [1, 2, 3, 4],
'bar_bottom':[d1_value, d2_value, d3_value, d4_value],
'bar_top':[0.2, d1_value, d2_value, d3_value],
}
#bar_color
determine_top = new_data['bar_top']
determine_bottom = new_data['bar_bottom']
determine_color = []
for i in range(0,4):
if (determine_top[i] > determine_bottom[i]):
determine_color.append('green') #green
else:
determine_color.append('red')
i+=1
new_data['determine_colors'] = determine_color
source.data = new_data
for w in [select, d1, d2, d3, d4]:
w.on_change('value', update_data)
# Set up layouts and add to document
layout = row(widgetbox(select, d1, d2, d3, d4), plot)
def modify_doc(doc):
doc.add_root(row(layout, width=800))
doc.title = "Sliders"
handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)
Thanks for any suggestions.
I tried to specify x and y axis ticks on Bokeh plot as [0,25,50,75,100] and try major_label_overrides x as distance {0:'alone(0)',25: 'not close(25)', 50: 'alright close(50)', 75: 'middle close(75)', 100:'very close(100)'}, y axis custom as frequency {0:'never',25: 'once a year', 50: 'once a month', 75: 'once a week', 100:'everyday(100)'}. However, it shows an error. Thank you.
ValueError: expected an instance of type Ticker, got [25, 50, 75, 100]
of type list
I have tried
p.xaxis.ticker = FixedTicker(ticks=[0, 25, 50,75,100])
it fixes the tick problem but I can't customise it to frequency.
Below is my code and github repository.
https://github.com/Lastget/Covid19_Job_risks.git
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Select
from bokeh.models import HoverTool, Label, LabelSet
from bokeh.io import curdoc
from bokeh.layouts import row
from bokeh.models.renderers import GlyphRenderer
from math import pi
from bokeh.models import FixedTicker
# Improt files
expose = pd.read_csv(r'Exposed_to_Disease_or_Infections.csv',encoding='gbk')
expose.head() #context, code, occupation
expose.shape #(968,3)
physical = pd.read_csv('Physical_Proximity.csv')
physical.head()
physical.shape #(967,3)
TW_job = pd.read_excel('Small_Chinese.xlsx',encoding='utf-8')
TW_job.shape #(968,3)
TW_job = TW_job.iloc[:,:2]
TW_job.head()
#Merge
temp_df = pd.merge(expose,physical,on=['Code','Occupation'])
temp_df.head()
temp_df.columns=['Expose_frequency','Code','Occupation','Physical_proximity']
temp_df.head()
full_table = temp_df.merge(TW_job,how='left',on='Code')
full_table.shape #967,5
# Delete Expose frequency for Rock splitter, timing deivce
full_table = full_table.iloc[:965,:]
# change expose to int64
full_table['Expose_frequency']=full_table['Expose_frequency'].astype('int64')
full_table.info()
# Start plotting
source = ColumnDataSource(full_table)
p = figure(title="各職業對新型冠狀病毒之風險圖", x_axis_label='工作時與人接近程度', y_axis_label='工作時暴露於疾病頻率',
plot_width=900, plot_height=600)
p.circle('Physical_proximity','Expose_frequency',
name = 'allcircle',
size=10,fill_alpha=0.2, source=source, fill_color='gray', hover_fill_color='firebrick', hover_line_color="firebrick", line_color=None)
hover = HoverTool(tooltips=[('職業','#TW_Occupation'),('Occupation','#Occupation'),('暴露於疾病指數','#Expose_frequency'),('與人接近距離指數','#Physical_proximity')])
p.add_tools(hover)
p.xaxis.ticker = [0, 25, 50,75,100]
p.xaxis.major_label_overrides = {0:'獨自工作(0)',25: '不近(25)', 50: '稍微近(50)', 75: '中等距離(75)', 100:'非常近(100)'}
p.yaxis.ticker = [0, 25, 50,75,100]
p.yaxis.major_label_overrides = {0:'從不(0)',25: '一年一次(25)', 50: '一個月一次(50)', 75: '一週一次(75)', 100:'每天(100)'}
p.yaxis.major_label_orientation = pi/4
# remove tool bar
p.toolbar.logo = None
p.toolbar_location = None
def remove_glyphs(figure, glyph_name_list):
renderers = figure.select(dict(type=GlyphRenderer))
for r in renderers:
if r.name in glyph_name_list:
col = r.glyph.y
r.data_source.data[col] = [np.nan] * len(r.data_source.data[col])
# Define a callback function
def update_plot(attr, old, new):
remove_glyphs(p,['point_select'])
old_choice=full_table[full_table['TW_Occupation']==old]
choice=full_table[full_table['TW_Occupation']==new]
a=choice['Physical_proximity']
b=choice['Expose_frequency']
p.circle(a,b,size=10,fill_alpha=1,fill_color=None,line_color="firebrick", name='point_select')
# Add Select
select = Select(title='請選擇工作', options=sorted(full_table['TW_Occupation'].tolist()), value='')
# Attach the update_plot callback to the 'value' property of select
select.on_change('value', update_plot)
#layout
layout = row(p, select)
# Add the plot to the current document
curdoc().add_root(layout)
I've set some charts up in Bokeh, which I'm generally very happy with on desktop screens.
However, when I open them on a mobile screen I have several functionality issues that makes them really horrible to use:
The hover functionality won't work (or at least is very buggy). When I click certain lines nothing happens and other times it recognizes that I'm clicking a completely difference part of the screen
There is a huge white space after each chart, when I embed the HTML in a site.
Code for my charts below (I embed the resulting HTML on a website).
Any advice?
Thanks!
### V3: Top Positive words
from bokeh.io import output_notebook, show, output_file, save, output
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool, Legend
from bokeh.embed import file_html
from google.colab import files
from bokeh.io import curdoc # removes old html runs # https://github.com/bokeh/bokeh/issues/5681
curdoc().clear()
output_notebook()
use = pos_tailor
# set out axes
x = 'time_rnd'
y = 'count'
# Add annotations
# set colour palette
col_brew = ['#8dd3c7','#ffffb3','#bebada','#fb8072','#80b1d3','#fdb462','#b3de69','#fccde5','#d9d9d9','#bc80bd','#ccebc5','#ffed6f']
# map out figure
plot = figure(tools='', x_axis_type='datetime', sizing_mode='scale_both', toolbar_location='above')
# add HoverTool
hover_info = [('time', '#hover_time'),
('word', '#word'),
('count', '#count')]
hover = HoverTool(names=['use'],tooltips=hover_info,
mode='mouse',
show_arrow=True
)
plot.add_tools(hover)
# FORMAT
plot.title.text = 'Positive Deep Dive: Key Callouts'
plot.title.align = "center"
plot.axis.minor_tick_in = -3
plot.axis.minor_tick_out = 6
plot.xaxis.axis_label = 'Time'
plot.xaxis.axis_label_standoff = 15
plot.yaxis.axis_label = 'Count'
plot.yaxis.major_label_text_color = "orange"
plot.yaxis.axis_label_standoff = 15
### FOR LOOP Data
# https://stackoverflow.com/questions/46730609/position-the-legend-outside-the-plot-area-with-bokeh
legend_it = []
for i in use:
df_eng_word = df_eng_timeline[df_eng_timeline['word']==i]
source = ColumnDataSource(df_eng_word)
c = plot.line(x, y, line_width = 3,
line_alpha = 0.5, line_color=col_brew[use.tolist().index(i)],
hover_line_alpha = 1.0,
hover_line_color = 'grey',
#hover_line_color = col_brew[top_wds.index(i)],
source = source, name = 'use'
)
# materialize the plot
show(plot)
# output html file
output_file("v2_postail.html")
save(plot)
files.download('v2_postail.html')
Based on the following code sample, I want to extract the data (for example the x value) in the CustomJS function to save it in the python list rect_data. Although the variable x is synchronized with the ColumnDataSource object source, the python list rect_data remains an empty list when I draw a rectangular selection in the figure of the executed code below. What am I doing wrong and how can I solve this problem?
Thank you in advance!
# You must first run "bokeh serve" to view this example
from bokeh.models import CustomJS, ColumnDataSource, BoxSelectTool, Range1d, Rect
from bokeh.plotting import figure, show
from bokeh.client import push_session
from bokeh.io import curdoc
source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var geometry = cb_data['geometry'];
var width = geometry['x1'] - geometry['x0'];
var height = geometry['y1'] - geometry['y0'];
var x = geometry['x0'] + width/2;
var y = geometry['y0'] + height/2;
data['x'].push(x);
data['y'].push(y);
data['width'].push(width);
data['height'].push(height);
source.trigger('change');
""")
box_select = BoxSelectTool(callback=callback)
p = figure(plot_width=400,
plot_height=400,
tools=[box_select],
title="Select Below",
x_range=Range1d(start=0.0, end=1.0),
y_range=Range1d(start=0.0, end=1.0))
rect = Rect(x='x',
y='y',
width='width',
height='height',
fill_alpha=0.3,
fill_color='#009933')
p.add_glyph(source, rect, selection_glyph=rect, nonselection_glyph=rect)
session = push_session(curdoc())
def update():
global rect_data
global source
rect_data = source.data['x']
print(rect_data)
curdoc().add_periodic_callback(update,10)
session.show()
session.loop_until_closed()
You can use the ToolEvents for this purpose. See example below.
from bokeh.plotting import figure
from bokeh.client import push_session
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource, CustomJS, BoxSelectTool, Range1d, Rect
source = ColumnDataSource(data=dict(x=[], y=[], width=[], height=[]))
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var geometry = cb_data['geometry'];
var width = geometry['x1'] - geometry['x0'];
var height = geometry['y1'] - geometry['y0'];
var x = geometry['x0'] + width/2;
var y = geometry['y0'] + height/2;
data['x'].push(x);
data['y'].push(y);
data['width'].push(width);
data['height'].push(height);
source.trigger('change');
""")
box_select = BoxSelectTool(callback=callback)
p = figure(plot_width=400,
plot_height=400,
tools=[box_select],
title="Select Below",
x_range=Range1d(start=0.0, end=1.0),
y_range=Range1d(start=0.0, end=1.0))
rect = Rect(x='x',
y='y',
width='width',
height='height',
fill_alpha=0.3,
fill_color='#009933')
p.add_glyph(source, rect, selection_glyph=rect, nonselection_glyph=rect, name="selectionbox")
session = push_session(curdoc())
def toolEventsCallback(attr, old, new):
print("callback", new)
x0 = new[0]['x0']
x1 = new[0]['x1']
print("x0=%f x1=%f" % (x0, x1))
p.tool_events.on_change("geometries", toolEventsCallback)
session.show()
session.loop_until_closed()
At least in Bokeh 0.11.1 there are no events send back to Python for the ColumnDataSource.