Search code examples
pythonplotpyqtgraph

Using pre-downsampled data when plotting large time series in PyQtGraph


I need to plot a large time series in PyQtGraph (millions of points). Plotting it as is is practically impossible and when turning on the optimization options (downsampling using setDownsampling and clipping using setClipToView) it is still barely usable when zoomed out (only when zoomed in it becomes fast thanks to clipping).

I have an idea though. I could pre-downsample my data since they're static. Then, I could use the cached downsampled data when zoomed out and the raw data when zoomed in.

How can I achieve that?


Solution

  • The answer by @three_pineapples describes a really nice improvement over the default downsampling in PyQtGraph, but it still requires performing the downsampling on the fly, which in my case is problematic.

    Therefore, I decided to implement a different strategy, namely, pre-downsample the data and then select either the already downsampled data or the original data depending on the "zoom level".

    I combine that approach with the default auto-downsample strategy employed natively by PyQtGraph to yield further speed improvements (which could be further improved with @three_pineapples suggestions).

    This way, PyQtGraph always starts with data of much lower dimensionality, which makes zooming and panning instantaneous even with a really large amount of samples.

    My approach is summarized in this code, which monkey patches the getData method of PlotDataItem.

    # Downsample data
    downsampled_data = downsample(data, 100)
    
    # Replacement for the default getData function
    def getData(obj):
        # Calculate the visible range
        range = obj.viewRect()
        if range is not None:
            dx = float(data[-1, 0] - data[0, 0]) / (data.size[0] - 1)
            x0 = (range.left() - data[0, 0]) / dx
            x1 = (range.right() - data[0, 0]) / dx
        # Decide whether to use downsampled or original data
        if (x1 - x0) > 20000:
            obj.xData = downsampled_data[:, 0]
            obj.yData = downsampled_data[:, 1]
        else:
            obj.xData = data[:, 0]
            obj.yData = data[:, 1]
        # Run the original getData of PlotDataItem
        return PlotDataItem.getData(obj)
    
    # Replace the original getData with our getData
    plot_data_item.getData = types.MethodType(getData, plot_data_item)