multiple bokeh plots with different tooltips - bokeh

Here is a minimal example of what I want to achieve
from bokeh.plotting import ColumnDataSource, figure, output_file, save
output_file("test_plot.html")
source = ColumnDataSource(data=dict(
x=[1,3],
y=[3,7],
label=['a','b']
))
TOOLTIPS = [ ("label", "#label")]
p = figure(plot_width=400, plot_height=400, tooltips=TOOLTIPS, title="plot")
p.circle('x', 'y', size=20,fill_alpha=0.5, source=source)
source = ColumnDataSource(data=dict(
x=[0],
y=[0],
info1 = ['info 1'],
info2 = ['info 2']
))
p.circle('x','y', size=10,fill_alpha=0.5, source=source)
save(p)
and I need to add a different TOOLTIPS to the scatter at (0,0), which can display info1 and info2.

You have to create the tools manually. Note that it will also display multiple tools on the toolbar.
from bokeh.models import HoverTool
from bokeh.plotting import ColumnDataSource, figure, save
source = ColumnDataSource(data=dict(x=[1, 3], y=[3, 7],
label=['a', 'b']))
p = figure(plot_width=400, plot_height=400, title="plot")
circle1 = p.circle('x', 'y', size=20, fill_alpha=0.5, source=source)
source = ColumnDataSource(data=dict(x=[0], y=[0],
info1=['info 1'], info2=['info 2']))
circle2 = p.circle('x', 'y', size=10, fill_alpha=0.5, source=source)
p.add_tools(HoverTool(renderers=[circle1],
tooltips=[('label', '#label')]),
HoverTool(renderers=[circle2],
tooltips=[('info1', '#info1'),
('info2', '#info2')]))
save(p)

Related

bokeh: custom callback for hovertool for figure.line

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)

How to achieve a responsive dashboard layout?

I am working on a dashboard with a responsive map in the center that should take most available space (stretch_both) and two performance plots on either side with fixed width and a stretching height.
Below the map is a slider that should have default height but stretch to the width of the map. Finally there is a button that should take a corner space and be of fixed size to avoid looking awkward.
Here is the design:
Here is a minimal not working example of a directory app:
from bokeh.models import Button, Slider
from bokeh.plotting import figure, curdoc
from bokeh.tile_providers import CARTODBPOSITRON
from bokeh.layouts import column, row, Spacer
map = figure(x_range=(2479280, 2497644), y_range=(5882088, 5901322))
map.add_tile(CARTODBPOSITRON)
plot = figure(plot_width=100)
plot.circle(x=[1, 2, 3], y=[5, 5, 6])
button = Button(label='click me', width=100)
slider = Slider(start=0, end=100, value=0, step=1, title='value')
col1 = column(children=[plot, plot, button])
col2 = column(children=[map, slider], sizing_mode='stretch_both')
col3 = column(children=[plot, plot, Spacer()])
layout = row(children=[col1, col2, col3])
curdoc().add_root(layout)
And here is what I get when I start the app:
Strangely, two of the four plots are not even visible and the columns don't have the same height.
What can I do to get the layout to look more like the design?
The reason the plots are not showing is that, in general, Bokeh objects such as plots cannot be re-used more than once in a layout. For a layout like this, the grid function is preferable:
from bokeh.models import Button, Slider
from bokeh.plotting import figure, curdoc
from bokeh.tile_providers import CARTODBPOSITRON
from bokeh.layouts import grid, column
map = figure(x_range=(2479280, 2497644), y_range=(5882088, 5901322), sizing_mode="stretch_both")
map.add_tile(CARTODBPOSITRON)
p1 = figure(plot_width=100)
p1.circle(x=[1, 2, 3], y=[5, 5, 6])
p2 = figure(plot_width=100)
p2.circle(x=[1, 2, 3], y=[5, 5, 6])
p3 = figure(plot_width=100)
p3.circle(x=[1, 2, 3], y=[5, 5, 6])
p4 = figure(plot_width=100)
p4.circle(x=[1, 2, 3], y=[5, 5, 6])
button = Button(label='click me', width=100, sizing_mode="fixed")
slider = Slider(start=0, end=100, value=0, step=1, title='value')
layout = grid([
[column(p1, p2, sizing_mode="stretch_height"), map, column(p3, p4, sizing_mode="stretch_height")],
[button, slider, None]
], sizing_mode='stretch_width')
curdoc().add_root(layout)
which yields:

Slider based on Networkx node attribute value with Bokeh

I am attempting to develop a slider which will limit the number of nodes visible in a network graph based on the value of a node's attribute. The below Pandas DataFrame (df) represents the nodes, and the node's associated attributes (count information).
source target source_count target_count
A C 15 10
A D 15 20
A E 15 30
B F 25 10
B G 25 20
B H 25 30
I have used the following code to generate a network graph for the nodes and their associated attributes.
import pandas as pd
from bokeh.layouts import column, widgetbox,layout,
from bokeh.plotting import figure, show, output_file,
from bokeh.models import HoverTool, value,PanTool, LabelSet, Legend, ColumnDataSource,Circle,Plot, Range1d, MultiLineBoxSelectTool,ResetTool,LassoSelectTool,Slider
from bokeh.models.callbacks import CustomJS
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes
df = pd.DataFrame({
"source":["A", "A", "A", "B", "B","B"],
"target":["C", "D", "E", "F", "G","H"],
"source_count":["15", "15", "15", "25","25","25"]
"target_count":["10", "20", "30", "10","20","30"]
})
net_graph = from_pandas_edgelist(df, 'source', 'target')
#assign attributes
for index, row in df.iterrows():
net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
net_graph.nodes[row['target']]['yearly_count'] = row['target_count']
graph_plot= Plot(plot_width=800, plot_height=600,
x_range=Range1d(-1.1, 1.1), y_range=Range1d(-1.1, 1.1))
node_hover_tool = HoverTool(tooltips=[("Name", "#index"),("Yearly Count", "#yearly_count")])
graph_plot.add_tools(node_hover_tool)
graph_setup = from_networkx(net_graph, nx.spring_layout, scale=1, center=(0, 0))
graph_setup.node_renderer.glyph = Circle(size=20,fill_color = 'blue')
graph_setup.edge_renderer.glyph = MultiLine(line_color="red", line_alpha=0.8, line_width=1)
graph_plot.renderers.append(graph_setup)
output_file("test_1.html")
show(graph_plot)
The slider I am trying to would use the yearly_count attribute to limit the number of nodes on display. I know that Bokeh allows the embedding of a JavaScript Callback, however, I have not seen a use-case integrated with NetworkX.
Any assistance that anyone could provide would be greatly appreciated.
If you can run your app with bokeh serve then I would try:
from bokeh.models import Slider
graph_plot= Plot()
graph_setup.node_renderer.glyph = Circle()
graph_setup.edge_renderer.glyph = MultiLine()
def callback(attr, old, new):
//filter your data here to show less nodes and edges based
graph_setup.node_renderer.data_source.data = data
graph_setup.edge_renderer.data_source.data = data
slider = Slider()
slider.on_change('value', callback)
If you want to run a Bokeh standalone app then replace slider callback with:
code = """
//filter your data here to show less nodes and edges
graph_setup.node_renderer.data_source.data = data;
graph_setup.edge_renderer.data_source.data = data; """
callback = CustomJS(args = dict(graph_setup = graph_setup, data = data), code = code)
slider = Slider()
slider.js_on_change('value', callback)
See complete JS callback example below:
import networkx as nx
from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, TapTool, OpenURL, HoverTool, CustomJS, Slider, Column
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4
from dask.dataframe.core import DataFrame
import pandas as pd
data = {'source': ['A', 'A', 'A', 'A', 'A', 'A'], 'target': ['C', 'D', 'E', 'F', 'G', 'H'], 'source_count': [15, 15, 15, 25, 25, 25], 'target_count': [10, 20, 30, 10, 20, 30]}
df = pd.DataFrame(data)
net_graph = nx.from_pandas_edgelist(df, 'source', 'target')
for index, row in df.iterrows():
net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
net_graph.nodes[row['target']]['yearly_count'] = row['target_count']
graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))
node_hover_tool = HoverTool(tooltips = [("Name", "#index"), ("Yearly Count", "#yearly_count")])
graph_plot.add_tools(node_hover_tool)
graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'blue')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "red", line_alpha = 0.8, line_width = 1)
graph_plot.renderers.append(graph_setup)
code = """
var new_start = start.slice();
var new_end = end.slice();
new_index = end.slice();
new_start = new_start.splice(0, cb_obj.value)
new_end = new_end.splice(0, cb_obj.value)
new_index = ['A'].concat(new_end)
new_data_edge = {'start': new_start, 'end': new_end};
new_data_nodes = {'index': new_index};
graph_setup.edge_renderer.data_source.data = new_data_edge;
graph_setup.node_renderer.data_source.data = new_data_nodes;
"""
callback = CustomJS(args = dict(graph_setup = graph_setup,
start = df['source'].values,
end = df['target'].values), code = code)
slider = Slider(title = 'Slider', start = 1, end = 6, value = 6)
slider.js_on_change('value', callback)
layout = Column(graph_plot, slider)
show(layout)
Result:
Newer versions of Bokeh uses strict mode for JavaScript (see release log), which implies that code from Tony's accepted answer does not work for Bokeh version 2.0.0 and upwards. Only a few small explicit declarations of variables are needed for the code to work for newer Bokeh versions:
code = '''
var new_start = start.slice();
var new_end = end.slice();
var new_index = end.slice();
new_start = new_start.splice(0, cb_obj.value)
new_end = new_end.splice(0, cb_obj.value)
new_index = ['A'].concat(new_end)
var new_data_edge = {'start': new_start, 'end': new_end};
var new_data_nodes = {'index': new_index};
graph_setup.edge_renderer.data_source.data = new_data_edge;
graph_setup.node_renderer.data_source.data = new_data_nodes;
'''

Is there a way to specify how un-hovered points should appear in Bokeh?

I see how to customize the appearance of a hovered point in a Bokeh plot, for example with something like
p.add_glyph(ColumnDataSource(source), initial_circle, hover_glyph=hovered_circle, ...)
but can't see to find anything about customizing the un-hovered points when a point is hovered.
Is there a way to specify how un-hovered points should appear, distinct from their default ("initial") appearance, analogous to nonselection_glyph vs. selection_glyph?
The Bokeh GlyphRenderer documentation doesn't specifies anything like this. The easy alternative is to create your own HoverTool callback to give the un-hovered circles your desired appearance like in this example (working for Bokeh v1.0.4)
from bokeh.models import ColumnDataSource, HoverTool, CustomJS
from bokeh.plotting import show, figure
source = ColumnDataSource(dict(x = [1, 2], y = [3, 4], color = ['blue', 'blue']))
p = figure(tools = 'hover')
c = p.circle(x = 'x', y = 'y', color = 'color', size = 20, source = source)
code = ''' if (cb_data.index.indices.length > 0){
selected_index = cb_data.index.indices[0];
for (index in source.data['color']){
if (index == selected_index)
source.data['color'][index] = 'red';
else
source.data['color'][index] = 'yellow';
source.change.emit();
}
else{
for (index in source.data['color'])
source.data['color'][index] = 'blue';
} '''
p.select(HoverTool).callback = CustomJS(args = dict(source = source), code = code)
show(p)
Result;

Select Datatable(Bokeh) to Update plot

Plot generated out of below code doesn't change when different rows of Datatable is Selected.Would like to know where I am going wrong.Ideally I would like plot to show depending on the selected datatable row. Thanks
from datetime import date
from random import randint
from bokeh.io import output_file, show, curdoc
from bokeh.plotting import figure
from bokeh.layouts import widgetbox, row
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn,Button
output_file("data_table.html")
data = dict(
dates=[date(2014, 3, i+1) for i in range(10)],
downloads=[randint(0, 100) for i in range(10)],
)
def update(attr,old,new):
#set inds to be selected
inds = [1]
source.selected = {'0d': {'glyph': None, 'indices': []},
'1d': {'indices': inds}, '2d': {}}
# set plot data
plot_dates = [data['dates'][i] for i in inds]
plot_downloads = [data['downloads'][i] for i in inds]
plot_source.data['dates'] = plot_dates
plot_source.data['downloads'] = plot_downloads
source = ColumnDataSource(data)
plot_source = ColumnDataSource({'dates':[],'downloads':[]})
#table_button = Button(label="Press to set", button_type="success")
columns = [
TableColumn(field="dates", title="Date", formatter=DateFormatter()),
TableColumn(field="downloads", title="Downloads"),
]
data_table = DataTable(source=source, columns=columns, width=400, height=280)
p = figure(plot_width=400, plot_height=400)
# add a circle renderer with a size, color, and alpha
p.circle('dates','downloads',source=plot_source, size=20, color="navy", alpha=0.5)
source.on_change('selected',update)
curdoc().add_root(row([data_table,p]))
Only change needed in above code-:
def update(attr,old,new):
data = Hits_File
selected = source.selected['1d']['indices']
if selected:
data = data.iloc[selected, :]
data.columns = data.columns.str.strip()
print("<{}>".format(data.columns[5]))
testvalue = data.iloc[0,5]
BarrierLine.location = data.iloc[0,7]
update_plot(data,testvalue)
def update_plot(data,testvalue):
src_data_table = ColumnDataSource(data)
plot_source.data.update(src_data_table.data)
data1 = get_all_price_dataset(Combined_AllDates,testvalue)
print(testvalue)
price_src_data_table = ColumnDataSource(data1)
price_plot_source.data.update(price_src_data_table.data)

Resources