I am developing a feature that consist of creating a Bokeh graph with interactive widgets and tables (including javascript code), that are then exported as an HTML file.
I have managed to show this HTML file inside a PyQt5 widget through QtWebEngine and everything works as it should when run through my IDE. The issue arises when I build the executable with PyInstaller. The HTML still work when I open it manually with a web browser but QtWebBrowser outputs this:
This text goes on and on with my non-interactive graph shown in the middle of it all.
I don't know much about HTML but I suspect the issue is linked with QtWebEngine's inability to execute script tags in HTML files (even though it works flawlessly when I run it in my IDE?).
I tried a whole lot of things so far including using both CDN and INLINE options when building the HTML, adding sys.argv.append("--disable-web-security")
to my code, and downloading manually the javascript libraries needed in the HTML file (although im not sure I did it correctly).
So first of all, here's a snippet of my code that works in my IDE:
self.m_output = QtWebEngineWidgets.QWebEngineView()
html_file = Bgf.plot_bokeh(my_data) #function that creates the Bokeh HTML file
self.ui.Vlayout_network.addWidget(self.m_output) # put the browser in the PyQt5 widget
page = WebEnginePage(self.m_output) # class webenginepage required for download signals
self.m_output.setPage(page)
self.m_output.setHtml(html_file)
self.m_output.show()
the HTML file is too big to show here but here's the required libraries:
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-2.2.3.min.js" integrity="sha384-T2yuo9Oe71Cz/I4X9Ac5+gpEa5a8PpJCDlqKYO0CfAuEszu1JrXLl8YugMqYe3sM" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.2.3.min.js" integrity="sha384-98GDGJ0kOMCUMUePhksaQ/GYgB3+NH9h996V88sh3aOiUNX3N+fLXAtry6xctSZ6" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.2.3.min.js" integrity="sha384-89bArO+nlbP3sgakeHjCo1JYxYR5wufVgA3IbUvDY+K7w4zyxJqssu7wVnfeKCq8" crossorigin="anonymous"></script>
I tried going to those URLs and downloading manually those libraries and here's my final attempt, based on this answer.
self.m_output = QtWebEngineWidgets.QWebEngineView()
html_file = Bgf.plot_bokeh(my_variables) #function that creates the Bokeh HTML file
sys.argv.append("--disable-web-security")
# paths to the downloaded javascript libraries
js_library_1 = QDir.current().filePath("lib/Network/bokeh_js/bokeh-2.2.3.min.js")
js_library_2 = QDir.current().filePath("lib/Network/bokeh_js/bokeh-tables-2.2.3.min.js")
js_library_3 = QDir.current().filePath("lib/Network/bokeh_js/bokeh-widgets-2.2.3.min.js")
local_js_library_1 = QUrl.fromLocalFile(js_library_1).toString()
local_js_library_2 = QUrl.fromLocalFile(js_library_2).toString()
local_js_library_3 = QUrl.fromLocalFile(js_library_3).toString()
#merging the JS libs and my html file
final_html = '<html><head><meta charset="utf-8" />'
final_html += '<script src="{}"></script></head>'.format(local_js_library_1)
final_html += '<script src="{}"></script></head>'.format(local_js_library_2)
final_html += '<script src="{}"></script></head>'.format(local_js_library_3)
final_html += html_file
self.ui.Vlayout_network.addWidget(self.m_output) # browser in widget
page = WebEnginePage(self.m_output) # class webenginepage required for download signals
self.m_output.setPage(page)
self.m_output.setHtml(final_html)
self.m_output.show()
Any clue about what I could do in order to properly show the HTML file?
edit 1
I built a small pyqt5 app as a minimal reproducible example:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import pyqtSlot
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, CustomJS, Slider
from bokeh.plotting import Figure, output_file
from bokeh.embed import file_html
from bokeh.resources import CDN
class App(QWidget):
def __init__(self):
super().__init__()
self.resize(500, 500)
self.initUI()
def initUI(self):
button = QPushButton('start', self)
button.move(100, 70)
button.clicked.connect(self.on_click)
self.layout = QVBoxLayout()
self.layout1 = QVBoxLayout()
self.layout1.addWidget(button)
self.layout2 = QVBoxLayout()
self.layout.addLayout(self.layout1)
self.layout.addLayout(self.layout2)
self.setLayout(self.layout)
self.show()
@pyqtSlot()
def on_click(self):
print('PyQt5 button click')
self.m_output = QWebEngineView()
self.layout2.addWidget(self.m_output)
html_file = self.bokeh_function()
self.m_output.setHtml(html_file)
self.m_output.show()
def bokeh_function(self):
# taken from https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
output_file("js_on_change.html")
x = [x * 0.005 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(width=400, height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
const data = source.data;
const f = cb_obj.value
const x = data['x']
const y = data['y']
for (let i = 0; i < x.length; i++) {
y[i] = Math.pow(x[i], f)
}
source.change.emit();
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)
layout = column(slider, plot)
html = file_html(layout, CDN, "my plot")
return html
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
This should give you an interactive graph like this:
However, if you then create an executable with Pyinstaller (with this code in the console/powershell):
pyinstaller .\main.py --onefile --noconsole
And launch the executable, you'll get the following output:
No matter what I try, I cant seem to get an interactive graph as an output.