How to pass the slider value in Bokeh back to Python code - python

I want to pass a slider value (that I've built with Bokeh) back to my Python code. The code generates 2 lines on a plot and allow me to alter the slope and intercept of one of them. But it fails when I introduce the callback javascript to pass the slider value as "ff" back into my Python code.
Can you help me with the callback syntax for getting the slider value back to python (eg see the print(ff) final line of the code) - I do want to do something more interesting than just print it out ultimately!
The error message from the callback is:
ValueError: expected an element of Dict(String, Instance(Model)), got {'my_dict': {'s': 0.5}}
My code is:-
from ipywidgets import interact
import numpy as np
from bokeh.io import push_notebook, show, output_notebook
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.callbacks import CustomJS
output_notebook()
x = np.linspace(0, 20, 200) # create equally spaced points.
s = 0.5 # slope.
i = 3 # intercept.
y = s * x + i # straight line.
my_dict = dict(s=s) # need to create a dict object to hold what gets passed in the callback.
callback = CustomJS(args=dict(my_dict=my_dict), code="""
var ff = cb_obj.value
my_dict.change.emit()
""")
// ff should be the slider value.
p = figure(title="simple line example", plot_height=300, plot_width=600, y_range=(-20,20),
background_fill_color='#efefef')
r = p.line(x, y, color="#8888cc", line_width=1.5, alpha=0.8) # 1st line. This line can be controlled by sliders.
q = p.line(x, 2*x+1.2, color="#0088cc", line_width=1.9, alpha=0.2) # 2nd line.
def update(w=s, a=i):
r.data_source.data['y'] = w * x + a # allow updates for the line r.
push_notebook()
show(p, notebook_handle=True)
interact(update, w=(-10,10), a=(-12,12) )
print(ff) # Return what the slider value is. I want ff accessible back in my python code.

I don't have Jupyter Notebook so these 2 examples are pure Bokeh apps, first one is using JS callback and the second one is using Python callback (Bokeh v1.0.4).
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, Slider, CustomJS, Range1d
from bokeh.layouts import column
slider_slope = Slider(start = 0, end = 1, value = 0.5, step = 0.1)
slider_intercept = Slider(start = 0, end = 20, value = 10, step = 1)
slider_code = ''' i = slider_intercept.value
s = slider_slope.value
x = r.data_source.data['x'];
y = [];
for (index = 0; index < x.length; index ++)
y.push((s * x[index]) + i);
r.data_source.data['y'] = y
r.data_source.change.emit(); '''
s = slider_slope.value # slope.
i = slider_intercept.value # intercept.
x = np.linspace(-40, 20, 200)
y = [(s * xx + i) for xx in x]
p = figure(title = "simple line example", plot_height = 500, plot_width = 600, y_range = Range1d(start = -80, end = 40), background_fill_color = '#efefef')
r = p.line(x, y, color = "red", line_width = 1.5, alpha = 0.8) # 1st line. This line can be controlled by sliders.
q = p.line(x, 2 * x + 1.2, color = "blue", line_width = 1.9, alpha = 0.2) # 2nd line.
slider_callback = CustomJS(args = dict(slider_slope = slider_slope,
slider_intercept = slider_intercept,
r = r), code = slider_code)
slider_slope.callback = slider_callback
slider_intercept.callback = slider_callback
layout = column(p, slider_slope, slider_intercept)
show(layout, notebook_handle = True)
You can easily translate it to Bokeh server app with Python callback:
import numpy as np
from bokeh.plotting import figure, show, curdoc
from bokeh.models import Slider, CustomJS
from bokeh.layouts import column
slider_slope = Slider(title = 'Slope', start = 0, end = 1, value = 0.5, step = 0.1)
slider_intercept = Slider(title = 'Intercept', start = 0, end = 20, value = 10, step = 1)
s = slider_slope.value # slope.
i = slider_intercept.value # intercept.
x = np.linspace(-40, 20, 200)
y = [(s * xx + i) for xx in x]
p = figure(title = "simple line example", plot_height = 500, plot_width = 600, y_range = Range1d(start = -80, end = 40), background_fill_color = '#efefef')
r = p.line(x, y, color = "red", line_width = 1.5, alpha = 0.8) # 1st line. This line can be controlled by sliders.
q = p.line(x, 2 * x + 1.2, color = "blue", line_width = 1.9, alpha = 0.2) # 2nd line.
def update(attr, old, new):
s = slider_slope.value # slope.
i = slider_intercept.value # intercept
x = r.data_source.data['x'];
y = []
for value in x:
y.append((s * value) + i)
r.data_source.data['y'] = y
slider_slope.on_change('value', update)
slider_intercept.on_change('value', update)
layout = column(p, slider_slope, slider_intercept)
curdoc().add_root(layout)
Result:

This is the solution. It creates a bokeh server app. It is run (from spyder) by using the file called: 20190328_start_bokeh_server.py
There is a straight line plotted and controlled by sliders. Clicking the button saves the slider values to a csv file.
To get the code below to run use this code (that's contained in 20190404_start_bokeh_server.py) in the console:
import os
os.chdir("C:\Users") # Change the working directory to be the script location.
os.system("start call bokeh serve --show 20190404_bokeh_server.py ") # Alternatively: This command can be typed into the anacondas prompt, once I've navigated to the directory holding the .py file.
"""
import pandas as pd
import numpy as np
from random import random
from numpy.random import randn
from bokeh.plotting import figure, show, curdoc
from bokeh.models import Slider, CustomJS, Range1d, Button
from bokeh.layouts import column
from bokeh.plotting import figure, curdoc
import os
slider_slope = Slider(title = 'Slope', start = 0, end = 1, value = 0.5, step = 0.1)
slider_intercept = Slider(title = 'Intercept', start = 0, end = 20, value = 10, step = 1)
s = slider_slope.value # slope.
i = slider_intercept.value # intercept.
x = np.linspace(-40, 20, 200)
y = [(s * xx + i) for xx in x]
p = figure(title = "simple line example", plot_height = 500, plot_width = 600, y_range = Range1d(start = -80, end = 40), background_fill_color = '#efefef')
r = p.line(x, y, color = "red", line_width = 1.5, alpha = 0.8) # 1st line. This line can be controlled by sliders.
q = p.line(x, 2 * x + 1.2, color = "blue", line_width = 1.9, alpha = 0.2) # 2nd line. This could be actuals.
def update(attr, old, new):
s = slider_slope.value # slope.
i = slider_intercept.value # intercept
x = r.data_source.data['x'];
y = []
for value in x:
y.append((s * value) + i)
r.data_source.data['y'] = y
# create a callback that will save the slider settings to a csv file when the button is clicked.
def callback():
os.chdir("C:\\Users") # Change the working directory to where I want to save the csv.
mydf = pd.DataFrame.from_dict({'slope':[0],'intercept':[0]}) # Create a DataFrame using pandas, based on a dictionary definition. Set the values to be 0 by default.
mydf.loc[0] = [slider_slope.value, slider_intercept.value] # Assign the first row to slope and intercept.
mydf.to_csv('slider.csv',index=True) # Write to the csv the final values of the button.
# add a button widget and configure with the call back
button = Button(label="Save slope and intercept to csv")
button.on_click(callback)
slider_slope.on_change('value', update)
slider_intercept.on_change('value', update)
layout = column(p, slider_slope, slider_intercept, button)
curdoc().add_root(layout)
show(layout, notebook_handle = True) # Launch the chart in the web browser.

Related

Dynamically change the coordinates and the text of annotation with slider in Bokeh plot

I have a Bokeh plot in which I have a slider. I want to change the coordinates of the line drawn with the slider, as shown in the screenshot of the figure. When I change the slider, the line changes its coordinates.
I tried using a slider widget with columndatasource. But, as I am new to Python, I cannot get to move the location and text of the label with the slider. Is there a way to do that?
My code is given below:
import math
import numpy as np
from bokeh.io import output_file
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider, Label, LabelSet
from bokeh.plotting import ColumnDataSource, figure, show
from bokeh.models import Arrow, OpenHead, NormalHead, VeeHead
theta = 0 #input the value here
theta = np.radians(-theta)
#Inputs to be made text boxes
sig_x = 10
# line
x=[1,1]
y=[-1,1]
x1=[1,1]
y1=[1,1]
I want to introduce a variable which will change with the slider also, which, for now is 10 here.
sig_1 = 10*sig_x
then i introduced dictionaries, and along with x=x, y=y the x1=x1, y1=y1.
source = ColumnDataSource(data=dict(x=x, y=y))
fig = figure(title = 'Test of Text Rotation',
plot_height = 300, plot_width = 300,
x_range = (-3,3), y_range=(-3,3),
toolbar_location = None)
I could not find a way to add label to the line, so I added layout (from tutorial example). However, unlike fig.line command, the 'x' and 'y' cannot be added as variables (pardon me if i do not use the right jargon).
citation = Label(x=1, y=1, text = str(sig_1))
fig.line('x', 'y',source=source, line_width = 2) # Main Stress block
fig.add_layout(citation)
amp_slider = Slider(start=0, end=360, value=theta, step=1, title="theta")
# Adding callback code,
callback = CustomJS(args=dict(source=source ,val=amp_slider),
code="""
const data = source.data;
var x = data['x'];
var y = data['y'];
var pi = Math.PI;
var theta = -1*(val.value) * (pi/180);
x[0]=(1*Math.cos(theta))-(1*Math.sin(theta)); // addition
x[1]=(1*Math.cos(theta))+(1*Math.sin(theta)); // addition
y[0]=(-1*Math.sin(theta))-(1*Math.cos(theta)); // addition
y[1]=(-1*Math.sin(theta))+(1*Math.cos(theta)); // addition
source.change.emit();
""")
amp_slider.js_on_change('value', callback)
layout = row(fig, column(amp_slider),)
show(layout)
I added the lines of x1[0]=(1*Math.cos(theta))-(1*Math.sin(theta)), x1[1]=(1*Math.cos(theta))+(1*Math.sin(theta));, y[0]=(-1*Math.sin(theta))-(1*Math.cos(theta)); and y[1]=(-1*Math.sin(theta))+(1*Math.cos(theta));
This code, as anticipated does not move the label along with the line. Any explanation of what i am doing wrong, and the possibility of doing it will be very helpful.
You can pass the Lable to the CustomJS-callback as well and modify the values of this model like you do with the ColumnDataSource. Don't forget to call lable.change.emit();.
See the complete example below.
import numpy as np
from bokeh.plotting import figure, show, output_notebook
from bokeh.layouts import row
from bokeh.models import CustomJS, Slider, Label, ColumnDataSource
output_notebook()
theta = 0 #input the value here
theta = np.radians(-theta)
#Inputs to be made text boxes
sig_x = 10
source = ColumnDataSource(data=dict(x=[1,1], y=[-1,1]))
fig = figure(
title = 'Test of Text Rotation',
plot_height = 300,
plot_width = 300,
x_range = (-3,3),
y_range=(-3,3),
toolbar_location = None
)
fig.line('x', 'y',source=source, line_width = 2)
citation = Label(x=1, y=1, text = str(10*sig_x))
fig.add_layout(citation)
amp_slider = Slider(start=0, end=360, value=theta, step=1, title="theta")
# Adding callback code
callback = CustomJS(args=dict(source=source ,val=amp_slider, lable=citation),
code="""
const data = source.data;
var x = data['x'];
var y = data['y'];
var pi = Math.PI;
var theta = -1*(val.value) * (pi/180);
x[0]=(1*Math.cos(theta))-(1*Math.sin(theta));
x[1]=(1*Math.cos(theta))+(1*Math.sin(theta));
y[0]=(-1*Math.sin(theta))-(1*Math.cos(theta));
y[1]=(-1*Math.sin(theta))+(1*Math.cos(theta));
source.change.emit();
lable['x'] = x[1]
lable['y'] = y[1]
lable.change.emit();
"""
)
amp_slider.js_on_change('value', callback)
layout = row(fig, amp_slider)
show(layout)
Result
If you want to modify the text of the lable, you can use a similar approach.

How do I change the color of scatter plot in bokeh real time?

I'm new in Bokeh, and I'm trying to construct an interactive scatter plot.
In this plot, I would like to change the p-value cut-off and, then, change the colors according to the rule (if FC < 0, the dots are blue; if FC > 0, dots are red.
I've tried:
from bokeh.plotting import ColumnDataSource, figure, output_file, show, curdoc
from bokeh.models import BoxSelectTool, LassoSelectTool,CustomJS, Slider
from bokeh.transform import linear_cmap
from bokeh.layouts import gridplot, column, row
import pandas as pd
import numpy as np
fc = np.random.uniform(0, 4, 1000)
p_value = np.random.uniform(0,1,1000)
df = pd.DataFrame([fc, p_value], ['FC', 'pvalue']).transpose()
df['log2(fc)'] = np.log2(df['FC'])
df['-log10(p)'] = -np.log10(df['pvalue'])
output_file("df.html")
y = df['-log10(p)']
x = df['log2(fc)']
col = np.where(y> -np.log10(0.5), np.where( x>-0, np.where( x<0, '#606060','#E4001B'), '#6194BC'), '#606060' )
source = ColumnDataSource(data=dict(
x=df['log2(fc)'],
y=df['-log10(p)'],
pvalue=df['pvalue'],
FC = df['log2(fc)'],
color = col))
TOOLTIPS = [
("FC", "#FC"),
("pvalue", "#pvalue")
]
TOOLS="hover,crosshair,pan,wheel_zoom,zoom_in,zoom_out,box_zoom,undo,redo,reset,tap,save,box_select,poly_select,lasso_select,"
p = figure(plot_width=500, plot_height=500, tooltips=TOOLTIPS,
title="test", tools=TOOLS)
p.circle('x', 'y', size=5, source=source, alpha = 0.5, fill_color = 'color', line_color='color')
p.xaxis.axis_label = "log2(FC)"
p.yaxis.axis_label = "-log10(p)"
p.background_fill_color = "#fafafa"
p_slider = Slider(start=0.0, end=5, step=.01, title="p_value")
callback = CustomJS(args=dict(source=source,plot=p, color_mapper = col, pvalue=p_slider),
code="""
const data = source.data;
var P = pvalue.value;
const x = df['p_value']
const y = df['log2(fc)']
source.change.emit();
""")
p_slider.js_on_change('value', callback)
layout = row(
p,
column(p_slider),
)
show(layout)
Despite this approach show me the bar to control the p-value, I'm not getting to control the bar, neither change the colors according to p-value...
Thanks for your help!

In python bokeh how to modify the field of a fill_color interactively without js?

I am trying to use bokeh to plot the iris data and modify the fill color of the circles interactively but I am running into a problem. I call the plot and the circle with the following:
plot = figure(plot_height=600, plot_width=1000, title="Iris Data",
x_axis_label = 'Sepal length (cm)',
y_axis_label = 'Sepal width (cm)',
tools = "crosshair, pan, reset, save, wheel_zoom")
plot_circle = plot.circle(x='sepal_length', y='sepal_width', source=source,
line_color=None, fill_color={'field':'petal_width','transform':color_mapper},
size='size', fill_alpha = 0.2)
which works but when I try to add the interactivity in the call back it is not clear to me how to modify the 'field' parameter in the fill_color argument to circle. I have tried this:
def update_bubble_color(attrname, old, new):
if new=='petal_width':
color_mapper.low = min(flowers['petal_width'])
color_mapper.high = max(flowers['petal_width'])
fill_color.field='petal_width'
return
if new=='petal_length':
color_mapper.low = min(flowers['petal_length'])
color_mapper.high = max(flowers['petal_length'])
fill_color.field='petal_length'
return
select_bubble_color.on_change('value', update_bubble_color)
the color mapper limits are handled correctly but the colors are not scaled according to the new choice. When I attempt to change it to petal_length with fill_color.field='petal_length' I get an "'name 'fill_color' is not defined" error.
Any help greatly appreciated!
Full code below for reference
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.models.widgets import Select
from bokeh.plotting import figure
# Load Data
from bokeh.sampledata.iris import flowers
# Global constants (even if python dies not like it)
min_bubble_size = 10
max_bubble_size = 90
def get_scaled_size(vector):
min_vector = min(vector)
max_vector = max(vector)
scaling = (max_bubble_size-min_bubble_size)/(max_vector-min_vector)
scaled_size = [ scaling*(item-min_vector) + min_bubble_size for item in vector]
return scaled_size
# Color Mapper
color_mapper = LinearColorMapper(palette='Inferno256',
low = min(flowers['petal_width']),
high = max(flowers['petal_width']) )
# Define source
flowers['size'] = get_scaled_size(flowers['petal_length'])
source = ColumnDataSource(flowers)
# Set up plot
plot = figure(plot_height=600, plot_width=1000, title="Iris Data",
x_axis_label = 'Sepal length (cm)',
y_axis_label = 'Sepal width (cm)',
tools = "crosshair, pan, reset, save, wheel_zoom")
plot_circle = plot.circle(x='sepal_length', y='sepal_width', source=source,
line_color=None, fill_color={'field':'petal_width','transform':color_mapper},
size='size', fill_alpha = 0.2)
# Set up widgets
select_bubble_size = Select(title ='Bubble size by', value='petal_width',
options = ['petal_width','petal_length'],
width = 200)
select_bubble_color = Select(title ='Bubble color by', value='petal_width',
options = ['petal_width', 'petal_length'],
width = 200)
# Colorbar
from bokeh.models import ColorBar
bar = ColorBar(color_mapper=color_mapper,location=(0,0))
plot.add_layout(bar, 'left')
# Set up callbacks=
# Bubble size call back
def update_bubble_size(attrname, old, new):
if new=='petal_width':
source.data['size'] = get_scaled_size(flowers['petal_width'])
return
if new=='petal_length':
source.data['size'] = get_scaled_size(flowers['petal_length'])
return
select_bubble_size.on_change('value', update_bubble_size)
# bubble color call back
def update_bubble_color(attrname, old, new):
if new=='petal_width':
color_mapper.low = min(flowers['petal_width'])
color_mapper.high = max(flowers['petal_width'])
fill_color.field='petal_width'
return
if new=='petal_length':
color_mapper.low = min(flowers['petal_length'])
color_mapper.high = max(flowers['petal_length'])
fill_color.field='petal_length'
return
select_bubble_color.on_change('value', update_bubble_color)
# Set up layouts and add to document
curdoc().add_root(column(plot, row(select_bubble_size,select_bubble_color), width=800))
curdoc().title = "Iris Data"
fill_color is a property of the glyph, you will need to access it through the glyph:
plot_circle.glyph.fill_color
In your script there is not free variable fill_color anywhere, which is the source of the NameError.

how to change chart type based on user slection radion button in bokeh

enter image description here I want to change bokeh chart at run time when click on a radio button. Here is what I've tried till now:
import numpy as np
import pandas as pd
from bokeh.core.properties import value
from bokeh.models.widgets import Paragraph,PreText,RadioButtonGroup
from bokeh.layouts import widgetbox
from bokeh.models.widgets import CheckboxGroup, RadioGroup
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource,LabelSet,CustomJS,Row
from bokeh.plotting import figure, show,save
from bokeh.transform import dodge
from bokeh.palettes import Viridis
colors = ["#c9d9d3", "#718dbf", "#e84d60"]
dataframe = pd.read_csv('Experience.csv')
source = ColumnDataSource(dataframe)
exp = dataframe['Experience']
ys = list(dataframe.keys())
ys.remove('Experience')
TOOLTIPS = [("Experience", "#Experience")]
p = figure(x_range=exp, y_range=(0, 100), plot_height=350, tools=['hover','save','reset','zoom_out','zoom_in','pan','box_zoom'],tooltips=TOOLTIPS)
stacked = p.vbar_stack(stackers=ys, x='Experience',color=colors,source=source,legend=[value(x) for x in ys],name=ys,width=0.5,)
colorList = Viridis[len(ys)]
labels = []
for y, offset, color in zip(ys, [-0.25, 0, 0.25], colorList):
bar = p.vbar(x=dodge('Experience', offset, range=p.x_range), top=y, width=0.2, source=source, legend=y + ' ', color=color)
bar.visible = False
radiogroup = RadioGroup(labels = ["StackedBar", "Bar"], active = 0,)
radiogroup.callback = CustomJS(args = dict(stacked = stacked, bar = bar), code = """
for (i in stacked)
stacked[i].visible = false;
bar.visible = false;
if (cb_obj.active == 0)
for (i in stacked)
stacked[i].visible = true;
else if (cb_obj.active == 1)
bar.visible = true; """)
layout = Row(p, radiogroup)
show(layout)
It is showing two graphs in one figure, but I want bar graph default and when I click on the radio button the graph should change based on click event.here is my full code..pl check and tell what i am doing wrong
The vbar_stack returns a list of glyphs so you need to toggle visibility each of them separately like this:
from bokeh.plotting import show, figure
from bokeh.models import RadioGroup, CustomJS, Row
from bokeh.models.sources import ColumnDataSource
import pandas as pd
data = {'fruits' : ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries'],
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
df = pd.DataFrame(data)
df['total'] = df.sum(axis = 1)
p = figure(x_range = data['fruits'], title = "Fruit Counts by Year", tools = "hover", tooltips = "$name #fruits: #$name")
vbar_stack = p.vbar_stack(["2015", "2016", "2017"], x = 'fruits', width = 0.9, color = ["#c9d9d3", "#718dbf", "#e84d60"], source = data)
vbar = p.vbar(x = 'fruits', width = 0.5, top = 'total', source = ColumnDataSource(df))
vbar.visible = False
radiogroup = RadioGroup(labels = ["StackedBar", "Bar"], active = 0,)
radiogroup.callback = CustomJS(args = dict(vbar_stack = vbar_stack, vbar = vbar), code = """
for (i in vbar_stack)
vbar_stack[i].visible = false;
vbar.visible = false;
if (cb_obj.active == 0)
for (i in vbar_stack)
vbar_stack[i].visible = true;
else if (cb_obj.active == 1)
vbar.visible = true; """)
layout = Row(p, radiogroup)
show(layout)
See below your code with some small corrections:
import os
import numpy as np
import pandas as pd
from bokeh.core.properties import value
from bokeh.models.widgets import Paragraph, PreText, RadioButtonGroup
from bokeh.layouts import widgetbox
from bokeh.models.widgets import CheckboxGroup, RadioGroup
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, LabelSet, CustomJS, Row
from bokeh.plotting import figure, show, save
from bokeh.transform import dodge
from bokeh.palettes import Viridis
colors = ["#c9d9d3", "#718dbf", "#e84d60"]
dataframe = pd.read_csv(os.path.join(os.path.dirname(__file__), 'Experience.csv'))
source = ColumnDataSource(dataframe)
exp = dataframe['Experience']
ys = list(dataframe.keys())
ys.remove('Experience')
TOOLTIPS = [("Experience", "#Experience")]
p = figure(x_range = exp, y_range = (0, 100), plot_height = 350, tools = ['hover', 'save', 'reset', 'zoom_out', 'zoom_in', 'pan', 'box_zoom'], tooltips = TOOLTIPS)
stacked = p.vbar_stack(stackers = ys, x = 'Experience', color = colors, source = source, legend = [value(x) for x in ys], name = ys, width = 0.5,)
colorList = Viridis[len(ys)]
labels = []
bars = []
for y, offset, color in zip(ys, [-0.25, 0, 0.25], colors):
bar = p.vbar(x = dodge('Experience', offset, range = p.x_range), top = y, width = 0.2, source = source, color = color)
bar.visible = False
bars.append(bar)
radiogroup = RadioGroup(labels = ["StackedBar", "Bar"], active = 0,)
radiogroup.callback = CustomJS(args = dict(stacked = stacked, bars = bars), code = """
for (i in stacked)
stacked[i].visible = false;
for (i in bars)
bars[i].visible = false;
if (cb_obj.active == 0)
for (i in stacked)
stacked[i].visible = true;
else if (cb_obj.active == 1)
for (i in bars)
bars[i].visible = true; """)
layout = Row(p, radiogroup)
show(layout)

How to add a python callback to pointdrawtool

fairly new to python/bokeh so apologies. I'm trying to use the pointdrawtool to add arrows to my figure on a bokeh server generated plot. I can add it by making it add invisible circles which share the same columndatasource and therefore arrows are drawn but I then want to adjust the arrow start points via a callback so that they are arrows rather than just triangles.
I've tried various things I've seen here and elsewhere but I've so far failed. I don't have a good understanding of what can and cannot produce a callback. If there's a better simpler way of doing it then that would be fine too.
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.layouts import layout, row, column
from bokeh.plotting import figure, output_file, show, save, reset_output
from bokeh.models import Label, BoxAnnotation, CustomJS, Slider, Button, ColumnDataSource, BoxEditTool, FreehandDrawTool,PointDrawTool, Arrow, NormalHead
from bokeh.models.widgets import Select
import pandas as pd
import numpy as np
import webbrowser as wb
def make_document(doc):
try:
#set the dimensions for the plot
x_start=1600
x_end=2530
y_start=1800
y_end=5300
#### set up figure
p = figure(plot_width=1000, plot_height=600, x_range=(x_start,x_end),
y_range=(y_end,y_start), tools="pan, wheel_zoom,box_zoom,reset, undo,
redo")
#### set up annotation color and thickness:
thick_ann=10.0
col_ann="yellow"
alpha_ann=0.7
### source dataset and associated code for for any added arrows
#source_ar=ColumnDataSource( {"xs":[0,0],"ys":[0,3],"xe":[1,1], "ye":[1,4]})
source_ar=ColumnDataSource( {"xs":[],"ys":[],"xe":[], "ye":[]})
a1=Arrow(end=NormalHead(size=thick_ann*3, fill_color=col_ann, line_color=col_ann, line_alpha=alpha_ann, fill_alpha=alpha_ann),x_start='xs', y_start='ys', x_end='xs', y_end='ys', source=source_ar, line_color=col_ann, line_width=thick_ann, line_alpha=alpha_ann)
p.add_layout(a1)
### add invisible circle - use this to add and remove arrows
c1=p.circle('xs','ys', size=thick_ann*3,alpha=0.0, source=source_ar)
artool=PointDrawTool(renderers=[c1])
p.add_tools(artool)
#### example callback I think I want to run when adding an arrow via the tool - adjust start
#### values so is actual arrow
def arr_callback(attr, old, new):
source_ar.data["xe"][-1]=source_ar.data["xs"][-1] +5
source_ar.data["ye"][-1]=source_ar.data["ys"][-1] +5
#c1.glyph.data_source.on_change('selected',arr_callback)
doc.add_root(p)
except:
server.stop()
apps = {'/': Application(FunctionHandler(make_document))}
server = Server(apps, port=5003)
server.start()
wb.open('http://localhost:5003', new=2)
Expected result - add a point which adds an invisible circle, an arrow is also drawn and the start point then adjusted so it is an arrow not a triangle.
As far as I know only JS callbacks can be added to the tools in Bokeh in general (CrossHairTool. TapTool, etc...). Unfortunately it is not well documented why some tools doesn't support callbacks at all (like ResetTool or PointDrawTool, etc...). Trying to attach a callback to PointDrawTool gives error.
But if you just want to add a new arrow at each mouse click then another option would be to use e.g. JS callback attached to the plot canvas (see code below for Bokeh v1.0.4). Run the code as python app.py
from tornado.ioloop import IOLoop
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.plotting import figure, show
from bokeh.models import CustomJS, ColumnDataSource, Arrow, NormalHead, Segment
def make_document(doc):
p = figure(plot_width = 1000, plot_height = 600, x_range = (0, 10),
y_range = (0, 6), tools = "pan, wheel_zoom,box_zoom,reset,undo,redo")
#### set up annotation color and thickness:
thick_ann = 10.0
col_ann = "red"
alpha_ann = 0.7
#### source dataset and associated code for for any added arrows
source = ColumnDataSource(data = {"xs":[1, 2, 3], "ys":[1, 2, 3], "xe":[4, 5, 6], "ye":[1, 2, 3], 'width': [30] * 3, 'color': [col_ann] * 3 })
a1 = Arrow(end = NormalHead(size = thick_ann * 3, fill_color = col_ann, line_color = col_ann, line_alpha = alpha_ann, fill_alpha = alpha_ann), x_start = 'xs', y_start = 'ys', x_end = 'xe', y_end = 'ye', source = source, line_color = col_ann, line_alpha = alpha_ann)
s1 = p.segment(x0 = 'xs', y0 = 'ys', x1 = 'xe', y1 = 'ye', color = 'color', source = source)
p.add_layout(a1)
code = """ new_x = Number(cb_obj.x);
new_y = Number(cb_obj.y);
data = {xe: [new_x], ys: [new_y], ye: [new_y]};
data['xs'] = [Number(data['xe']) - 3];
data['color'] = ['red'];
data['width'] = [90];
source.stream(data); """
p.js_on_event('tap', CustomJS(args = dict(source = source), code = code))
doc.add_root(p)
io_loop = IOLoop.current()
server = Server(applications = {'/': Application(FunctionHandler(make_document))}, io_loop = io_loop, port = 5001)
server.start()
server.show('/')
io_loop.start()
Result:
I am very new to Bokeh but I wanted to do that this morning and came up with that solution:
from bokeh.plotting import figure, output_notebook, show
from bokeh.models import ColumnDataSource, PointDrawTool
from bokeh.events import Tap
output_notebook()
def bkapp(doc):
p = figure(x_range=(0, 10), y_range=(0, 10), width=400,
height=400, tools=[])
source = ColumnDataSource({
'x': [1, 5, 9], 'y': [1, 5, 9], 'color': ['red', 'green', ' yellow']
})
def callback(event):
source.data["x"].append(event.x)
source.data["y"].append(event.y)
print(source.data)
renderer = p.scatter(x="x", y="y", source=source, color="color",
size=10)
draw_tool = PointDrawTool(renderers=[renderer],
empty_value='black')
p.on_event(Tap, callback)
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool
doc.add_root(p)
show(bkapp)
This is using an embedded server in the Jupyter notebook.
I am just drawing points...
Does this help?

Categories