Adding simple dropdown to Bokeh plot using pandas data - python

I am very new to Bokeh and cannot seem to find any really good examples of a simple dropdown plot with data from a pandas dataframe. I am working with a dictionary that has 4 different keys, where each key contains a dataframe.
df_vals.keys()
dict_keys(['corn', 'soybeans', 'winterwheat', 'springwheat'])
df_vals['corn'].head()
time 2m_temp_prod 2m_temp_area total_precip_prod total_precip_area
0 2020-09-16 00:00:00 299.346777 299.799234 0.000000 0.000000
1 2020-09-16 06:00:00 294.039512 294.443352 0.191070 0.286952
2 2020-09-16 12:00:00 292.959274 293.182931 0.155765 0.216606
3 2020-09-16 18:00:00 301.318046 301.767516 0.421768 0.485691
4 2020-09-17 00:00:00 300.623567 300.979650 0.363572 0.501164
Next, I can add this data to a source.
source=ColumnDataSource(data=df_vals['corn'])
Plotting from here is simple.
p1=figure(x_axis_type='datetime')
p1.line(x='time', y='2m_temp_prod',source=source)
show(p1)
This does exactly what I want. It plots a line plot with datetime as the x-axis. However, now I want to add a dropdown widget to switch between 2 columns in df_vals['corn'] (2m_temp_prod and total_precip_prod). I have tried this code below but it is not working and I am not sure if that's even the right way to go about it.
def update_plot(attr, old, new):
if new == 'total_precip_prod':
source.data = {
'x' : df_vals['corn']['time'],
'y' : df_vals['corn']['total_precip_prod'].cumsum()
}
select = Select(title="hi", options=['2m_temp_area', 'total_precip_prod'], value='2m_temp_area')
select.on_change('value', update_plot)
# Create layout and add to current document
layout = row(select, p1)
curdoc().add_root(layout)
Ideally, the button would have two options: temps and precip. How would I go about do this?

You can use Bokeh CustomJS callbacks to toggle visibility of the plots according to what's selected in a dropdown. A basic example would look like this:
from bokeh.models import ColumnDataSource, CustomJS, Range1d, Select
from bokeh.plotting import figure, output_notebook, show
from bokeh.layouts import column
import pandas as pd
import numpy as np
# you can also output to an HTML file
output_notebook()
# toy dataframe with two y columns you can switch between
df = pd.DataFrame(data={
"x" : range(0,100),
"y1" : np.random.randint(10, 20, 100),
"y2" : np.random.randint(40, 50, 100)
})
# fix the y_range to be the same for two lines
p = figure(y_range=Range1d(0, 60), plot_width=600, plot_height=400)
# shared datasource
source = ColumnDataSource(df)
plot_1 = p.line(x="x", y="y1", color="teal", source=source, line_width=1)
plot_2 = p.line(x="x", y="y2", color="firebrick", source=source, line_width=1)
# initialise the plot with only y1 visible - to match the dropdown default
plot_2.visible = False
# dropdown widget + Javascript code for interactivity
select = Select(title="Plot to show:", value="Line 1", options=["Line 1", "Line 2"])
select.js_on_change("value", CustomJS(args=dict(line_1=plot_1, line_2=plot_2), code="""
line_1.visible = true
line_2.visible = true
if (this.value === "Line 1") {
line_2.visible = false
} else {
line_1.visible = false
}
"""))
layout = column(select, p)
show(layout)

Related

Bokeh: Open a new data table with each time a point on plot is clicked

I have two data sources with the same keys. Source 1 has key/value pairs with unique keys to be plotted. Source 2 has the same keys as source 1, but each with potentially multiple values.
Is it possible for clicking on a point on the plot (source1) to open a new data table showing all corresponding rows in source2? ie if 3 different points are selected on the plot, then 3 different data tables would be visible.
So there are two pieces I'm hoping for help with:
Only show values for one key in a data table.
Make a new data table appear each time a point is selected on the plot, and disappear when it's unselected.
I figured out how to link the plot to a data table with a different source & the same keys, and have the corresponding rows highlight when a point on the plot is clicked.
import numpy as np
import pandas as pd
from bokeh.models.callbacks import CustomJS
from bokeh.plotting import figure
from bokeh.io import show
from bokeh.layouts import row
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.models import ColumnDataSource, CustomJSFilter, TapTool, HoverTool
source = ColumnDataSource(pd.DataFrame(
np.array([[i, i] for i in range(0, 10)]), columns=['x', 'y']))
data = [[i, "%s.%d" % ("data", i)] for i in range(0, 10)]
data.extend([[i, "%s.%d" % ("data", i)] for i in range(0, 10)])
source2 = ColumnDataSource(pd.DataFrame(data, columns = ['x', 'data']))
columns = [
TableColumn(field='x', title='x'),
TableColumn(field='data', title='data'),
]
data_table = DataTable(source=source2, columns=columns, width=600, height=400)
selection_callback = CustomJS(args=dict(source=source, source2=source2, p2=data_table), code="""
var indices = []
var source_x = source.data.x[source.selected.indices[0]]
source2.data.x.forEach((source2_x, i) => {
if (source2_x == source_x) {
indices.push(i)
}
});
source2.selected.indices = indices
p2.change.emit()
""")
plot = figure(title="Plot1", tools="tap,pan,box_zoom,wheel_zoom,save,reset")
plot.select(TapTool).callback = selection_callback
source.selected.js_on_change('indices', selection_callback)
plot.circle('x', 'y', source=source, line_color=None, color='red', size=6)
show(row(plot, data_table))
I tried replacing "source2.selected.indices" with "source2.indices" to try to hide unselected indices, but that didn't seem to do anything.
And I haven't been able to find any information about making plots or tables appear/disappear with interactions.
Played around with this some more and I was able to figure it out.
Specifically:
Created a master ColumnDataSource for the data table.
Set the data in the data table to just the relevant subset.
set the data table visibility to false to hide it, unless a specific value is selected.
Here's the relevant CustomJS code snippet:
callback = CustomJS(args=dict(source=source, source2_master=source2_master, source2=source2, p2=data_table), code="""
var x_vals = []
var y_vals = []
var source_x = source.data.x[source.selected.indices[0]]
source2_master.data.x.forEach((source2_x, i) => {
if (source2_x == source_x) {
x_vals.push(source2_master.data.x[i])
y_vals.push(source2_master.data.y[i])
}
});
source2.data = {'x': [x_vals], 'y': [y_vals]}
p2.visible = x_vals.length != 0;
p2.change.emit()
""")

How to plot visualization with Interactive Feature Selection in Bokeh, Python

The task is to automate the Visualization. The CSV file contains large nos of features (column names e:g. 32 nos it may increase in future). The task is to plot Interactive Visualization. All the examples I found are hardcoded for the dynamic features selection.
But the requirement is to make the stuff dynamic. How to make it dynamic? Please guide.
I have successfully plotted the graph dynamically, but could not connect the interactive part. The code is as follows:
import pandas as pd
from bokeh.plotting import figure
from bokeh.io import show
from bokeh.models import CustomJS,HoverTool,ColumnDataSource,Select
from bokeh.models.widgets import CheckboxGroup
from bokeh.models.annotations import Title, Legend
import itertools
from bokeh.palettes import inferno
from bokeh.layouts import row
def creat_plot(dataframe):
data=dataframe
#Converting the timestamp Column to Timestamp datatype so that it can be used for Plotting on X-axis
data['timestamp'] = pd.to_datetime(data['timestamp'])
#Segregating Date and Time from timestamp column. It will be used in Hover Tool
date = lambda x: str(x)[:10]
data['date'] = data[['timestamp']].applymap(date)
time= lambda x: str(x)[11:]
data['time'] = data[['timestamp']].applymap(time)
#Converting whole dataframe ColumnDatasource for easy usage in hover tool
source = ColumnDataSource(data)
# List all the tools that you want in your plot separated by comas, all in one string.
TOOLS="crosshair,pan,wheel_zoom,box_zoom,reset,hover"
# New figure
t = figure(x_axis_type = "datetime", width=1500, height=600,tools=TOOLS,title="Plot for Interactive Features")
#X-axis Legend Formatter
t.xaxis.formatter.days = '%d/%m/%Y'
#Axis Labels
t.yaxis.axis_label = 'Count'
t.xaxis.axis_label = 'Date and Time Span'
#Grid Line Formatter
t.ygrid.minor_grid_line_color = 'navy'
t.ygrid.minor_grid_line_alpha = 0.1
t.xgrid.visible = True
t.ygrid.visible= True
#Hover Tool Usage
t.select_one(HoverTool).tooltips = [('Date', '#date'),('Time', '#time')]
#A color iterator creation
colors = itertools.cycle(inferno(len(data.columns)))
#A Line type iterator creation
line_types= ['solid','dashed','dotted','dotdash','dashdot']
lines= itertools.cycle(line_types)
column_name=[]
#Looping over the columns to plot the Data
for m in data.columns[2:len(data.columns)-2]:
column_name.append(m)
a=t.line(data.columns[0], m ,color=next(colors),source=source,line_dash=next(lines), alpha= 1)
#Adding Label Selection Check Box List
column_name= list(column_name)
checkboxes = CheckboxGroup(labels = column_name, active= [0,1,2])
show(row(t,checkboxes))
The above function can be used as follows:
dataframe= pd.read_csv('data.csv')
creat_plot(dataframe)
**The above code is executed on following requirements:
Bokeh version: 2.2.3
Panda Version: 1.1.3
The plot should be linked with the checkbox values. The features selected through the checkboxes shall be plotted only.
The solution to the above requirement is as follows:
import pandas as pd
from bokeh.plotting import figure
from bokeh.io import show,output_file
from bokeh.models import CustomJS,HoverTool,ColumnDataSource,Select
from bokeh.models.widgets import CheckboxGroup
from bokeh.models.annotations import Title, Legend
import itertools
from bokeh.palettes import inferno
from bokeh.layouts import row
def creat_plot(dataframe):
data=dataframe
#Converting the timestamp Column to Timestamp datatype so that it can be used for Plotting on X-axis
data['timestamp'] = pd.to_datetime(data['timestamp'])
#Segregating Date and Time from timestamp column. It will be used in Hover Tool
date = lambda x: str(x)[:10]
data['date'] = data[['timestamp']].applymap(date)
time= lambda x: str(x)[11:]
data['time'] = data[['timestamp']].applymap(time)
#Converting whole dataframe ColumnDatasource for easy usage in hover tool
source = ColumnDataSource(data)
# List all the tools that you want in your plot separated by comas, all in one string.
TOOLS="crosshair,pan,wheel_zoom,box_zoom,reset,hover"
# New figure
t = figure(x_axis_type = "datetime", width=1500, height=600,tools=TOOLS,title="Plot for Interactive Visualization")
#X-axis Legend Formatter
t.xaxis.formatter.days = '%d/%m/%Y'
#Axis Labels
t.yaxis.axis_label = 'Count'
t.xaxis.axis_label = 'Date and Time Span'
#Grid Line Formatter
t.ygrid.minor_grid_line_color = 'navy'
t.ygrid.minor_grid_line_alpha = 0.1
t.xgrid.visible = True
t.ygrid.visible= True
#Hover Tool Usage
t.select_one(HoverTool).tooltips = [('Date', '#date'),('Time', '#time')]
#A color iterator creation
colors = itertools.cycle(inferno(len(data.columns)))
#A Line type iterator creation
line_types= ['solid','dashed','dotted','dotdash','dashdot']
lines= itertools.cycle(line_types)
feature_lines = []
column_name=[]
#Looping over the columns to plot the Data
for m in data.columns[2:len(data.columns)-2]:
column_name.append(m)
#Solution to my question is here
feature_lines.append(t.line(data.columns[0], m ,color=next(colors),source=source,line_dash=next(lines), alpha= 1, visible=False))
#Adding Label Selection Check Box List
column_name= list(column_name)
#Solution to my question,
checkbox = CheckboxGroup(labels=column_name, active=[])
#Solution to my question
callback = CustomJS(args=dict(feature_lines=feature_lines, checkbox=checkbox), code="""
for (let i=0; i<feature_lines.length; ++i) {
feature_lines[i].visible = i in checkbox.active
}
""")
checkbox.js_on_change('active', callback)
output_file('Interactive_data_visualization.html')
show(row(t, checkbox))

bokeh: How to edit a df or CDS-object through box_select?

I'm trying to label a pandas-df (containing timeseries data) with the help of
a bokeh-lineplot, box_select tool and a TextInput widget in a jupyter-notebook. How can I access the by the box_select selected data points?
I tried to edit a similar problems code (Get selected data contained within box select tool in Bokeh) by changing the CustomJS to something like:
source.callback = CustomJS(args=dict(p=p), code="""
var inds = cb_obj.get('selected')['1d'].indices;
[source.data['xvals'][i] for i in inds] = 'b'
"""
)
but couldn't apply a change on the source of the selected points.
So the shortterm goal is to manipulate a specific column of source of the selected points.
Longterm I want to use a TextInput widget to label the selected points by the supplied Textinput. That would look like:
EDIT:
That's the current code I'm trying in the notebook, to reconstruct the issue:
from random import random
import bokeh as bk
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, HoverTool
from bokeh.plotting import figure, output_file, show, output_notebook
output_notebook()
x = [random() for x in range(20)]
y = [random() for y in range(20)]
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(data=dict(x=x, y=y, label=[i for i in "a"*20]))
p1 = figure(plot_width=400, plot_height=400, tools="box_select", title="Select Here")
p1.circle('x', 'y', source=source, alpha=0.6)
p1.add_tools(hovertool)
source.selected.js_on_change('indices', CustomJS(args=dict(source=source), code="""
var inds = cb_obj.indices;
for (var i = 0; i < inds.length; i++) {
source.data['label'][inds[i]] = 'b'
}
source.change.emit();
""")
)
layout = row(p1)
show(layout)
The main thing to note is that BokehJS can only automatically notice updates when actual assignments are made, e.g.
source.data = some_new_data
That would trigger an update. If you update the data "in place" then BokehJS is not able to notice that. You will have to be explicit and call source.change.emit() to let BokehJS know something has been updated.
However, you should also know that you are using three different things that are long-deprecated and will be removed in the release after next.
cb_obj.get('selected')
There is no need to ever use .get You can just access properties directly:
cb_obj.selected
The ['1d'] syntax. This dict approach was very clumsy and will be removed very soon. For most selections you want the indices property of the selection:
source.selected.indices
source.callback
This is an ancient ad-hoc callback. There is a newer general mechanism for callbacks on properties that should always be used instead
source.selected.js_on_change('indices', CustomJS(...))
Note that in this case, the cb_obj is the selection, not the data source.
With the help of this guide on how to embed a bokeh server in the notebook I figured out the following minimal example for my purpose:
from random import random
import pandas as pd
import numpy as np
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, BoxSelectTool
from bokeh.models.widgets import TextInput
output_notebook()
def modify_doc(doc):
# create a plot and style its properties
TOOLS="pan,wheel_zoom,reset"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
hovertool=HoverTool(tooltips=[("Index", "$index"), ("Label", "#label")])
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
label = [i for i in "a"*10]
))
p.scatter("xvals", "yvals",source=source, color="white")
p.line("xvals", "yvals",source=source)
p.add_tools(BoxSelectTool(dimensions="width"))
p.add_tools(hovertool)
# create a callback that will add a number in a random location
def callback():
inds = source.selected.indices
for i in inds:
source.data['label'][i] = label_input.value.strip()
print(source.data)
new_data = pd.DataFrame(source.data)
new_data.to_csv("new_data.csv", index=False)
# TextInput to specify the label
label_input = TextInput(title="Label")
# add a button widget and configure with the call back
button = Button(label="Label Data")
button.on_click(callback)
# put the button and plot in a layout and add to the document
doc.add_root(column(button,label_input, p))
show(modify_doc, notebook_url="http://localhost:8888")
That generates the following UI:
BTW: Due to the non-existing box_select tool for the line glyph I use a workaround by combining it with invisible scatter points.
So far so good, is there a more elegant way to access the data.source/new_data df in the notebook outside modify_doc() than exporting it within the callback?

Creating dynamic HoverTool tooltips in Bokeh based off of a dropdown using CustomJS callbacks

I've got a scatterplot I've created in Bokeh, and I'd very much like to be able to change what tooltips display on the Hovertool depending on your selection in a dropdown menu. If "Stat Set 1" is selected, I want to show stats 1&2. If "Stat Set 2" is selected, I want to show stats 3,4,5.
I'd like the end result to be a html file, so using CustomJS for the call back is probably a must. Here is where I have managed to get with the code. The issue is likely with the callback, as I'm not at all sure how to mess with tooltips via a callback.
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import CustomJS, ColumnDataSource, HoverTool, ColumnDataSource, Select
from bokeh.layouts import row
#Create a dataframe with x and y coordinates and 4 different statistics
df = pd.DataFrame({
'x':[1,2,3],
'y':[1,2,3],
'stat1':[1,2,3],
'stat2':[4,5,6],
'stat3':[7,8,9],
'stat4':[10,11,12],
'stat5':[13,14,15]
})
#Create Bokeh's ColumnDataSource
source=ColumnDataSource(data=df)
#Create the different options for the Hovertool tooltips
option1=[('Stat1','#stat1'),
('Stat2','#stat2')]
option2=[('Stat3','#stat3'),
('Stat4','#stat4'),
('Stat5','#stat5')]
#Set the default option for the Hovertool tooltips
hover=HoverTool(tooltips=option1)
#Create the figure
plot = figure(tools=[hover])
plot.circle('x', 'y', source=source)
#Create the callback that will update the tooltips
callback = CustomJS (args=dict(tt=plot.hover), code="""
if (cb_obj.value='Stat Set 1') {
tt.tooltips=option1
} else {
tt.tooltips=option2
}
""")
#Create a dropdown menu that allows you to change which set of stats will populate the tooltips
stat_select=Select(options=['Stat Set 1', 'Stat Set 2'],
value='Stat Set 1',
title='What stats set do you want to see when you hover?', callback=callback)
show(row(stat_select,plot))
I found someone with the answer over on the Bokeh Discourse.
plot.hover returns a list, as a plot can have multiple tooltips.
So instead of using tt.tooltips = option1, tt must be subscripted to use the first entry of the list, ie tt[0].tooltips = option1.
options = [option1, option2]
#Create the callback that will update the tooltips
callback = CustomJS (args=dict(tt=plot.hover, opts=options), code="""
if (cb_obj.value=='Stat Set 1') {
tt[0].tooltips=opts[0]
} else {
tt[0].tooltips=opts[1]
}
""")

Python - Bokeh vbar hover tool

I have a simple multiple data bar graph (non stacked) and wish to be able to be shown the (max) value of the bar chart upon a hover over with the mouse.
I'm having trouble linking the hover location to the data though. I'm not sure how of the syntax/coding for calling an index from the bar chart.
Here is my code:
from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import figure
output_file("bars.html")
LOCATIONS = ['CPC','OG2','HS82-83','IG6','IG4','IG10']
CHECKS = ['AID CHECKS', 'ITEMS SCREENED', 'PERSONS SCREENED']
data = {'LOCATIONS' : LOCATIONS,
'AID CHECKS' : [208,622,140,1842,127,1304],
'PERSONS SCREENED' : [201,484,126,1073,81,676],
'ITEMS SCREENED' : [28,71,31,394,32,207]}
x = [ (location, check) for location in LOCATIONS for check in CHECKS ]
counts = sum(zip(data['AID CHECKS'], data['PERSONS SCREENED'], data['ITEMS SCREENED']), ()) # like an hstack
source = ColumnDataSource(data=dict(x=x, counts=counts))
p = figure(x_range=FactorRange(*x), plot_height=600, plot_width=990, title="NPS Locations by Security Checks",
tools="pan,wheel_zoom,box_zoom,reset, save")
p.xaxis.axis_label_text_font_size = "5pt"
p.xaxis.axis_label_text_font_style='bold'
p.vbar(x='x', top='counts', width=0.9, source=source)
p.add_tools(HoverTool(tooltips=[("LOCATION", "#location"), ("TOTAL", "#check")]))
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
show(p)
Adjust the following line:
p.add_tools(HoverTool(tooltips=[("LOCATION", "#x"), ("TOTAL", "#counts")]))
See the documentation:
Field names that begin with # are associated with columns in a
ColumnDataSource. For instance the field name "#price" will display
values from the "price" column whenever a hover is triggered. If the
hover is for the 17th glyph, then the hover tooltip will
correspondingly display the 17th price value.

Categories