Related
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
I am trying to change a NetworkX graph by clicking a value in bokeh data table. I can see and visualize the required value in the layout box but the same value is not getting used to display/ change the NetworkX Graph.
I have tried using python call back, but its not working either.It says "Only JavaScript callbacks may be used with standalone output".
#from bokeh.io import vplot
from bokeh.plotting import figure
from bokeh.models.graphs import
from_networkx,NodesAndLinkedEdges,EdgesAndLinkedNodes
from bokeh.plotting import figure
from bokeh.models import Plot,Range1d
from bokeh.io import output_file, show
from datetime import date
from random import randint
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter,
TableColumn,Spinner
import bokeh.layouts as layouts
import bokeh.models.widgets as widgets
from bokeh.io import curdoc
source= ColumnDataSource(dict(CUSTOMER_NO = pr, priority =
np.arange(1,len(pr)+1,1)))
columns = [TableColumn(field = "CUSTOMER_NO", title = "CUSTOMER_NO"),
TableColumn(field = "priority", title = "PRIORITY")]
data_table = DataTable(source = source, columns = columns, width = 200,
height = 280, editable = True, reorderable = False)
text_row = TextInput(value = None, title = "Row index:", width = 420)
text_CUSTOMER_NO = TextInput(value = None, title = "CUSTOMER_NO:", width
= 420)
text_priority = TextInput(value = None, title = "priority:", width = 420)
plot=figure(title='test',x_range=Range1d(-1.1,1.1),
y_range=Range1d(-1.1,1.1),tools="",toolbar_location=None)
s =
G.subgraph(nx.shortest_path(G.to_undirected(),text_CUSTOMER_NO.value))
pos=nx.planar_layout(s)
graph=from_networkx(s,pos)
plot.renderers.append(graph)
source_code = """
row = cb_obj.indices[0]
text_row.value = String(row);
text_CUSTOMER_NO.value = String(source.data['CUSTOMER_NO'][row])
text_priority.value = String(source.data['priority'][row]);"""
callback = CustomJS(args = dict(source = source, text_row =
text_row,text_CUSTOMER_NO=text_CUSTOMER_NO,text_priority=text_priority),
code = source_code)
source.selected.js_on_change('indices', callback)
layout = column(data_table, text_row,text_CUSTOMER_NO,text_priority,
plot)
#show(layout)
bokeh.io.output_notebook()
bokeh.io.show(layout)
#bokeh.io.show(plot)
There is no error message given, the output shows a correct bokeh data table, the clicks on the customer no shows me correct values in layout boxs but the networkx shows a black triangle.
I was able to somehow achieve this using python callback.Can some one please help me to achieve the same using CustomJS. The code is below.
from datetime import date
from random import randint
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
import bokeh.layouts as layouts
import bokeh.models.widgets as widgets
from bokeh.io import curdoc
from bokeh.io import vplot
from bokeh.plotting import figure
from bokeh.models.graphs import
from_networkx,NodesAndLinkedEdges,EdgesAndLinkedNodes
from bokeh.plotting import figure
from bokeh.models import Plot,Range1d
from bokeh.io import output_file, show
from datetime import date
from random import randint
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
import bokeh.layouts as layouts
import bokeh.models.widgets as widgets
from bokeh.io import curdoc
G = nx.Graph()
G.add_nodes_from([1,2,3,4,5,6,7,8,9,10])
G.add_edges_from([(1, 2), (1, 3),(3,4),(5,6),(6,7),(8,9),(9,10)])
data = dict(
CUSTOMER_NO=[1,2,3,4,5,6,7,8,9,10],
priority=[1,2,3,4,5,6,7,8,9,10],
)
d_source= ColumnDataSource(data)
columns = [TableColumn(field = "CUSTOMER_NO", title = "CUSTOMER_NO"), TableColumn(field = "priority", title = "PRIORITY")]
data_table = DataTable(source = source, columns = columns, width = 200, height = 280, editable = True, reorderable = False)
def table_select_callback(attr, old, new):
# print 'here'
# print new
# selected_row = new['1d']['indices'][0]
selected_row = new[0]
s = G.subgraph(nx.shortest_path(G.to_undirected(), data['CUSTOMER_NO'][selected_row]))
#download_count = data['downloads'][selected_row]
#chart_data = np.random.uniform(0, 100, size=download_count)
plot = figure(title='test',x_range=Range1d(-1.1,1.1),y_range=Range1d(-1.1,1.1), tools = ['reset', 'pan','wheel_zoom','save', 'lasso_select', ])
#p = figure(title='bla')
pos=nx.random_layout(s)
graph=from_networkx(s,pos)
plot.renderers.append(graph)
#r = p.line(x=range(len(chart_data)), y=chart_data)
root_layout.children[1] = plot
d_source.selected.on_change('indices', table_select_callback)
root_layout = layouts.Column(data_table, widgets.Div(text='Select customer number'))
curdoc().add_root(root_layout)
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?
while using bokeh tap tool with bar chart I need to get the id of selected bar.how can I get this id of each bar without using customJS
the code is running using bokeh server
source = ColumnDataSource(data=df)
p = figure(x_range=source.data['month'], plot_height=600, toolbar_location=None, tools="tap,hover", title="month analysis")
p.vbar(x='month', top='count', width=0.5, source=source, legend="month",
line_color='white', fill_color=factor_cmap('month', palette=Spectral6, factors=df['month']))
hover = HoverTool(tooltips=[("count", "#count")])
p.legend.orientation = "horizontal"
p.legend.location = "top_right"
taptool = p.select(type=TapTool)
curdoc().add_root(row( p, width=800))
This should work. The ID of a selected glyph can be found in source.selected.indices.
#!/usr/bin/python3
import pandas as pd
from bokeh.plotting import figure, show, curdoc
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool, TapTool
from bokeh.transform import factor_cmap
from bokeh.palettes import Spectral6
from bokeh.layouts import row
from bokeh.events import Tap
df = pd.read_csv('data.csv')
df['count'] = df['count'].astype(dtype='int32')
source = ColumnDataSource(data=df)
p = figure(x_range=source.data['month'], plot_height=600, toolbar_location=None, tools="tap,hover", title="month analysis")
p.vbar(x='month', top='count', width=0.5, source=source, legend="month",
line_color='white', fill_color=factor_cmap('month', palette=Spectral6, factors=df['month']))
hover = HoverTool(tooltips=[("count", "#count")])
p.legend.orientation = "horizontal"
p.legend.location = "top_right"
taptool = p.select(type=TapTool)
def callback(event):
selected = source.selected.indices
print(selected)
p.on_event(Tap, callback)
curdoc().add_root(row( p, width=800))
I have a ColumnDataSource like:
MONTH | YEAR | TYPE | COUNT
1 2018 A 5
3 2019 A 3
2 2018 B 6
....
5 2018 C 1
5 2017 C 4
I create a heatmap with a hovertool and radio buttons that alternate through the different TYPE views i.e.
heat_source=ColumnDataSource(data=df)
A_view= CDSView(source=heat_source, filters=[GroupFilter(column_name="TYPE", group="A")])
B_view= CDSView(source=heat_source, filters=[GroupFilter(column_name="TYPE", group="B")])
p_heat= figure(x_range=months, y_range=years,
y_axis_type='datetime',plot_width=405, plot_height=650, toolbar_location=None)
h=HoverTool(tooltips=[('Type','#TYPE'),('Transactions','#COUNT{0,0}'),
('Date','#MONTH-#YEAR')])
p_heat.add_tools(h)
radio_group=RadioGroup(labels=["A","B",'C'], active=0)
radio_group.on_change('active',lambda attr,old,new: update())
def update():
if radio_group.active==0:
p_heat.rect(x="MONTH",y="YEAR", width=1, height=1, source=heat_source, view=A_view,line_color=None)
if radio_group.active==1:
p_heat.rect(x="MONTH",y="YEAR", width=1, height=1, source=heat_source, view=B_view,line_color=None)
Everything works fine, except when I toggle the TYPEs, the hover data is still unchanged. I've tried embedded the hovertool within the radio button callback for each If, but no avail.
You can create the rect GlyphRenderer once, and change the group property of the filter in a javascript callback:
from bokeh.plotting import figure, output_notebook, show
from bokeh.models import ColumnDataSource, CDSView, GroupFilter, RadioGroup, CustomJS, HoverTool
from bokeh.palettes import viridis
from bokeh.transform import linear_cmap
from bokeh.layouts import column
import pandas as pd
import numpy as np
from itertools import product
output_notebook()
df = pd.DataFrame(list(product(range(1, 13), range(2000, 2018), ["A", "B"])), columns=["MONTH", "YEAR", "TYPE"])
df["COUNT"] = np.random.randint(0, 100, size=df.shape[0])
heat_source=ColumnDataSource(data=df)
view= CDSView(source=heat_source, filters=[GroupFilter(column_name="TYPE", group="A")])
p_heat= figure(plot_width=405, plot_height=650, toolbar_location=None)
h=HoverTool(tooltips=[('Type','#TYPE'),('Transactions','#COUNT{0,0}'),
('Date','#MONTH-#YEAR')])
p_heat.add_tools(h)
rect = p_heat.rect(x="MONTH",y="YEAR", width=1, height=1, source=heat_source, view=view,line_color=None,
fill_color=linear_cmap("COUNT", viridis(256), 0, 100))
type_widget = RadioGroup(labels=["A", "B"], active=0)
def callback(source=heat_source, filter=view.filters[0], cmap=rect.glyph.fill_color["transform"]):
filter.group = cb_obj.labels[cb_obj.active]
if filter.group == "A":
cmap.low=0
cmap.high=100
else:
cmap.low=-100
cmap.high=200
source.change.emit()
type_widget.js_on_change("active", CustomJS.from_py_func(callback))
show(column(type_widget, p_heat))