Matplotlib with QtQuick (QML)

How to display Matplotlib plots inside a QML application using PySide6
Heads up! You've already completed this tutorial.

How can I display a simple Matplotlib plot inside a QtQuick (QML) application? Is it possible to use Matplotlib with QML at all?

If you've been working with QML for your application's UI, you might have wondered how to get a Matplotlib chart in there. Matplotlib's built-in Qt support is designed around the Qt Widgets API, which is a completely separate UI system from QML. You can't simply drop a Matplotlib widget into a QML layout the way you would in a QMainWindow.

That said, there are ways to make this work. In this tutorial we'll look at two practical approaches:

  1. Rendering Matplotlib plots as images and displaying them in a QML Image element.
  2. Using the matplotlib-backend-qtquick package, which provides a proper QML-native backend for Matplotlib.

Both approaches work with PySide6.

Why Matplotlib Doesn't Work Directly in QML

Matplotlib's Qt backend renders plots onto a QWidget-based canvas (FigureCanvasQTAgg). QML uses its own scene graph for rendering, and there is no supported way to embed a QWidget inside a QML view in modern versions of Qt. Earlier versions of QtQuick had experimental widget embedding, but that has been removed.

So we need a different strategy.

Approach 1: Render to Image

The simplest approach is to have Matplotlib render the plot to an image file (or an in-memory buffer), and then display that image in QML using a standard Image element. This is straightforward and doesn't require any additional packages.

Here's the idea:

  1. Create your Matplotlib figure in Python.
  2. Save it to a temporary file or a bytes buffer.
  3. Use a QML Image element to load and display it.
  4. Optionally, use a custom QQuickImageProvider so that QML can request the image directly from Python without writing to disk.

Using a QQuickImageProvider

A QQuickImageProvider lets you serve images to QML from Python code. QML requests an image by URL (e.g., "image://matplotlib/myplot"), and your provider generates and returns it.

Here's a complete working example:

python
import sys
from io import BytesIO

import matplotlib
matplotlib.use("Agg")  # Use non-interactive backend
import matplotlib.pyplot as plt

from PySide6.QtCore import QSize
from PySide6.QtGui import QGuiApplication, QImage
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuick import QQuickImageProvider


class MatplotlibImageProvider(QQuickImageProvider):
    def __init__(self):
        super().__init__(QQuickImageProvider.ImageType.Image)

    def requestImage(self, id, size, requestedSize):
        """Generate a Matplotlib plot and return it as a QImage."""
        # Create a figure
        fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
        ax.plot([0, 1, 2, 3, 4], [0, 1, 4, 9, 16], "b-o")
        ax.set_title("Simple Matplotlib Plot")
        ax.set_xlabel("X axis")
        ax.set_ylabel("Y axis")
        fig.tight_layout()

        # Render to a bytes buffer
        buf = BytesIO()
        fig.savefig(buf, format="png")
        plt.close(fig)
        buf.seek(0)

        # Load into a QImage
        image = QImage()
        image.loadFromData(buf.getvalue())

        return image, image.size()


def main():
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    # Register the image provider
    engine.addImageProvider("matplotlib", MatplotlibImageProvider())

    # Load QML from a string for simplicity
    qml_content = """
    import QtQuick
    import QtQuick.Window
    import QtQuick.Controls

    Window {
        visible: true
        width: 640
        height: 520
        title: "Matplotlib in QML"

        Column {
            anchors.centerIn: parent
            spacing: 10

            Image {
                id: plotImage
                source: "image://matplotlib/plot"
                width: 600
                height: 400
                cache: false
            }

            Button {
                text: "Refresh Plot"
                anchors.horizontalCenter: parent.horizontalCenter
                onClicked: {
                    // Force reload by changing the source briefly
                    plotImage.source = ""
                    plotImage.source = "image://matplotlib/plot?" + Date.now()
                }
            }
        }
    }
    """

    # Write QML to a temporary file
    import tempfile
    import os

    qml_file = os.path.join(tempfile.gettempdir(), "matplotlib_qml.qml")
    with open(qml_file, "w") as f:
        f.write(qml_content)

    engine.load(qml_file)

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

When you run this, a QML window appears with a Matplotlib plot rendered as an image. The "Refresh Plot" button forces QML to re-request the image from the provider.

This approach is good for static or infrequently updated plots. Each time QML loads the image, your Python provider generates a fresh plot. You could pass parameters through the image URL id string to control what gets plotted.

Making It Dynamic

To update the plot with new data, you can expose a Python object to QML that holds the data and triggers image refreshes. Here's an expanded version with dynamic data:

python
import sys
import random
from io import BytesIO

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

from PySide6.QtCore import QObject, Signal, Slot, Property
from PySide6.QtGui import QGuiApplication, QImage
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuick import QQuickImageProvider


class PlotController(QObject):
    """Controls the plot data and triggers updates."""

    dataChanged = Signal()

    def __init__(self):
        super().__init__()
        self._x_data = list(range(10))
        self._y_data = [random.randint(0, 20) for _ in range(10)]

    @Slot()
    def randomize(self):
        """Generate new random data."""
        self._y_data = [random.randint(0, 20) for _ in range(10)]
        self.dataChanged.emit()

    def create_figure(self):
        """Create and return a Matplotlib figure with current data."""
        fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
        ax.bar(self._x_data, self._y_data, color="steelblue")
        ax.set_title("Dynamic Bar Chart")
        ax.set_xlabel("Category")
        ax.set_ylabel("Value")
        ax.set_ylim(0, 25)
        fig.tight_layout()
        return fig


class MatplotlibImageProvider(QQuickImageProvider):
    def __init__(self, controller):
        super().__init__(QQuickImageProvider.ImageType.Image)
        self.controller = controller

    def requestImage(self, id, size, requestedSize):
        fig = self.controller.create_figure()

        buf = BytesIO()
        fig.savefig(buf, format="png")
        plt.close(fig)
        buf.seek(0)

        image = QImage()
        image.loadFromData(buf.getvalue())
        return image, image.size()


def main():
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    controller = PlotController()

    engine.addImageProvider("matplotlib", MatplotlibImageProvider(controller))
    engine.rootContext().setContextProperty("plotController", controller)

    qml_content = """
    import QtQuick
    import QtQuick.Window
    import QtQuick.Controls

    Window {
        visible: true
        width: 640
        height: 520
        title: "Dynamic Matplotlib in QML"

        Column {
            anchors.centerIn: parent
            spacing: 10

            Image {
                id: plotImage
                source: "image://matplotlib/plot"
                width: 600
                height: 400
                cache: false
            }

            Button {
                text: "Randomize Data"
                anchors.horizontalCenter: parent.horizontalCenter
                onClicked: {
                    plotController.randomize()
                }
            }
        }

        Connections {
            target: plotController
            function onDataChanged() {
                plotImage.source = ""
                plotImage.source = "image://matplotlib/plot?" + Date.now()
            }
        }
    }
    """

    import tempfile
    import os

    qml_file = os.path.join(tempfile.gettempdir(), "matplotlib_dynamic_qml.qml")
    with open(qml_file, "w") as f:
        f.write(qml_content)

    engine.load(qml_file)

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Each time you click "Randomize Data", the controller generates new random values, emits dataChanged, and the QML Connections block forces the image to reload. The plot updates with fresh data.

Approach 2: Using matplotlib-backend-qtquick

There is a third-party package called matplotlib-backend-qtquick that provides a proper QML backend for Matplotlib. This registers a custom QML type that acts as a Matplotlib canvas, giving you a more integrated experience with features like mouse interaction.

Install it with:

bash
pip install matplotlib-backend-qtquick

This package works by creating a FigureCanvas QML item that you can place in your QML layout like any other element. The Python side creates the figure and connects it to the QML canvas.

Be aware that this package may require some experimentation to get working with your specific versions of PySide6 and Matplotlib. Check the package documentation for compatibility notes.

Which Approach Should You Use?

The image provider approach (Approach 1) is the most reliable and doesn't require any extra dependencies beyond Matplotlib itself. It works well for:

  • Static plots that don't need mouse interaction
  • Dashboards that update periodically
  • Situations where you want full control over rendering

The matplotlib-backend-qtquick package is worth trying if you need interactive features like zooming and panning directly on the plot within your QML interface.

An Alternative: QtGraphs

If your plotting needs are simple (line charts, bar charts, scatter plots), consider using Qt's own charting module instead of Matplotlib. PySide6 provides QtGraphs (previously QtCharts) which works natively in QML with no bridging required. This gives you smooth integration with QML animations, theming, and the rest of your UI.

Matplotlib really shines when you need its vast library of plot types, customization options, or scientific visualization features. For straightforward charts in a QML app, the native Qt solution is often a simpler path.

Complete Working Example

Here's the full dynamic example from Approach 1, ready to copy and run:

python
import sys
import random
from io import BytesIO

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

from PySide6.QtCore import QObject, Signal, Slot
from PySide6.QtGui import QGuiApplication, QImage
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuick import QQuickImageProvider


class PlotController(QObject):
    """Controls the plot data and triggers updates."""

    dataChanged = Signal()

    def __init__(self):
        super().__init__()
        self._x_data = list(range(10))
        self._y_data = [random.randint(0, 20) for _ in range(10)]

    @Slot()
    def randomize(self):
        """Generate new random data."""
        self._y_data = [random.randint(0, 20) for _ in range(10)]
        self.dataChanged.emit()

    def create_figure(self):
        """Create and return a Matplotlib figure with current data."""
        fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
        ax.bar(self._x_data, self._y_data, color="steelblue")
        ax.set_title("Dynamic Bar Chart")
        ax.set_xlabel("Category")
        ax.set_ylabel("Value")
        ax.set_ylim(0, 25)
        fig.tight_layout()
        return fig


class MatplotlibImageProvider(QQuickImageProvider):
    """Serves Matplotlib plots as images to QML."""

    def __init__(self, controller):
        super().__init__(QQuickImageProvider.ImageType.Image)
        self.controller = controller

    def requestImage(self, id, size, requestedSize):
        fig = self.controller.create_figure()

        buf = BytesIO()
        fig.savefig(buf, format="png")
        plt.close(fig)
        buf.seek(0)

        image = QImage()
        image.loadFromData(buf.getvalue())
        return image, image.size()


QML_CONTENT = """
import QtQuick
import QtQuick.Window
import QtQuick.Controls

Window {
    visible: true
    width: 640
    height: 520
    title: "Dynamic Matplotlib in QML"

    Column {
        anchors.centerIn: parent
        spacing: 10

        Image {
            id: plotImage
            source: "image://matplotlib/plot"
            width: 600
            height: 400
            cache: false
        }

        Button {
            text: "Randomize Data"
            anchors.horizontalCenter: parent.horizontalCenter
            onClicked: {
                plotController.randomize()
            }
        }
    }

    Connections {
        target: plotController
        function onDataChanged() {
            plotImage.source = ""
            plotImage.source = "image://matplotlib/plot?" + Date.now()
        }
    }
}
"""


def main():
    import tempfile
    import os

    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    controller = PlotController()
    engine.addImageProvider("matplotlib", MatplotlibImageProvider(controller))
    engine.rootContext().setContextProperty("plotController", controller)

    # Write QML to a temporary file and load it
    qml_file = os.path.join(tempfile.gettempdir(), "matplotlib_qml_demo.qml")
    with open(qml_file, "w") as f:
        f.write(QML_CONTENT)

    engine.load(qml_file)

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Run this script and you'll see a window with a bar chart. Click "Randomize Data" to generate a new set of random values and watch the chart update. The Matplotlib figure is rendered on the Python side using the Agg backend, converted to a QImage, and served to QML through the image provider — all without needing any Qt Widget embedding.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

PyQt/PySide 1:1 Coaching with Martin Fitzpatrick

Save yourself time and frustration. Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.

Book Now 60 mins ($195)

Martin Fitzpatrick

Matplotlib with QtQuick (QML) was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.