Related
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!
I am trying to update a worldmap tooltip using a slicer or dropdown select. I got following question which sorted the most of the stuff for a Bokeh Slider custom JS callback
import pandas as pd
import random
from datetime import timedelta
df = pd.DataFrame({'base' : ["2017-01-01" for t in range(10000)],
'Date' : [random.randint(0, 1035) for t in range(10000)],
'Sales' : [random.random() for t in range(10000)]})
df['base'] = pd.to_datetime(df['base'])
df["Date2"] = df.apply(lambda x: x["base"] + timedelta(days=x['Date']), axis=1)
df.drop(['base', 'Date'], axis=1, inplace=True)
df.set_index('Date2', inplace=True)
df['month'] = df.index.month
df['year'] = df.index.year
df['day'] = df.index.day
df.head()
from bokeh.models.widgets import Slider,Select
from bokeh.io import output_notebook, show, output_file
from bokeh.layouts import widgetbox, column
from bokeh.models import Slider, ColumnDataSource, CustomJS
from bokeh.plotting import figure, curdoc
from bokeh.core.properties import value
from bokeh.models.ranges import FactorRange
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import ColumnDataSource, CDSView, IndexFilter, BooleanFilter, HoverTool
source1=df.groupby(['year','month','day'], as_index = False).sum()
source = source1[source1['year']== 2017]
sourcex = source[source['month'] ==1]
Overall=ColumnDataSource(source)
Curr=ColumnDataSource(sourcex)
boolinit = source['month']==1
view = CDSView(source=Overall, filters=[BooleanFilter(boolinit)])
hover3 = HoverTool(tooltips = [('day', '#day'),('Sales','#{Sales}{0,0}')],
formatters = {'day': 'datetime','Sales': 'numeral'})
p = figure(title='YEARLY SALES', plot_width=600, plot_height=400, min_border=3,
tools = [hover3,'box_zoom','wheel_zoom', 'pan','reset'],
toolbar_location="above")
r = p.vbar(x='day', top='Sales', width=0.2, color='#e8bc76', source=Curr)
p.xaxis.axis_label = 'Day'
p.xaxis.axis_label_text_font_style = 'normal'
p.xaxis.axis_label_text_font_size = '12pt'
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = select.value;
sc.data['day'] = [];
sc.data['Sales'] = [];
for (var i = 0; i <= source.get_length(); i++){
if (source.data['month'][i] == f){
sc.data['day'].push(source.data['day'][i])
sc.data['Sales'].push(source.data['Sales'][i])
}
}
sc.change.emit();
""")
select = Select(options=["1","2","3"], title="Month", callback=callback)
callback.args["select"] = select
layout = column(select, p)
#Display plot inline in Jupyter notebook
output_notebook()
output_file("Filterdata.html")
show(layout)
Now, I replicated the same for a worldmap as below:
import pandas as pd
import geopandas as gpd
current_week = 4
shapefile = 'data/countries_110m/ne_110m_admin_0_countries.shp'
gdf = gpd.read_file(shapefile)[['ADMIN', 'ADM0_A3', 'geometry']]
gdf.columns = ['country', 'country_code', 'geometry']
gdf = gdf.drop(gdf.index[159])
df = pd.DataFrame({'Country':['India','India'],
'SalesGain':['10%','20%'],
'Week':[4,5],
'Color':[0.2,0.4]
})
import json
from bokeh.models.widgets import Slider,Select
from bokeh.io import output_notebook, show, output_file
from bokeh.layouts import widgetbox, column
from bokeh.models import Slider, ColumnDataSource, CustomJS
from bokeh.plotting import figure, curdoc
from bokeh.core.properties import value
from bokeh.models.ranges import FactorRange
from bokeh.palettes import brewer
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import ColumnDataSource, CDSView, IndexFilter, BooleanFilter, HoverTool,GeoJSONDataSource, LinearColorMapper, ColorBar
from bokeh.plotting import figure, output_file, show
output_file("worldmap.html")
merged = gdf.merge(df, left_on = 'country', right_on = 'Country', how = 'left')
merged_json = json.loads(merged.to_json())
json_data = json.dumps(merged_json)
geosource_all = GeoJSONDataSource(geojson = json_data)
df_curr = df[df['Week']==current_week]
merged_curr = gdf.merge(df_curr, left_on = 'country', right_on = 'Country', how = 'left')
merged_json_curr = json.loads(merged_curr.to_json())
json_data_curr = json.dumps(merged_json_curr)
geosource_curr = GeoJSONDataSource(geojson = json_data_curr)
# boolinit = merged['Week']!=current_week
boolinit = merged['Week']==current_week
view = CDSView(source=geosource_all, filters=[BooleanFilter(boolinit)])
hover3 = HoverTool(tooltips = [('Country', '#Country'),('Sales','#SalesGain')])
#Define a sequential multi-hue color palette.
palette = brewer['YlGnBu'][8]
#Reverse color order so that dark blue is highest value
palette = palette[::-1]
#Instantiate LinearColorMapper that linearly maps numbers in a range, into a sequence of colors. Input nan_color.
color_mapper = LinearColorMapper(palette = palette, low = 0, high = 12, nan_color = '#d9d9d9')
#Define custom tick labels for color bar.
tick_labels = {'0': '0', '2':'2%', '4':'4%', '6':'6%', '8':'8%','10':'10%','12':'12%'}
#Create color bar.
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=6,width = 500, height = 20,
border_line_color=None,location = (0,0), orientation = 'horizontal', major_label_overrides = tick_labels)
#Create figure object.
p = figure(title='Covid-19 Impact', plot_width=900, plot_height=600, min_border=3,
tools = [hover3,'box_zoom','wheel_zoom', 'pan','reset'],toolbar_location="above")
p.title.text_font_size = '20pt'
p.title.text_color = "darkblue"
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
#Add patch renderer to figure.
p.patches('xs','ys', source = geosource_curr,fill_color = {'field' :'Color', 'transform' : color_mapper},
line_color = 'black', line_width = 0.25, fill_alpha = 1)
p.add_layout(color_bar, 'below')
callback = CustomJS(args=dict(source=geosource_all, sc=geosource_curr), code="""
var f = slider.value;
sc.data['Country'] = [];
sc.data['Week'] = [];
sc.data['SalesGain'] = [];
for (var i = 0; i <= source.get_length(); i++){
if ((source.data['Week'][i] == f ) || (source.data['Country'][i] == null) ){
sc.data['SalesGain'].push(source.data['SalesGain'][i])
sc.data['Week'].push(source.data['Week'][i])
sc.data['Country'].push(source.data['Country'][i])
}
}
sc.change.emit();
""")
# select = Select(options=["201951","201952","201953"], title="Week", callback=callback)
# callback.args["select"] = select
# layout = column(select, p)
slider = Slider(start=1, end=5, value=current_week, step=1, title="Month", callback=callback)
callback.args["slider"] = slider
layout = column(slider, p)
#Display plot inline in Jupyter notebook
output_notebook()
show(layout)
But in this case, as soon as I click on the slider, tooltip data vanish away. World map input file can be found here to smoothly run the code:
https://github.com/CrazyDaffodils/Interactive-Choropleth-Map-Using-Python/tree/master/bokeh-app/data
A tooltip disappears after a small delay after you move your mouse off the glyph that has triggered it.
Right now, Bokeh doesn't have any built-in way of changing that behavior. There's an open issue for that with a workaround that you might be able to adapt to your needs: https://github.com/bokeh/bokeh/issues/5724
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)
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.
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?