Related
I have a bokeh server app which is similar to the below code with call back functions and I have been trying to explore ways to deploy the bokeh app to any public server or html so that people even without python will be able to access the app.
Please let me know if there is a way to deploy the app to public
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput
from bokeh.plotting import figure
# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# Set up plot
plot = figure(plot_height=400, plot_width=400, title="my sine wave",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
# Set up widgets
text = TextInput(title="title", value='my sine wave')
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)
# Set up callbacks
def update_title(attrname, old, new):
plot.title.text = text.value
text.on_change('value', update_title)
def update_data(attrname, old, new):
# Get the current slider values
a = amplitude.value
b = offset.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w) + b
source.data = dict(x=x, y=y)
for w in [offset, amplitude, phase, freq]:
w.on_change('value', update_data)
# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq)
curdoc().add_root(row(inputs, plot, width=800))
curdoc().title = "Sliders"
Goal - I would like to slide the sliders around THEN when i'm ready to update press the button to update the values based upon where the current sliders are at.
Below is an example borrowed from Bokeh's website. Ideally, I would like to change the slider parameters, then when i'm ready for them all to update, click the button, have all the sliders update and display the changes. This process would be repeated over and over. I've tried the below but I'm not getting the desired result.
import numpy as np
from bokeh.io import curdoc,output_file, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Slider, TextInput, Button
from bokeh.plotting import figure
# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# Set up plot
plot = figure(plot_height=400, plot_width=400, title="my sine wave",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
# Set up widgets
text = TextInput(title="title", value='my sine wave')
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)
button = Button(label="Update Changes", button_type="success")
# Set up callbacks
def update_title(attrname, old, new):
plot.title.text = text.value
text.on_change('value', update_title)
def update_data(attrname, old, new):
# Get the current slider values
a = amplitude.value
b = offset.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w) + b
source.data = dict(x=x, y=y)
def update():
for w in [offset, amplitude, phase, freq]:
w.on_change('value', update_data)
button.on_click(update)
# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq, button)
curdoc().add_root(row(inputs, plot, width=800))
Delete the code that sets up the callbacks on slider change (because you don't want that) and call update_data from the button instead (after updating the callback function signature appropriately):
def update_data():
# Get the current slider values
a = amplitude.value
b = offset.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w) + b
source.data = dict(x=x, y=y)
button.on_click(update_data)
I am trying to use bokeh to plot the iris data and modify the fill color of the circles interactively but I am running into a problem. I call the plot and the circle with the following:
plot = figure(plot_height=600, plot_width=1000, title="Iris Data",
x_axis_label = 'Sepal length (cm)',
y_axis_label = 'Sepal width (cm)',
tools = "crosshair, pan, reset, save, wheel_zoom")
plot_circle = plot.circle(x='sepal_length', y='sepal_width', source=source,
line_color=None, fill_color={'field':'petal_width','transform':color_mapper},
size='size', fill_alpha = 0.2)
which works but when I try to add the interactivity in the call back it is not clear to me how to modify the 'field' parameter in the fill_color argument to circle. I have tried this:
def update_bubble_color(attrname, old, new):
if new=='petal_width':
color_mapper.low = min(flowers['petal_width'])
color_mapper.high = max(flowers['petal_width'])
fill_color.field='petal_width'
return
if new=='petal_length':
color_mapper.low = min(flowers['petal_length'])
color_mapper.high = max(flowers['petal_length'])
fill_color.field='petal_length'
return
select_bubble_color.on_change('value', update_bubble_color)
the color mapper limits are handled correctly but the colors are not scaled according to the new choice. When I attempt to change it to petal_length with fill_color.field='petal_length' I get an "'name 'fill_color' is not defined" error.
Any help greatly appreciated!
Full code below for reference
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource, LinearColorMapper
from bokeh.models.widgets import Select
from bokeh.plotting import figure
# Load Data
from bokeh.sampledata.iris import flowers
# Global constants (even if python dies not like it)
min_bubble_size = 10
max_bubble_size = 90
def get_scaled_size(vector):
min_vector = min(vector)
max_vector = max(vector)
scaling = (max_bubble_size-min_bubble_size)/(max_vector-min_vector)
scaled_size = [ scaling*(item-min_vector) + min_bubble_size for item in vector]
return scaled_size
# Color Mapper
color_mapper = LinearColorMapper(palette='Inferno256',
low = min(flowers['petal_width']),
high = max(flowers['petal_width']) )
# Define source
flowers['size'] = get_scaled_size(flowers['petal_length'])
source = ColumnDataSource(flowers)
# Set up plot
plot = figure(plot_height=600, plot_width=1000, title="Iris Data",
x_axis_label = 'Sepal length (cm)',
y_axis_label = 'Sepal width (cm)',
tools = "crosshair, pan, reset, save, wheel_zoom")
plot_circle = plot.circle(x='sepal_length', y='sepal_width', source=source,
line_color=None, fill_color={'field':'petal_width','transform':color_mapper},
size='size', fill_alpha = 0.2)
# Set up widgets
select_bubble_size = Select(title ='Bubble size by', value='petal_width',
options = ['petal_width','petal_length'],
width = 200)
select_bubble_color = Select(title ='Bubble color by', value='petal_width',
options = ['petal_width', 'petal_length'],
width = 200)
# Colorbar
from bokeh.models import ColorBar
bar = ColorBar(color_mapper=color_mapper,location=(0,0))
plot.add_layout(bar, 'left')
# Set up callbacks=
# Bubble size call back
def update_bubble_size(attrname, old, new):
if new=='petal_width':
source.data['size'] = get_scaled_size(flowers['petal_width'])
return
if new=='petal_length':
source.data['size'] = get_scaled_size(flowers['petal_length'])
return
select_bubble_size.on_change('value', update_bubble_size)
# bubble color call back
def update_bubble_color(attrname, old, new):
if new=='petal_width':
color_mapper.low = min(flowers['petal_width'])
color_mapper.high = max(flowers['petal_width'])
fill_color.field='petal_width'
return
if new=='petal_length':
color_mapper.low = min(flowers['petal_length'])
color_mapper.high = max(flowers['petal_length'])
fill_color.field='petal_length'
return
select_bubble_color.on_change('value', update_bubble_color)
# Set up layouts and add to document
curdoc().add_root(column(plot, row(select_bubble_size,select_bubble_color), width=800))
curdoc().title = "Iris Data"
fill_color is a property of the glyph, you will need to access it through the glyph:
plot_circle.glyph.fill_color
In your script there is not free variable fill_color anywhere, which is the source of the NameError.
In my Bokeh .py app, run by Bokeh server, I'm importing a module. In this module, a part of code depends on whether it is used in the Bokeh app or not (it can be used in "normal" Python script, too). How do I know whether the code is currently being used by Bokeh server or not?
You can check if a bokeh process is running with psutil. I have also made an example where show is used when a bokeh process is running (server) or show when no bokeh prcoess is running ("normal").
import psutil
import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import row, column
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Slider, TextInput
from bokeh.plotting import figure, show
def checkIfProcessRunning(processName):
'''
Check if there is any running process that contains the given name processName.
'''
#Iterate over the all the running process
for proc in psutil.process_iter():
try:
# Check if process name contains the given name string.
if processName.lower() in proc.name().lower():
return True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return False;
# Set up data
N = 200
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
# Set up plot
plot = figure(plot_height=400, plot_width=400, title="my sine wave",
tools="crosshair,pan,reset,save,wheel_zoom",
x_range=[0, 4*np.pi], y_range=[-2.5, 2.5])
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
# Set up widgets
text = TextInput(title="title", value='my sine wave')
offset = Slider(title="offset", value=0.0, start=-5.0, end=5.0, step=0.1)
amplitude = Slider(title="amplitude", value=1.0, start=-5.0, end=5.0, step=0.1)
phase = Slider(title="phase", value=0.0, start=0.0, end=2*np.pi)
freq = Slider(title="frequency", value=1.0, start=0.1, end=5.1, step=0.1)
# Set up callbacks
def update_title(attrname, old, new):
plot.title.text = text.value
text.on_change('value', update_title)
def update_data(attrname, old, new):
# Get the current slider values
a = amplitude.value
b = offset.value
w = phase.value
k = freq.value
# Generate the new curve
x = np.linspace(0, 4*np.pi, N)
y = a*np.sin(k*x + w) + b
source.data = dict(x=x, y=y)
for w in [offset, amplitude, phase, freq]:
w.on_change('value', update_data)
# Set up layouts and add to document
inputs = column(text, offset, amplitude, phase, freq)
if checkIfProcessRunning('bokeh'):
print('A bokeh process is running')
curdoc().add_root(row(inputs, plot, width=800))
curdoc().title = "Sliders"
else:
print('No bokeh process is running')
show(row(inputs, plot, width=800))
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.