Related
I'm trying to write a code which adds hovertool performing customer js-code over figure.line object. In order to do it I used userguide code as example (https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-hover) but simplified and modified (see below). I've found that it works over figure.segment however doesn't work over figure.line.
Here is complete example:
from bokeh.models import CustomJS, HoverTool
from bokeh.plotting import figure, output_file, show
output_file("hover_callback.html")
# define some points and a little graph between them
x = [2, 3, 5, 6, 8, 7]
y = [6, 4, 3, 8, 7, 5]
p = figure(plot_width=400, plot_height=400, tools="", toolbar_location=None)
lines = p.line(x, y, line_color='blue', line_width=5)
seg = p.segment(x[0:3], y[0:3], x1=x[3:], y1=y[3:], color='olive', alpha=0.6, line_width=3)
code = """
const indices = cb_data.index.indices
if (indices.length > 0) {
console.log('Hello!')
}
"""
callback = CustomJS(code=code)
p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[lines, seg]))
show(p)
cb_data.index.indices is not empty when I put pointer over segment (olive-color) but not over line (blue).
Is it sort of expected behaviour? If so I would appreciate reference to some doc where it's explained. THanks!
You can activate the HoverTool for lines, too. But you have to adapt your JavaScript quite a bit. Lines return the information of the index a bit different, you have to ask for const line_indices = cb_data.index.line_indices.
Here is your complete example:
from bokeh.models import CustomJS, HoverTool
from bokeh.plotting import figure, output_file, show
output_file("hover_callback.html")
# define some points and a little graph between them
x = [2, 3, 5, 6, 8, 7]
y = [6, 4, 3, 8, 7, 5]
p = figure(plot_width=400, plot_height=400, tools="", toolbar_location=None)
lines = p.line(x, y, line_color='blue', line_width=5)
seg = p.segment(x[0:3], y[0:3], x1=x[3:], y1=y[3:], color='olive', alpha=0.6, line_width=3)
code = """
const indices = cb_data.index.indices
const line_indices = cb_data.index.line_indices
if (indices.length + line_indices.length > 0) {
console.log('Hello!')
}
"""
callback = CustomJS(code=code)
p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[lines, seg]))
show(p)
I am trying to plot several scatter glyphs to one figure, using a loop. The goal is to use an own glyph for each brand and to update the values if another value is chosen by the Select widgets.
But the figure shows an empty graph. I assume that the problem is the ColumnDataSource and the update in the function "update()".
This is an executable example. Do you have any idea?
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool
from bokeh.plotting import figure, show
import pandas as pd
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
id = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
df = pd.DataFrame(zip(brands, product, price, size, value, id), columns=col)
# Widgets:
select_x_axis = Select(title="x-Axis:", value=label[0], options=label)
select_y_axis = Select(title="y-Axis:", value=label[1], options=label)
# Set up figure
hover = HoverTool(tooltips=[
("index", "#id"),
('Brand', '#brand'),
('Product', '#product'),
(select_x_axis.value, '#x'),
(select_y_axis.value, '#y')
])
# Set up plots:
fig = figure(plot_height=400, plot_width=800, title="xyz",
# tooltips=TOOLTIPS,
tools=[hover, 'reset'],
x_axis_label=select_x_axis.value,
y_axis_label=select_y_axis.value)
source = {}
plots = {}
for brand in brands:
# Create Column Data Source that will be used by the plot
source[brand] = ColumnDataSource(data=dict(x=[], y=[], id=[], product=[], brand=[]))
plots[brand] = fig.scatter(x='x', y='y', size=5, source=source[brand])
def update():
x_name = select_x_axis.value
y_name = select_y_axis.value
fig.xaxis.axis_label = x_name
fig.yaxis.axis_label = y_name
for brand in brands:
df1 = df.loc[df['brand'] == brand]
source[brand].data = dict(
x=df1[x_name],
y=df1[y_name],
id=df1['ID'],
product=df1['product'],
brand=df1['brand']
)
# Set up layouts and add to document
controls = [select_x_axis, select_y_axis]
for control in controls:
control.on_change('value', lambda attr, old, new: update())
inputs = column(select_x_axis, select_y_axis)
update() # initial load of the data
show(row(inputs, fig, width=1000))
#curdoc().add_root(row(inputs, fig, width=1000))
#curdoc().title = "xyz"
resultig plot
Something like that ?
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool, CustomJS
from bokeh.plotting import figure, show
import pandas as pd
def make_data() :
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
ident = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
return pd.DataFrame(zip(brands, product, price, size, value, ident), columns=col)
if __name__ == "__main__" :
df = make_data()
default_xcol = "price"
default_ycol = "size"
df["xvalues"] = df[default_xcol]
df["yvalues"] = df[default_ycol]
# Scatter plot
hover = HoverTool(tooltips=[(name, "#" + name) for name in df.columns])
figure = figure(tools=[hover, 'reset'])
# Source
source = ColumnDataSource(df)
figure.scatter("xvalues", "yvalues", source=source)
# Selects
options = ["product", "price", "size", "value"]
select_x_axis = Select(title="x-Axis:", value=default_xcol, options=options)
select_y_axis = Select(title="y-Axis:", value=default_ycol, options=options)
# callback
callback = CustomJS(args={"source":source, "axis":figure.xaxis[0]}, code="""
source.data['xvalues'] = source.data[cb_obj.value];
source.change.emit();
axis.axis_label = cb_obj.value;
""")
select_x_axis.js_on_change("value", callback)
callback = CustomJS(args={"source":source, "axis":figure.yaxis[0]}, code="""
source.data['yvalues'] = source.data[cb_obj.value];
source.change.emit();
axis.axis_label = cb_obj.value;
""")
select_y_axis.js_on_change("value", callback)
show(row(
column(select_x_axis, select_y_axis),
figure, width=1000))
#jsgounot Thanks for your help. I figured something out that works quiet well at the moment:
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool, CustomJS
from bokeh.plotting import figure, show
from bokeh.palettes import d3
import pandas as pd
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
id = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
colors = d3["Category20c"][len(brands)]
markers = ['circle', 'square', 'triangle', 'asterisk', 'circle_x', 'square_x', 'inverted_triangle', 'x', 'circle_cross', 'square_cross', 'diamond', 'cross']
df = pd.DataFrame(zip(id, brands, product, price, size, value), columns=col)
default_xcol = "price"
default_ycol = "size"
df["xvalues"] = df[default_xcol]
df["yvalues"] = df[default_ycol]
# Widgets:
select_x_axis = Select(title="x-Axis:", value=label[0], options=label)
select_y_axis = Select(title="y-Axis:", value=label[1], options=label)
# Set up figure
hover = HoverTool(tooltips=[
("index", "#ID"),
('Brand', '#brand'),
('Product', '#product'),
(select_x_axis.value, '#xvalues'),
(select_y_axis.value, '#yvalues')
])
# Set up plots:
fig = figure(plot_height=400, plot_width=800, title="xyz",
# tooltips=TOOLTIPS,
tools=[hover, 'reset'],
x_axis_label=select_x_axis.value,
y_axis_label=select_y_axis.value)
df_brand = []
sources = []
plots = {}
for index, brand in enumerate(df.brand.unique()):
df_brand.append(df.loc[df['brand'] == brand])
sources.append(ColumnDataSource(df_brand[index]))
plots[brand] = fig.scatter(x='xvalues', y='yvalues', source=sources[index],
legend_label=brand,
marker=markers[index],
color=colors[index])
fig.legend.click_policy="hide"
callback_x = CustomJS(args={'sources': sources, 'axis': fig.xaxis[0], 'brands': df.brand.unique()}, code="""
for (var i = 0; i <= 2; i++){
var source = sources[i]
source.data['xvalues'] = source.data[cb_obj.value];
source.change.emit();
}
axis.axis_label = cb_obj.value;
""")
callback_y = CustomJS(args={'sources': sources, 'axis': fig.yaxis[0], 'hov': fig.hover, 'brands': df.brand.unique()}, code="""
for (var i = 0; i <= 2; i++){
var source = sources[i]
source.data['yvalues'] = source.data[cb_obj.value];
source.change.emit();
}
axis.axis_label = cb_obj.value;
""")
select_x_axis.js_on_change('value', callback_x)
select_y_axis.js_on_change('value', callback_y)
show(row(
column(select_x_axis, select_y_axis),
fig, width=1000))
Any ideas what's supposed to go where the triple '?'s are?
import pandas as pd
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider, Select
import bokeh.plotting as bp
from bokeh.plotting import Figure, output_file, show
from bokeh.models import HoverTool, DatetimeTickFormatter
# Create an output file
bp.output_file('columnDataSource.html')
# Create your plot as a bokeh.figure object
myPlot = bp.figure(height = 600,
width = 800,
y_range=(0,3))
x_values = [1, 2, 3, 4, 5]
y_values = [1, 2, 3, 4, 5]
myPlot.line(x = x_values, y= y_values, line_width=2)
callback = CustomJS(args={
'source1': {'x': [1,2,3,4], 'y':[1,1,1,1]},
'source2': {'x': [0,0,0,0], 'y':[2,2,2,2]},
'source3': {'x': [1,2,3,4], 'y':[1,1,1,1]}},
code="""
var data1 = source1;
var data2 = source2;
var data3 = source3;
var f = cb_obj.value;
if(f == 'A'){
console.log("A selected from dropdown.");
data1.x = data1.x;
data1.y = data1.y;
}
else if(f == 'B'){
// Substitute all old data1 values in with data2 values
console.log("B selected from dropdown.");
data1.x = data2.x;
data1.y = data2.y;
}
else{
console.log("C selected.");
// Substitute all old data1 values in with data3 values
data1.x = data3.x;
data1.y = data3.y;
}
// Problematic line!
???.change.emit();
""")
select = Select(title='Choose', value='A', options=['A','B','C'])
select.js_on_change('value', callback)
layout = column(select, myPlot)
show(layout) # et voilĂ .
I expect my x and y values to change and plot accordingly to my Bokeh graph.
Nothing is changing at the moment as I don't know what object's "trigger" function I'm supposed to be calling. Please help, I'm new to Bokeh.
You do ColumnDataSource.change.emit() if you updated the data source fields by reference e.g. when you update only x or only y:
ColumnDataSource.data['x'] = [4, 3, 2, 1]
ColumnDataSource.change.emit()
When you update them both you do:
ColumnDataSource.data = new_data
Where new_data is a new json object like {'x': [1], 'y':[2]}.
The reason for this is that JS can automatically detect a change when existing object is replaced with a new one but it cannot detect changes by reference so in those cases you need explicitly to call: ColumnDataSource.change.emit() to update the BokehJS model.
Here is your modified code:
from bokeh.models import CustomJS, ColumnDataSource, Select, Column
from bokeh.plotting import figure, show
myPlot = figure(y_range = (0, 4))
data = {'A': {'x': [1, 2, 3, 4], 'y':[1, 1, 1, 1]},
'B': {'x': [1, 2, 3, 4], 'y':[2, 2, 2, 2]},
'C': {'x': [1, 2, 3, 4], 'y':[3, 3, 3, 3]} }
source = ColumnDataSource(data['A'])
myPlot.line('x', 'y', line_width = 2, source = source)
callback = CustomJS(args = {'source': source, 'data': data},
code = """source.data = data[cb_obj.value]; """)
select = Select(title = 'Choose', value = 'A', options = ['A', 'B', 'C'])
select.js_on_change('value', callback)
layout = Column(select, myPlot)
show(layout)
I plot data in a bar chart. The data is grouped in two levels where one level is year. I add a range slider in order to alter the x_range of the plot with respect to which years to show. This I have tried to implement through a CustomJS callback (first time I try CustomJS).
Using the slider the factors on the x-axis gets updated as expected. However if I then use the zoom tool and afterwards use the reset tool I get an error message in the web console:
Error: duplicate factor or subfactor: 2016
Not sure what I'm doing wrong with the setup of the data. Is the update of the factor range wrong?
I'm using version 1.1.0 of Bokeh on MacOS. Same error observed in Safari and Firefox.
The code below will reproduce the error.
from bokeh.models import ColumnDataSource, FactorRange, RangeSlider, CustomJS
from bokeh.plotting import figure
from bokeh.layouts import column
import pandas as pd
output_file("grouped_customJS.html")
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
data = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 3, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
df = pd.DataFrame.from_dict(data)
df=df.set_index('fruits').stack().reset_index()
df=df.rename(columns={'level_1':'year', 0:'value'})
# add year as int column for slider
df['year_int'] = df['year'].astype(int)
df=df.set_index(['fruits','year'])
cats = df.index.values
source = ColumnDataSource(
data = {
'categories': cats,
'values': df['value'],
'year': df['year_int']
}
)
p = figure(
x_range=FactorRange(*cats),
plot_height=250,
title="Fruit Counts by Year",
)
p.vbar(x='categories', top='values', width=0.9, source=source)
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
slider = RangeSlider(
start=df['year_int'].min(),
end=df['year_int'].max(),
step = 1,
value = (df['year_int'].min(), df['year_int'].max()),
)
callback = CustomJS(args=dict(slider=slider, source=source, plt = p), code="""
plt.x_range.factors = [];
for (var i = 0; i < source.get_length(); i++){
if (source.data['year'][i] >= slider.value[0] && source.data['year'][i] <= slider.value[1]){
plt.x_range.factors.push(source.data['categories'][i]);
}
}
""")
slider.js_on_change('value', callback)
p.x_range.js_on_change('factors', callback)
show(column(p, slider))
Try this (works fine with Bokeh v1.1.0):
callback = CustomJS(args = dict(slider = slider, source = source, plt = p), code = """
var factors = []
for (var i = 0; i < source.get_length(); i++){
if (source.data['year'][i] >= slider.value[0] && source.data['year'][i] <= slider.value[1]){
factors.push(source.data['categories'][i]);
}
}
plt.x_range.factors = factors; """)
Considering a vertical stacked bar plot in which every column is composed of multiple bars (segments). Is it possible to add a tooltip on every segment? At the moment the same tooltip is attached to the all the segments that compose the column.
from bokeh.core.properties import value
from bokeh.io import show, output_file
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.models import HoverTool
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]
colors = ["#c9d9d3", "#718dbf", "#e84d60"]
data = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
source = ColumnDataSource(data=data)
p = figure(x_range=fruits, plot_height=250, title="Fruit Counts by Year",
toolbar_location=None, tools="")
p.vbar_stack(years, x='fruits', width=0.9, color=colors, source=source,
legend=[value(x) for x in years])
tooltips = HoverTool(
tooltips=[
("2015", "#2015"),
("2016", "#2016"),
("2017", "#2017"),
("index", "$index")
]
)
p.add_tools(tooltips)
show(p)
This can be done by using basic glyphs. Add bar for each year separately, and add a hovertool to that.
from bokeh.core.properties import value
from bokeh.io import show, output_file, output_notebook
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.models import HoverTool
from copy import deepcopy
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]
colors = ["#c9d9d3", "#718dbf", "#e84d60"]
data = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
#deepcopy the data for later use
data1 =deepcopy(data)
#create cumulative sum over years for plotting using vbar
for i in range(1,len(years)):
data[years[i]] = [sum(x) for x in zip(data[years[i]], data[years[i-1]])]
p = figure(x_range=fruits, plot_height=250, title="Fruit Counts by Year",
toolbar_location=None, tools="")
#create bars for each years
for i in range(len(years)):
if i==0:
rx = p.vbar(x=fruits, top=data[years[i]], bottom=[0]*len(fruits), width=0.9, color=colors[i], legend=years[i])
rx.data_source.add(data1[years[i]], "count") #add a column in data source for just the count
else:
rx = p.vbar(x=fruits, top=data[years[i]], bottom=data[years[i-1]], width=0.9, color=colors[i], legend=years[i])
rx.data_source.add(data1[years[i]], "count") #add a column in data source for just the count
#add hover tool for each bar chart
for i in range(len(years)):
p.add_tools(HoverTool(tooltips=[(str(years[i]), "#count"),("Fruit", "#x")], renderers=[r[i]]))
#output_notebook()
show(p)
I tweaked Aritesh's code so it would run for me:
from bokeh.core.properties import value
from bokeh.io import show, output_file, output_notebook
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.models import HoverTool
from copy import deepcopy
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]
colors = ["#c9d9d3", "#718dbf", "#e84d60"]
data = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
#deepcopy the data for later use
data1 =deepcopy(data)
#create cumulative sum over years for plotting using vbar
for i in range(1,len(years)):
data[years[i]] = [sum(x) for x in zip(data[years[i]], data[years[i-1]])]
p = figure(x_range=fruits, plot_height=250, title="Fruit Counts by Year",
toolbar_location=None, tools="")
#create bars for each years
rx = []
for i in range(len(years)):
if i==0:
rx.append(p.vbar(x=fruits, top=data[years[i]], bottom=[0]*len(fruits), width=0.9, color=colors[i], legend=years[i]))
rx[i].data_source.add(data1[years[i]], "count") #add a column in data source for just the count
else:
rx.append(p.vbar(x=fruits, top=data[years[i]], bottom=data[years[i-1]], width=0.9, color=colors[i], legend=years[i]))
rx[i].data_source.add(data1[years[i]], "count") #add a column in data source for just the count
#add hover tool for each bar chart
for i in range(len(years)):
p.add_tools(HoverTool(tooltips=[(str(years[i]), "#count"),("Fruit", "#x")], renderers=[rx[i]]))
#output_notebook()
show(p)