Joubert Lucas
Joubert Lucas

Reputation: 163

Using a local file in html for a PyQt5 webengine

I am trying to embed a plotly graph into a PyQt5 webengine view. I was able to do so using the following:

open plotly in qwebview in interactive mode

If you read it, the article explains that you can't directly include the javascript in the HTML when using the webengine view (it has issues loading files above 2 MB). However, I'm trying to make it so that the source for the javascript is a local copy of plotly-min.js (saved in the project folder) so that anyone using the program doesn't need an internet connection to view the graphs it generates. I've tried to follow a few examples on html, but to no avail.

The original code that does work with an internet connection is:

raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="https://cdn.plot.ly/plotly-latest.min.js"></script></head>'
raw_html += '<body>'
raw_html += plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'

temp_view = QWebEngineView()
temp_view.setHtml(graph)

If I understand it correctly, I need to change this part:

<script src="https://cdn.plot.ly/plotly-latest.min.js">

I've already tried:

<script type="text/javascript" src="file:///C:/Users/UserName/PycharmProjects/ProjectFolder/plotly-latest.min.js"></script>

I honestly do not know HTML. This is the only HTML in the code (the rest is in Python 3). Does anyone know why the above might not work and what I need to change? I have a suspicion I might somehow be running into the 2 MB limit the above question referenced, as different variations I've found online for how to reference a local file in HTML don't seem to change anything.

Upvotes: 6

Views: 10385

Answers (3)

William
William

Reputation: 421

In case it's helpful to anybody, I went a different direction with a solution. Instead of relying on the QWebEngineView.setHTML method, I decided to use a named temporary file to write the html to the disk, then load that into a QUrl to load into the QWebEngineView using the QUrl.fromLocalFile and QWebEngineView.setUrl methods.

Altogether I applied this into a custom PlotlyWidget class, so here's the full thing in case it's helpful to anyone.

Some special care had to be taken to ensure the temporary files get deleted when the PlotlyWiget goes out of scope and gets garbage collected (i.e. the __del__ override).

import os
import tempfile
import plotly
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl

class PlotlyWidget(QWebEngineView):

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.plotly_fig = None
        self.html_file = None

    def load_fig(self, plotly_fig):
        self.plotly_fig = plotly_fig
        self.plotly_fig.update_layout(font={'size':18})
        self.update_fig()

    def update_fig(self):
        html = '<html><body>'
        html += plotly.offline.plot(self.plotly_fig, output_type='div', include_plotlyjs=True)
        html += '</body></html>'

        if self.html_file is not None: # close previous tempfiles so that they delete from disk properly
            self.html_file.close()
            os.unlink(self.html_file.name)

        self.html_file = tempfile.NamedTemporaryFile(suffix='.html', delete=False)
        with open(self.html_file.name, 'w') as file:
            file.write(html)
        url = QUrl.fromLocalFile(self.html_file.name)
        self.setUrl(url)

    def closeEvent(self, event):
        """Delete the temp file on close event."""
        super().closeEvent(event)
        if self.html_file is not None:
            self.html_file.close()
            os.unlink(self.html_file.name)
            self.html_file = None

    def __del__(self):
        """Handle temp file close (and deletion) if QWebEngineView is garbage collected instead of closed."""
        if self.html_file is not None:
            self.html_file.close()
            os.unlink(self.html_file.name)

Typical usage is then to generate a plotly figure and then send it to the load_fig method like:

# from within QMainWindow or other QWidget instance
    fig = go.Figure()
    fig.add_scatter(y=ydata, x=xdata)

    self.ploty_widget = PlotlyWidget()
    self.ploty_widget.load_fig(fig)
    self.ploty_widget.show()

Upvotes: 2

swiesend
swiesend

Reputation: 1100

I had problems running the example of @eyllanesc under Ubuntu 16.04 / 18.04 with NVIDIA driver.

First I had to downgrade PyQt5 5.10.1 to 5.10.0 to avoid:

[0525/114545.216138:WARNING:stack_trace_posix.cc(648)] Failed to open file: /home/tmp/.gl5Qb0Tf (deleted)
  Error: Datei oder Verzeichnis nicht gefunden
Could not find QtWebEngineProcess
[3032:3032:0525/114545.495351:FATAL:zygote_host_impl_linux.cc(182)] Check failed: ReceiveFixedMessage(fds[0], kZygoteBootMessage, sizeof(kZygoteBootMessage), &boot_pid). 
...

But this should be fixed for the future in PyQt5 5.11

The second problem was the linking of the shader (ubuntu 16.04):

QOpenGLShaderProgram::uniformLocation(qt_Matrix): shader program is not linked
QOpenGLShaderProgram: could not create shader program
QOpenGLShader: could not create shader
QOpenGLShader: could not create shader
shader compilation failed: 

Under ubuntu 18.04 in VirtualBox:

Received signal 11 SEGV_MAPERR 000000000000
#0 0x7faa83f029a5 <unknown>
#1 0x7faa82c42501 <unknown>
#2 0x7faa83f02d3d <unknown>
#3 0x7faa9228a890 <unknown>
  r8: 0000000000000001  r9: 0000000002d5d3d0 r10: 0000000000000002 r11: 00007faa8ec4a000
 r12: 0000000002d386b0 r13: 0000000002d59b50 r14: 0000000000000000 r15: 00007faa8ed3b280
  di: 0000000000001f01  si: 00007faa8ed3d210  bp: 0000000002d52e40  bx: 0000000000000001
  dx: 0000000002e52220  ax: 0000000002e52220  cx: 0000000000000000  sp: 00007ffce38c2b78
  ip: 0000000000000000 efl: 0000000000010202 cgf: 002b000000000033 erf: 0000000000000014
 trp: 000000000000000e msk: 0000000000000000 cr2: 0000000000000000
[end of stack trace]
Calling _exit(1). Core file will not be generated.

that can be solved by following the advise of dcortesi fixing an 4 year old Ubuntu bug:

if sys.platform.startswith( 'linux' ) :
    from OpenGL import GL

Putting it all togehter:

requirements.txt

PyQt5 == 5.10.0
PyOpenGL
plotly

webview.py

import sys

if sys.platform.startswith( 'linux' ) :
    from OpenGL import GL

from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QDir, QUrl

import plotly
import plotly.graph_objs as go

sys.argv.append("--disable-web-security")
app = QApplication(sys.argv)

x1 = [10, 3, 4, 5, 20, 4, 3]
trace1 = go.Box(x = x1)
layout = go.Layout(showlegend = True)
data = [trace1]

fig = go.Figure(data=data, layout = layout)

path = QDir.current().filePath('plotly-latest.min.js') 
local = QUrl.fromLocalFile(path).toString()

raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="{}"></script></head>'.format(local)
raw_html += '<body>'
raw_html += plotly.offline.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'

view = QWebEngineView()
view.setHtml(raw_html)
view.show()

sys.exit(app.exec_())

One can download the the latest plotly.js here. I used plotly.js v1.38.0 for this example.

Upvotes: 2

eyllanesc
eyllanesc

Reputation: 244003

Many browsers for security reason disable the loading of local files, but as noted in the following forum you can enable that capability with:

sys.argv.append("--disable-web-security")

Assuming the .js is next to your .py file:

.
├── main.py
└── plotly-latest.min.js

you can use the following example:

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QDir, QUrl

import plotly
import plotly.graph_objs as go

sys.argv.append("--disable-web-security")
app = QApplication(sys.argv)

x1 = [10, 3, 4, 5, 20, 4, 3]
trace1 = go.Box(x = x1)
layout = go.Layout(showlegend = True)
data = [trace1]

fig = go.Figure(data=data, layout = layout)

path = QDir.current().filePath('plotly-latest.min.js') 
local = QUrl.fromLocalFile(path).toString()

raw_html = '<html><head><meta charset="utf-8" />'
raw_html += '<script src="{}"></script></head>'.format(local)
raw_html += '<body>'
raw_html += plotly.offline.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'

view = QWebEngineView()
view.setHtml(raw_html)
view.show()

sys.exit(app.exec_())

enter image description here

Upvotes: 5

Related Questions