Bokeh Multi-Select widget callback not Working - python

I am new to Bokeh. Trying to implement few widget callbacks recently and hard to find any resources online.
Scenario: I have a multi_select bokeh widget with the list of the companies. Based on the companies selcted, data in the Vbar has to be changed. For this, i am trying to change the datasource that is used in the Vbar based on the values from Multi select. I am unable to get desired results. Can someone please help me out with this.
I am poor at CustomJs and hence doing this on Bokeh Server for callabcks. If there is a solution on CustomJs too, that would help me a lot.
Thank you very much for your time in Advance !
Below is the code I am using
source =source1[source1['year']== dt.now().year]
sourcex = source[source['month'] ==1 &
source['CompanyNo'].isin(['01','02','03','04','05','08']) ]
Overall= ColumnDataSource(source)
Curr= ColumnDataSource(sourcex)
boolinit = source['month']==1
view = CDSView(source=Overall, filters=[BooleanFilter(boolinit)])
hover3 = HoverTool(
tooltips = [
('day', '#day'),
('ExtendedPrice','#{ExtendedPrice}{0,0}'),
],
formatters = {
'day': 'datetime',
'ExtendedPrice': '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")
p.vbar(x='day', top='ExtendedPrice', 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'
p.yaxis[0].formatter = NumeralTickFormatter(format="0,0")
def Multi_Selectupdate(attrname, old, new):
curr=sourcex[sourcex['CompanyNo'].isin(new)]
source.data=curr.data
companies=['All']+sourcex['CompanyNo'].unique().tolist()
multi_select = MultiSelect(title="Select:", value=['01'], options=companies,
height=200, width=100)
multi_select.on_change('value',Multi_Selectupdate )
layout = column(multi_select, p )
show(layout)

Since I don't have the data, I generated it using following script. I have assumed your data has 3 columns - Date, ExtendedPrice, CompanyNo. I have generated first a random data of 10K rows and then summarized it at CompanyNo, day, month, year level.
import pandas as pd
import random
CopmanyList = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15']
df = pd.DataFrame({'base' : ["2017-01-01" for t in range(10000)],
'Date' : [random.randint(0, 1035) for t in range(10000)],
'ExtendedPrice' : [random.random() for t in range(10000)],
'CompanyNo' : [CopmanyList[random.randint(0, 15)] 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
source1=df.groupby(['year','month','day', 'CompanyNo'], as_index = False)['ExtendedPrice'].sum()
source =source1[source1['year']== dt.now().year]
sourcex = source[source['month'] ==1]
sourcex = sourcex[sourcex['CompanyNo'].isin(['01'])]
sourcex.sort_values(by='day', inplace=True)
Now, what you want to accomplish is that given a day and set of company names, you need to publish a bar chart of some type of aggregation of ExtendedPrice, i.e. say there are 2 rows for company '01' and '02' (which are selected), and for month 2 (that is also selected by a slider) for current year, the ExtendedPrice value for these two is say 100, and 200. In case we want to show a sum, you need to show 300 in barchart. This summary, you need to calculate dynamically.
There are two ways to accomplish it.
1. Bokeh Callbacks
This will use native python functions, but you need to run it with bokeh serve , e.g bokeh serve --show <filename.py>. See the code below, a native python function compsel is filtering pandas dataframe and summarizing it and replacing the chart backend data with newly created data -
from datetime import timedelta
from datetime import datetime as dt
from bokeh.models.widgets import MultiSelect, Slider
from bokeh.layouts import widgetbox, column
from bokeh.models.ranges import FactorRange
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, HoverTool, CustomJS
Overall= ColumnDataSource(source)
Curr= ColumnDataSource(sourcex[['day', 'ExtendedPrice']])
Curr.remove('index')
hover3 = HoverTool(tooltips = [('day', '#day'),('Sales','#{ExtendedPrice}{0,0.00}')],
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='ExtendedPrice', 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'
def compsel(attr, old, new):
vcomp = multi_select.value
vmonth = slider.value
sourcex = source[source['month'] ==vmonth]
sourcex = sourcex[sourcex['CompanyNo'].isin(vcomp)]
sourcex = sourcex.groupby(['day'], as_index = False)['ExtendedPrice'].sum()
Currnew= ColumnDataSource(sourcex[['day', 'ExtendedPrice']])
Currnew.remove('index')
r.data_source.data=Currnew.data
companies=source['CompanyNo'].unique().tolist()
companies.sort()
multi_select = MultiSelect(title="Select:", value=['01'], options=companies, height=200, width=100)
slider = Slider(start=1, end=12, value=1, step=1, title="Month")
slider.on_change("value", compsel)
multi_select.on_change("value", compsel)
layout = column(multi_select, slider, p)
curdoc().add_root(layout)
There are other options to host this application. See other option by typing bokeh serve --help in terminal
2. JavaScript Callbacks
This will generate a javascript function to interact with the chart, which will not require bokeh server and work directly on the browser. This will require very simple code in javascript. In the example solved here, I am summarizing ExtendedPrice as sum, but code will require small tweaks to calculate other stats e.g. mean. The js callback function here does the similar thing as bokeh callback function, it reads the larger datasets and filters it based on the select and slider values -
source =source1[source1['year']== dt.now().year]
sourcex = source[source['month'] ==1]
sourcex = sourcex[sourcex['CompanyNo'].isin(['01'])]
sourcex.sort_values(by='day', inplace=True)
Overall= ColumnDataSource(source)
Curr= ColumnDataSource(sourcex[['day', 'ExtendedPrice']])
Curr.remove('index')
hover3 = HoverTool(tooltips = [('day', '#day'),('Sales','#{ExtendedPrice}{0,0.00}')],
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='ExtendedPrice', 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'
callms = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var comp=msel.attributes.value;
var f = slider.value;
sc.data['ExtendedPrice'] = [];
sc.data['day'] = [];
for (var i = 0; i <= source.get_length(); i++){
if (source.data['month'][i] == f){
if (comp.indexOf(source.data['CompanyNo'][i]) >=0){
var d1 = source.data['day'][i]
if(typeof sc.data['day'][d1-1]=="undefined"){
sc.data['day'][d1-1] = d1
sc.data['ExtendedPrice'][d1-1] = source.data['ExtendedPrice'][i]
}
else{
sc.data['ExtendedPrice'][d1-1] = sc.data['ExtendedPrice'][d1-1] + source.data['ExtendedPrice'][i]
}
}
}
}
sc.change.emit();
""")
companies=source['CompanyNo'].unique().tolist()
companies.sort()
multi_select = MultiSelect(title="Select:", value=['01'], options=companies, height=200, width=100, callback=callms)
slider = Slider(start=1, end=12, value=1, step=1, title="Month", callback=callms)
callms.args["msel"] = multi_select
callms.args["slider"] = slider
layout = column(multi_select, slider, p)
output_file("Filterdata.html")
show(layout)

Related

bokehjs: computing a new plot automatically as sum of all activated plots

Having 3 line plots in Bokehjs, I would like Bokeh to show a fourth one, which is the sum of the other 3.
Example:
y1=[1,2,3,4,5]
y2=[4,5,6,7,8]
y3=[1,8,2,6,4]
Automatically generated plot would be:
y_all = [6,15,11,17,17]
Is there a way to accomplish this?
Maybe with a js callback?
I am not sure what you want, so I start with a very basic approche.
I assume you can use pandas. And your given DataFrame is this:
import pandas as pd
from bokeh.plotting import figure, show, output_notebook
output_notebook()
df = pd.DataFrame({
'y1':[1,2,3,4,5],
'y2':[4,5,6,7,8],
'y3':[1,8,2,6,4],
})
Static solution
With pandas.DataFrame.sum() you can create the sum and then you can use multi_line from bokeh.
df['y_all'] = df.sum(axis=1)
p = figure(width=300, height=300)
p.multi_line(
xs=[df.index]*4, ys=list(df.values.T), color=['red', 'green','blue', 'black']
)
show(p)
Interactive solution
Because you mentioned JS, I created an interactive solution. This solution is based on this post.
Here the sum is calculated on the fly by the selection given by the active CheckBoxes.
import pandas as pd
from bokeh.models import CheckboxGroup, CustomJS, ColumnDataSource
from bokeh.layouts import row
from bokeh.plotting import figure, show, output_notebook
output_notebook()
df = pd.DataFrame({
'y1':[1,2,3,4,5],
'y2':[4,5,6,7,8],
'y3':[1,8,2,6,4],
})
df['y_all'] = df.sum(axis=1)
source = ColumnDataSource(df)
colors = ['red', 'green','blue', 'black']
p = figure(width=300, height=300)
line_renderer = []
names = list(df.columns)
for name, color in zip(names, colors):
line_renderer.append(
p.line(
x = 'index',
y = name,
color=color,
name =name,
source=source
)
)
checkbox = CheckboxGroup(labels=names, active=list(range(len(names))), width=100)
callback = CustomJS(args=dict(lines=line_renderer,checkbox=checkbox, source=source),
code="""
const data = source.data;
for (let i = 0; i < data['y_all'].length; i++) {
data['y_all'][i] = 0
}
for(var j=0; j<lines.length; j++){
lines[j].visible = checkbox.active.includes(j);
}
console.log(data)
console.log(checkbox)
for(var item of checkbox.active){
let next_y = lines[item]["properties"]["name"]["spec"]["value"]
if (next_y != 'y_all'){
for (let i = 0; i < data[next_y].length; i++) {
data['y_all'][i] += data[next_y][i]
}
}
}
source.change.emit();
"""
)
checkbox.js_on_change('active', callback)
layout = row(p,checkbox)
show(layout)

How to get Bokeh hovertool working for candlesticks chart?

The goal was to have my mouse hover over the candlesticks and have the data appear while I do so. I was able to get the Bokeh hover tool working but, I am not able to pull and display the data. I was able to get the (x,y) data to appear but, not the rest. This is what I have so far:
#Importing Libraries
import requests
import pandas as pd
import datetime
from bokeh.plotting import ColumnDataSource, figure, output_file, show
from math import pi
#Request ticker symbol
API_KEY = 'YOUR API KEY'
symbol = input('Enter ticker symbol: ')
r = requests.get('https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=' + symbol + '&apikey=' + API_KEY)
print(r.status_code)
result = r.json()
dataForAllDays = result['Time Series (Daily)']
#convert to dataframe
df = pd.DataFrame.from_dict(dataForAllDays, orient='index')
df = df.reset_index()
#rename columns
df = df.rename(index=str, columns={"index": "date", "1. open": "open", "2. high": "high", "3. low": "low", "4. close": "close","5. volume":"volume"})
#Changing to datetime
df['date'] = pd.to_datetime(df['date'])
#Sort according to date
df = df.sort_values(by=['date'])
#Changing the datatype
df.open = df.open.astype(float)
df.close = df.close.astype(float)
df.high = df.high.astype(float)
df.low = df.low.astype(float)
df.volume = df.volume.astype(int)
#check the data
df.head()
#Hovertool Tooltips
source = ColumnDataSource(data=dict(date=df['date'], open=df.open, close=df.close))
TOOLTIPS = [
("(x,y)", "($x, $y)"),
("date", "#date"),
("open", "df.open"),
("open", "#open"),
("close", "df.close"),
("close", "#close"),
("percent","#changepercent"),
]
#Check the datatype
df.info()
inc = df.close > df.open
dec = df.open > df.close
w = 12*60*60*1000 # half day in ms
TOOLS = "pan,wheel_zoom,box_zoom,reset,save"
title = symbol + ' Chart'
p = figure(x_axis_type="datetime", tools=TOOLS, tooltips=TOOLTIPS, plot_width=1000, title = title)
p.xaxis.major_label_orientation = pi/4
p.grid.grid_line_alpha=0.3
p.segment(df.date, df.high, df.date, df.low, color="black")
p.vbar(df.date[inc], w, df.open[inc], df.close[inc], fill_color="#a30e15", line_color="black")
p.vbar(df.date[dec], w, df.open[dec], df.close[dec], fill_color="#53680c", line_color="black")
#Store as a HTML file
output_file("stock_information.html", title="candlestick.py")
# Display in browser
show(p)
Here is a full example of functioning one minute candlestick with hover tooltips and bbands thrown in.
source = ColumnDataSource(data=df)
hover = HoverTool(
tooltips=[
('d', '#timestamp{%H:%M}'),
('o', '#open{0}'),
('h', '#high{0}'),
('l', '#low{0}'),
('c', '#close{0}'),
('v', '#volume{0}'),
],
formatters={
'#timestamp': 'datetime'
},
mode='mouse'
)
inc_b = source.data['close'] > source.data['open']
inc = CDSView(source=source, filters=[BooleanFilter(inc_b)])
dec_b = source.data['open'] > source.data['close']
dec = CDSView(source=source, filters=[BooleanFilter(dec_b)])
w = 60000
p = figure(x_axis_type="datetime", sizing_mode="stretch_width", height=400)
p.segment(source=source, x0='timestamp', x1='timestamp', y0='high', y1='low', color="black")
p.vbar(source=source, view=inc, x='timestamp', width=w, top='open', bottom='close', fill_color="green", line_color="green")
p.vbar(source=source, view=dec, x='timestamp', width=w, top='open', bottom='close', fill_color="red", line_color="red")
p.line(source=source, x='timestamp', y='bband_up', line_width=1, alpha=0.8, legend_label='bband_up', color='green')
p.line(source=source, x='timestamp', y='bband_mid', line_width=1, alpha=0.8, legend_label='bband_mid', color='blue')
p.line(source=source, x='timestamp', y='bband_low', line_width=1, alpha=0.8, legend_label='bband_low', color='red')
p.add_tools(hover)
p.legend.location = "top_left"
p.legend.click_policy = "hide"
show(p)
The only values that are meaningful in the tooltip specification, are:
pre-defined "special variables" denoted by starting with $
references to columns in the ColumnDataSource, denoted by starting with #
So things like "df.open" are meaningless in this context. The actual code to make the hovertool executes in the browser, in javascript, where Python and Pandas and your DataFrame df do not exist. If you want data to show up in a HoverTool then you will have to put a column in your CDS with that data, and reference it in the hover tool specification using the #column_name syntax.
You may refer to the documentation for more information:
https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#hovertool

Python bokeh slider not refreshing plot

I am creating a bokeh plot with a slider to refresh plot accordingly. There are 2 issues with the code posted.
1. The plot is not refreshed as per the slider. Please help in providing a fix for this issue.
2. Plot is not displayed with curdoc() when bokeh serve --show fn.ipynb is used
I'm trying to visualise this CSV file.
import pandas as pd
import numpy as np
from bokeh.models import ColumnDataSource, CategoricalColorMapper, HoverTool, Slider
from bokeh.plotting import figure, curdoc
from bokeh.palettes import viridis
from bokeh.layouts import row, widgetbox
#Importing and processing data file
crop = pd.read_csv('crop_production.csv')
#Cleaning Data
crop.fillna(np.NaN)
crop['Season'] = crop.Season.str.strip()
#Removing Whitespace #Filtering the dataset by Season
crop_season = crop[crop.Season == 'Whole Year']
crop_dt = crop_season.groupby(['State_Name', 'District_Name', 'Crop_Year']).mean().round(1)
#Creating Column Data Source
source = ColumnDataSource({
'x' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].Area,
'y' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].Production,
'state' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].index.get_level_values('State_Name'),
'district' : crop_dt[crop_dt.index.get_level_values('Year')==2001].loc[(['ABC']), :].index.get_level_values('District_Name')
})
#Creating color palette for plot
district_list = crop_dt.loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
# Creating the figure
#xmin, xmax = min(data.Crop_Year), max(data.Crop_Year)
#ymin, ymax = min(data.Production), max(data.Production)
p = figure(
title = 'Crop Area vs Production',
x_axis_label = 'Area',
y_axis_label = 'Production',
plot_height=900,
plot_width=1200,
tools = [HoverTool(tooltips='#district')]
)
p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district')
p.legend.location = 'top_right'
def update_plot(attr, old, new):
yr = slider.value
new_data = {
'x' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].Area,
'y' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].Production,
'state' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].index.get_level_values('State_Name'),
'district' : crop_dt[crop_dt.index.get_level_values('Year')==yr].loc[(['ABC']), :].index.get_level_values('District_Name')
}
source.data = new_data
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year')
slider.on_change('value',update_plot)
layout = row(widgetbox(slider), p)
curdoc().add_root(layout)
show(layout)
Also tried a different option using CustomJS as below, but still no luck.
callback = CustomJS(args=dict(source=source), code="""
var data = source.data;
var yr = slider.value;
var x = data['x']
var y = data['y']
'x' = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['ABC']), :].Area;
'y' = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['ABC']), :].Production;
p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district');
}
source.change.emit();
""")
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
yr_slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year', callback=callback)
callback.args["slider"] = yr_slider
Had a lot of issues trying to execute your code and I have changed some things, so feel free to correct me if did something wrong.
The error was caused by the creation of the ColumnDataSource, I had to change the level value to Crop_Year instead of Year. The loc 'ABC' also caused an error so I removed that too (And I had to add source = ColumnDataSource({, you probably forgot to copy that)
I also added a dropdown menu so it's possible to only show the data from one district.
Also, I'm not quite sure if it's possible to start a bokeh server by supplying a .ipynb file to --serve. But don't pin me down on that, I never use notebooks. I've tested this with a .py file.
#!/usr/bin/python3
import pandas as pd
import numpy as np
from bokeh.models import ColumnDataSource, CategoricalColorMapper, HoverTool
from bokeh.plotting import figure, curdoc
from bokeh.palettes import viridis
from bokeh.layouts import row, widgetbox
from bokeh.models.widgets import Select, Slider
#Importing and processing data file
crop = pd.read_csv('crop_production.csv')
#Cleaning Data
crop.fillna(np.NaN)
crop['Season'] = crop.Season.str.strip()
#Removing Whitespace #Filtering the dataset by Season
crop_season = crop[crop.Season == 'Whole Year']
crop_dt = crop_season.groupby(['State_Name', 'District_Name', 'Crop_Year']).mean().round(1)
crop_dt_year = crop_dt[crop_dt.index.get_level_values('Crop_Year')==2001]
crop_dt_year_state = crop_dt_year[crop_dt_year.index.get_level_values('State_Name')=='Tamil Nadu']
#Creating Column Data Source
source = ColumnDataSource({
'x': crop_dt_year_state.Area.tolist(),
'y': crop_dt_year_state.Production.tolist(),
'state': crop_dt_year_state.index.get_level_values('State_Name').tolist(),
'district': crop_dt_year_state.index.get_level_values('District_Name').tolist()
})
#Creating color palette for plot
district_list = crop_dt.loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
# Creating the figure
p = figure(
title = 'Crop Area vs Production',
x_axis_label = 'Area',
y_axis_label = 'Production',
plot_height=900,
plot_width=1200,
tools = [HoverTool(tooltips='#district')]
)
glyphs = p.circle(x='x', y='y', source=source, size=12, alpha=0.7,
color=dict(field='district', transform=color_mapper),
legend='district')
p.legend.location = 'top_right'
def update_plot(attr, old, new):
#Update glyph locations
yr = slider.value
state = select.value
crop_dt_year = crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr]
crop_dt_year_state = crop_dt_year[crop_dt_year.index.get_level_values('State_Name')==state]
new_data = {
'x': crop_dt_year_state.Area.tolist(),
'y': crop_dt_year_state.Production.tolist(),
'state': crop_dt_year_state.index.get_level_values('State_Name').tolist(),
'district': crop_dt_year_state.index.get_level_values('District_Name').tolist()
}
source.data = new_data
#Update colors
district_list = crop_dt.loc[([state]), :].index.get_level_values('District_Name').unique().tolist()
call_colors = viridis(len(district_list))
color_mapper = CategoricalColorMapper(factors=district_list, palette=call_colors)
glyphs.glyph.fill_color = dict(field='district', transform=color_mapper)
glyphs.glyph.line_color = dict(field='district', transform=color_mapper)
#Creating Slider for Year
start_yr = min(crop_dt.index.get_level_values('Crop_Year'))
end_yr = max(crop_dt.index.get_level_values('Crop_Year'))
slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year')
slider.on_change('value',update_plot)
#Creating drop down for state
options = list(set(crop_dt.index.get_level_values('State_Name').tolist()))
options.sort()
select = Select(title="State:", value="Tamil Nadu", options=options)
select.on_change('value', update_plot)
layout = row(widgetbox(slider, select), p)
curdoc().add_root(layout)
#Jasper Thanks a lot. This works, however it doesnt work with .loc[(['Tamil Nadu']), :]. Reason for having this is to filter the data by adding a bokeh dropdown or radio button object and refresh the plot based on the filters. The below code works only if .loc[(['Tamil Nadu']), :] is removed. Is there any other way to fix this please?
def update_plot(attr, old, new):
yr = slider.value
new_data = {
'x' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].Area.tolist(),
'y' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].Production.tolist(),
'state' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].index.get_level_values('State_Name').tolist(),
'district' : crop_dt[crop_dt.index.get_level_values('Crop_Year')==yr].loc[(['Tamil Nadu']), :].index.get_level_values('District_Name').tolist()
}
source.data = new_data

Python: Bokeh hover date time

I am trying to get a line plot via Bokeh in Python. I am new to Bokeh and I am trying to apply hover tool tips over the plot. The x-axis of the plot has Timestamp values which are converted into epoch string. I've reviewed some same problems here and tried to use the workaround for my case but it doesn't seem to work. On the plot it gives ??? where the time should show up.
Any suggestions for my code?
Timestamp is in format 2016-12-29 02:49:12
Also can someone tell how do I format x-axis ticks to show up vertically ?
p = figure(width=1100,height=300,tools='resize,pan,wheel_zoom,box_zoom,reset,previewsave,hover',logo=None)
p.title.text = "Time Series for Price in Euros"
p.grid.grid_line_alpha = 0
p.xaxis.axis_label = "Day"
p.yaxis.axis_label = "Euros"
p.ygrid.band_fill_color = "olive"
p.ygrid.band_fill_alpha = 0.1
p.circle(df['DateTime'],df['EuP'], size=4, legend='close',
color='darkgrey', alpha=0.2)
p.xaxis.formatter = DatetimeTickFormatter(formats=dict(
hours=["%d %B %Y"],
days=["%d %B %Y"],
months=["%d %B %Y"],
years=["%d %B %Y"],
))
source = ColumnDataSource(data=dict(time=[x.strftime("%Y-%m-%d %H:%M:%S")for x in df['DateTime']]))
hover = p.select(dict(type=HoverTool))
hover.tooltips = {"time":'#time', "y":"$y"}
hover.mode = 'mouse'
p.line(df['DateTime'],df['EuP'],legend='Price',color='navy',alpha=0.7)
Since this answer was originally posted, new work has gone into Bokeh to make things simpler. A datetime field can be formatted as a datetime directly by the hover tool, by specifying a formatter, e.g.:
HoverTool(tooltips=[('date', '#DateTime{%F}')],
formatters={'#DateTime': 'datetime'})
It is no longer necessary to pre-format date fields in the data source as below. For more information see Formatting Tooltip Fields
OLD ANSWER:
The problem with your tooltip is you created a source with the string representation of the dates, but the p.line() call is unaware of it. So you have to pass in a columndatasource that has the tooltip, the x and y values.
Here is a working variant of your code:
from bokeh.plotting import figure, show
from bokeh.models.formatters import DatetimeTickFormatter
from bokeh.models import ColumnDataSource
from bokeh.models.tools import HoverTool
import pandas as pd
import numpy as np
data = {
'DateTime' : pd.Series(
['2016-12-29 02:49:12',
'2016-12-30 02:49:12',
'2016-12-31 02:49:12'],
dtype='datetime64[ns]'),
'EuP' : [20,40,15]
}
df = pd.DataFrame(data)
df['tooltip'] = [x.strftime("%Y-%m-%d %H:%M:%S") for x in df['DateTime']]
p = figure(width=1100,height=300,tools='resize,pan,wheel_zoom,box_zoom,reset,previewsave,hover',logo=None)
p.title.text = "Time Series for Price in Euros"
p.grid.grid_line_alpha = 0
p.xaxis.axis_label = "Day"
p.yaxis.axis_label = "Euros"
p.ygrid.band_fill_color = "olive"
p.ygrid.band_fill_alpha = 0.1
p.circle(df['DateTime'],df['EuP'], size=4, legend='close',
color='darkgrey', alpha=0.2)
p.xaxis.formatter = DatetimeTickFormatter(formats=dict(
hours=["%d %B %Y"],
days=["%d %B %Y"],
months=["%d %B %Y"],
years=["%d %B %Y"],
))
hover = p.select(dict(type=HoverTool))
tips = [('when','#tooltip'), ('y','$y')]
hover.tooltips = tips
hover.mode = 'mouse'
p.line(x='DateTime', y='EuP', source=ColumnDataSource(df),
legend='Price',color='navy',alpha=0.7)
show(p)
Also note there is an open issue about the lack of formatting options in the bokeh tooltip. There might be an easier way to not have to format the datestrings as a separate column:
https://github.com/bokeh/bokeh/issues/1239
Also can someone tell how do I format x-axis ticks to show up vertically ?
They look fine to me, sorry I cannot help on that one.
Hope this helps!
PS it would be better next time if you posted a working script with import statements, and a mocked up dataframe to make it possible to test. It took some time to sort it all out. But I am learning Bokeh so that is fine :)
Sorry for not commenting, I don't have enough reputation for that.
The accepted answer by #Alex doesn't work for me (Bokeh 2.0.1), because it is lacking a simple #-sign in the formatter. The working code is this:
HoverTool(tooltips=[('date', '#DateTime{%F}')],
formatters={'#DateTime': 'datetime'})
I have created a wrapper for ScatterPlot in bokeh.
class Visualization():
WIDTH = 1000
TOOLS = "pan,wheel_zoom,box_zoom,reset,save"
class ScatterChart(Visualization):
def __init__(self, data, spec:Dict):
self.data = data
self.x_column = spec["x_axis"]
self.y_column = spec["y_axis"]
self.series_ = spec["series_column"]
self.xlabel = spec['xlabel']
self.ylabel = spec['ylabel']
self.title = spec['title']
def prepare_data(self):
# Get Axis Type
self.xtype = 'datetime' if self.data.dtypes[self.x_column].type is np.datetime64 else 'linear'
self.ytype = 'datetime' if self.data.dtypes[self.x_column].type is np.datetime64 else 'linear'
return self.data
def render(self):
df_ = self.prepare_data()
format_= {}
tool_tip=[]
# For axis
for col in [self.x_column, self.y_column , self.series_]:
if self.data.dtypes[col].type is np.datetime64:
format_['#' + str(col) ] = "datetime" # formatter
tool_tip.append(tuple([str(col) , '#' + str(col) + '{%F}'])) # tool-tip
else:
format_['#' + str(col) ] = "numeral" #
tool_tip.append(tuple([str(col) , '#' + str(col)]))
# print(format_)
# print(tool_tip)
# Add Hover parameters
hover = HoverTool(tooltips= tool_tip
, formatters=format_ )
p=figure(
width = super().WIDTH,
height = 500,
x_axis_label = self.xlabel,
x_axis_type=self.xtype,
y_axis_label = self.ylabel,
y_axis_type=self.ytype,
title = self.title,
tools = super().TOOLS
)
# Get Only Top 10 groups/series to display
for value, color in zip(islice(self.data.groupby(by=[self.series_]
)[self.series_].count().sort_values(ascending=False).rename('cnt').reset_index()[self.series_].tolist(), 10), Category10[10]):
p.scatter(
x=self.x_column,
y=self.y_column,
source=df_.loc[(df_[self.series_]==value)],
color=color,
legend_group=self.series_
)
p.add_tools(hover)
p.toolbar.logo = None
p.legend.location = "top_left"
p.legend.click_policy="hide"
return p
# end of ScatterChart
This is how I initialize this
from visualization import ScatterChart
sc = ScatterChart(
df,
{'x_axis' :'ts',
'y_axis': 'Discus',
'series_column': 'Competition',
'xlabel':'Discus',
'ylabel':'Javeline',
'title':'Discus Vs Javeline'
})
d = sc.render()
show(d)

Bokeh widget-Working Checkbox Group Example

I am evaluating Bokeh to see if it is ready for more extensive use. I have plotted two columns of a dataframe (code at the end), "Close" and "Adj Close".
I want to put in checkboxes to toggle the display of both the line graphs in the plot. So if the relevant checkbox is unchecked the line does not appear. The Bokeh documentation at http://docs.bokeh.org/en/latest/docs/user_guide/interaction.html does talk about checkbox group but doesn't provide an explicit working example. I would appreciate any help in getting checkboxes working for columns of a dataframe.
import pandas as pd
from bokeh.plotting import figure, output_file, show
IBM = pd.read_csv(
"http://ichart.yahoo.com/table.csv?s=IBM&a=0&b=1&c=2011&d=0&e=1&f=2016",
parse_dates=['Date'])
output_file("datetime.html")
p = figure(width=500, height=250, x_axis_type="datetime")
p.line(IBM['Date'], IBM['Close'], color='navy', alpha=0.5)
p.line(IBM['Date'], IBM['Adj Close'], color='red', alpha=0.5)
show(p)
This is obviously a late reply but I'm currently trying to learn python and bokeh to hack out some sort of data dashboard. I was trying to figure out how the checkboxes worked and I stumbled on your question. This solution only works with bokeh serve . I don't know how to make it work in an HTML output.
I'm only modifying the line visibility and not the source. I didn't try it yet but I'm sure the legends would still show the invisible lines
Apologies for duct tape code.
#-| bokeh serve
#-|
import pandas as pd
from bokeh.io import curdoc,output_file, show
from bokeh.layouts import row, widgetbox
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import *
#Widgets
ticker = TextInput(title='Ticker Symbol',value='IBM')
button=Button(label='Lookup',button_type='success')
log = Paragraph(text="""log""",
width=200, height=100)
cb_group = CheckboxButtonGroup(labels=['Close', 'Adj Close'],active=[0,1])
cb_group.labels.append('Placebo')
#Plot
p = figure(title='',width=500, height=250, x_axis_type='datetime')
source = ColumnDataSource({'x': [], 'y1': [],'y2': []})
lineClose=p.line('x','y1',source=source, color='navy', alpha=0.5)
lineAdj=p.line('x','y2',source=source, color='red', alpha=0.5)
lines=[lineClose,lineAdj]
#Event handling
def error(msg):
log.text=msg
def update_data():
try:
src='http://ichart.yahoo.com/table.csv?s={symb}&a=0&b=1&c=2011&d=0&e=1&f=2016'.format(symb=ticker.value)
df=pd.read_csv(src,parse_dates=['Date'])
source.data=({'x': df['Date'], 'y1': df['Close'],'y2': df['Adj Close']})
except:
error('Error ticker')
def update_plot(new):
switch=cb_group.active
for x in range(0,len(lines)):
if x in switch:
lines[x].visible=True
else:
lines[x].visible=False
error('<CheckboxButtonGroup>.active = '+str(switch))
button.on_click(update_data)
cb_group.on_click(update_plot)
inputs=widgetbox(ticker,button,cb_group,log)
curdoc().add_root(row(inputs,p,width=800))
curdoc().title = 'Bokeh Checkbox Example'
button.clicks=1
I added the 'Placebo' checkbox to see if I could append to the checkbox group
instead of the typical method so I'm sure there's a way to more elegantly and dynamically add checkboxes.
I haven't been able to get the check boxes to work yet, although I wouldn't be surprised if that functionality is coming soon. In the meantime, here is a workaround using the multiselect widget:
from bokeh.io import vform
from bokeh.models import CustomJS, ColumnDataSource, MultiSelect
from bokeh.plotting import figure, output_file, show
import pandas as pd
IBM = pd.read_csv(
"http://ichart.yahoo.com/table.csv?s=IBM&a=0&b=1&c=2011&d=0&e=1&f=2016",
parse_dates=['Date'])
output_file("datetime.html")
source = ColumnDataSource({'x': IBM['Date'], 'y1': IBM['Close'], \
'y2': IBM['Adj Close'], 'y1p': IBM['Close'], 'y2p': IBM['Adj Close']})
p = figure(width=500, height=250, x_axis_type="datetime")
p.line('x', 'y1', source=source, color='navy', alpha=0.5)
p.line('x', 'y2', source=source, color='red', alpha=0.5)
callback = CustomJS(args=dict(source=source), code="""
var data = source.get('data');
var f = cb_obj.get('value')
y1 = data['y1']
y2 = data['y2']
y1p = data['y1p']
y2p = data['y2p']
if (f == "line2") {
for (i = 0; i < y1.length; i++) {
y1[i] = 'nan'
y2[i] = y2p[i]
}
} else if (f == "line1") {
for (i = 0; i < y2.length; i++) {
y1[i] = y1p[i]
y2[i] = 'nan'
}
} else if (f == "none") {
for (i = 0; i < y2.length; i++) {
y1[i] = 'nan'
y2[i] = 'nan'
}
} else {
for (i = 0; i < y2.length; i++) {
y1[i] = y1p[i]
y2[i] = y2p[i]
}
}
source.trigger('change');
""")
multi_select = MultiSelect(title="Lines to plot:", \
value=["line1", "line2", "none"], \
options=["line1", "line2", "none"], callback=callback)
layout = vform(multi_select, p)
show(layout)
The output looks like this:
For people still looking for this. It was solved by mosc9575. Here is a slightly modified version of mosc9575's solution code:
import numpy as np
from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider, CheckboxGroup
from bokeh.plotting import ColumnDataSource, figure, show
# initial input data
x = np.linspace(0, 10, 500)
y = np.sin(x)
z = np.cos(x)
name_lst = ['sin', 'cos']
# dataframe
source = ColumnDataSource(data=dict(x=x, y=y, z=z))
# initialize figure
fig = figure()
line_renderer = [
fig.line('x', 'y', source=source, name=name_lst[0]),
fig.line('x', 'z', source=source, name=name_lst[1])
]
line_renderer[0].visible = False
# create a slider and a couple of check boxes
freq_slider = Slider(start=0.1, end=10, value=1, step=.1, title="Frequency")
checkbox = CheckboxGroup(
labels=name_lst,
active=[1, 1],
width=100
)
# callbacks
callback = CustomJS(args=dict(source=source, freq=freq_slider),
code="""
const data = source.data;
const k = freq.value;
const x = data['x'];
const y = data['y'];
const z = data['z'];
for (let i = 0; i < x.length; i++) {
y[i] = Math.sin(k*x[i]);
z[i] = Math.cos(k*x[i]);
}
source.change.emit();
""")
callback2 = CustomJS(args=dict(lines=line_renderer, checkbox=checkbox),
code="""
lines[0].visible = checkbox.active.includes(0);
lines[1].visible = checkbox.active.includes(1);
""")
# changes upon clicking and sliding
freq_slider.js_on_change('value', callback)
checkbox.js_on_change('active', callback2)
layout = row(
fig,
column(freq_slider, checkbox)
)
show(layout)
cos(kx) and a sin(kx) functions can be turned on and off using the checkboxes. With a slider k can be changed. This does not require a bokeh server.

Categories

Resources