Bokeh get selected glyphs/annotations from TapTool - python

For my project I need to add and remove glpyhs and annotations in bokeh (line, multiline and arrows). I want to make it as interactive as possible. So in order to remove a glyph/annotation in want to select it with a mouse click and then e.g. delete it with a button. The minimal example would look like that:
import numpy as np
import random
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button, TapTool,Arrow,NormalHead
from bokeh.layouts import layout
from bokeh.application import Application
from bokeh.server.server import Server
from bokeh.application.handlers.function import FunctionHandler
plot = figure(plot_height=300, plot_width=600, x_range=(0, 8), y_range=(0, 11),
title="Testplot", tools='save, reset, tap')
Lay = layout(children=[])
#adds the glyphs/annotaions to figure
def Click_action():
x = np.array((random.randrange(1,10),random.randrange(1,10)))
y = np.array((random.randrange(1,10),random.randrange(1,10)))
source = ColumnDataSource(data=dict(x = x,
y = y))
arro = Arrow(end=NormalHead(size=5, fill_color="#C0392B"),
x_start=random.randrange(0,10),
y_start=random.randrange(0,10),
x_end=random.randrange(0,10),
y_end=random.randrange(0,10),
line_width=3,
line_color="#C0392B")
plot.multi_line(xs=[[1,5],[1,1],[3,3],[5,5]],ys=[[5,5],[5,1],[5,1],[5,1]], color='blue', selection_color='red' )
plot.add_layout(arro)
plot.line(x='x',y='y', source = source,selection_color='red')
def Click_delet():
""" Delete the selected Glyphs/Annotations"""
def make_document(doc):
btn1 = Button(label="Click", button_type="success")
btn2 = Button(label="Click_delet", button_type="success")
btn1.on_click(Click_action)
btn2.on_click(Click_delet)
Lay.children.append(plot)
Lay.children.append(btn1)
Lay.children.append(btn2)
doc.add_root(Lay)
if __name__ == '__main__':
bkapp = {'/': Application(FunctionHandler(make_document))}
server = Server(bkapp, port=5004)
server.start()
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
The problems I currently have are:
How can I select the arrow ?
How do I get all selected glyphs and annotations? (If possible without a CoustomJS callback since I do not know java that well)
Is it possible to select the multiline as one glyph?
I have already solved the issue how to delete lines and arrows from a plot. But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.

Annotations are not interactive in Bokeh
See a minimal example below
No
But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.
Ideally, your workflow should abstain from dynamically creating and removing Bokeh models, especially low-levels ones such as glyphs. If you need to remove a glyph and add a new one with new properties, consider just changing properties of the old glyph. Or maybe just clear the data of the old glyph to hide it.
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure, ColumnDataSource
line_ds = ColumnDataSource(dict(x=[0, 3, 7],
y=[1, 8, 2]))
multi_line_ds = ColumnDataSource(dict(xs=[[1, 5], [1, 1], [3, 3], [5, 5]],
ys=[[5, 5], [5, 1], [5, 1], [5, 1]]))
p = figure(x_range=(0, 8), y_range=(0, 11), tools='save, reset, tap')
p.line('x', 'y', source=line_ds, selection_color='red')
p.multi_line('xs', 'ys', source=multi_line_ds, color='blue', selection_color='red')
b = Button(label="Delete selected", button_type="success")
def delete_rows(ds, indices):
print(indices)
if indices:
print(ds.data)
ds.data = {k: [v for i, v in enumerate(vs) if i not in set(indices)]
for k, vs in ds.data.items()}
print(ds.data)
def delete_selected():
delete_rows(line_ds, line_ds.selected.line_indices)
delete_rows(multi_line_ds, multi_line_ds.selected.indices)
b.on_click(delete_selected)
curdoc().add_root(column(p, b))

Related

How to define Python Bokeh RangeSlider.on_change callback function to alter IndexFilter for plots?

I'm trying to implement a python callback function for a RangeSlider. The Slider Value should tell which Index a IndexFilter should get for display.
For example: If rangeslider.value is (3, 25) my plots should only contain/view data with the Index from 3 to 25.
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, GMapOptions, CustomJS, CDSView, IndexFilter
from bokeh.plotting import gmap, ColumnDataSource, figure
from bokeh.layouts import column, row
from bokeh.models.widgets import RangeSlider
import numpy as np
def slider_callback(attr, old, new):
p.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
v.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
# data set
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4, 5]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data = {'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(np.arange(0, lenght_dataset))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("MY_API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
# plot lines on map
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
# slider to limit plotted data
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
range_slider.on_change('value', slider_callback)
# Layout to plot and output
layout = row(column(p, range_slider),
column(v)
)
output_file("diag_plot_bike_data.html")
show(layout)
Some notes:
time is longer than the rest of the columns - you will receive a warning about it. In my code below, I just removed its last element
view with filters in general should not be used for continuous glyphs like lines (v.line in particular - multi_line is fine). You will receive a warning about it. But if the indices in IndexFilter are always continuous, then you should be fine. Either way, you can use the segment glyph to avoid the warning
In your callback, you're trying to set view on the figures - views only exist on glyph renderers
In general, you don't want to recreate views, you want to recreate as few Bokeh models as possible. Ideally, you would have to just change the indices field of the filter. But there's some missing wiring in Bokeh, so you will have to set the filters field of the view, as below
new argument of Python callbacks receives the new value for the attribute passed as the first parameter to the corresponding on_change call. In this case, it will be a tuple, so instead of new.value[0] you should use new[0]
Since you've decided to use Python callbacks, you can no longer use show and have a static HTML file - you will have to use curdoc().add_root and bokeh serve. The UI needs that Python code to run somewhere in runtime
When changing the slider values, you will notice that the separate segments of multi_line will be joined together - it's a bug and I just created https://github.com/bokeh/bokeh/issues/10589 for it
Here's a working example:
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import GMapOptions, CDSView, IndexFilter
from bokeh.models.widgets import RangeSlider
from bokeh.plotting import gmap, ColumnDataSource, figure
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data={'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(list(range(lenght_dataset)))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
def slider_callback(attr, old, new):
view.filters = [IndexFilter(list(range(*new)))]
range_slider.on_change('value', slider_callback)
layout = row(column(p, range_slider), column(v))
curdoc().add_root(layout)

Select DropDown unable to change ColumnDataSource using JS Callback

I have tried changing multiple parameters(arguments) but this doesn't work.
The bokeh version is 1.3.4.
from bokeh.layouts import column
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider, Select
from bokeh.plotting import Figure, output_notebook, show
import numpy as np
a = 20
bokeh_tools = "pan,wheel_zoom"
output_notebook()
plot_2s = ColumnDataSource(data=dict(x=[1, 2, 3], y=[1, 2, 3]))
plot_3s = ColumnDataSource(data=dict(x=[3, 4, 5], y=[1, 2, 3]))
line_source = ColumnDataSource(data=dict(x=[1, 2, 3], y=[1, 2, 3]))
plot_1 = figure(x_axis_type="datetime", plot_width=800, tools=bokeh_tools, title="plot_1")
plot_1.line(x = 'x', y='y', source=plot_2s)
plot_2 = figure(x_axis_type="datetime", plot_width=800, tools=bokeh_tools, title="plot_2")
plot_2.line(x='x', y='y', source=line_source)
select = Select(title="SELECT", options=["val1", "val2"])
column = column(select, plot_2)
show(column)
select.callback = CustomJS(args={"cds2": plot_2s, "cds3": plot_3s, "ls": line_source}, code="""
if(cb_obj.value === "val1"){
ls.data = cds2.data;
}else if(cb_obj.value === "val2"){
ls.data = cds3.data;
}
ls.change.emit();
""")
There are no error message the callback is not implemented
You callback is never executed, because it is never added to the output. As soon as you call show the HTML output is generated and displayed. Anything after that point is effectively a no-op, and does not exist as far as the output is concerned. Typically show should be called last.

How to create the empty selection?

I am working a lot with selections that users can modify. I have noticed that bokeh does not distinguish between select all and select nothing. Could this be true?
So for some column data source with data key x the following results in identical selections:
source.selected.indices = []
and
all_indices = list(range(len(source.data['x'])))
source.selected.indices = all_indices
How do I deselect all elements of a data source?
EDIT: A minimal working example.
import bokeh.plotting
from bokeh.io import curdoc
from bokeh.models import ColumnDataSource
data = dict(x=[15,2,21], y=[8,8,6])
source = ColumnDataSource(data)
plot = bokeh.plotting.figure()
plot.circle(x='x', y='y', source=source)
source.selected.indices = [0,1,2]
source.selected.indices = [1]
source.selected.indices = []
curdoc().add_root(plot)
All circles are visualized as "selected" when source.selected.indices = [] and also when source.selected.indices = [0,1,2]. Clearly the selections are different and should not look the same, right?
The default action on selection is to "wash out" the non-selected points (by lowering their alpha and setting color to grey), but leave the selected points looking the way they were (which stands out against the non-selected points). If you want something different, there is a section in the Users Guide that describes how to configure Selected and Unselected Glyphs. E.g:
plot.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=50,
# set visual properties for selected glyphs
selection_color="firebrick",
# set visual properties for non-selected glyphs
nonselection_fill_alpha=0.2,
nonselection_fill_color="blue",
nonselection_line_color="firebrick",
nonselection_line_alpha=1.0)

Jupyter Bokeh: Non-existent column name in glyph renderer

I have a GlyphRenderer whose data_source.data is
{'index': [0, 1, 2, 3, 4, 5, 6, 7],
'color': ['#3288bd', '#66c2a5', '#abdda4', '#e6f598', '#fee08b', '#fdae61', '#f46d43', '#d53e4f']}
The renderer's glyph is
Oval(height=0.1, width=0.2, fill_color="color")
When rendering, I see
E-1001 (BAD_COLUMN_NAME): Glyph refers to nonexistent column name: color [renderer: GlyphRenderer(id='1d1031f5-6ee3-4744-a0f7-22309798e313', ...)]
I'm clearly missing something, but this is pretty much lifted from published examples. I verified in a debugger that data_source.column_names is just ['index']; what I don't understand is why the 'color' column doesn't appear in the data source's column_names, or why Bokeh produces this warning (the graph appears to be correctly rendered).
The complete source is available at https://pastebin.com/HXAEEujP
It's generally better to provide all relevant arguments when constructing an object rather than mutating the object after it's already been created. It's especially true for Bokeh - in many cases it does some additional work based on the arguments passed to __init__.
Take a look at this version of your code:
import math
from bokeh.io import show
from bokeh.models import GraphRenderer, StaticLayoutProvider, Oval, GlyphRenderer, ColumnDataSource, MultiLine
from bokeh.palettes import Spectral8
from bokeh.plotting import figure
N = 8
node_indices = list(range(N))
plot = figure(title="Graph Layout Demonstration", x_range=(-1.1, 1.1), y_range=(-1.1, 1.1),
plot_width=250, plot_height=250,
tools="", toolbar_location=None)
node_ds = ColumnDataSource(data=dict(index=node_indices,
color=Spectral8),
name="Node Renderer")
edge_ds = ColumnDataSource(data=dict(start=[0] * N,
end=node_indices),
name="Edge Renderer")
### start of layout code
circ = [i * 2 * math.pi / 8 for i in node_indices]
x = [math.cos(i) for i in circ]
y = [math.sin(i) for i in circ]
graph_layout = dict(zip(node_indices, zip(x, y)))
graph = GraphRenderer(node_renderer=GlyphRenderer(glyph=Oval(height=0.1, width=0.2, fill_color="color"),
data_source=node_ds),
edge_renderer=GlyphRenderer(glyph=MultiLine(),
data_source=edge_ds),
layout_provider=StaticLayoutProvider(graph_layout=graph_layout))
plot.renderers.append(graph)
show(plot)

source.data is updated in bokeh serve but plot does not change

I am trying to make a bokeh serve plot with a CheckButtonGroup. I manage to update my source.data but the plot does not get updated. What am I doing wrong?
In reality, I import the dataset from my computer, but for now I will create an example pandas dataframe. I want to select the 'x' column (as x-axis variable) and one or more of the other columns (as y-axis variables).
import pandas as pd
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import row, widgetbox
from bokeh.models.widgets import CheckboxButtonGroup
from bokeh.models import ColumnDataSource
dataset = pd.DataFrame(columns=['x','y1','y2','y3'])
dataset['x'] = [1, 2, 3, 4]
dataset['y1'] = [10, 20, 30, 40]
dataset['y2'] = [11, 21, 31, 41]
dataset['y3'] = [12, 22, 32, 43]
pos_cols = ['y1', 'y2', 'y3'] # possible column names
col_list = ['y1', 'y2'] # default columns in plotted data
use_data = dataset[col_list]
use_data['x'] = dataset.loc[:, 'x']
source = ColumnDataSource(use_data)
p = figure(
tools="pan,box_zoom,wheel_zoom,reset,save",
x_axis_label='xtitle', y_axis_label='ytitle',
title="Simulations"
)
# make default plot with the two columns
for column in col_list:
p.line('x', column, source=source)
check = CheckboxButtonGroup(labels=["y1", "y2", "y3"], active=[0, 1]) # A check box for every column
def update_lines(new):
col_list = [pos_cols[i] for i in new]
use_data = dataset[col_list]
use_data['x'] = dataset.loc[:, 'x']
source.data = source.from_df(use_data)
print(source.data) # source.data is correctly updated, but does not seem to trigger a new plot
check.on_click(update_lines)
doc = curdoc()
doc.add_root(row(check, p, width=800))
doc.title = "Simulations"
I save the code as try.py and run it from the windows prompt with bokeh serve try.py. The plot is visible at http://localhost:5006
The problem is that you are creating glyphs for columns like 'y3' up front, but not actually sending any column 'y3' to start. Bokeh does not like that (you can see error messages about trying to access non-existent columns in the browser JS console)
A better approach, that also does not unnecessarily re-send all the data, might be to just toggle the .visible attribute of the glyph renderers. Here is a minimal example (that starts with all lines visible, but you could change that):
import pandas as pd
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import row
from bokeh.models import CheckboxButtonGroup, ColumnDataSource
dataset = pd.DataFrame(columns=['x','y1','y2','y3'])
dataset['x'] = [1, 2, 3, 4]
dataset['y1'] = [10, 20, 30, 40]
dataset['y2'] = [11, 21, 31, 41]
dataset['y3'] = [12, 22, 32, 43]
source = ColumnDataSource(data=dataset)
p = figure( )
lines = []
for column in ['y1', 'y2', 'y3']:
lines.append(p.line('x', column, source=source))
check = CheckboxButtonGroup(labels=["y1", "y2", "y3"], active=[0, 1, 2])
def update_lines(new):
for i in [0, 1, 2]:
if i in new:
lines[i].visible = True
else:
lines[i].visible = False
check.on_click(update_lines)
doc = curdoc()
doc.add_root(row(check, p, width=800))
Alternatively, if you are just looking to be able to hide or mute lines, an much easier way would be to use Bokeh's built in Interactive Legends:
http://docs.bokeh.org/en/latest/docs/user_guide/interaction/legends.html#userguide-interaction-legends

Categories