pythonplotlypyside6qwebengineview

Trying to load plotly-latest.min.js locally for Pyside6 WebEngine


I am building a PySide6 GUI application that uses Plotly for interactive visualizations. Initially, I used the Plotly CDN to load the plotly-latest.min.js script in my HTML, and everything works fine. However, I would like to switch from using the CDN to loading the script locally from a file on my machine.

Here’s a test code that currently works with the CDN:

import sys
import plotly.graph_objects as go
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout
from PySide6.QtWebEngineWidgets import QWebEngineView
import plotly.offline as po

class GraphWidget(QWebEngineView):
    def __init__(self):
        super().__init__()

        # Create a random graph (scatter plot example)
        fig = go.Figure(data=go.Scatter(x=[1, 2, 3, 4, 5], y=[5, 4, 3, 2, 1], mode='lines+markers'))

        # Convert the figure to HTML
        raw_html = """
        <html>
        <head>
            <meta charset="utf-8" />
            <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>  <!-- CDN version -->
            <style>
                body {{
                    margin: 0;
                    background-color: rgb(44, 49, 60); /* Ensure the body matches the dark theme */
                }}
                .plot-container {{
                    width: 100%; /* Ensure plot takes full width */
                    height: 100%; /* Ensure plot takes full height */
                }}
            </style>
        </head>
        <body>
            {}
        </body>
        </html>
        """.format(po.plot(fig, include_plotlyjs=False, output_type='div'))

        # Set the HTML for the graph in the QWebEngineView
        self.setHtml(raw_html)

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('Plotly Graph in GUI')
        self.setGeometry(100, 100, 800, 600)

        # Create the Graph widget
        self.graph_widget = GraphWidget()

        # Set the layout
        layout = QVBoxLayout(self)
        layout.addWidget(self.graph_widget)

        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())

This works perfectly, but now I want to use a local version of plotly-latest.min.js to avoid relying on the internet. I tried to modify the <script> tag to load the file locally by referencing its local path, like this: <script src="file:///path/to/plotly-latest.min.js"></script>

However, these changes did not work, and I am unable to load the file properly. I tested some solutions mentioned in this Stack Overflow question Using a local file in html for a PyQt5 webengine, but none of them seem to work in my case.

Could anyone help me modify this code to load plotly-latest.min.js from a local path instead of using the CDN? Any advice on resolving this issue would be greatly appreciated.

Here's my attempt at making it local:

import sys
import plotly.graph_objects as go
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout
from PySide6.QtWebEngineWidgets import QWebEngineView
import plotly.offline as po
from PyQt5.QtCore import QDir, QUrl
import plotly.io as pio

class GraphWidget(QWebEngineView):
    def __init__(self):
        super().__init__()

        # Create a random graph (scatter plot example)
        fig = go.Figure(data=go.Scatter(x=[1, 2, 3, 4, 5], y=[5, 4, 3, 2, 1], mode='lines+markers'))


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

        # Convert the figure to HTML
        raw_html = f"""
        <html>
        <head>
            <meta charset="utf-8" />
            <script src="{local_js_url}"></script>
            <style>
                body {{
                    margin: 0;
                    background-color: rgb(44, 49, 60);
                }}
                .plot-container {{
                    width: 100%;
                    height: 100%;
                }}
            </style>
        </head>
        <body>
            {pio.to_html(fig, include_plotlyjs=False, full_html=False)}
        </body>
        </html>
        """
        

        # Set the HTML for the graph in the QWebEngineView
        self.setHtml(raw_html)

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('Plotly Graph in GUI')
        self.setGeometry(100, 100, 800, 600)

        # Create the Graph widget
        self.graph_widget = GraphWidget()

        # Set the layout
        layout = QVBoxLayout(self)
        layout.addWidget(self.graph_widget)

        self.show()

if __name__ == '__main__':
    sys.argv.append("--disable-web-security")  # Allow local file loading



    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())

which gives an error :

js: Uncaught ReferenceError: Plotly is not defined


Solution

  • As the documentation indicates:

    void QWebEngineView::setHtml(const QString &html, const QUrl &baseUrl = QUrl())

    Sets the content of the web view to the specified html content.

    baseUrl is optional and used to resolve relative URLs in the document, such as referenced images or stylesheets. For example, if html is retrieved from http://www.example.com/documents/overview.html, which is the base URL, then an image referenced with the relative URL, diagram.png, should be at http://www.example.com/documents/diagram.png.

    The HTML document is loaded immediately, whereas external objects are loaded asynchronously.

    When using this method, Qt WebEngine assumes that external resources, such as JavaScript programs or style sheets, are encoded in UTF-8 unless otherwise specified. For example, the encoding of an external script can be specified through the charset attribute of the HTML script tag. Alternatively, the encoding can be specified by the web server.

    This is a convenience function equivalent to setContent(html, "text/html;charset=UTF-8", baseUrl).

    (emphasis mine)

    The baseUrl is used to resolve resources such as the path of the .js file you want to load.

    Another more silent error is using QDir.current() since it returns the application's current directory (in this case the executable is python), which often does not match the .py path, breaking the functionality. One option is to use __file__ to get the .py path and then use those to build the .js path:

    from pathlib import Path
    
    CURRENT_DIRECTORY = Path(__file__).resolve().parent
    

    local_js_url = QUrl.fromLocalFile(CURRENT_DIRECTORY / "plotly-latest.min.js").toString()
    

    self.setHtml(raw_html, QUrl.fromLocalFile(CURRENT_DIRECTORY))
    

    Also change PyQt5 to PySide6 in your imports, you should not use both libraries together as you can generate silent bugs that are difficult to solve.