Here’s a test script that does it: (pardon the ugly formatting - the boldface text were comments.
# Generate linked plots + TABLE displaying data + save button to export cvs of selected data
from random import random
import numpy as np
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, Div
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=["box_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")
# demo smart error msg: `box_zoom`, vs `BoxZoomTool`
fig03 = figure(
plot_width=400,
plot_height=400,
x_range=(0, 1),
tools=["box_zoom", "wheel_zoom", "reset", "save"],
title="Watch Histogram Here",
)
hist, bins = np.histogram(x, bins=20)
source_hist = ColumnDataSource(data=dict(hist=hist, bins=bins[0:-1]))
fig03.vbar(top="hist", x="bins", width=0.05, source=source_hist, 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
# https://stackoverflow.com/questions/37445495/binning-an-array-in-javascript-for-a-histogram
s1.selected.js_on_change(
"indices",
CustomJS(
args=dict(s1=s1, s2=s2, s3=source_hist, table=table),
code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
var dhist = s3.data;
var bhist = dhist['bins'];
// get the selected items and push them into the s2 CDS
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();
// grab the x values to re-histogram
var dh = []
for (var i = 0; i < inds.length; i++) {
dh.push(d1['x'][inds[i]])
}
// get the histogram numbins, bin width from initial histogram
var bins = [];
var binCount = 0;
var interval = bhist[1] - bhist[0];
var numOfBuckets = bhist.length;
//Setup Bins
for(var i = 0; i < numOfBuckets; i += interval){
bins.push({
binNum: binCount,
minNum: bhist[i],
maxNum: bhist[i] + interval,
count: 0
})
binCount++;
}
//Loop through data and add to bin's count
for (var i = 0; i < dh.length; i++){
var item = dh[i];
var a = (item - bhist[0])/interval;
var j = Math.floor(a);
var bin = bins[j];
bin.count++;
}
// push the bin values back into the original histogram]]]}
dhist['hist'] = []
for (var j = 0; j < 20; j++) {
dhist['hist'].push(bins[j].count)
}
s3.change.emit();
""",
),
)
# 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")
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);
""",
)
savebutton.js_on_click(callback)
# 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
get_d3 = '<script src="https://d3js.org/d3.v5.min.js"></script>'
d3_div = Div(text=get_d3)
layout = grid([fig01, fig02, fig03, table, savebutton], ncols=3)
show(layout)