如何在 PyQt 小部件中渲染 Altair/Vega

How to render Altair / Vega in a PyQt widget

是否可以将 Altair 或 Vega(-Lite) 渲染到 PyQt 小部件,类似于支持多个后端的 Matplotlib?我知道我可以使用 Qt WebView 小部件来呈现带有 Vega 嵌入的网页,但我想防止必须提供此服务的开销,即使是在本地也是如此。

目前 Altair 唯一的渲染后端是 Vega-Embed,因此在 PyQt 小部件中渲染它需要 运行 一些 Javascript 引擎。我怀疑 Qt WebView 可能是最好的选择。

如果您不介意失去图表的交互性,您还可以使用 altair_saver 作为后端来存储图表的静态 PNG、SVG 或 PDF 并在 QtWidget 中显示它。

使用 Altair 可视化绘图的最佳选择是使用 QWebEngineView,因为 altair 会根据您设置的指令创建 javascript 代码。恕我直言,最好的解决方案是获取图表的 html 并将其设置在 QWebEngineView 中。在下面的示例中,除了启用将图像保存为 svg 或 png 等特性外,我还展示了如何执行上述操作

from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets

from io import StringIO


class WebEngineView(QtWebEngineWidgets.QWebEngineView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.page().profile().downloadRequested.connect(self.onDownloadRequested)
        self.windows = []

    @QtCore.pyqtSlot(QtWebEngineWidgets.QWebEngineDownloadItem)
    def onDownloadRequested(self, download):
        if (
            download.state()
            == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadRequested
        ):
            path, _ = QtWidgets.QFileDialog.getSaveFileName(
                self, self.tr("Save as"), download.path()
            )
            if path:
                download.setPath(path)
                download.accept()

    def createWindow(self, type_):
        if type_ == QtWebEngineWidgets.QWebEnginePage.WebBrowserTab:
            window = QtWidgets.QMainWindow(self)
            view = QtWebEngineWidgets.QWebEngineView(window)
            window.resize(640, 480)
            window.setCentralWidget(view)
            window.show()
            return view

    def updateChart(self, chart, **kwargs):
        output = StringIO()
        chart.save(output, "html", **kwargs)
        self.setHtml(output.getvalue())


if __name__ == "__main__":
    import sys

    import altair as alt
    from vega_datasets import data

    app = QtWidgets.QApplication(sys.argv)
    w = QtWidgets.QMainWindow()

    cars = data.cars()

    chart = (
        alt.Chart(cars)
        .mark_bar()
        .encode(x=alt.X("Miles_per_Gallon", bin=True), y="count()",)
        .properties(title="A bar chart")
        .configure_title(anchor="start")
    )

    view = WebEngineView()
    view.updateChart(chart)
    w.setCentralWidget(view)
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())