1

So I have a PyQt5 application in python that displays a Plotly plot (plotly being an interactive grapher app). Plotly runs on JavaScript, and I want to be able to catch JavaScript events and use them in python. So I wrote the code below, but I'm stuck on a seemingly intractable error/issue: the code stubbornly refuses to correctly register the object self.qt_bridge to the name "qtBridge" in setupWebChannel(), leading to a "ReferenceError: qtBridge is not defined" error later in the html_content.

I've narrowed it down to this line:

self.web_channel.registerObject("qtBridge", self.qt_bridge)

I've tested it extensively, and I'm pretty sure it's not due to one of these reasons:

  1. Problems in the html_content itself: I checked that self.qt_bridge does indeed exist right before html_content gets defined, and except for the "qtBridge undefined" error works perfectly;
  2. Timing issues that could lead to qt_bridge being added to the web_channel too fast or being used in the html_content too fast;
  3. Name conflicts or syntax: checked this like a hundred times already, no error here;
  4. QWebChannel or QWebEngineView not being defined or not being set-up fast enough before doing the registerObject() thing;
  5. Numerous other things I've checked, both in python and in javascript.

At this point I'm pretty much stumped and don't even know what to do to troubleshoot this any further. I have the impression of having tried everything (ofc this is very certainly just an impression :p). I've found a couple of stackoverflow posts that could help in solving this, but idk, either I don't understand those posts, or my mind is just too fried at this point after hours of troubleshooting this to be able to understand them. I'll post their links anyway:

Any help would be greatly appreciated, thanks in advance!

from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineSettings
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QComboBox, QWidget, QSizePolicy, QMainWindow, QPushButton
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QUrl, pyqtSlot
from PyQt5.QtWebChannel import QWebChannel

import sys
import time
import threading
import json
import numpy as np

import plotly.subplots as sp
import plotly.graph_objs as go


class PlotWidget(QWidget):
    def __init__(self, plotting_mode, parent=None):
        super().__init__(parent)

        self.plotting_mode = plotting_mode

        # Initialize the figure object
        self.fig = go.Figure()
        
        # Setup the webchannel to catch the in-plot javascript events
        self.setupWebChannel()
        self.set_fig_to_webview()


    def setupWebChannel(self):
        # Create webview
        self.webview = QWebEngineView()
        self.webview.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding
        )

        # Create webchannel
        self.web_channel = QWebChannel()
        self.webview.page().setWebChannel(self.web_channel)

        # Create QtBridge to link webview and webchannel
        self.qt_bridge = QtBridge()
        self.qt_bridge.update_it.connect(self.update_plot)

        # Register the QtBridge object with the WebChannel
        self.web_channel.registerObject("qtBridge", self.qt_bridge)

        # Create PyQt layout and add webview widget
        self.widget_layout = QVBoxLayout()
        self.widget_layout.addWidget(self.webview)  # Add the webview to the widget layout


    def set_fig_to_webview(self):
        # Generate a html_content file for a fig, containing various event listeners, and set it to the webview
        fig_json = self.fig.to_json()
        
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>My Plotly Plot</title>
            <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
        </head>
        <body>
            <div id="plotly-container"></div>
            <script>
                
                document.addEventListener("DOMContentLoaded", function() {{
                    var plotlyDiv = document.getElementById('plotly-container');

                    // Parse the Plotly data from the Python variable
                    var plotlyData = {fig_json};

                    // Create the Plotly plot using the extracted data
                    Plotly.newPlot(plotlyDiv, plotlyData);

                    // Delay execution by 500 milliseconds to ensure qtBridge is defined
                    setTimeout(function() {{
                        var x = 0;
                        var y = 0;
                        var message = {{ x: x, y: y }};
                        try {{
                            qtBridge.onPlotClick(JSON.stringify(message))
                        }} catch (error) {{
                            console.error('Error calling qtBridge.onPlotClick():', error);
                        }}
                    }}, 500);

                }});
            </script>
        </body>
        </html>
        """

        self.webview.setHtml(html_content)


    def update_plot(self, eeg_data):
        ### @STACKOVERFLOW: NOT IMPORTANT, JUST ADDS A TRACE TO self.fig ###


# Create a custom QObject to handle communication between JavaScript and Python
class QtBridge(QObject):
    update_it = pyqtSignal()

    @pyqtSlot()
    def onPlotClick(self):
        try:
            print("User clicked in plot!")
            # Your existing code for onPlotClick here
        except Exception as e:
            print("Error in onPlotClick:", str(e))



if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = QMainWindow()
    widget = PlotWidget("Superpose")
    window.setCentralWidget(widget.webview)
    window.show()
    sys.exit(app.exec_())
2
  • You are missing parts in the HTML that are also explained in the very first link you posted: you need to include the qwebchannel.js script and you must also declare a QWebChannel class within the <script>. Right now that qtBridge object in the HTML is an undeclared variable. Note that adding the webview to a layout and then setting it as a central widget doesn't make any sense. Commented Oct 6, 2023 at 0:13
  • Ah ok, thanks a lot! I'll try it out but at the moment I switched to PyQtGraph instead of Plotly. It's more work to achieve the same results, but at least I don't have to deal with catching js events :p. And yeah I know that adding the webview to a layout and then placing it as a central widget doesn't make much sense, it's just to test the code during development. Anyways, I'll post the result here if I ever get back to that script. Commented Oct 6, 2023 at 12:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.