
I am trying to find a method to let the users of my app download a html version of the app, which can then be opened when the Bokeh server is not accessible.
The downloadable version has to be cleaned up before downloading to hide unnecessary elements or elements triggering a python callback, which would not work in a standalone html file.
My trial has two basic steps:
- Creating the html file with
file_html() - Downloading the file using a Javascript callback based on this example Export CSV, the code of which can be found here: export_csv.
I came to the following working code:
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button, CustomJS, Div, Paragraph
from bokeh.resources import CDN
from bokeh.embed import file_html
js_code = """
const filetext = html_doc.text
const filename = "HTML Export.html"
const blob = new Blob([filetext], { type: 'application/xhtml+xml;charset=utf-8;' })
const link = document.createElement('a') // Creates an <a> type element (anchor = hyperlink)
link.href = URL.createObjectURL(blob) // Sets the blob as the hyperlink the anchor element points to
link.download = filename // Tells that the file has to be downloaded and sets the file name.
link.target = '_blank' // For some browsers, may influence the way the file is downloaded
link.style.visibility = 'hidden' // Actually not necessary, sinse object not in DOM
link.dispatchEvent(new MouseEvent('click')) // Simulates clicking of the link.
URL.revokeObjectURL(link.href) // Release the object URL
link.remove() // Remove anchor element
"""
my_div = Div(text="Some display element (could be a chart)")
my_div2 = Div(text="Another display element (could be a second chart)")
html_create_button = Button(label="Create HTML")
# Create a model to pass the up-to-date file content to Javascript.
# Since it only works if added to the layout but does not have to be seen, make it invisible.
html_doc = Paragraph(visible=False)
def html_create_callback():
# Preparation: hide button which would be dead in the standalone html:
html_create_button.visible = False
# Create html file:
html_doc.text = file_html(models=my_layout, resources=CDN, title="HTML Export", suppress_callback_warning=True)
html_create_button.visible = True
# Python callback:
html_create_button.on_click(html_create_callback)
# Create a Javascript callback that automatically starts when html_doc.text changes:
html_doc.js_on_change("text", CustomJS(args=dict(html_doc=html_doc), code=js_code))
my_layout = column(my_div, my_div2, html_create_button, html_doc)
curdoc().add_root(my_layout)
This code works but raised for me several questions:
- I first need to clean up the app, which is done with a Python callback (in my real app, I have a lot of changes which would be difficult to write in Javascript), then the Javascript download code has to be executed.
My question: would it be possible to execute the Javascript code “inline” in the Python code without having to configure an extra Javascript callback which also has to be somehow triggered?
Something like:
Execute Python code
RunJS(args=dict(some_input=some_input), code="Some Javascript code to be executed now")
Execute further Python code
-
In my example, I use a Paragraph widget to handover the file content and to trigger the Javascript callback with
paragraph.js_on_change("text", CustomJS(…
I only managed to trigger the Javascript callback when the Paragraph widget was added to the app layout, although I actually don’t want to show it.
I know I can have the handover part done withDataModel(see use-a-variable-as-argument-to-customjs), though I don’t know how to trigger the Javascript callback with aDataModel.
Is there a better way? -
Feel free to review the Javascript code of my example (including comments). Do the two last rows
URL.revokeObjectURL(link.href)
link.remove()
make sense? Expecially if the file is big and the user downloads it several times, the memory should not stay occupied unnecessarily. -
General question about
file_html(): Is it possible to add an icon to be displayed in the tab of the browser? Currently, there is only a blank page symbol:

-
Would it make sense to implement in Bokeh a
download()function which could be used as follows:
download(file=file_content, name=file_name)
wherefile_contentwould be a previously prepared file, like for instance csv, picture, html file,…?
The preparation of the file wouldn’t necessary have to be done with Bokeh itself. For instance,pandas.DataFrame.to_csvcould be used to prepare a csv.
I would see such a function as a reverseFileInput. Though, I don’t think that any graphical widget would be necessary for such adownloadfunction, since the user has the possibility to connect the function to widgets like buttons on his/her own.
My system:
Windows 10
Python 3.12.0
Bokeh 3.3.0

