I am having struggles with making an interactive plot in Jupyter Notebook with the use of Bokeh. I want to plot a map of the world and display the development of some data over time. I succeeded in making a plot and having a slider to adjust the year, but as I change the slider, the slider value won't update. The code for the slider is below:
#creating the data source as a dict
source = ColumnDataSource({
'x': p_df['x'],
'y': p_df['y'],
'Country': p_df['Country'],
'nkill': p_df['nkill']
})
#making a slider and assign the update_plot function to changes
slider = Slider(start=start_yr, end=end_yr, step=1, value=start_yr, title='Year')
slider.on_change('value',update_plot)
#the update_plot function which needs to run based on the new slider.value
def update_plot(attr, old, new):
#Update glyph locations
yr = slider.value
Amountkills_dt_year = p_df[p_df['Year'] ==yr]
new_data = {
'x': Amountkills_dt_year['x'],
'y': Amountkills_dt_year['y'],
'Country': Amountkills_dt_year['Country'],
'nkill': Amountkills_dt_year['nkill']
}
source.data = new_data
#Update colors
color_mapper = LinearColorMapper(palette='Viridis256',
low = min(Amount_of_Terrorist_Attacks['nkill']),
high = max(Amount_of_Terrorist_Attacks['nkill']))
Where I want to update the plot with the update_plot() function. I tried the solution in Python bokeh slider not refreshing plot but I still encoutered the same error.
Bokeh widgets like sliders don't work in jupyter notebooks (at least, not without using some javascript). As the documentation says:
To use widgets, you must add them to your document and define their functionality. Widgets can be added directly to the document root or nested inside a layout. There are two ways to program a widget’s functionality:
Use the CustomJS callback (see JavaScript Callbacks). This will work in standalone HTML documents.
Use bokeh serve to start the Bokeh server and set up event handlers with .on_change (or for some widgets, .on_click).
As #bigreddot hints, you will need to use the bokeh server or else
perhaps some of the features in the discussion of jupyter interactors that are discussed here.
Related
I've written a function which basically makes some calculations and returns a Bokeh plot object.
Then I'm calling that function to display some initial output to the user. After that I have a function which is there to check for updates.
I also have a Select, so the user can select option he/she wants. Finally, I'm updating the plot.
Here's the structure of the code:
plot = my_custom_function(dataset, 'input_parameter')
def update_plot(attr, old, new):
if new == 'some_other':
plot = my_custom_function(dataset, new)
else:
plot = my_custom_function(dataset, old)
select = Select(title='Charging Station', options=['the_first', 'some_other'], value='the_first')
select.on_change('value', update_plot)
layout = row(select, plot)
curdoc().add_root(layout)
The problem is, the chart is not updating? What is the problem?
There are a number of things to mention here:
First, are you running this with the Bokeh server, i.e. bokeh serve maypp.py? Real Python callbacks (e.g. with on_change) only work in the Bokeh server (the Bokeh server is the Python process that actually runs the callback code)
Your callback, as written, has no effect whatsoever. You assign to a local variable plot that only exists inside the callback function, and then disappears as soon as the function ends. You have not actually updated anything, so the entire callback is a no-op. What the callback needs to do is modify the plot you made earlier, e.g. by updating the existing data sources. A typical Bokeh app has a structure along the lines of:
source = ColumnDataSource(...)
p = figure(...)
p.line(..., source=source)
def update(attr, old, new):
source.data = some_new_data # Update the *existing* data source
p.title.text = "new title" # Update properties on *existing* objects
select = Select(...)
select.on_change('value', update)
All of the example apps in repository follow this kind of pattern.
The last thing to mention is that it is always 100% best practice to make the smallest change possible. I.e. you should update the .data for an existing data source, not replace entire data sources (or plots) with new ones. Bokeh is optimized for this kind of updating.
How can I create an interactive bar plot with bokeh through a selection widget on Jupyter Notebook?
Why the Notebook raise the following error:
"RuntimeError: Models must be owned by only a single document, Title(id='1044', ...) is already in a doc
ERROR:tornado.access:500 GET /autoload.js?bokeh-autoload-element=1002&bokeh-absolute-url=http://localhost:54277&resources=none (::1) 117.01ms"
I read carefully this example from github and this similar situation from a Google Bokeh Group, in the latter they run a bokeh server not a the jupyter kernel
output_notebook()
dct={'Date' : ["2018-01-07", "2018-01-12", "2018-01-13", "2018-01-14", "2018-01-20", "2018-01-24"],'Activity' : ['A','B','A','B','A','B'],'Count' : [1, 2, 5, 3, 7, 1]}
df=pd.DataFrame(dct)
activity_list=df['Activity'].unique().tolist().copy()
activity_selected='A'
def modify_doc(doc):
def make_plot(cdf):
plot = figure()
plot.vbar(x=cdf.Date, top=cdf.Count, width=0.9)
push_notebook()
show(plot, notebook_handle = True)
return plot
def update_plot(attr, old, new):
activity = select.value
sdf = df.copy()
sdf = sdf[sdf['Activity'] == activity]
layout.children[0] = make_plot(sdf)
select = Select(title='Select Activity', value=activity_selected, options=activity_list)
select.on_change('value', update_plot)
p=make_plot(df)
layout=column(select, p)
doc.add_root(layout)
show(modify_doc)
What I expect is something like this in the snapshoot:
I am using Bokeh 1.0.4
It's possible we need to make some documentation improvements, because there are several parts of your code that do not make sense.
push_notebook is complementary to showing a real Bokeh server app (i.e. passing modify_doc to show). I can't think of any situation where it would make sense to use them together. The Bokeh server app capability is a strict superset of push_notebook, so since you are already making a Bokeh server app, you should just update everything in the standard way.
show should never be called inside the Bokeh server app code (in this case, inside modify_doc) This is in fact the proximate cause of the exception you are getting. You should
You should not make a new plot every time! The entire purpose of both push_notebook and the Bokeh server is that updates can happen efficiently without making a whole new plot every time. You should be updating the data and attributes of the existing plot.
There is a complete wokring example of a Bokeh server app in a notebook here:
https://github.com/bokeh/bokeh/blob/master/examples/howto/server_embed/notebook_embed.ipynb
You should study and emulate that as it represents best practice.
The correct code for selecting the plot through the Select widget is:
activity_list=df['Activity'].unique().tolist().copy()
df['Date']=pd.to_datetime(df['Date'])
activity_selected='A'
def modify_doc(doc):
df_r=df.copy()
source = ColumnDataSource(data=df_r)
plot=figure(title='Daily Hours',x_axis_type="datetime")
plot.vbar(x="Date", top="Count",source=source, width=4)
def update_plot(attr, old, new):
activity = select.value
data = df_r[df_r['Activity'] == activity]
source.data = ColumnDataSource(data=data).data
select = Select(title='Select Activity', value=activity_selected, options=activity_list)
select.on_change('value', update_plot)
layout=column(select, plot)
doc.add_root(layout)
show(modify_doc)
And this will be the output that you see:
I'm trying to create a dashboard with two holoviews objects: a panel pn.widgets.Select object that contains a list of xarray variables, and a hvplot object that takes the selected variable on input, like this:
def hvmesh(var=None):
mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs,
width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')
return mesh
Here's what an example mesh looks like for a particular variable (one that has both time and height dimensions):
I would like to have the map update when I select a variable from the panel widget:
I tried to do this as a dynamic map, like this:
from holoviews.streams import Params
import holoviews as hv
var_stream = Params(var_select, ['value'], rename={'value': 'var'})
mesh = hv.DynamicMap(hvmesh, streams=[var_stream])
but when I try to display the map, I get:
Exception: Nesting a DynamicMap inside a DynamicMap is not supported.
It would seem a common need to select the variable for hvplot from a panel widget. What is the best way to accomplish this with pyviz?
In case it's useful, here is my full attempt Jupyter Notebook.
Because the groupby changes with each variable selected, a list of variables can not be passed to hvplot. So one solution is to just recreate the plot each time a new variable is selected. This works:
import holoviews as hv
from holoviews.streams import Params
def plot(var=None, tiles=None):
var = var or var_select.value
tiles = tiles or map_select.value
mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs, title=var,
width=600, height=400, groupby=list(ds[var].dims[:-2]),
cmap='jet')
return mesh.opts(alpha=0.7) * tiles
def on_var_select(event):
var = event.obj.value
col[-1] = plot(var=var)
def on_map_select(event):
tiles = event.obj.value
col[-1] = plot(tiles=tiles)
var_select.param.watch(on_var_select, parameter_names=['value']);
map_select.param.watch(on_map_select, parameter_names=['value']);
col = pn.Column(var_select, map_select, plot(var_select.value) * tiles)
producing:
Here is the full notebook.
So there is a long answer and a short answer here. Let's start with the short answer, which is that there's no need to create a custom select widget for the data variable since hvPlot allows selecting between multiple data variables automatically, so if you change it to this:
rasterized_mesh = ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=time_vars[::-1], crs=crs, width=600, height=400,
groupby=list(ds[var].dims[:-2]), rasterize=True, cmap='jet')
You will get a DynamicMap that lets you select the non-spatial dimensions and the data variable and you can now embed that in your panel, no extra work needed. If that's all you care about stop here as we're about to get into some of the internals to hopefully deliver a better understanding.
Let us assume for a minute hvPlot did not allow selecting between data variables, what would we do then? So the main thing you have to know is that HoloViews allows chaining DynamicMaps but does not allow nesting them. This can be a bit hard to wrap your head around but we'll break the problem down into multiple steps and then see how we can achieve what we want. So what is the chain of events that would give us our plot?
Select a data variable
Apply a groupby over the non-spatial dimensions
Apply rasterization to each QuadMesh
As you know, hvPlot takes care of steps 2. and 3. for us, so how can we inject step 1. before 2. and 3. In future we plan to add support for passing panel widgets directly into hvPlot, which means you'll be able to do it all in a single step. Since panel is still a very new project I'll be pointing out along the way how our APIs will eventually make this process trivial, but for now we'll have to stick with the relatively verbose workaround. In this case we have to rearrange the order of operations:
Apply a groupby over the non-spatial dimensions
Select a data variable
Apply rasterization to each QuadMesh
To start with we therefore select all data variables and skip the rasterization:
meshes = ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=time_vars, crs=crs, width=600, height=400,
groupby=list(ds[var].dims[:-2]))
Now that we have a DynamicMap which contains all the data we might want to display we can apply the next operations. Here we will make use of the hv.util.Dynamic utility which can be used to chain operations on a DynamicMap while injecting stream values. In particular in this step we create a stream from the var_select widget which will be used to reindex the QuadMesh inside our meshes DynamicMap:
def select_var(obj, var):
return obj.clone(vdims=[var])
var_stream = Params(var_select, ['value'], rename={'value': 'var'})
var_mesh = hv.util.Dynamic(meshes, operation=select_var, streams=[var_select])
# Note starting in hv 1.12 you'll be able to replace this with
# var_mesh = meshes.map(select_var, streams=[var_select])
# And once param 2.0 is out we intend to support
# var_mesh = meshes.map(select_var, var=var_select.param.value)
Now we have a DynamicMap which responds to changes in the widget but are not yet rasterizing it so we can apply the rasterize operation manually:
rasterized_mesh = rasterize(var_mesh).opts(cmap='jet', width=600, height=400)
Now we have a DynamicMap which is linked to the Selection widget, applies the groupby and is rasterized, which we can now embed in the panel. Another approach hinted at by #jbednar above would be to do all of it in one step by making the hvPlot call not dynamic and doing the time and height level selection manually. I won't go through that here but it also a valid (if less efficient) approach.
As I hinted at above, eventually we also intend to have all hvPlot parameters become dynamic, which means you'll be able to do something like this to link a widget value to a hvPlot keyword argument:
ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=var_select.param.value, rasterize=True, crs=crs,
width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')
I've spent the last few weeks learning the Bokeh package (which for visualizations, is excellent in my opinion).
Unfortunately, I have come across a problem that I can't for the life of me, figure out how to solve.
The below two links have been helpful, but I can't seem to replicate for my problem.
Using bokeh to plot interactive pie chart in Jupyter/Python - refer to answer #3
https://github.com/bokeh/bokeh/blob/0.12.9/examples/howto/notebook_comms/Jupyter%20Interactors.ipynb
The below code (in Jupyter) displays the graph correctly and displays the slider correctly, but I'm unsure how to connect the two as when I move the slider, the graph remains static.
I am using Python 3.6 and Bokeh 12.9
N = 300
source = ColumnDataSource(data={'x':random(N), 'y':random(N)})
plot = figure(plot_width=950, plot_height=400)
plot.circle(x='x', y='y', source=source)
callback = CustomJS(code="""
if (IPython.notebook.kernel !== undefined) {
var kernel = IPython.notebook.kernel;
cmd = "update_plot(" + cb_obj.value + ")";
kernel.execute(cmd, {}, {})};
""")
slider = Slider(start=100, end=1000, value=N, step=10, callback=callback)
def callback(attr, old, new):
N = slider.value
source.data={'x':random(N), 'y':random(N)}
slider.on_change('value', callback)
layout = column(slider, plot)
curdoc().add_root(layout)
show(widgetbox(slider, width = 300))
show(plot)
After reading the bokeh documentation and reading a view threads on GitHub, the 'callback' function is a little unclear for me as I'm not entirely sure what to parse to it (if in fact attr, old, new need certain elements parsed too it)
Any help would be greatly appreciated
Hopefully, I haven't missed anything glaringly obvious.
Kind Regards,
Adrian
You are currently mixing different ways for interactivity but unfortunately you always miss something for each different way.
The slider you use is from bokeh, but unfortunately it looks like slider.on_change only works if you run through the bokeh server. From the documentation:
Use bokeh serve to start the Bokeh server and set up event handlers with .on_change (or for some widgets, .on_click).
I couldn't really find that much on running jupyter notebook and bokeh server, but this issue seems to discuss that possibility. It also mentions bokeh.application but I've never used that, so no idea how that works.
You also use additionally a custom js callback, which calls into the jupyter kernel and tries to execute update_plot(value), but you never defined such a function, so it does nothing.
Then you need a method to push the data to the output. I guess bokeh server can somehow do that nativly, for jupyter notebooks without the bokeh server push_notebook seems to be the solution. Note that you need show(..., notebook_handle=True) to be able to push.
Solution 1 use the bokeh server
Sliders and others widgets automatically sync their state back to python, so you can use slider.on_change. You don't need the CustomJS. Data flow should look as following:
python script -> bokeh server -> html -> userinput -> bokeh server -> python callbacks -> bokeh server updates plots
Solution 2 use bokeh sliders but sync via CustomJS
If you don't want to run a seperate process you can use the jupyter kernel to execute code in your python notebook. Dataflow:
jupyter notebook -> html -> user input -> customjs -> jupyter kernel -> python callbacks -> push_notebook to update plots
output_notebook()
N = 300
source = ColumnDataSource(data={'x':random(N), 'y':random(N)})
plot = figure(plot_width=950, plot_height=400)
plot.circle(x='x', y='y', source=source)
callback = CustomJS(code="""
if (IPython.notebook.kernel !== undefined) {
var kernel = IPython.notebook.kernel;
cmd = "update_plot(" + cb_obj.value + ")";
kernel.execute(cmd, {}, {})};
""")
slider = Slider(start=100, end=1000, value=N, step=10, callback=callback)
# must have the same name as the function that the CustomJS tries to call
def update_plot(N):
source.data={'x':random(N), 'y':random(N)}
# push notebooks to update plots
push_notebook()
layout = column(slider, plot)
# notebook_handle must be true, otherwise push_notebook will not work
h1 = show(layout, notebook_handle=True)
Solution 3 use ipywidgets
If you are not married to the bokeh widgets you can use the ipywidgets which are designed for interactivity in the jupyter notebook. The data flow is as following:
jupyter notebook -> html -> user input -> ipywidgets sync automatically -> python callbacks -> push_notebook
I use here interact but the other widgets should work as expected.
from ipywidgets import interact
output_notebook()
N = 300
source = ColumnDataSource(data={'x':random(N), 'y':random(N)})
plot = figure(plot_width=950, plot_height=400)
plot.circle(x='x', y='y', source=source)
def update_plot(v):
N = v
print(N)
source.data={'x':random(N), 'y':random(N)}
# push changed plots to the frontend
push_notebook()
# notebook_handle must be true so that push_notebook works
show(plot, notebook_handle=True)
Note that you need to install ipywidgets properly, which inlcudes calling jupyter nbextension enable --py --sys-prefix widgetsnbextension if you are not using conda. For details see the documentation
I suppose your question relates to the server although you have both a CustomJS and a server callback.
I am not familiar with the previous way of doing bokeh server in notebook (push_notebook).
The new way would be like this: you wrap your code in a function taking one parameter (a document) and your call to add_layout is made on that document. Then you build an app with that function and show it.
This gives:
from bokeh.models import ColumnDataSource, Slider
from bokeh.layouts import column
from bokeh.plotting import figure, show, output_notebook
from numpy.random import random
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
output_notebook()
def modify_doc(doc):
N = 300
source = ColumnDataSource(data={'x':random(N), 'y':random(N)})
plot = figure(plot_width=950, plot_height=400)
plot.circle(x='x', y='y', source=source)
slider = Slider(start=100, end=1000, value=N, step=10)
def callback(attr, old, new):
N = new # but slider.value would also work
source.data={'x': random(N), 'y': random(N)}
slider.on_change('value', callback)
layout = column(slider, plot)
doc.add_root(layout)
app = Application(FunctionHandler(modify_doc))
show(app, notebook_url="localhost:8888")
Say I have a class that holds some data and implements a function that returns a bokeh plot
import bokeh.plotting as bk
class Data():
def plot(self,**kwargs):
# do something to retrieve data
return bk.line(**kwargs)
Now I can instantiate multiple of these Data objects like exps and sets and create individual plots. If bk.hold() is set they'll, end up in one figure (which is basically what I want).
bk.output_notebook()
bk.figure()
bk.hold()
exps.scatter(arg1)
sets.plot(arg2)
bk.show()
Now I want aggregate these plots into a GridPlot() I can do it for the non overlayed single plots
bk.figure()
bk.hold(False)
g=bk.GridPlot(children=[[sets.plot(arg3),sets.plot(arg4)]])
bk.show(g)
but I don't know how I can overlay the scatter plots I had earlier as exps.scatter.
Is there any way to get a reference to the currently active figure like:
rows=[]
exps.scatter(arg1)
sets.plot(arg2)
af = bk.get_reference_to_figure()
rows.append(af) # append the active figure to rows list
bg.figure() # reset figure
gp = bk.GridPlot(children=[rows])
bk.show(gp)
As of Bokeh 0.7 the plotting.py interface has been changed to be more explicit and hopefully this will make things like this simpler and more clear. The basic change is that figure now returns an object, so you can just directly act on those objects without having to wonder what the "currently active" plot is:
p1 = figure(...)
p1.line(...)
p1.circle(...)
p2 = figure(...)
p2.rect(...)
gp = gridplot([p1, p2])
show(gp)
Almost all the previous code should work for now, but hold, curplot etc. are deprecated (and issue deprecation warnings if you run python with deprecation warnings enabled) and will be removed in a future release.
Ok apparently bk.curplot() does the trick
exps.scatter(arg1)
sets.plot(arg2)
p1 = bk.curplot()
bg.figure() # reset figure
exps.scatter(arg3)
sets.plot(arg4)
p2 = bk.curplot()
gp = bk.GridPlot(children=[[p1,p2])
bk.show(gp)