0

I have a potly graph in a PyQt5 QWebEngine and I would like to be able to import the click points on python.

This is not the complete code but the important pieces. I can already intercept the coordinates of the points through javascript code and insert in an alert on click, but I would like to be able to use them in Python.


    self.browser = QtWebEngineWidgets.QWebEngineView(self)
    vbox.addWidget(self.browser)
    vbox.setSpacing(0)
    vbox.setContentsMargins(0, 0, 0, 0) 
    
    self.show_graph(input_dict)
    
def show_graph(self, input_dict):
    # Initialize figure with 3 3D subplots
    ...
    #details of graph
    ...
    fig = go.FigureWidget(fig.to_dict()) 
    self.browser.setHtml(fig.to_html(include_plotlyjs='cdn', post_script="document.getElementsByClassName('plotly-graph-div')[0].on('plotly_click', function(data){alert(data.points[0].x + '-' + data.points[0].y)});"))

So the javascript part of code works, but I would like the data available in python instead of the alert. How could I do?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241

1 Answers1

1

One possible solution is to use QWebChannel. The logic is to export a QObject that serves as a bridge.

import json

import plotly.graph_objects as go

from PyQt5.QtCore import pyqtSlot, QFile, QIODevice, QObject
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineScript, QWebEngineView
from PyQt5.QtWebChannel import QWebChannel


def get_webchannel_source():
    file = QFile(":/qtwebchannel/qwebchannel.js")
    if not file.open(QIODevice.ReadOnly):
        return ""
    content = file.readAll()
    file.close()
    return content.data().decode()


class Backend(QObject):
    @pyqtSlot(str, name="handleClicked")
    def handle_clicked(self, o):
        py_obj = json.loads(o)
        print(py_obj)


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

        self.backend = Backend()

        self.browser = QWebEngineView()

        script = QWebEngineScript()
        script.setName("create_connection")
        script.setSourceCode(get_webchannel_source())
        script.setInjectionPoint(QWebEngineScript.DocumentReady)
        script.setWorldId(QWebEngineScript.MainWorld)
        script.setRunsOnSubFrames(False)
        self.browser.page().profile().scripts().insert(script)

        channel = QWebChannel(self)
        channel.registerObject("backend", self.backend)
        self.browser.page().setWebChannel(channel)

        vbox = QVBoxLayout(self)
        vbox.addWidget(self.browser)
        vbox.setSpacing(0)
        vbox.setContentsMargins(0, 0, 0, 0)

        self.browser.loadFinished.connect(print)

        self.build_plot()

    def build_plot(self):
        trace = go.Heatmap(
            z=[[1, 20, 30, 50, 1], [20, 1, 60, 80, 30], [30, 60, 1, -10, 20]],
            x=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
            y=["Morning", "Afternoon", "Evening"],
        )
        data = [trace]
        layout = go.Layout(title="Activity Heatmap")
        fig = go.Figure(data=data, layout=layout)
        fig = go.FigureWidget(fig.to_dict())

        script = """
        window.backend = null;
        window.onload = function(){
            new QWebChannel(qt.webChannelTransport, function(channel) {
                window.backend = channel.objects.backend;
            });
        }
        var elements = document.getElementsByClassName('plotly-graph-div');
        if(elements.length > 0){
            var element = elements[0];
            element.on("plotly_click", function(data){
                var point = data.points[0];
                if(window.backend != null){
                    var json_str = JSON.stringify({x: point.x, y: point.y})
                    window.backend.handleClicked(json_str);
                }
            })
        }
        """
        html = fig.to_html(include_plotlyjs="cdn", post_script=script)
        self.browser.setHtml(html)


def main():
    app = QApplication([])

    w = Widget()
    w.resize(640, 480)
    w.show()

    app.exec_()


if __name__ == "__main__":
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I will try this solution, what are the other possible solutions? Could be possible to save the data in a json file via javascript and read it in python? – Davide Serafini Nov 02 '21 at 21:53
  • @DavideSerafini Your proposed solution has several drawbacks, for example: - Tracking when the file is written as well as being less elegant. QWebChannel implements communication via websockets (websockets code is not shown but they are there) making the sending of information simple. I do not understand why you require another type of solution. – eyllanesc Nov 02 '21 at 21:56
  • Your solution seems excellent to me and I am trying to implement it in my code to make it work, unfortunately the code above does not generate any results. Since you wrote that it is one of the possible solutions I was curious to understand what the other possibilities were – Davide Serafini Nov 02 '21 at 22:03
  • @DavideSerafini 1. I always say that it is one of the possible solutions in the sense that I do not have the capacity that it is the only possible solution, that perhaps others have more solutions. 2. First try my solution like this (don't try to apply it in your code directly), if it works then just try to apply it to your project but I am not responsible for the latter. – eyllanesc Nov 02 '21 at 22:07