bokeh: custom callback for hovertool for figure.line - bokeh

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)

Related

How to wrap/align long yaxis labels in Bokeh?

import hvplot.pandas
from bokeh.sampledata.autompg import autompg_clean
autompg_clean['origin']=autompg_clean.origin.map({'North America': 'North America '*5,
'Asia': 'Asia '*5,
'Europe': 'Europe '*5,
})
Here is the corresponding annotated output. I have tried using p=hv.render() to get the Bokeh figure object back, but doing something like p.yaxis.major_label_text_align = 'left' does not seem to do anything even if I inject newline \n characters into the long string label.
Multiline labels are available with the newline charactert \n for categorical factors.
I was not able to reproduce your example, but I think the solution is to set you y-axis to a FactorRange and set the factors with a list of your wanted strings, which can include \n.
See the example below, which is adapted from here.
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource
from bokeh.palettes import GnBu3, OrRd3
from bokeh.plotting import figure
output_file("stacked_split.html")
fruits = [f'{item}\n{item}' for item in ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']]
years = ["2015", "2016", "2017"]
exports = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
imports = {'fruits' : fruits,
'2015' : [-1, 0, -1, -3, -2, -1],
'2016' : [-2, -1, -3, -1, -2, -2],
'2017' : [-1, -2, -1, 0, -2, -2]}
p = figure(y_range=fruits, height=250, x_range=(-16, 16), title="Fruit import/export, by year",
toolbar_location=None)
p.hbar_stack(years, y='fruits', height=0.9, color=GnBu3, source=ColumnDataSource(exports),
legend_label=["%s exports" % x for x in years])
p.hbar_stack(years, y='fruits', height=0.9, color=OrRd3, source=ColumnDataSource(imports),
legend_label=["%s imports" % x for x in years])
p.y_range.range_padding = 0.1
p.ygrid.grid_line_color = None
p.legend.location = "top_left"
p.axis.minor_tick_line_color = None
p.outline_line_color = None
show(p)
Output

Once a dropdown option is selected, how do I "change.emit" or "trigger change" on the plot?

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)

Bokeh: Duplicate factor or sub factor error with CustomJS change of x_range

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; """)

Change ColorMapper via callback JS in standalone Bokeh plot

I would like to create a standalone document, similar to the server app example 'Crossfilter': to select different columns for coloring the circles and to update the colorbar as well.
I define a CustomJS with the code below, where I create a new LinearColorMapper with the calculated low and high values. (For debugging purposes, I intentionally add different palette than the one set via the Python code).
var low = Math.min.apply(Math,source.data[cb_obj.value]);
var high = Math.max.apply(Math,source.data[cb_obj.value]);
var color_mapper = new Bokeh.LinearColorMapper({palette:'Viridis5', low:low, high:high});
cir.glyph.fill_color = {field: cb_obj.value, transform: color_mapper};
cir.glyph.line_color = {field: cb_obj.value, transform: color_mapper};
color_bar.color_mapper = color_mapper;
source.change.emit();
As a result, when selecting the column, the circles become white, the line black, the ticks of the color bar change correctly, but the palette does not change.
Could you help me setting the proper attributes in the callback? Thank you in advance.
Original state, color mapper set from Python code
After selecting the column 'd'
I created a "minimal working example" to show how far I got. The full project with the template it can be found here: https://github.com/pintergreg/bokehjscolormapperexample
It looks like you cannot reference a colour pallet like this in BokehJS. Just pass Viridis5 variable to JS callback and it works (tested on Bokeh v1.0.4):
import pandas as pd
from bokeh.models import ColumnDataSource, ColorBar, Select, CustomJS
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
from bokeh.palettes import Spectral5, Viridis5
from bokeh.transform import linear_cmap
from bokeh.embed import components
from jinja2 import Environment, FileSystemLoader
df = pd.DataFrame({"a": [2, 6, 5, 3, 7, 8, 1, 9, 2, 4],
"b": [3, 5, 7, 1, 0, 6, 5, 4, 2, 9],
"c": [11, 12, 13, 14, 11, 13, 15, 14, 15, 12],
"d": [21, 23, 24, 25, 21, 22, 23, 24, 25, 22]})
source = ColumnDataSource(df)
mapper = linear_cmap(field_name = "c", palette = Spectral5,
low = min(df["c"]), high = max(df["c"]))
fig = figure(plot_width = 400, plot_height = 400)
cir = fig.circle(x = "a", y = "b", size = 12,
source = source, line_color = mapper, color = mapper)
color_bar = ColorBar(color_mapper = mapper["transform"], width = 8,
location = (0, 0))
fig.add_layout(color_bar, "right")
codec = """
var low = Math.min.apply(Math,source.data[cb_obj.value]);
var high = Math.max.apply(Math,source.data[cb_obj.value]);
var color_mapper = new Bokeh.LinearColorMapper({palette:viridis5, low:low, high:high});
cir.glyph.fill_color = {field: cb_obj.value, transform: color_mapper};
cir.glyph.line_color = {field: cb_obj.value, transform: color_mapper};
color_bar.color_mapper.low = low;
color_bar.color_mapper.high = high;
color_bar.color_mapper.palette = viridis5;
source.change.emit();
"""
cb_cselect_c = CustomJS(args = dict(cir = cir, source = source, color_bar = color_bar, viridis5 = Viridis5),
code = codec)
c_select = Select(title = "Select variable for color: ", value = "None",
options = ["c", "d"], callback = cb_cselect_c)
layout = gridplot([[fig], [c_select]])
show(layout)
# env = Environment(loader=FileSystemLoader("."))
# template = env.get_template("template.html")
#
# script, div = components(layout)
#
# with open("output.html", "w") as f:
# print(template.render(script=script, div=div), file=f)
You also need to manually add this line to header section of the generated HTML file:
<script type="text/javascript" src="http://cdn.bokeh.org/bokeh/release/bokeh-api-1.0.4.min.js"></script>

Add tooltips to bokeh stacked bar plot

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)

Resources