How to achieve a responsive dashboard layout? - bokeh

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:

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

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)

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)

How to display Dataset labels inside a HoverTool in a Sankey diagram using Holoviews and Bokeh

I am using Holoviews to display a Sankey Diagram and would like to customize the information displayed when positioning a cursor over the diagram. However, I don't know how to display the correct labels.
Taking the 2nd example from the docs, I can add a custom HoverTool
import holoviews as hv
from holoviews import opts
from bokeh.models import HoverTool
nodes = ["PhD", "Career Outside Science", "Early Career Researcher", "Research Staff",
"Permanent Research Staff", "Professor", "Non-Academic Research"]
nodes = hv.Dataset(enumerate(nodes), 'index', 'label')
edges = [
(0, 1, 53), (0, 2, 47), (2, 6, 17), (2, 3, 30), (3, 1, 22.5), (3, 4, 3.5), (3, 6, 4.), (4, 5, 0.45)
]
value_dim = hv.Dimension('Percentage', unit='%')
careers = hv.Sankey((edges, nodes), ['From', 'To'], vdims=value_dim)
# this is my custom HoverTool
hover = HoverTool(
tooltips = [
("From": "#From"), # this displays the index: "0", "1" etc.
("To": "#To"), # How to display the label ("PhD", "Career Outside Science", ...)?
]
)
careers.opts(
opts.Sankey(labels='label', tools=[hover]))
Same as in the example shown in the docs, the HoverTool displays the index values for "From" and "To" (e.g. "0", "1") etc., which do not necessarily mean anything to the user.
Is there a way to display the associated label (e.g. "PhD", "Career Outside Science", ...) in the HooverTool syntax?
I am using Holoviews 1.11.2 and Bokeh 1.0.4.
The easiest way to do this is simply to provide the labels instead of the indices to the Sankey element:
nodes = ["PhD", "Career Outside Science", "Early Career Researcher", "Research Staff",
"Permanent Research Staff", "Professor", "Non-Academic Research"]
edges = [
(0, 1, 53), (0, 2, 47), (2, 6, 17), (2, 3, 30), (3, 1, 22.5), (3, 4, 3.5), (3, 6, 4.), (4, 5, 0.45)
]
# Replace the indices with the labels
edges = [(nodes[s], nodes[e], v) for s, e, v in edges]
value_dim = hv.Dimension('Percentage', unit='%')
careers = hv.Sankey(edges, ['From', 'To'], vdims=value_dim)
careers.opts(labels='index', tools=['hover'])
That said I think your expectation that defining labels would make it to use the label column in the nodes to fetch the edge hover labels makes sense and labels may not be unique, so the approach above is not generally applicable. I'll file an issue in HoloViews.

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