@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()