Bokeh, lasso select to table update - python

I hope everyone is doing well.
I am trying to develop a Bokeh interaction whereby selecting a part of a scatter plot will update a table.
I am using a lot of the sample code from the Bokeh documentation. My workplace is running an older version of Bokeh (0.12.5) so I had to change the last line in the Custom JS (from s2.change.emit() to s2.trigger('change). I then added in a few lines to create a DataTable.
I naively thought that since sourcing 's1' in the Datatable works, sourcing 's2' will allow me to link the table to the lasso select. I even tried adding in an extra trigger to the table widget in the JS callback.
Does anyone know how to create a table from a lasso select in a graph?
Code
Thanks in advance.
from random import random
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.plotting import figure, output_file, show
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
output_file("callback.html")
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)
s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(plot_width=400, plot_height=400, x_range=(0, 1), y_range=(0, 1),
tools="", title="Watch Here")
p2.circle('x', 'y', source=s2, alpha=0.6)
###New code##
columns = [TableColumn(field ="x", title = "X axis"),
TableColumn(field ="y", title = "Y axis")]
table = DataTable(source =s2, columns = columns, width =400, height = 280)
##Added in table.trigger('change') hoping this would link to the lasso select.
s1.callback = CustomJS(args=dict(s2=s2), code="""
var inds = cb_obj.selected['1d'].indices;
var d1 = cb_obj.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.trigger('change');
table.trigger('change');
""")
##having 'table' in the layout, stops the callback from working, deleting table from the layout makes it work.
layout = row(p1, p2, table)
show(layout)

Here's a working version for bokeh 1.0.4
from random import random
import bokeh.io
from bokeh.io import output_notebook, show
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.plotting import figure
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.resources import INLINE
bokeh.io.output_notebook(INLINE)
x = [random() for x in range(500)]
y = [random() for y in range(500)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)
s2 = ColumnDataSource(data=dict(x=[], y=[]))
p2 = figure(plot_width=400, plot_height=400, x_range=(0, 1), y_range=(0, 1),
tools="", title="Watch Here")
p2.circle('x', 'y', source=s2, alpha=0.6)
columns = [TableColumn(field ="x", title = "X axis"),
TableColumn(field ="y", title = "Y axis")]
table = DataTable(source =s2, columns = columns, width =155, height = 380)
s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2, table=table), code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (var i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.change.emit();
table.change.emit();
""")
)
layout = row(p1, p2, table)
show(layout)
You can even select rows/values in the table and the points in the second plot will change opacity (and you can sort the table by clicking the headers)

Right now, your callback doesn't know, what "table" is. You need to pass it as an argument to your CustomJS function:
s1.callback = CustomJS(args=dict(s2=s2, table=table), code="""
var inds = cb_obj.selected['1d'].indices;
var d1 = cb_obj.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.trigger('change');
table.trigger('change');
""")

Related

Hovering over the legend will hide all lines except the corresponding line

When you hover over the legend, all lines except the corresponding line will be hidden. How to achieve this via bokeh, python or javascript. I have no idea what to do to achieve this function. It would be great if we could provide a simple example with three lines.Thanks for your help.My code example is as follows:
import bokeh.palettes as bp
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import LinearAxis, Range1d, NumeralTickFormatter, Legend
from bokeh.layouts import column
import numpy as np
if __name__ == '__main__':
num = 5
color_list2 = bp.magma(num)
color_list1 = bp.viridis(num)
plotTools = 'box_zoom, wheel_zoom, pan, tap, crosshair, hover, reset, save'
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
p.title.text = 'Hover and Hide'
items_c1 = []
i = 0
pictures = []
labels = ['a', 'b', 'c', 'd', 'e']
for label in labels:
n = np.random.randint(low=3, high=6)
xs = np.random.random(n) * 1000
y1s = np.random.random(n) - 2.5
temp_line = p.line(xs, y1s, line_width=2, color=color_list1[i % num],
alpha=0.3, hover_color='red', hover_alpha=0.9) # , legend_label=label
items_c1.append((label + '_BER', [temp_line]))
i = i + 1
if i % num == 0:
legend_1 = Legend(items=items_c1)
p.add_layout(legend_1, 'left')
p.xaxis.axis_label = 'run_time'
p.yaxis[0].axis_label = 'BER'
p.legend[0].orientation = 'vertical'
p.legend[0].location = 'bottom_center'
p.legend[0].click_policy = 'hide'
pictures.append(p)
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
items_c1 = []
file = "test_ask_5"
file_path = file + '.html'
output_file(file_path)
show(column(pictures))
The solution below hides all lines but the one that is being clicked (not hovered). This code works for Bokeh v1.3.4
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import CustomJS
colors = ['orange', 'cyan', 'lightgreen']
p = figure()
lines = [p.line(np.arange(10), np.random.random(10), line_color = colors[i], line_width = 3, legend=colors[i], name=colors[i]) for i in range(3)]
code = '''if(Bokeh != 'undefined' && (Bokeh.active_line === 'undefined' || Bokeh.active_line == null))
{
Bokeh.active_line = cb_obj.name;
}'''
[line.js_on_change('visible', CustomJS(code = code)) for line in lines]
code = ''' for(i = 0; i < lines.length; i++) {
if (lines[i].name == Bokeh.active_line) {
lines[i].visible = true
}
else {
lines[i].visible = false
}
}
Bokeh.active_line = null'''
p.js_on_event('tap', CustomJS(args = {'lines' : lines}, code = code))
code = ''' for(i = 0; i < lines.length; i++) {
lines[i].visible = true
}
Bokeh.active_line = null'''
p.js_on_event('reset', CustomJS(args = dict(lines = lines), code = code))
p.legend.click_policy = 'hide'
show(p)
The first callback is applied to all glyph renderers (lines) and is being triggered when the line must be hidden, that is when a user clicks a legend item. The callback just sets the global variable Bokeh.active_line which remembers the renderer (line) name, e.g. "orange" or "cyan"
The second callback is attached to the plot canvas and is triggered every time the user clicks anywhere on the plot. What is basically does is inverting the glyphs (lines) visibility. It only shows the line specified by
Bokeh.active_line
The third callback is attached to the plot and is triggered when user clicks on the reset tool in the toolbar. It restores visibility of all lines.

Filter Bokeh DataTable on plot selection

I am trying to filter by dataTable based on the plot selection. I am getting stuck on incorporating cb_obj
I produce a scatterplot where I will like to select any point. Once selected I want to populate the table with values where the selected y (or x) value is greater than the values in the second datasource i.e. the datasource for the DataTable.
from random import random
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, TapTool,BoxZoomTool, Column
from bokeh.plotting import figure, output_file, show
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
output_file("callback.html")
x = [random()*10 for x in range(10)]
y = [random()*10 for y in range(10)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, title="Select Here")
p1.add_tools(TapTool(), BoxZoomTool())
p1.circle('x', 'y', source=s1, alpha=10)
x1=range(1, 10)
y1=range(1, 10)
s2 = ColumnDataSource(data=dict(x=x1, y=y1))
C1 = [
TableColumn(field="x", title="x"),
TableColumn(field="y", title="y"),
]
f_data_table = DataTable(columns=C1,source=s2)
s3 = ColumnDataSource(data=dict(x=[],y=[]))
C2 = [
TableColumn(field="x", title="x"),
TableColumn(field="y", title="y"),
]
data_table = DataTable(columns=C2,source=s3)
s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2,s3=s3), code="""
var data = s1.data;
var f = cb_obj.indices;
var d2 = s2.data;
var d3 = s3.data;
d3['x']=[]
d3['y']=[]
for(i = 0; i < d2['y'].length;i++){
if(d2['y'][i]>f['y']){
d3['x'].push(d2['x'][i])
d3['y'].push(d2['y'][i])
}
}
s3.change.emit()
// trigger change on datatable
data_table.change.emit()
""")
)
layout = Column(p1, data_table, f_data_table )
show(layout)
EDIT:
Got it working
from random import random
from bokeh.layouts import row
from bokeh.models import CustomJS, ColumnDataSource, TapTool,BoxZoomTool, Column
from bokeh.plotting import figure, output_file, show
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
output_file("callback.html")
x = [random()*10 for x in range(10)]
y = [random()*10 for y in range(10)]
s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, title="Select Here")
p1.add_tools(TapTool(), BoxZoomTool())
p1.circle('x', 'y', source=s1, alpha=10)
x1=range(1, 10)
y1=range(1, 10)
s2 = ColumnDataSource(data=dict(x=x1, y=y1))
C1 = [
TableColumn(field="x", title="x"),
TableColumn(field="y", title="y"),
]
f_data_table = DataTable(columns=C1,source=s2)
s3 = ColumnDataSource(data=dict(x=[],y=[]))
C2 = [
TableColumn(field="x", title="x"),
TableColumn(field="y", title="y"),
]
data_table = DataTable(columns=C2,source=s3)
s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2,s3=s3), code="""
var data = s1.data;
var f = cb_obj.indices;
var d2 = s2.data;
var d3 = s3.data;
d3['x']=[]
d3['y']=[]
for (var j = 0; j < f.length; j++) {
for(i = 0; i < d2['y'].length;i++){
if(d2['y'][i]>data['y'][f] && d2['x'][i]>data['x'][f]){
d3['x'].push(d2['y'][i])
d3['y'].push(d2['y'][i])
}
}
}
s3.change.emit()
// trigger change on datatable
data_table.change.emit()
""")
)
layout = Column(p1, data_table, f_data_table )
show(layout)

Changing colors on bokeh patches plot real time

I'm trying to create a bokeh plot of the US States, and color each of the state according to some data. Now using this tutorial I managed to create this, but I also want to enhance it, and add a slider to it, to change the values displayed. For example like displaying separate years.
With the help of this tutorial, I managed to add the slider, and the underlying data does change, according to the hover text, but the colors aren't recalculated, and so the visual representation does not match the values.
This is the code I've used, from a Jupyter notebook, so anybody who wants to try can reproduce
from bokeh.io import show, output_notebook
from bokeh.models import (
ColumnDataSource,
HoverTool,
LogColorMapper,
Range1d, CustomJS, Slider
)
from bokeh.palettes import Inferno256 as palette
from bokeh.plotting import figure
from bokeh.layouts import row, widgetbox
from bokeh.sampledata.us_counties import data as counties
from bokeh.sampledata.us_states import data as states
from bokeh.sampledata.unemployment import data as unemployment
import pandas as pd
import random
output_notebook()
palette.reverse()
states_accumulated ={}
available_state_codes = states.keys()
for key, value in counties.items():
state_name = value["state"].upper()
if state_name in states.keys() and "number" not in states[state_name]:
states[state_name]["number"] = key[0]
for key,state in states.items():
state["code"] = key
state_list = []
for key,state in states.items():
state_list.append(state)
unemployment_transf = []
for key,value in unemployment.items():
unemployment_transf.append({
"State":key[0],
"County":key[1],
"Value":value
})
unemp_df = pd.DataFrame(unemployment_transf)
unemp_sum = unemp_df.groupby("State").mean()["Value"]
unemp_sum = unemp_sum.sort_index()
unemp_sum_flat = {key:value for key, value in unemp_sum.items()}
for state in state_list:
state["value"] = unemp_sum_flat[state["number"]]
state_df = pd.DataFrame(state_list)
color_mapper = LogColorMapper(palette=palette)
state_xy = (list(state_df["lons"].values),list(state_df["lats"].values))
max_x = max([max(l) for l in state_xy[0]])
max_y = max([max(l) for l in state_xy[1]])
min_x = min([min(l) for l in state_xy[0]])
min_y = min([min(l) for l in state_xy[1]])
data=dict(
x=state_xy[0],
y=state_xy[1],
name=list(state_df["name"].values),
used = list(state_df["value"].values)
)
data['1999'] = list(state_df["value"].values)
data['2000'] = [random.randrange(0,10) for i in range(len(state_xy[0]))]
source = ColumnDataSource(data)
TOOLS = "pan,wheel_zoom,reset,hover,save"
p = figure(
title="States", tools=TOOLS,
x_axis_location=None, y_axis_location=None
)
p.width=450
p.height = 450
p.x_range= Range1d(-170,-60)
p.y_range = Range1d(min_y-10,max_y+10)
p.grid.grid_line_color = None
renderer = p.patches('x', 'y', source=source,
fill_color={'field': 'used', 'transform': color_mapper},
fill_alpha=0.7, line_color="white", line_width=0.5)
hover = p.select_one(HoverTool)
hover.point_policy = "follow_mouse"
hover.tooltips = [
("Name", "#name"),
("Unemployment rate)", "#used%"),
("(Long, Lat)", "($x, $y)"),
]
callback = CustomJS(args=dict(source=source,plot=p,color_mapper = color_mapper,renderer = renderer), code="""
var data = source.data;
var year = year.value;
used = data['used']
should_be = data[String(year)]
for (i = 0; i < should_be.length; i++) {
used[i] = should_be[i];
}
""")
year_slider = Slider(start=1999, end=2000, value=1999, step=1,
title="year", callback=callback)
callback.args["year"] = year_slider
layout = row(
p,
widgetbox(year_slider),
)
show(layout)
Sample images of the plot:
What I would like to accomplish, is that when I change the slider, the colors on the plot should change. Now I think the JS callback should call some kind of redraw or recalculate, but I haven't found any documentation about it. Is there a way to do this?
append source.change.emit() to the Javascipt code to trigger the change event.
Appending source.trigger("change"); to the CustomJS seems to solve the problem, now as the slider changes, the colors change.

Link a Span or Cursor in between plots with Bokeh in Python

I would like a cursor that is linked between to plots in Bokeh. So if I move my cursor on one plot, an equivalent line shows up on an adjacent plot. I haven't figured out how to do it with the built in cursor tool. So my current solution is to draw a line on each plot that shares a source. Then when I hover over either plot, the source is updated.
I have 2 issues with this method:
1. It seems like a workaround
2. Currently the lines are finite length. I would like the line to be infinite, so no matter how the graph is resized, the line runs off the edge. Currently the line I draw is finite. The right way to draw an infinite horizontal line is a Span annotation, but I am having a hard time figuring out how to pass/update the Span location through my callback. See my code below.
from bokeh.io import gridplot, show, output_notebook, output_file
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, CustomJS, Span
output_notebook()
x = list(range(11))
y1 = x
y2 = [10 - i for i in x]
source = ColumnDataSource({'x0': [0], 'y0': [2], 'x1': [10], 'y1': [2]})
# create a new plot
s1 = figure(width=250, plot_height=250, tools="", title=None)
cr1 = s1.circle(x, y1, size=10, color="navy", alpha=0.5)
sr1 = s1.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=1, line_width=1, source=source, )
sp1 = Span(location=source.data['y0'][0], dimension='width', line_color='green')
s1.renderers.extend([sp1,])
# create another one
s2 = figure(width=250, height=250, title=None)
cr2 = s2.triangle(x, y1, size=10, color="firebrick", alpha=0.5)
sr2 = s2.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=1, line_width=1, source=source, )
# put all the plots in an HBox
p = gridplot([[s1,s2],[]])
code = """
var data = {'x0': [], 'y0': [], 'x1': [], 'y1': []};
var cdata = circle.get('data');
var indices = cb_data.index['1d'].indices;
for (i=0; i < indices.length; i++) {
ind0 = indices[i];
data['x0'].push(0);
data['y0'].push(cdata.y[ind0]);
data['x1'].push(10);
data['y1'].push(cdata.y[ind0]);
}
segment.set('data', data);
"""
callback1 = CustomJS(args={'circle': cr1.data_source, 'segment': sr2.data_source}, code=code)
s1.add_tools(HoverTool(tooltips=None, callback=callback1, renderers=[cr1]))
callback2 = CustomJS(args={'circle': cr2.data_source, 'segment': sr2.data_source}, code=code)
s2.add_tools(HoverTool(tooltips=None, callback=callback2, renderers=[cr2]))
# show the results
show(p)
Thanks to #bigreddot for his answer on the spans. I had tried and not gotten in to work, but figured it out with his hints. The working code is below. I implemented a span in each plot and then edit the location of each.
from bokeh.io import gridplot, show, output_notebook, output_file
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource, CustomJS, Span
output_file('Test.html')
#output_notebook()
x = list(range(11))
y1 = x
y2 = [10 - i for i in x]
# create a new plot
s1 = figure(width=250, plot_height=250, tools="", title=None)
cr1 = s1.circle(x, y1, size=10, color="navy", alpha=0.5)
sp1 = Span(location=source.data['y0'][0], dimension='width', line_color='green', render_mode='css')
s1.renderers.extend([sp1,])
# create another one
s2 = figure(width=250, height=250, title=None)
cr2 = s2.triangle(x, y1, size=10, color="firebrick", alpha=0.5)
sp2 = Span(location=source.data['y0'][0], dimension='width', line_color='green', render_mode='css')
s2.renderers.extend([sp2,])
# put all the plots in an HBox
p = gridplot([[s1,s2],[]])
code = """
var cdata = circle.get('data');
var indices = cb_data.index['1d'].indices;
var sum = 0;
for (i=0; i < indices.length; i++) {
sum += cdata.y[indices[i]];
}
var avg = sum/indices.length
span1.set('location', [avg])
span2.set('location', [avg])
"""
callback1 = CustomJS(args={'circle': cr1.data_source, 'span1': sp1, 'span2': sp2}, code=code)
s1.add_tools(HoverTool(tooltips=None, callback=callback1, renderers=[cr1]))
callback2 = CustomJS(args={'circle': cr2.data_source, 'span1': sp1, 'span2': sp2}, code=code)
s2.add_tools(HoverTool(tooltips=None, callback=callback2, renderers=[cr2]))
# show the results
show(p)

Get selected data contained within box select tool in Bokeh

If I have a scatter plot in bokeh and I've enabled the Box Select Tool, suppose I select a few points with the Box Select Tool. How can I access the (x,y) position location information of the points that I've selected?
%matplotlib inline
import numpy as np
from random import choice
from string import ascii_lowercase
from bokeh.models.tools import *
from bokeh.plotting import *
output_notebook()
TOOLS="pan,wheel_zoom,reset,hover,poly_select,box_select"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
letters = [choice(ascii_lowercase) for _ in range(10)]
)
)
p.scatter("xvals", "yvals",source=source,fill_alpha=0.2, size=5)
select_tool = p.select(dict(type=BoxSelectTool))[0]
show(p)
# How can I know which points are contained in the Box Select Tool?
I can't call the "callback" attribute and the "dimensions" attribute just returns a list ["width", "height"]. If I can just get the dimensions and the location of the Selected Box, I can figure out which points are in my dataset from there.
You can use a callback on the ColumnDataSource that updates a Python variable with the indices of the selected data:
%matplotlib inline
import numpy as np
from random import choice
from string import ascii_lowercase
from bokeh.models.tools import *
from bokeh.plotting import *
from bokeh.models import CustomJS
output_notebook()
TOOLS="pan,wheel_zoom,reset,hover,poly_select,box_select"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
letters = [choice(ascii_lowercase) for _ in range(10)]
)
)
p.scatter("xvals", "yvals",source=source,fill_alpha=0.2, size=5)
select_tool = p.select(dict(type=BoxSelectTool))[0]
source.callback = CustomJS(args=dict(p=p), code="""
var inds = cb_obj.get('selected')['1d'].indices;
var d1 = cb_obj.get('data');
console.log(d1)
var kernel = IPython.notebook.kernel;
IPython.notebook.kernel.execute("inds = " + inds);
"""
)
show(p)
Then you can access the desired data attributes using something like
zip([source.data['xvals'][i] for i in inds],
[source.data['yvals'][i] for i in inds])
Here is a working example with Python 3.7.5 and Bokeh 1.4.0
public github link to this jupyter notebook:
https://github.com/surfaceowl-ai/python_visualizations/blob/master/notebooks/bokeh_save_linked_plot_data.ipynb
environment report:
virtual env python version: Python 3.7.5
virtual env ipython version: 7.9.0
watermark package reports:
bokeh 1.4.0
jupyter 1.0.0
numpy 1.17.4
pandas 0.25.3
rise 5.6.0
watermark 2.0.2
# Generate linked plots + TABLE displaying data + save button to export cvs of selected data
from random import random
from bokeh.io import output_notebook # prevent opening separate tab with graph
from bokeh.io import show
from bokeh.layouts import row
from bokeh.layouts import grid
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.models import Button # for saving data
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.models import HoverTool
from bokeh.plotting import figure
# create data
x = [random() for x in range(500)]
y = [random() for y in range(500)]
# create first subplot
plot_width = 400
plot_height = 400
s1 = ColumnDataSource(data=dict(x=x, y=y))
fig01 = figure(
plot_width=plot_width,
plot_height=plot_height,
tools=["lasso_select", "reset", "save"],
title="Select Here",
)
fig01.circle("x", "y", source=s1, alpha=0.6)
# create second subplot
s2 = ColumnDataSource(data=dict(x=[], y=[]))
# demo smart error msg: `box_zoom`, vs `BoxZoomTool`
fig02 = figure(
plot_width=400,
plot_height=400,
x_range=(0, 1),
y_range=(0, 1),
tools=["box_zoom", "wheel_zoom", "reset", "save"],
title="Watch Here",
)
fig02.circle("x", "y", source=s2, alpha=0.6, color="firebrick")
# create dynamic table of selected points
columns = [
TableColumn(field="x", title="X axis"),
TableColumn(field="y", title="Y axis"),
]
table = DataTable(
source=s2,
columns=columns,
width=400,
height=600,
sortable=True,
selectable=True,
editable=True,
)
# fancy javascript to link subplots
# js pushes selected points into ColumnDataSource of 2nd plot
# inspiration for this from a few sources:
# credit: https://stackoverflow.com/users/1097752/iolsmit via: https://stackoverflow.com/questions/48982260/bokeh-lasso-select-to-table-update
# credit: https://stackoverflow.com/users/8412027/joris via: https://stackoverflow.com/questions/34164587/get-selected-data-contained-within-box-select-tool-in-bokeh
s1.selected.js_on_change(
"indices",
CustomJS(
args=dict(s1=s1, s2=s2, table=table),
code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (var i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.change.emit();
table.change.emit();
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
""",
),
)
# create save button - saves selected datapoints to text file onbutton
# inspriation for this code:
# credit: https://stackoverflow.com/questions/31824124/is-there-a-way-to-save-bokeh-data-table-content
# note: savebutton line `var out = "x, y\\n";` defines the header of the exported file, helpful to have a header for downstream processing
savebutton = Button(label="Save", button_type="success")
savebutton.callback = CustomJS(
args=dict(source_data=s1),
code="""
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(file);
elem.download = 'selected-data.txt';
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
""",
)
# add Hover tool
# define what is displayed in the tooltip
tooltips = [
("X:", "#x"),
("Y:", "#y"),
("static text", "static text"),
]
fig02.add_tools(HoverTool(tooltips=tooltips))
# display results
# demo linked plots
# demo zooms and reset
# demo hover tool
# demo table
# demo save selected results to file
layout = grid([fig01, fig02, table, savebutton], ncols=3)
output_notebook()
show(layout)
# things to try:
# select random shape of blue dots with lasso tool in 'Select Here' graph
# only selected points appear as red dots in 'Watch Here' graph -- try zooming, saving that graph separately
# selected points also appear in the table, which is sortable
# click the 'Save' button to export a csv
# TODO: export from Bokeh to pandas dataframe

Categories