I am trying to get a callback function to work in bokeh based on this simple example:
from bokeh.plotting import figure, curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource
TOOLS = "tap,reset"
p = figure(title="Some Figure", tools=TOOLS)
source = ColumnDataSource(dict(x=[[1, 3, 2], [3, 4, 6, 6]],
y=[[2, 1, 4], [4, 7, 8, 5]], name=['A', 'B']))
pglyph = p.patches('x', 'y', source=source)
def callback(attr, old, new):
# The index of the selected glyph is : new['1d']['indices'][0]
print("In callback")
patch_name = source.data['name'][new['1d']['indices'][0]]
print("TapTool callback executed on Patch {}".format(patch_name))
pglyph.data_source.on_change('selected',callback)
curdoc().add_root(column(p))
When I load the page and click on a polygon, I do not see the callback getting executed.
What is missing?
That's because the selected attribute value is not changed. The contained object is changed instead, and Bokeh doesn't detect deep changes.
Try replacing the callback function and the next line with:
def callback(attr, old, new):
print("In callback")
patch_name = source.data['name'][new[0]]
print("TapTool callback executed on Patch {}".format(patch_name))
pglyph.data_source.selected.on_change('indices', callback)
Related
I have a class Collection that holds a bunch of other class objects Thing that all have the same attributes with different values. The Collection.plot(x, y) method makes a scatter plot of the x values vs. the y values of all the collected Thing objects like so:
from bokeh.plotting import figure, show
from bokeh.models import TapTool
class Thing:
def __init__(self, foo, bar, baz):
self.foo = foo
self.bar = bar
self.baz = baz
def plot(self):
# Plot all data for thing
fig = figure()
fig.circle([1,2,3], [self.foo, self.bar, self.baz])
return fig
class Collection:
def __init__(self, things):
self.things = things
def plot(self, x, y):
# Configure plot
title = '{} v {}'.format(x, y)
fig = figure(title=title, tools=['pan', 'tap'])
taptool = fig.select(type=TapTool)
taptool.callback = RUN_THING_PLOT_ON_CLICK()
# Plot data
xdata = [getattr(th, x) for th in self.things]
ydata = [getattr(th, y) for th in self.things]
fig.circle(xdata, ydata)
return fig
Then I would make a scatter plot of all four Thing sources' 'foo' vs. 'baz' values with:
A = Thing(2, 4, 6)
B = Thing(3, 6, 9)
C = Thing(7, 2, 5)
D = Thing(9, 2, 1)
X = Collection([A, B, C, D])
X.plot('foo', 'baz')
What I would like to have happen here is have each point on the scatter plot able to be clicked. On click, it would run the plot method for the given Thing, making a separate plot of all its 'foo', 'bar', and 'baz' values.
Any ideas on how this can be accomplished?
I know I can just load ALL the data for all the objects into a ColumnDataSource and make the plot using this toy example, but in my real use case the Thing.plot method does a lot of complicated calculations and may be plotting thousands of points. I really need it to actually run the Thing.plot method and draw the new plot. Is that feasible?
Alternatively, could I pass the Collection.plot method a list of all the Thing.plot pre-drawn figures to then display on click?
Using Python>=3.6 and bokeh>=2.3.0. Thank you very much!
I edited your code and sorry i returned too late.
from bokeh.plotting import figure, show
from bokeh.models import TapTool, ColumnDataSource
from bokeh.events import Tap
from bokeh.io import curdoc
from bokeh.layouts import Row
class Thing:
def __init__(self, foo, bar, baz):
self.foo = foo
self.bar = bar
self.baz = baz
def plot(self):
# Plot all data for thing
t_fig = figure(width=300, height=300)
t_fig.circle([1, 2, 3], [self.foo, self.bar, self.baz])
return t_fig
def tapfunc(self):
selected_=[]
'''
here we get selected data. I select by name (foo, bar etc.) but also x/y works. There is a loop because taptool
has a multiselect option. All selected names adds to selected_
'''
for i in range(len(Collection.source.selected.indices)):
selected_.append(Collection.source.data['name'][Collection.source.selected.indices[i]])
print(selected_) # your selected data
# now create a graph according to selected_. I use only first item of list. But you can use differently.
if Collection.source.selected.indices:
if selected_[0] == "foo":
A = Thing(2, 4, 6).plot()
layout.children = [main, A]
elif selected_[0] == "bar":
B = Thing(3, 6, 9).plot()
layout.children = [main, B]
elif selected_[0] == 'baz':
C = Thing(7, 2, 5).plot()
layout.children = [main, C]
class Collection:
# Columndata source. Also could be added in __init__
source = ColumnDataSource(data={
'x': [1, 2, 3, 4, 5],
'y': [6, 7, 8, 9, 10],
'name': ['foo', 'bar', 'baz', None, None]
})
def __init__(self):
pass
def plot(self):
# Configure plot
TOOLTIPS = [
("(x,y)", "(#x, #y)"),
("name", "#name"),
]
fig = figure(width=300, height=300, tooltips=TOOLTIPS)
# Plot data
circles = fig.circle(x='x', y='y', source=self.source, size=10)
fig.add_tools(TapTool())
fig.on_event(Tap, tapfunc)
return fig
main = Collection().plot()
layout = Row(children=[main])
curdoc().add_root(layout)
The problem is when you select something every time Thing class creates a new figure. It's not recommended. So, you could create all graphs and make them visible/invisible as your wishes OR you could change the source of the graph. You could find lots of examples about changing graph source and making them visible/invisible. I hope it works for you :)
There are two ways to do that. This is basic example. First, you could use Tap event to do that and create a function to get information from glyph. Second, you could directly connect source to function.
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.events import Tap
from bokeh.models import TapTool, ColumnDataSource
def tapfunc():
print(source.selected.indices)
def sourcefunc(attr, old, new):
print(source.selected)
source = ColumnDataSource(data={
'x': [1,2,3,4,5],
'y': [6,7,8,9,10]
})
p = figure(width=400, height=400)
circles = p.circle(x='x', y='y', source=source, size=20, color="navy", alpha=0.5)
p.add_tools(TapTool())
p.on_event(Tap, tapfunc)
source.selected.on_change('indices', sourcefunc)
curdoc().add_root(p)
selected return a list a selected values index. so, you should add index to your source. You could use with pandas for index. For more information about selection check here. So in function you could create a new figure and glyph (line etc.) and update it. Here, very good example. You could pull and run it from your pc.
So with Bokeh I can do something like this to create a hover option:
From bokeh.models import HoverTool #add hover functionality
Hover = HoverTool(tooltips=[(name1:#column1), (name2:#columns2)])
Plot = figure(tools=[hover])
Plot.circle(x,y,hover_color=’red’)
However, by doing so, I lose the standard tools you get when you call figure() like pan, box_zoom, wheel_zoom, etc. I know I can add them back 1 by 1 inside the figure(tools=[]), but is there a way to only add hover to the rest of the default tools of figure(), after it is defined??
Thanks!
Use the add_tools() method, as outlined in the docs:
https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#specifying-tools
Slightly modified example from the docs:
from bokeh.plotting import figure, output_file, show
from bokeh.models import HoverTool
output_file("toolbar.html")
# create a new plot with the toolbar below
p = figure(plot_width=400, plot_height=400,
title=None, toolbar_location="below")
p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=10)
p.add_tools(HoverTool())
show(p)
I am trying to make Bokeh Panels that have relatively complex layouts, so I tried moving half of my current layout into one Panel and half into another just to play around, like so:
selects = HBox(top_users_select, new_users_select, t1_users_select, t2_users_select, top_recent_users_select)
tab1 = Panel(inputs)
tab2 = Panel(VBox(HBox(plot2, plot1, plot3, plot4), HBox(plot5, plot6, plot7, plot8), data_table))
tabs = tabs(tab1, tab2)
show(tabs)
However this is giving me the following error:
File "main_panel.py", line 589, in <module>:
tab1 = Panel(inputs) Traceback (most recent call last):
File "/Users/joe/anaconda3/lib/python3.5/site-packages/bokeh/application/handlers/code_runner.py", line 71, in run
exec(self._code, module.__dict__)
File "/Users/joe/Desktop/scripts/src/main/python/Bokeh apps/insights/main_panel.py", line 589, in <module>
tab1 = Panel(inputs)
TypeError: __init__() takes 1 positional argument but 2 were given
I am fairly new to Bokeh, and looking at the docs I don't know exactly how to parse this error and get around it. Can someone point me to an example of laying out fairly complex grids in a Bokeh panel or tell me what the error means and how I can address it?
You can use row or column to create complex layouts within tabs.
You can even mix them like row(column(button1, button2), button3)
Example:
from bokeh.models.widgets import Panel, Tabs, Toggle, TextInput
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.layouts import column, row
p1 = figure(plot_width=300, plot_height=300)
p1.circle([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], size=20, color="navy", alpha=0.5)
p2 = figure(plot_width=300, plot_height=300)
p2.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], line_width=3, color="navy", alpha=0.5)
toggle1 = Toggle(label="Foo", button_type="success")
toggle2 = Toggle(label="Foo", button_type="warning")
text_input = TextInput(value="default", title="Label:")
tab1 = Panel(child=row(p1,toggle2), title="circle")
tab2 = Panel(child=column(p2,toggle1, text_input), title="line")
tabs = Tabs(tabs=[ tab1, tab2 ])
output_notebook()
show(tabs)
See https://docs.bokeh.org/en/latest/docs/user_guide/layout.html for more details
I think you want to write selects instead of inputs, but there are basically some things wrong with your declaration.
Look at the examples at http://docs.bokeh.org/en/latest/docs/user_guide/interaction/widgets.html#tabs
from bokeh.models.widgets import Panel, Tabs
from bokeh.io import output_file, show
from bokeh.plotting import figure
output_file("slider.html")
p1 = figure(plot_width=300, plot_height=300)
p1.circle([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], size=20, color="navy", alpha=0.5)
tab1 = Panel(child=p1, title="circle")
p2 = figure(plot_width=300, plot_height=300)
p2.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], line_width=3, color="navy", alpha=0.5)
tab2 = Panel(child=p2, title="line")
tabs = Tabs(tabs=[ tab1, tab2 ])
show(tabs)
Your input in Panel should be assigned to child= and a Panel needs a title=.
Also change
tabs = tabs(tab1, tab2)
to
tabs = Tabs(tabs=[tab1,tab2,tab3,tab4])
I'm slamming my head against the wall with this problem. I simply want to be able to make a selection (lasso, box_select) of points in a streaming bokeh scatterplot that will be remembered when the figure updates (e.g., with new data in the time series).
I think this will require me to be able to access the list of the indices of currently selected points, but I can't figure out how to. Here's an example where I try (slightly modified from the example at
http://docs.bokeh.org/en/latest/docs/user_guide/server.html#streaming-data-with-the-server
Note that selected points are deselected when the plot updates to the new streamed (shuffled in this example) data.
import time
from random import shuffle
from bokeh.plotting import figure, output_server, cursession, show
# prepare output to server
output_server("remember_selected")
p = figure(plot_width=400, plot_height=400,tools="lasso_select,box_select,help")
p.scatter([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], name='ex_line')
show(p)
# create some simple animation..
# first get our figure example data source
renderer = p.select(dict(name="ex_line"))
ds = renderer[0].data_source
while True:
# Update y data of the source object
shuffle(ds.data["y"])
#Can I access currently selected points? (NO!)
print ds.selected['2d']
print ds.selected['1d']
print ds.selected['0d']
# store the updated source on the server
cursession().store_objects(ds)
time.sleep(2.)
To get callbacks, you need to setup a complete Bokeh application. The following demonstrated this:
from bokeh.models import ColumnDataSource, Plot
from bokeh.plotting import figure
from bokeh.properties import Instance
from bokeh.server.app import bokeh_app
from bokeh.models.widgets import HBox
from bokeh.server.utils.plugins import object_page
from random import shuffle
class App(HBox):
extra_generated_classes = [["App", "App", "HBox"]]
jsmodel = "HBox"
# plots
plot = Instance(Plot)
source = Instance(ColumnDataSource)
#classmethod
def create(cls):
# create layout widgets
obj = cls()
# outputs
obj.source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5],
y=[6, 7, 2, 4, 5]))
obj.plot = figure(x_range=(0, 6), y_range=(0, 7),
tools="lasso_select,box_select,help", plot_width=400,
plot_height=400)
obj.plot.scatter('x', 'y', source=obj.source, name='ex_line')
# layout
obj.children = [obj.plot]
obj.setup_events()
return obj
def setup_events(self):
super(App, self).setup_events()
if self.source:
self.source.on_change('selected', self, 'click')
def click(self, cds, name, prev, new):
print(new['1d']['indices'])
shuffle(self.source.data['y'])
#bokeh_app.route("/bokeh/select/")
#object_page("select")
def make():
app = App.create()
return app
Note from maintainers: The specifics of this question concern the bokeh.charts API which is obsolete and was removed several years ago. In modern Bokeh, specify toolbar_location:
p = figure(toolbar_location=None)
OBSOLETE:
I don't seem to be able to remove the toolbar from a bokeh Bar chart. Despite setting the tools argument to None (or False or '') I always end up with the bokeh logo and a grey line, e.g. with this code:
from bokeh.charts import Bar, output_file, show
# prepare some data
data = {"y": [6, 7, 2, 4, 5], "z": [1, 5, 12, 4, 2]}
# output to static HTML file
output_file("bar.html")
# create a new line chat with a title and axis labels
p = Bar(data, cat=['C1', 'C2', 'C3', 'D1', 'D2'], title="Bar example",
xlabel='categories', ylabel='values', width=400, height=400,
tools=None)
# show the results
show(p)
However, when I try the same with a bokeh plot, it works perfectly fine and the toolbar is gone, e.g. with this code:
from bokeh.plotting import figure, output_file, show
output_file("line.html")
p = figure(plot_width=400, plot_height=400, toolbar_location=None)
# add a line renderer
p.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], line_width=2)
show(p)
Does anyone know what I'm doing wrong?
If you want to remove the logo and the toolbar you can do:
p.toolbar.logo = None
p.toolbar_location = None
Hope this resolves your problem
On any Bokeh plot object you can set:
p.toolbar_location = None