Issue
I'm trying to display some Plot.ly or Plot.ly Dash plots ( I haven't settled on using one or the other, so I'm experimenting with both right now) in a PyQt5 GUI using QWebEngineView. This doesn't work for any plots larger than 2MB due to some Chromium-level hardcoded restriction.
I found one similar question that is pretty much identical in terms of our needs. It looks like the OP actually found an answer, but unfortunately for me, they didn't post an example of working code or explain what they did to make it work. I do not understand enough of the underlying theory to piece together an answer with the resources linked in this other question, and my Stack reputation isn't high enough to comment and ask the OP what exactly worked.
Here is a minimum reproducible example that displays a plot embedded in the GUI. It's a modification of an answer to a question about embedding Plotly plots in PyQt5 GUIs here:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
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 += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
fig_view.setHtml(raw_html)
# fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
# t = np.arange(0, 200000, 1)
# y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
Here is a modified version that demonstrates how a large data set cannot be displayed the same way:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
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 += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
fig_view.setHtml(raw_html)
# fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
# fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
Lastly, here is something I tried to get the large plot to display with QUrl pointing to a local html plot on the disk:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
def show_qt(fig):
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 += po.plot(fig, include_plotlyjs=False, output_type='div')
raw_html += '</body></html>'
fig_view = QWebEngineView()
# setHtml has a 2MB size limit, need to switch to setUrl on tmp file
# for large figures.
# fig_view.setHtml(raw_html)
fig_view.setUrl(QUrl('temp-plot.html'))
fig_view.show()
fig_view.raise_()
return fig_view
if __name__ == '__main__':
app = QApplication(sys.argv)
# Working small plot:
# fig = go.Figure(data=[{'type': 'scattergl', 'y': [2, 1, 3, 1]}])
# Not working large plot:
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
# po.plot(fig)
fig_view = show_qt(fig)
sys.exit(app.exec_())
The plot was generated with:
import numpy as np
import plotly.offline as po
import plotly.graph_objs as go
t = np.arange(0, 200000, 1)
y = np.sin(t/20000)
fig = go.Figure(data=[{'type': 'scattergl', 'y': y}])
po.plot(fig)
Solution
As this answer indicates a possible solution is to use a QWebEngineUrlSchemeHandler
, in the next section I have created a class that allows you to register functions that are invoked through custom urls:
qtplotly.py
from PyQt5 import QtCore, QtWebEngineCore, QtWebEngineWidgets
import plotly.offline as po
import plotly.graph_objs as go
class PlotlySchemeHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler):
def __init__(self, app):
super().__init__(app)
self.m_app = app
def requestStarted(self, request):
url = request.requestUrl()
name = url.host()
if self.m_app.verify_name(name):
fig = self.m_app.fig_by_name(name)
if isinstance(fig, go.Figure):
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 += po.plot(fig, include_plotlyjs=False, output_type="div")
raw_html += "</body></html>"
buf = QtCore.QBuffer(parent=self)
request.destroyed.connect(buf.deleteLater)
buf.open(QtCore.QIODevice.WriteOnly)
buf.write(raw_html.encode())
buf.seek(0)
buf.close()
request.reply(b"text/html", buf)
return
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.UrlNotFound)
class PlotlyApplication(QtCore.QObject):
scheme = b"plotly"
def __init__(self, parent=None):
super().__init__(parent)
scheme = QtWebEngineCore.QWebEngineUrlScheme(PlotlyApplication.scheme)
QtWebEngineCore.QWebEngineUrlScheme.registerScheme(scheme)
self.m_functions = dict()
def init_handler(self, profile=None):
if profile is None:
profile = QtWebEngineWidgets.QWebEngineProfile.defaultProfile()
handler = profile.urlSchemeHandler(PlotlyApplication.scheme)
if handler is not None:
profile.removeUrlSchemeHandler(handler)
self.m_handler = PlotlySchemeHandler(self)
profile.installUrlSchemeHandler(PlotlyApplication.scheme, self.m_handler)
def verify_name(self, name):
return name in self.m_functions
def fig_by_name(self, name):
return self.m_functions.get(name, lambda: None)()
def register(self, name):
def decorator(f):
self.m_functions[name] = f
return f
return decorator
def create_url(self, name):
url = QtCore.QUrl()
url.setScheme(PlotlyApplication.scheme.decode())
url.setHost(name)
return url
main.py
import numpy as np
import plotly.graph_objs as go
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets
from qtplotly import PlotlyApplication
# PlotlyApplication must be created before the creation
# of QGuiApplication or QApplication
plotly_app = PlotlyApplication()
@plotly_app.register("scatter")
def scatter():
t = np.arange(0, 200000, 1)
y = np.sin(t / 20000)
fig = go.Figure(data=[{"type": "scattergl", "y": y}])
return fig
@plotly_app.register("scatter2")
def scatter2():
N = 100000
r = np.random.uniform(0, 1, N)
theta = np.random.uniform(0, 2 * np.pi, N)
fig = go.Figure(
data=[
{
"type": "scattergl",
"x": r * np.cos(theta),
"y": r * np.sin(theta),
"marker": dict(color=np.random.randn(N), colorscale="Viridis"),
}
]
)
return fig
@plotly_app.register("scatter3")
def scatter3():
x0 = np.random.normal(2, 0.45, 30000)
y0 = np.random.normal(2, 0.45, 30000)
x1 = np.random.normal(6, 0.4, 20000)
y1 = np.random.normal(6, 0.4, 20000)
x2 = np.random.normal(4, 0.3, 20000)
y2 = np.random.normal(4, 0.3, 20000)
traces = []
for x, y in ((x0, y0), (x1, y1), (x2, y2)):
trace = go.Scatter(x=x, y=y, mode="markers")
traces.append(trace)
fig = go.Figure(data=traces)
return fig
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.m_view = QtWebEngineWidgets.QWebEngineView()
combobox = QtWidgets.QComboBox()
combobox.currentIndexChanged[str].connect(self.onCurrentIndexChanged)
combobox.addItems(["scatter", "scatter2", "scatter3"])
vlay = QtWidgets.QVBoxLayout(self)
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(QtWidgets.QLabel("Select:"))
hlay.addWidget(combobox)
vlay.addLayout(hlay)
vlay.addWidget(self.m_view)
self.resize(640, 480)
@QtCore.pyqtSlot(str)
def onCurrentIndexChanged(self, name):
self.m_view.load(plotly_app.create_url(name))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
# Init_handler must be invoked before after the creation
# of QGuiApplication or QApplication
plotly_app.init_handler()
w = Widget()
w.show()
sys.exit(app.exec_())
Structure:
├── main.py
└── qtplotly.py
Output:
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.