If you want to interact with the elements of the WEB you could use QWebChannel. In the following example, each time the "plotly_relayout" event is triggered, a slot of a QObject will be called to emit a signal. In the same way you can update the data from Python, for example when you press the Python button it will update the camera using the animation.
import json
import math
import sys
from PyQt5.QtCore import (
pyqtSignal,
pyqtSlot,
QFile,
QIODevice,
QObject,
QVariant,
QVariantAnimation,
)
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEngineScript, QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
import plotly
import plotly.graph_objs as go
import numpy as np
class PlotlyHelper(QObject):
cameraChanged = pyqtSignal(dict)
updateCamera = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._div_id = ""
self._camera = dict()
@property
def div_id(self):
return self._div_id
@pyqtSlot(str)
def _on_change_camera(self, data):
camera = json.loads(data)
self._camera = camera
self.cameraChanged.emit(self._camera)
@pyqtSlot(str)
def _update_id(self, id_):
self._div_id = id_
@property
def camera(self):
return self._camera
@camera.setter
def camera(self, camera):
self.updateCamera.emit(json.dumps(camera))
JS = """
var plotly_helper = null;
var gd = null;
function create_connection(){
plotly_helper._update_id(gd.id)
gd.on('plotly_relayout', function(){
plotly_helper._on_change_camera(JSON.stringify(gd.layout.scene.camera));
})
plotly_helper.updateCamera.connect(function(message){
Plotly.relayout(gd, 'scene.camera', JSON.parse(message))
});
}
// https://stackoverflow.com/a/7559453/6622587
(function wait_element() {
var elements = document.getElementsByClassName("js-plotly-plot")
if( elements.length == 1 ) {
gd = elements[0];
if(plotly_helper !== null){
create_connection();
}
} else {
setTimeout( wait_element, 500 );
}
})();
(function wait_qt() {
if (typeof qt != 'undefined') {
new QWebChannel(qt.webChannelTransport, function (channel) {
plotly_helper = channel.objects.plotly_helper;
if(gd !== null){
create_connection();
}
});
} else {
setTimeout( wait_qt, 500 );
}
})();
"""
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 Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QPushButton(
"Start Animation", checkable=True, clicked=self.on_clicked
)
self.view = QWebEngineView()
lay = QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.view)
self.resize(640, 480)
self.create_plot()
def create_plot(self):
t = np.linspace(0, 10, 50)
x, y, z = np.cos(t), np.sin(t), t
fig = go.Figure(data=[go.Scatter3d(x=x, y=y, z=z, mode="markers")])
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 += plotly.offline.plot(fig, include_plotlyjs=False, output_type="div")
raw_html += "</body></html>"
profile = QWebEngineProfile.defaultProfile()
script = QWebEngineScript()
script.setName("create_connection")
script.setSourceCode(get_webchannel_source() + "\n" + JS)
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setWorldId(QWebEngineScript.MainWorld)
script.setRunsOnSubFrames(False)
profile.scripts().insert(script)
self.plotly_helper = PlotlyHelper()
self.plotly_helper.cameraChanged.connect(
lambda camera: print("[LOG] camera:", camera)
)
channel = QWebChannel(self)
channel.registerObject("plotly_helper", self.plotly_helper)
self.view.page().setWebChannel(channel)
self.view.setHtml(raw_html)
self.animation = QVariantAnimation(
startValue=0.0,
endValue=2 * math.pi,
valueChanged=self.on_value_changed,
duration=1000,
loopCount=-1,
)
@pyqtSlot(bool)
def on_clicked(self, checked):
if checked:
self.button.setText("Stop Animation")
self.animation.start()
else:
self.button.setText("Start Animation")
self.animation.stop()
@pyqtSlot(QVariant)
def on_value_changed(self, value):
camera = dict(
up=dict(x=0, y=math.cos(value), z=math.sin(value)),
center=dict(x=0, y=0, z=0),
eye=dict(x=1.25, y=1.25, z=1.25),
)
self.plotly_helper.camera = camera
if __name__ == "__main__":
sys.argv.append("--remote-debugging-port=8000")
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())