Bokeh highlight values in second plot and show data table - python

I have two plots and a data table. I want to select a value in plot 1 and then the corresponding value of plot two should be highlighted in plot 2. In addition I would like to show a data table under both plots with the selected values. Here is what I have so far:
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.io import show
import numpy as np
import pandas as pd
from bokeh.layouts import row
from bokeh.models.widgets import DataTable, TableColumn
df2 = pd.DataFrame(np.array([[1, 3.280, 3.3925], [2, 3.3012, 3.4303], [3, 3.5972, 3.8696]]),
columns=['abspos', 'val1', 'val1_q'])
source = ColumnDataSource(data=df2)
p1 = figure(title="Plot1",
plot_width=1500,
plot_height=900,
x_range=[0, 5],
y_range=[0, 5])
p1.circle('abspos', 'val1', source=source, line_color=None, color='red', size=6)
pq1 = figure(title="Plot2",plot_width=900, plot_height=900)
pq1.circle('val1_q', 'val1', source=source, line_color=None, size=6)
columns = [
TableColumn(field="abspos", title="abspos"),
TableColumn(field="val1", title="val1"),
TableColumn(field="val1_q", title="val1_q")
]
data_table = DataTable(source=source, columns=columns, width=300, height=280)
def plot_both(plot1, plot2):
show(row(plot1, plot2))
plot_both(p1, pq1)
If I select the data point [2, 3.3012] in plot 1, the data point [3.3012, 3.4303] should be highlighted in plot 2. The data table underneath should show [2, 3.3012, 3.4303].
Question 1: How to achieve a highlight in plot 2 according to selected point in plot 1 (and vice versa).
Question 2: How to display a Table under both plots which shows the data of the selected data points.

The simplest way would be to use the tap tool (code below works for Bokeh v2.1.1)
from bokeh.plotting import show, figure
from bokeh.models import ColumnDataSource, Row, Column, CustomJS, DataTable, TableColumn
import numpy as np
import pandas as pd
df2 = pd.DataFrame(np.array([[1, 3.280, 3.3925], [2, 3.3012, 3.4303], [3, 3.5972, 3.8696]]),
columns=['abspos', 'val1', 'val1_q'])
source = ColumnDataSource(data=df2)
p1 = figure(title="Plot1",plot_width=900, plot_height=500, tools="tap,pan,box_zoom,wheel_zoom,save,reset")
p1.circle('abspos', 'val1', source=source, line_color=None, color='blue', size=10)
p2 = figure(title="Plot2",plot_width=900, plot_height=500, tools="tap,pan,box_zoom,wheel_zoom,save,reset")
p2.circle('val1_q', 'val1', source=source, line_color=None, color='blue', size=10)
columns = [
TableColumn(field="abspos", title="abspos"),
TableColumn(field="val1", title="val1"),
TableColumn(field="val1_q", title="val1_q")
]
dt1 = DataTable(source=source, columns=columns, width=900, height=300)
dt2 = DataTable(source=source, columns=columns, width=900, height=300)
show(Column(Row(p1, p2), Row(dt1, dt2)))
If you need more functionality you would need to add a JS callback like p1.js_on_event('tap', CustomJS(args={...}, code="..."))

Related

Can bokeh dynamically update the number of columns in a gridplot?

I have 2 bokeh rows. The top row contains a DataTable and a TextInput, both of which are able to stretch_width in order to fit the width of the browser. The bottom row contains a gridplot, which is able to stretch_width, but only does so by distorting the scale of the image. Ideally, I would like the gridplot to update the amount of columns displayed based on the size of the browser.
Consider the following example:
import pandas as pd
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import figure, output_file, save
from bokeh.layouts import row, column, gridplot
def get_datatable():
"""this can stretch width without issue"""
df = pd.DataFrame({'a': [0, 1, 2], 'b': [2, 3, 4]})
source = ColumnDataSource(df)
Columns = [TableColumn(field=i, title=i) for i in df.columns]
data_table = DataTable(columns=Columns, source=source, sizing_mode='stretch_width', max_width=9999)
return data_table
def get_text_input():
"""this can stretch width without issue"""
return TextInput(value='Example', title='Title', sizing_mode="stretch_width", max_width=9999)
def get_gridplot():
"""
this requires columns to be hard-coded
stretch_width is an option, but only distorts the images if enabled
"""
figs = []
for _ in range(30):
fig = figure(x_range=(0,10), y_range=(0,10))
_ = fig.image_rgba(image=[], x=0, y=0)
figs.append(fig)
return gridplot(children=figs, ncols=2)
top_row = row([get_datatable(), get_text_input()], max_width=9999, sizing_mode='stretch_width')
bottom_row = row(get_gridplot())
col = column(top_row, bottom_row, sizing_mode="stretch_width")
output_file("example.html")
save(col)
My end goal is to have the gridplot automatically update the amount of columns based on the width of the browser. Is there a way to do this natively in bokeh? If not, is it possible to do this via a CustomJs javascript callback?
Solution
Consider using sizing_mode=“scale_width” when calling figure.
fig = figure(x_range=(0,10), y_range=(0,10), sizing_mode=“scale_width”)
Note
It may be preferable to use scale_width instead of stretch_width more generally.
Bokeh Doc Example: https://docs.bokeh.org/en/latest/docs/user_guide/layout.html#multiple-objects

Bokeh: Add a line to a nested categorical plot

I have a Bokeh plot with a nested categorical x-axis. Here's the code for a toy problem. My real use case is naturally a larger, more complex data set.
import pandas as pd
from bokeh.io import output_notebook, show, reset_output
from bokeh.models import Band, Span, FactorRange, ColumnDataSource
from bokeh.plotting import figure
reset_output()
output_notebook()
data = {'fruit': ['Apples', 'Pears'],
'2015': [2, 1],
'2016': [5, 3]}
tidy_df = (pd.DataFrame(data)
.melt(id_vars=["fruit"], var_name="year")
.assign(fruit_year=lambda df: list(zip(df['fruit'], df['year'])))
.set_index('fruit_year'))
display(tidy_df)
p = figure(x_range=FactorRange(factors=tidy_df.index.unique()),
height=300,
width=300)
cds = ColumnDataSource(tidy_df)
p.circle(x='fruit_year',
y='value',
size=20,
source=cds,
line_color=None,
)
# this does not show anything or cause an error
p.line(
x=[("Apples", 2015), ("Apples", 2016)],
y=[3.5, 3.5],
color="red",
line_width=2
)
# this works, but does not scale to problems where location can't be manually specified
# also, the line does not line up with the data?
p.line(
x=[4, 5],
y=[2, 2],
color="red",
line_width=2
)
show(p)
Output:
This line does not appear on the plot, and does not throw an error:
p.line(
x=[("Apples", 2015), ("Apples", 2016)],
y=[3.5, 3.5],
color="red",
line_width=2
)
How do I specify x to get the line to show up? Can I specify it for an arbitrary sub-level, i.e. just for ("Apples", 2015)?
Very similar question / solution here. (I asked that question and built off that answer.)
The general concept involves creating dataframes based on one's initial data set, then building multiple ColumnDataSources off of those dataframes.
Here is the complete code:
p = figure(x_range=FactorRange(factors=tidy_df.index.unique()),
plot_height=400,
plot_width=400,
tooltips=[('Fruit', '#fruit'), # first string is user-defined; second string must refer to a column
('Year', '#year'),
('Value', '#value')])
cds = ColumnDataSource(tidy_df)
index_cmap = factor_cmap("fruit",
Spectral5[:2],
factors=sorted(tidy_df["fruit"].unique())) # this is a reference back to the dataframe
p.circle(x='fruit_year',
y='value',
size=20,
source=cds,
fill_color=index_cmap,
line_color=None,
)
# add global median
# how to add for each fruit?
median = Span(location=tidy_df["value"].median(), # median value for Apples
#dimension='height',
line_color='orange',
line_dash='dashed',
line_width=1.0
)
p.add_layout(median)
for fruit, stddev in list(zip(tidy_df["fruit"].unique(), tidy_df.groupby("fruit").std().values.flatten())):
b_df = tidy_df[tidy_df['fruit'] == fruit]\
.drop(columns=['fruit', 'year'])\
.assign(lower=lambda df: df['value'].median() - stddev,
upper=lambda df: df['value'].median() + stddev)\
.assign(median=lambda df: df["value"].median())\
.drop(columns='value')
display(b_df)
# create another cds
cds2 = ColumnDataSource(b_df)
p.add_layout(
Band(
base='fruit_year',
lower='lower',
upper='upper',
source=cds2)
)
p.line(x="fruit_year",
y="median",
source=cds2,
color="red",
line_width=2,
line_dash='dashed',
)
show(p)

Labels not appearing in python bokeh bar plot with groups

I want to add labels with the values above the bars like here: How to add data labels to a bar chart in Bokeh? but don't know how to do it. My code looks different then other examples, the code is working but maybe it is not the right way.
My code:
from bokeh.io import export_png
from bokeh.io import output_file, show
from bokeh.palettes import Spectral5
from bokeh.plotting import figure
from bokeh.sampledata.autompg import autompg_clean as df
from bokeh.transform import factor_cmap
from bokeh.models import ColumnDataSource, ranges, LabelSet, Label
import pandas as pd
d = {'lvl': ["lvl1", "lvl2", "lvl2", "lvl3"],
'feature': ["test1", "test2","test3","test4"],
'count': ["5", "20","8", "90"]}
dfn = pd.DataFrame(data=d)
sourceframe = ColumnDataSource(data=dfn)
groupn = dfn.groupby(by=['lvl', 'feature'])
index_cmapn = factor_cmap('lvl_feature', palette=Spectral5, factors=sorted(dfn.lvl.unique()), end=1)
pn = figure(plot_width=800, plot_height=300, title="Count",x_range=groupn, toolbar_location=None)
labels = LabelSet(x='feature', y='count', text='count', level='glyph',x_offset=0, y_offset=5, source=sourceframe, render_mode='canvas',)
pn.vbar(x='lvl_feature', top="count_top" ,width=1, source=groupn,line_color="white", fill_color=index_cmapn, )
pn.y_range.start = 0
pn.x_range.range_padding = 0.05
pn.xgrid.grid_line_color = None
pn.xaxis.axis_label = "levels"
pn.xaxis.major_label_orientation = 1.2
pn.outline_line_color = None
pn.add_layout(labels)
export_png(pn, filename="color.png")
I think it has something to do with my dfn.groupby(by=['lvl', 'feature']) and the (probably wrong) sourceframe = ColumnDataSource(data=dfn).
The plot at this moment:
You can add the groups names in the initial dictionary like this:
d = {'lvl': ["lvl1", "lvl2", "lvl2", "lvl3"],
'feature': ["test1", "test2","test3","test4"],
'count': ["5", "20","8", "90"],
'groups': [('lvl1', 'test1'), ('lvl2', 'test2'), ('lvl2', 'test3'), ('lvl3', 'test4')]}
And then call LabelSet using as x values the groups.
labels = LabelSet(x='groups', y='count', text='count', level='glyph',x_offset=20, y_offset=0, source=sourceframe, render_mode='canvas',)
In this way the labels appear. Note that I played a bit with the offset to check if that was the problem, you can fix that manually.

Setting absolute screen position of Bokeh Charts in Web App

I am trying to set the absolute position of a Bokeh Chart inside a Layout so that one of the plots is shown on top of another plot. Right now when I am plotting something like this:
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import layout
import numpy as np
x = np.arange(1,10.1,0.1)
y = [i**2 for i in x]
categories = ['A', 'B']
values = [1000, 1500]
fig1 = figure(width=600,plot_height=600, title="First Plot")
fig1.line(x=x, y=y)
fig2 = figure(width=200,plot_height=250,x_range=categories,
title="Second Plot") fig2.vbar(x=categories, top=values, width=0.2)
l = layout([[fig1,fig2]])
curdoc().add_root(l)
The result will be this:
What I am searching for is some way to make it look like that:
How can this result be achieved?
Thank you!
This is what I came up with (works for Bokeh v1.0.4). You need to move your mouse over the plot to get the other one jump inside but you could also copy the JS code from the callback and manually add it to the HTML generated by Bokeh so you achieve the same result.
from bokeh.plotting import figure, show
from bokeh.layouts import Row
from bokeh.models import ColumnDataSource, CDSView, BooleanFilter, CustomJS, BoxSelectTool, HoverTool
import pandas as pd
plot = figure(tools = 'hover', tooltips = [("x", "#x"), ("y", "#y")])
circles = plot.circle('x', 'y', size = 20, source = ColumnDataSource({'x': [1, 2, 3], 'y':[1, 2, 3]}))
inner_plot = figure(name = 'inner_plot', plot_width = 200, plot_height = 200)
lines = inner_plot.line('x', 'y', source = ColumnDataSource({'x': [8, 9, 10], 'y':[8, 6, 8]}))
code = """ div = document.getElementsByClassName('bk-root')[0];
tooltip_plot = div.children[0].children[1]
tooltip_plot.style = "position:absolute; left: 340px; top: 350px;"; """
callback = CustomJS(code = code)
plot.js_on_event('mousemove', callback)
show(Row(plot, inner_plot))
Result:

Bokeh: tools='reset' makes the graph fixed

I wanted to put only reset toolbar on by the graph, so I was trying to like
logo=None, tools='reset'
The reset button is actually placed but instead the graph is fixed and cannot move from original position.
How can I improve it?
To move the plot you just need to add the PanTool. Check this minimal example:
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.io import curdoc
plot = figure(
width=300,
height=300,
tools='pan,reset',
logo=None,
)
x = [1, 2, 3, 4]
y = [4, 3, 2, 1]
source = ColumnDataSource(data=dict(x=x, y=y))
plot.circle(
x='x',
y='y',
source=source,
size=5,
fill_alpha=1.0,
fill_color='green',
line_color=None,
)
curdoc().add_root(plot)
Run this with bokeh serve --show example.py

Categories

Resources