Kretep
Kretep

Reputation: 514

How to render Altair / Vega in a PyQt widget

Is it possible to have Altair or Vega(-Lite) render to a PyQt widget, similar to Matplotlib supporting multiple backends? I know I can use a Qt WebView widget to render a web page with Vega-embed, but I want to prevent the overhead of having to serve this, even if locally.

Upvotes: 3

Views: 1099

Answers (3)

Martin Ueding
Martin Ueding

Reputation: 8699

Building on the great answer by eyllanesc, this is the version adapted for PyQt6:

from io import StringIO
from typing import Optional

from PyQt6 import QtCore
from PyQt6 import QtWidgets
from PyQt6.QtWebEngineCore import QWebEngineDownloadRequest
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget


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

    @QtCore.pyqtSlot(QWebEngineDownloadRequest)
    def onDownloadRequested(self, download: QWebEngineDownloadRequest) -> None:
        if (
            download.state()
            == QWebEngineDownloadRequest.DownloadState.DownloadRequested
        ):
            path, _ = QtWidgets.QFileDialog.getSaveFileName(
                self, self.tr("Save as"), download.downloadFileName()
            )
            if path:
                download.setDownloadFileName(path)
                download.accept()

    def createWindow(
        self, web_window_type: QWebEnginePage.WebWindowType
    ) -> Optional[QWebEngineView]:
        if web_window_type == QWebEnginePage.WebWindowType.WebBrowserTab:
            window = QtWidgets.QMainWindow(self)
            view = QWebEngineView(window)
            window.resize(640, 480)
            window.setCentralWidget(view)
            window.show()
            return view

    def set_chart(self, chart, **kwargs) -> None:
        output = StringIO()
        chart.save(output, "html", **kwargs)
        self.setHtml(output.getvalue())

Upvotes: 0

eyllanesc
eyllanesc

Reputation: 243887

The best option to visualize a plot with Altair is to use QWebEngineView since altair what is to create javascript code based on the instructions you set. IMHO the best solution is to obtain the html of the chart and set it in a QWebEngineView. In the following example I show how to do the above, in addition to enabling the characteristics of saving the image as svg or png, etc.

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_())

Upvotes: 2

jakevdp
jakevdp

Reputation: 86310

Currently the only rendering backend for Altair is Vega-Embed, so rendering it in a PyQt widget will require running some Javascript engine. I suspect that Qt WebView is probably the best option.

If you're OK losing interactivity of charts, you could also use altair_saver as a backend to store a static PNG, SVG, or PDF of the chart and show it within a QtWidget.

Upvotes: 0

Related Questions