Looking to do something along the lines of a UI as here: Bokeh: Using Checkbox widget to hide and show plots wherein I can selectively show/hide the whole figure in a column of figures. A drop down menu (OptionMenu with multiple selections) where I could select which plots showed up (assuming I could name the figures) would be preferable.
I am not familiar with JS, any guidance? (Thanks in advance)
I'd hope that the image wouldn't be visible anymore and the next figure would jump up like so:
eg:
I have multiple figures in a column generated as:
from bokeh.io import output_file, show
from bokeh.layouts import column
from bokeh.plotting import figure
output_file("layout.html")
x = list(range(11))
y0 = x
y1 = [10 - i for i in x]
y2 = [abs(i - 5) for i in x]
# create a new plot
s1 = figure(plot_width=250, plot_height=250, title=None)
s1.circle(x, y0, size=10, color="navy", alpha=0.5)
# create another one
s2 = figure(plot_width=250, plot_height=250, title=None)
s2.triangle(x, y1, size=10, color="firebrick", alpha=0.5)
# create and another
s3 = figure(plot_width=250, plot_height=250, title=None)
s3.square(x, y2, size=10, color="olive", alpha=0.5)
# put the results in a column and show
show(column(s1, s2, s3))
Plots don't have a visible toggle, at least as of version 0.13. So you will have to reset the children value of the layout widget. I'm not quite sure what interaction you intend with a dropdown. Here is a complete example with a checkbox:
from bokeh.io import output_file, show
from bokeh.layouts import column, row
from bokeh.plotting import figure
from bokeh.models import CheckboxGroup, CustomJS
output_file("layout.html")
x = list(range(11))
y0 = x
y1 = [10 - i for i in x]
y2 = [abs(i - 5) for i in x]
s1 = figure(plot_width=250, plot_height=250, title=None)
s1.circle(x, y0, size=10, color="navy", alpha=0.5)
s2 = figure(plot_width=250, plot_height=250, title=None)
s2.triangle(x, y1, size=10, color="firebrick", alpha=0.5)
s3 = figure(plot_width=250, plot_height=250, title=None)
s3.square(x, y2, size=10, color="olive", alpha=0.5)
col = column(s1, s2, s3)
checkbox = CheckboxGroup(labels=["Plot 1", "Plot 2", "Plot 3"],
active=[0, 1, 2], width=100)
callback = CustomJS(args=dict(plots=[s1,s2, s3], col=col, checkbox=checkbox), code="""
const children = []
for (const i of checkbox.active) {
children.push(plots[i])
}
col.children = children
""")
checkbox.js_on_change('active', callback)
show(row(checkbox, col))
You could do something similar with a MultiSelect:
select = MultiSelect(options=[("0", "Plot 1"), ("1", "Plot 2"), ("2", "Plot 3")],
value=["0", "1", "2"], width=100)
callback = CustomJS(args=dict(plots=[s1,s2, s3], col=col, select=select), code="""
const children = []
for (const i of select.value) {
children.push(plots[i])
}
col.children = children
""")
select.js_on_change('value', callback)
Small FYI that that code is a little sloppy—it's relying on JS implicitly casting strings like "0" to numbers.
s1.tags, s2.tags, s3.tags = ['Foo'], ['Bar'], ['Arr'] # name your plots
plots = [s1, s2, s3]
labels = [(plots[i].tags[0]) for i in range(len(plots))]
active = list(range(0, len(plots)))
chkbx = CheckboxButtonGroup(labels=labels, active=active)
callback = CustomJS(args=dict(plots=plots, chkbx=chkbx), code="""
for (let i = 0; i < plots.length; i++){
plots[i].visible = chkbx.active.includes(i)
}
""")
chkbx.js_on_click(callback)
show(column([chkbx] + plots))
Thanks to #bigreddot and their answer for laying foundation for this solution.
Related
I'm new in Bokeh, and I'm trying to construct an interactive scatter plot.
In this plot, I would like to change the p-value cut-off and, then, change the colors according to the rule (if FC < 0, the dots are blue; if FC > 0, dots are red.
I've tried:
from bokeh.plotting import ColumnDataSource, figure, output_file, show, curdoc
from bokeh.models import BoxSelectTool, LassoSelectTool,CustomJS, Slider
from bokeh.transform import linear_cmap
from bokeh.layouts import gridplot, column, row
import pandas as pd
import numpy as np
fc = np.random.uniform(0, 4, 1000)
p_value = np.random.uniform(0,1,1000)
df = pd.DataFrame([fc, p_value], ['FC', 'pvalue']).transpose()
df['log2(fc)'] = np.log2(df['FC'])
df['-log10(p)'] = -np.log10(df['pvalue'])
output_file("df.html")
y = df['-log10(p)']
x = df['log2(fc)']
col = np.where(y> -np.log10(0.5), np.where( x>-0, np.where( x<0, '#606060','#E4001B'), '#6194BC'), '#606060' )
source = ColumnDataSource(data=dict(
x=df['log2(fc)'],
y=df['-log10(p)'],
pvalue=df['pvalue'],
FC = df['log2(fc)'],
color = col))
TOOLTIPS = [
("FC", "#FC"),
("pvalue", "#pvalue")
]
TOOLS="hover,crosshair,pan,wheel_zoom,zoom_in,zoom_out,box_zoom,undo,redo,reset,tap,save,box_select,poly_select,lasso_select,"
p = figure(plot_width=500, plot_height=500, tooltips=TOOLTIPS,
title="test", tools=TOOLS)
p.circle('x', 'y', size=5, source=source, alpha = 0.5, fill_color = 'color', line_color='color')
p.xaxis.axis_label = "log2(FC)"
p.yaxis.axis_label = "-log10(p)"
p.background_fill_color = "#fafafa"
p_slider = Slider(start=0.0, end=5, step=.01, title="p_value")
callback = CustomJS(args=dict(source=source,plot=p, color_mapper = col, pvalue=p_slider),
code="""
const data = source.data;
var P = pvalue.value;
const x = df['p_value']
const y = df['log2(fc)']
source.change.emit();
""")
p_slider.js_on_change('value', callback)
layout = row(
p,
column(p_slider),
)
show(layout)
Despite this approach show me the bar to control the p-value, I'm not getting to control the bar, neither change the colors according to p-value...
Thanks for your help!
I have a pandas dataframe with multiple columns. I have multiple scatter plots where the x-axis is a different column from the dataframe while the y-axis is the same across all the figures. I want to be able to change the y-axis based on the selected value in a dropwdown button. I'm new using bokeh, I haven't be able to figure how exactly to build the callback function for this case. Any help is much appreciated!
def callback(attr, old, new):
y_axis = select.value
source.data = {'y' : y_axis}
p1.circle.y = y_axis
columns = {'TC2B':'Temperature 2B','TC1B':'Temperature 1B', 'PCV-2006 Pos':'2006 valve position',
'TCV-2008 DownS Ctrl PV':'Down Stream'}
select = Select(title='Y axis:', value='TCV-2008 Pos', options=list(columns.keys()))
y_select = select.value
color_select = 'TC2B'#'PCV-2006 Pos'
#source = ColumnDataSource(data={'y':y_select})
source = ColumnDataSource(data)
p1 = figure(x_axis_label='Temperature 1B', y_axis_label=columns[y_select])
p1.circle(x='TC1B',
y=y_select,
source=source)
p2 = figure(x_axis_label='2006 valve position', y_axis_label=columns[y_select])
p2.circle(x='PCV-2006 Pos',
y=y_select,
source=source)
p3 = figure(x_axis_label='Down Stream', y_axis_label=columns[y_select])
p3.circle(x='TCV-2008 DownS Ctrl PV',
y=y_select,
source=source)
p1.y_range = p2.y_range = p3.y_range
select.on_change('value', callback)
layout = column(select, row(column(p1, p2, p3)))
curdoc().add_root(layout)
You can recreate the plots again from scratch in the onchange callback and update the layout content. Take a look at this:
from bokeh.models import ColumnDataSource, Select
from bokeh.plotting import figure
from bokeh.layouts import column, row
from bokeh.io import curdoc
import numpy as np
N = 3000
data = {
'X': np.random.random(size=N) * 100,
'Y': np.random.random(size=N) * 100,
'Z': np.random.random(size=N) * 100,
'A': np.random.random(size=N) * 100,
'B': np.random.random(size=N) * 100,
'C': np.random.random(size=N) * 100,
}
select = Select(title='Y axis:', value='Y', options=list(data.keys()))
source = ColumnDataSource(data=data)
p1 = figure(
x_axis_label='X',
y_axis_label='Y'
)
c1 = p1.circle(
x='X',
y='Y',
source=source
)
p2 = figure(
x_axis_label='A',
y_axis_label='Y'
)
c2 = p2.circle(
x='A',
y='Y',
source=source
)
p1.y_range = p2.y_range
r = row(children=[p1, p2])
layout = column(select, r)
def onchange_value(attr, old, new):
p1 = p2 = None
p1 = figure(
x_axis_label='X',
y_axis_label=new
)
p1.circle(
x='X',
y=new,
source=source
)
p2 = figure(
x_axis_label='A',
y_axis_label=new
)
p2.circle(
x='A',
y=new,
source=source
)
r.children = [p1, p2]
select.on_change('value', onchange_value)
curdoc().add_root(layout)
Note: You can also read this other question about updating the layout dynamically to get more ideas
I've been able to figure out how to update a Plotly graph with new data using buttons. That is, if I select a certain button (say X1 in the example below), the plot will change so that it'll plot that variable, etc.
However, I want to be able to select multiple buttons at once. For example, I want to be able to select X1 and X2 and plot both on the chart.
I haven't been able to make any progress on this, so I was hoping someone could provide some clues on a way forward.
import plotly
import plotly.graph_objs as go
import numpy as np
import pandas as pd
plotly.offline.init_notebook_mode(connected=True)
x0 = np.linspace(0,99,100)
y0 = np.random.normal(2, 0.4, 100)
y1 = np.random.normal(2, 0.4, 100)
y2 = np.random.normal(2, 0.4, 100)
trace0 = go.Scatter(x = x0, y = y0)
trace1 = go.Scatter(x = x0, y = y1, visible = False)
trace2 = go.Scatter(x = x0, y = y2, visible = False)
data = [trace0, trace1, trace2]
button1 = dict(label = 'X0',
method = 'update',
args = [{'visible': [True, False, False]}])
button2 = dict(label = 'X1',
method = 'update',
args = [{'visible': [False, True, False]}])
button3 = dict(label = 'X2',
method = 'update',
args = [{'visible': [False, False, True]}])
updatemenus = list([
dict(type="buttons", active = 0,
buttons = [button1, button2, button3], yanchor = 'top')])
layout = dict(title='Chart', showlegend=False,
updatemenus=updatemenus)
fig = dict(data=data, layout=layout)
plotly.offline.iplot(fig)
I am trying to implement checkboxes in bokeh where each checkbox should show/hide the line associated with it. I'm aware it's possible to achieve this with legends, but I want this effect to happen in two plots at the same time. Also, the legend should update as well. In the example below the checkboxes appear, but do nothing. I am clearly not grasping how to update de dataframe used as source. Thanks for any help.
from bokeh.io import show, curdoc
from bokeh.models import HoverTool, ColumnDataSource, Legend
from bokeh.plotting import figure
from bokeh.palettes import Category10
from bokeh.models.widgets import CheckboxGroup
from bokeh.layouts import row
import pandas as pd
def update(atrr, old, new):
lines_to_plot = [checkbox_group.labels[i] for i in checkbox_group.active]
cols = ['x']
for label in lines_to_plot:
cols += [label + 'y']
cols += [label]
newdf = df0[cols]
source.data.update(ColumnDataSource(newdf))
df0 = pd.DataFrame({'x': [1, 2, 3], 'Ay' : [1, 5, 3], 'A': [0.2, 0.1, 0.2], 'By' : [2, 4, 3], 'B':[0.1, 0.3, 0.2]})
columns = ['A', 'B']
checkbox_group = CheckboxGroup(labels=columns, active=[0, 1])
tools_to_show = 'box_zoom,save,hover,reset'
p = figure(plot_height =300, plot_width = 1200,
toolbar_location='above',
tools=tools_to_show)
legend_it = []
color = Category10[10]
columns = ['A', 'B']
source = ColumnDataSource(df0)
for i, col in enumerate(columns):
c = p.line('x', col, source=source, name=col, color=color[i])
legend_it.append((col, [c]))
legend = Legend(items=legend_it, location=(5,114))#(0, -60))
p.add_layout(legend, 'right')
hover = p.select(dict(type=HoverTool))
hover.tooltips = [("Name","$name"), ("Aux", "#$name")]
hover.mode = 'mouse'
layout = row(p,checkbox_group)
checkbox_group.on_change('active', update)
curdoc().add_root(layout)
You will have to manage LegendItem objects manually. Here is a complete example:
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import row
from bokeh.palettes import Viridis3
from bokeh.plotting import figure
from bokeh.models import CheckboxGroup, Legend, LegendItem
p = figure()
props = dict(line_width=4, line_alpha=0.7)
x = np.linspace(0, 4 * np.pi, 100)
l0 = p.line(x, np.sin(x), color=Viridis3[0], **props)
l1 = p.line(x, 4 * np.cos(x), color=Viridis3[1], **props)
l2 = p.line(x, np.tan(x), color=Viridis3[2], **props)
legend_items = [LegendItem(label="Line %d" % i, renderers=[r]) for i, r in enumerate([l0, l1, l2])]
p.add_layout(Legend(items=legend_items))
checkbox = CheckboxGroup(labels=["Line 0", "Line 1", "Line 2"], active=[0, 1, 2], width=100)
def update(attr, old, new):
l0.visible = 0 in checkbox.active
l1.visible = 1 in checkbox.active
l2.visible = 2 in checkbox.active
p.legend.items = [legend_items[i] for i in checkbox.active]
checkbox.on_change('active', update)
layout = row(checkbox, p)
curdoc().add_root(layout)
If one wants to define the active (default) tools for a bokeh plot, it can be set by passing "active_drag", "active_inspect", ... parameters to the figure instance as documented here.
I have not yet succeeded to set the standard-active tools for a gridplot, in which all single plots share a toolbar. This the relevant part of my code:
tools = [
PanTool(),
BoxZoomTool(),
WheelZoomTool(),
UndoTool(),
RedoTool(),
ResetTool(),
SaveTool(),
HoverTool(tooltips=[
("Value", "$y")
])
]
x_axes_range = Range1d(self.data.index[0], self.data.index[-1])
for plot_type, plot_settings in pcfg.plot_types[self.name].items():
plots.append(figure(x_axis_type="datetime", title=plot_type, plot_height = 400, x_range = x_axes_range,
tools = tools, active_drag = None, active_inspect = None, active_scroll = None, active_tap = None))
...
plots[plot_counter].line(self.data.index, self.data[parameter], color=parameter_settings[1], legend=parameter_settings[0])
...
gp = gridplot(plots, ncols = 1, sizing_mode = "scale_width")
script, div = components(gp)
So what happens is that the "BoxZoomTool()" is selected as active tool on the website I display it on, although I set the active tools to None in the figure initializations, but the available tools are those I passed to the figure() inits.
I did see the "toolbar_option" here, but I do not see how I can change the active tools using that parameter.
Update 6/4/2020
Well, it seems that somebody created a GitHub Issue and the changes were already merged. Let's see if this is working in the next Bokeh Version
Old Answer
Research
I believe you only can change the logo with toolbar_options like this:
toolbar_options=dict(logo='gray')
I hope there will more options in the future.
I have checked how to achieve what you want (I needed to do it as well) and it seems that the gridplot uses an special toolbar to join all the plot toolbars together: a ProxyToolbar
# Make the grid
tools = []
rows = []
for row in children:
row_tools = []
row_children = []
for item in row:
if merge_tools:
if item is not None:
for plot in item.select(dict(type=Plot)):
row_tools = row_tools + plot.toolbar.tools
plot.toolbar_location = None
if item is None:
width, height = 0, 0
for neighbor in row:
if isinstance(neighbor, Plot):
width = neighbor.plot_width
height = neighbor.plot_height
break
item = Spacer(width=width, height=height)
if isinstance(item, LayoutDOM):
item.sizing_mode = sizing_mode
if isinstance(item, Plot):
if plot_width:
item.plot_width = plot_width
if plot_height:
item.plot_height = plot_height
row_children.append(item)
else:
raise ValueError("Only LayoutDOM items can be inserted into Grid")
tools = tools + row_tools
rows.append(Row(children=row_children, sizing_mode=sizing_mode))
grid = Column(children=rows, sizing_mode=sizing_mode)
if not merge_tools:
return grid
if toolbar_location:
proxy = ProxyToolbar(tools=tools, **toolbar_options)
toolbar = ToolbarBox(toolbar=proxy, toolbar_location=toolbar_location)
The tools are gathered in a list to assign them to the special toolbar. I don´t see that the default active elements are collected anywhere.
Alternative solution
So what you could do is to create a toolbar and the gridplot manually, where you can set the attributes you want to the Toolbar class. Check this example I have built:
from bokeh.models import Button, ColumnDataSource, Range1d, Toolbar, ToolbarBox
from bokeh.models.tools import HoverTool, WheelZoomTool, PanTool, CrosshairTool
from bokeh.layouts import layout
from bokeh.plotting import curdoc, figure
x_range = Range1d(start=0, end=10)
y_range = Range1d(start=0, end=10)
# ------------------- PLOT 1 --------------------------- #
plot_1 = figure(
title='First figure',
width=400,
height=400,
x_range=x_range,
y_range=y_range,
toolbar_location=None,
x_axis_label='x axis',
y_axis_label='y axis',
)
x = [1, 2, 3, 4]
y = [4, 3, 2, 1]
source = ColumnDataSource(data=dict(x=x, y=y))
plot_1.circle(
x='x',
y='y',
source=source,
radius=0.5,
fill_alpha=0.6,
fill_color='green',
line_color='black',
)
# ------------------- PLOT 2 --------------------------- #
plot_2 = figure(
name='plot_2',
title='Second figure',
width=400,
height=400,
x_range=x_range,
y_range=y_range,
toolbar_location=None,
x_axis_label='x axis',
y_axis_label='y axis',
)
plot_2.circle(
x='x',
y='y',
source=source,
radius=0.5,
fill_alpha=0.6,
fill_color='red',
line_color='black',
)
# ---------------- ADD TOOLS TO THE PLOT --------------------- #
wheel_zoom = WheelZoomTool()
pan_tool = PanTool()
hover = HoverTool()
crosshair = CrosshairTool()
tools = (wheel_zoom, pan_tool, hover, crosshair)
toolbar = Toolbar(
tools=[wheel_zoom, pan_tool, hover, crosshair],
active_inspect=[crosshair],
# active_drag = # here you can assign the defaults
# active_scroll = # wheel_zoom sometimes is not working if it is set here
# active_tap
)
toolbar_box = ToolbarBox(
toolbar=toolbar,
toolbar_location='left'
)
plot_1.add_tools(*tools)
plot_2.add_tools(*tools)
# ----------------- PLOT LAYOUT -------------------------- #
layout_1 = layout(
children=[
[toolbar_box, plot_1, plot_2],
],
sizing_mode='fixed',
)
curdoc().add_root(layout_1)
Note: I am doing some tests and sometimes it is not working well. The tools are marked as default but randomly don´t work, I am afraid it has something to do with the JavaScript and asyncronous tasks. So maybe we should wait.
Second Alternative Solution
I think I found a solution that always work. This is a kind of workaround. In my example I use two plots, but only the toolbar of the first plot is shown. Anyway you need to set the default toolbar values to both plots.
from bokeh.models import Button, ColumnDataSource, Range1d, Toolbar, ToolbarBox
from bokeh.models.tools import HoverTool, WheelZoomTool, PanTool, CrosshairTool, LassoSelectTool
from bokeh.layouts import layout
from bokeh.plotting import curdoc, figure
x_range = Range1d(start=0, end=10)
y_range = Range1d(start=0, end=10)
# ------------------- PLOT 1 --------------------------- #
plot_1 = figure(
title='First figure',
width=400,
height=400,
x_range=x_range,
y_range=y_range,
toolbar_location='left', # show only the toolbar of the first plot
tools='',
x_axis_label='x axis',
y_axis_label='y axis',
)
x = [1, 2, 3, 4]
y = [4, 3, 2, 1]
source = ColumnDataSource(data=dict(x=x, y=y))
plot_1.circle(
x='x',
y='y',
source=source,
radius=0.5,
fill_alpha=0.6,
fill_color='green',
line_color='black',
)
# ------------------- PLOT 2 --------------------------- #
plot_2 = figure(
name='plot_2',
title='Second figure',
width=400,
height=400,
x_range=x_range,
y_range=y_range,
toolbar_location=None,
tools='',
x_axis_label='x axis',
y_axis_label='y axis',
)
plot_2.circle(
x='x',
y='y',
source=source,
radius=0.5,
fill_alpha=0.6,
fill_color='red',
line_color='black',
)
# ---------------- ADD TOOLS TO THE PLOT --------------------- #
wheel_zoom = WheelZoomTool()
lasso_select = LassoSelectTool()
pan_tool = PanTool()
hover = HoverTool()
crosshair = CrosshairTool()
tools = (wheel_zoom, lasso_select, pan_tool, hover, crosshair)
plot_1.add_tools(*tools)
plot_2.add_tools(*tools)
plot_1.toolbar.active_inspect=[crosshair] # defaults added to the first plot
plot_1.toolbar.active_scroll=wheel_zoom
plot_1.toolbar.active_tap=None
plot_1.toolbar.active_drag=lasso_select
plot_2.toolbar.active_inspect=[crosshair] # defaults added to the second plot
plot_2.toolbar.active_scroll=wheel_zoom
plot_2.toolbar.active_tap=None
plot_2.toolbar.active_drag=lasso_select
# ----------------- PLOT LAYOUT -------------------------- #
layout_1 = layout(
children=[
[plot_1, plot_2],
],
sizing_mode='fixed',
)
curdoc().add_root(layout_1)
Developers Feedback
In fact, Bryan (bokeh developer) told me on the chat
I was going to actually answer that default tool activation and grid plots had never been considered together and was not supported yet, tho if you have found something that works for your specific use case that's probably the best possible. As you say it's not generally the case that users should have to work with toolbars directly, they are finicky for a number of reasons.