Plotting Binary File Data with Matplotlib in PyQt6

Load binary files and plot them interactively using Matplotlib and PyQt6
Heads up! You've already completed this tutorial.

I got the Matplotlib plotting example working in my PyQt app, and now I'd like to plot data from a binary file. How do I read a binary file into an array for plotting — letting the user choose the file location, bytes per sample (1, 2, 3, or 4), and endianness — then plot the sample values on the Y axis and sample numbers on the X axis?

Reading binary data and plotting it is a common task in signal processing, embedded development, and data analysis. In this tutorial, we'll build a small PyQt6 application that lets the user select a binary file, configure how the bytes should be interpreted, and then plot the resulting waveform using Matplotlib.

Reading binary data into a NumPy array

Before we wire anything into a GUI, let's look at how binary file reading works in Python. A binary file is just a sequence of raw bytes. To interpret those bytes as numbers, you need to know two things:

  1. Bytes per sample — how many bytes make up a single data point (1, 2, 3, or 4).
  2. Endianness — whether the most significant byte (MSB) comes first (big-endian) or last (little-endian).

Python's struct module can unpack bytes into integers, but for large files NumPy is far more convenient. NumPy's numpy.frombuffer function reads raw bytes and returns an array of numbers, which is exactly what Matplotlib needs for plotting.

Here's a minimal example of reading a binary file into a NumPy array:

python
import numpy as np

def read_binary_file(filepath, bytes_per_sample, endianness="little"):
    with open(filepath, "rb") as f:
        raw = f.read()

    # Build a NumPy dtype string based on endianness and byte width
    endian_char = "<" if endianness == "little" else ">"
    type_map = {1: "u1", 2: "u2", 4: "u4"}

    if bytes_per_sample in type_map:
        dtype = np.dtype(f"{endian_char}{type_map[bytes_per_sample]}")
        data = np.frombuffer(raw, dtype=dtype)
    elif bytes_per_sample == 3:
        # NumPy doesn't have a native 3-byte integer type,
        # so we handle this manually.
        data = read_3byte_samples(raw, endianness)
    else:
        raise ValueError("bytes_per_sample must be 1, 2, 3, or 4")

    return data

NumPy dtype strings use < for little-endian and > for big-endian. The u means unsigned integer, and the digit is the number of bytes. So <u2 means "little-endian unsigned 2-byte integer."

Handling 3-byte samples

NumPy doesn't have a built-in 3-byte integer type, so we need to pad each 3-byte chunk to 4 bytes and then interpret the result. Here's a function that does that:

python
def read_3byte_samples(raw, endianness="little"):
    # Trim any trailing bytes that don't make a complete sample
    num_samples = len(raw) // 3
    trimmed = raw[:num_samples * 3]

    output = np.zeros(num_samples, dtype=np.uint32)
    for i in range(num_samples):
        three_bytes = trimmed[i * 3 : i * 3 + 3]
        if endianness == "little":
            padded = three_bytes + b'\x00'
        else:
            padded = b'\x00' + three_bytes
        output[i] = int.from_bytes(padded, byteorder=endianness)

    return output

This loops through the raw bytes three at a time, pads each chunk to four bytes, and converts it to an integer. For large files you could optimize this further, but this approach is clear and works well for most use cases.

Embedding Matplotlib in PyQt6

To plot inside a PyQt6 window, we use FigureCanvasQTAgg from the matplotlib.backends module. This gives us a Qt widget that contains a Matplotlib figure. If you haven't done this before, see our full guide to Plotting with Matplotlib in PyQt6 for a detailed introduction. The setup is straightforward — you create a Figure, wrap it in a canvas widget, and add it to your layout like any other widget.

Over 15,000 developers have bought Create GUI Applications with Python & Qt!
Create GUI Applications with Python & Qt6
Get the book

Downloadable ebook (PDF, ePub) & Complete Source code

Let's start building the application step by step.

Setting up the canvas

python
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=8, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)

This small class creates a Matplotlib figure with a single set of axes. We'll use self.axes to draw our plots.

Building the main window

The main window needs:

  • A toolbar for Matplotlib's built-in zoom/pan controls.
  • A canvas where the plot is drawn.
  • Controls to let the user choose a file, set bytes per sample, and pick endianness.
  • A Plot button to trigger the plotting.

If you're new to building PyQt6 applications, you may want to start with Creating your first PyQt6 window before continuing.

Here's the complete application:

python
import sys
import numpy as np

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QComboBox, QLabel, QFileDialog, QLineEdit,
)

from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


def read_3byte_samples(raw, endianness="little"):
    """Read raw bytes as 3-byte unsigned integers."""
    num_samples = len(raw) // 3
    trimmed = raw[:num_samples * 3]

    output = np.zeros(num_samples, dtype=np.uint32)
    for i in range(num_samples):
        three_bytes = trimmed[i * 3 : i * 3 + 3]
        if endianness == "little":
            padded = three_bytes + b"\x00"
        else:
            padded = b"\x00" + three_bytes
        output[i] = int.from_bytes(padded, byteorder=endianness)

    return output


def read_binary_file(filepath, bytes_per_sample, endianness="little"):
    """Read a binary file and return data as a NumPy array."""
    with open(filepath, "rb") as f:
        raw = f.read()

    endian_char = "<" if endianness == "little" else ">"
    type_map = {1: "u1", 2: "u2", 4: "u4"}

    if bytes_per_sample in type_map:
        dtype = np.dtype(f"{endian_char}{type_map[bytes_per_sample]}")
        # Trim any incomplete trailing bytes
        usable = len(raw) - (len(raw) % bytes_per_sample)
        data = np.frombuffer(raw[:usable], dtype=dtype)
    elif bytes_per_sample == 3:
        data = read_3byte_samples(raw, endianness)
    else:
        raise ValueError("bytes_per_sample must be 1, 2, 3, or 4")

    return data


class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=8, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Binary File Plotter")

        # Matplotlib canvas and toolbar
        self.canvas = MplCanvas(self, width=8, height=4, dpi=100)
        toolbar = NavigationToolbar(self.canvas, self)

        # File selection
        self.file_path_edit = QLineEdit()
        self.file_path_edit.setPlaceholderText("No file selected")
        self.file_path_edit.setReadOnly(True)

        browse_button = QPushButton("Browse…")
        browse_button.clicked.connect(self.browse_file)

        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("File:"))
        file_layout.addWidget(self.file_path_edit)
        file_layout.addWidget(browse_button)

        # Bytes per sample
        self.bytes_combo = QComboBox()
        self.bytes_combo.addItems(["1", "2", "3", "4"])
        self.bytes_combo.setCurrentText("2")

        # Endianness
        self.endian_combo = QComboBox()
        self.endian_combo.addItems(["little", "big"])

        options_layout = QHBoxLayout()
        options_layout.addWidget(QLabel("Bytes per sample:"))
        options_layout.addWidget(self.bytes_combo)
        options_layout.addWidget(QLabel("Endianness:"))
        options_layout.addWidget(self.endian_combo)
        options_layout.addStretch()

        # Plot button
        plot_button = QPushButton("Plot")
        plot_button.clicked.connect(self.plot_data)
        options_layout.addWidget(plot_button)

        # Main layout
        layout = QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(self.canvas)
        layout.addLayout(file_layout)
        layout.addLayout(options_layout)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.filepath = None

    def browse_file(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "Open Binary File", "", "Binary Files (*.bin);;All Files (*)"
        )
        if path:
            self.filepath = path
            self.file_path_edit.setText(path)

    def plot_data(self):
        if not self.filepath:
            return

        bytes_per_sample = int(self.bytes_combo.currentText())
        endianness = self.endian_combo.currentText()

        data = read_binary_file(self.filepath, bytes_per_sample, endianness)

        # X axis is simply the sample index
        x = np.arange(len(data))

        self.canvas.axes.clear()
        self.canvas.axes.plot(x, data)
        self.canvas.axes.set_xlabel("Sample Number")
        self.canvas.axes.set_ylabel("Value")
        self.canvas.axes.set_title(f"{len(data)} samples from {self.filepath}")
        self.canvas.draw()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Let's walk through what this does.

The file selection row

The QFileDialog.getOpenFileName method opens a native file picker. The filter "Binary Files (*.bin);;All Files (*)" shows .bin files by default but lets the user switch to all files. Once a file is selected, its path is stored in self.filepath and displayed in the read-only QLineEdit. For more on using dialogs in your PyQt6 applications, see our PyQt6 Dialogs tutorial.

The options row

Two QComboBox widgets let the user choose the bytes per sample (1, 2, 3, or 4) and the endianness (little or big). Little-endian is the default because it's the most common byte order on modern PCs and many embedded systems.

The Plot button

When clicked, plot_data reads the selected file using our read_binary_file function, generates an array of sample indices for the X axis, clears the previous plot, and draws the new data. Calling self.canvas.draw() at the end tells Matplotlib to refresh the display.

Creating a test file

If you don't have a binary file handy, you can generate one with this short script:

python
import numpy as np

# Generate a sine wave: 1000 samples, 16-bit unsigned, little-endian
num_samples = 1000
t = np.linspace(0, 4 * np.pi, num_samples)
values = ((np.sin(t) + 1) / 2 * 65535).astype(np.uint16)

values.tofile("sample.bin")
print(f"Wrote {num_samples} samples to sample.bin")

Run this first, then open the generated sample.bin in the plotter with 2 bytes per sample and little-endian selected. You should see a smooth sine wave.

Signed vs. unsigned data

The example above reads all values as unsigned integers. If your binary data contains signed values (for example, audio samples centered around zero), you can change the dtype from unsigned to signed by replacing u with i in the type map:

python
type_map = {1: "i1", 2: "i2", 4: "i4"}

You could add a third combo box to the UI to let the user toggle between signed and unsigned interpretation. The wiring would be the same — just pass the choice into read_binary_file and select the right dtype prefix.

Once you're ready to distribute your finished application, take a look at Packaging PyQt6 applications with PyInstaller on Windows for a step-by-step guide to creating standalone executables.

Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

Get the book

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

Plotting Binary File Data with Matplotlib in PyQt6 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.