@Ignalion One idea is to use a combination of on_event for the python side when the button is clicked and an invisible dummy Div for the JS side for the download of the file at the user end. I use js_on_change for text in the dummy Div, so that the JS code is executed when the text is changed. In the JS code I use fetch to get the file from the server.
In this example I use pandas df.to_csv to save the file to the server. Observe that I save the file to static folder; I run this this app in directory format. There is a similar question in SO and an explanation with respect to the use of static folder.
def btn_callback(src):
fout = join(app_path, 'static', FNAME)
df = src.to_df()
df.to_csv(fout, index = False)
file_ready_div.text = 'file_ready'
button.on_event('button_click', partial(btn_callback, source))
For the JS part I use the following where the dummy Div got visibility equal to False in order to not show it. The text is updated with a specific string when the file has been saved to server in btn_callback
download_cb_code = '''
function upload(file) {
fetch(file)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((blob) => downLoad(blob))
.catch((err) => console.error(`Fetch problem: ${err.message}`));
}
function downLoad(blob) {
const filename = 'data_result.csv'
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, filename)
} else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
}
const bk_file_div = window.Bokeh.documents[0].get_model_by_name('bk_file_div');
if (bk_file_div.text == 'file_ready') {
upload(file);
bk_file_div.text = 'file_notready';
}
'''
file_ready_div = Div(text = '', visible = False, name = 'bk_file_div')
file_ready_div.js_on_change(
'text',
CustomJS(
args={'file': join(app_endpoint, 'static', FNAME)},
code = download_cb_code
)
)
Complete code of main.py
''' A column salary chart with minimum and maximum values.
This example shows the capability of exporting a csv file from ColumnDataSource.
'''
from os.path import dirname, join, basename, normpath
import pandas as pd
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import Button, ColumnDataSource, CustomJS, DataTable
from bokeh.models import NumberFormatter, RangeSlider, TableColumn, Div
from functools import partial
df = pd.read_csv(join(dirname(__file__), 'salary_data.csv'))
source = ColumnDataSource(data=dict())
def update():
current = df[(df['salary'] >= slider.value[0]) & (df['salary'] <= slider.value[1])].dropna()
source.data = {
'name' : current.name,
'salary' : current.salary,
'years_experience' : current.years_experience,
}
slider = RangeSlider(
title="Max Salary",
start=10000,
end=110000,
value=(10000, 50000),
step=1000,
format="0,0"
)
slider.on_change('value', lambda attr, old, new: update())
button = Button(label="Download", button_type="success")
FNAME = 'data_export.csv'
app_path = normpath(dirname(__file__))
app_endpoint = basename(app_path)
download_cb_code = '''
function upload(file) {
fetch(file)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.blob();
})
.then((blob) => downLoad(blob))
.catch((err) => console.error(`Fetch problem: ${err.message}`));
}
function downLoad(blob) {
const filename = 'data_result.csv'
//addresses IE
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, filename)
} else {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.target = '_blank'
link.style.visibility = 'hidden'
link.dispatchEvent(new MouseEvent('click'))
}
}
const bk_file_div = window.Bokeh.documents[0].get_model_by_name('bk_file_div');
if (bk_file_div.text == 'file_ready') {
upload(file);
bk_file_div.text = 'file_notready';
}
'''
file_ready_div = Div(text = '', visible = False, name = 'bk_file_div')
file_ready_div.js_on_change(
'text',
CustomJS(
args={'file': join(app_endpoint, 'static', FNAME)},
code = download_cb_code
)
)
def btn_callback(src):
fout = join(app_path, 'static', FNAME)
df = src.to_df()
df.to_csv(fout, index = False)
file_ready_div.text = 'file_ready'
button.on_event('button_click', partial(btn_callback, source))
columns = [
TableColumn(field="name", title="Employee Name"),
TableColumn(field="salary", title="Income", formatter=NumberFormatter(format="$0,0.00")),
TableColumn(field="years_experience", title="Experience (years)")
]
data_table = DataTable(source=source, columns=columns, width=800)
controls = column(slider, button, file_ready_div)
curdoc().add_root(row(controls, data_table))
curdoc().title = "Export CSV"
update()