Repository: pyqtgraph/pyqtgraph

Issue:
Program loops indefinitely when I start an application with plot set on autosampling.
### Short description
When I start a pyqtgraph application with a plot with auto Downsampling, it loops over different values of the downsampling parameter and then crashes. I also have remarks about the optimization of the Downsampling but will put them in a discussion.

### Code to reproduce
<!-- Please provide a minimal working example that reproduces the issue in the code block below.
     Ideally, this should be a full example someone else could run without additional setup. -->

```
import pyqtgraph as pg
from PyQt6 import QtCore, QtWidgets

import numpy as np
pp = pg.mkQApp()
mw = QtWidgets.QMainWindow()
mw.setWindowTitle('pyqtgraph example: PlotWidget')
mw.resize(800,800)
cw = QtWidgets.QWidget()
mw.setCentralWidget(cw)
l = QtWidgets.QVBoxLayout()
cw.setLayout(l)

pw = pg.PlotWidget()  ## giving the plots names allows us to link their axes together
pw.setClipToView( True)
pw.enableAutoRange(False)
pw.setDownsampling( ds=None, auto=1, mode="subsample")
l.addWidget(pw)
data = np.arange(10000000)+np.random.normal(0,10,10000000)
pw.plot(data, title="Simplest possible plotting example")
mw.show()

if __name__ == '__main__':
    pg.exec()
```

### Expected behavior
<!-- What should happen? -->

### Real behavior
The program loops. When I print the "ds" parameter, it goes down until the program crashes.


### Tested environment(s)

 * PyQtGraph version: 0.13.3 <!-- output of pyqtgraph.__version__ -->
 * Qt Python binding: PyQt6<!-- output of pyqtgraph.Qt.VERSION_INFO -->
 * Python version: Python 3.10
 * NumPy version: 1.21.5<!-- output of numpy.__version__ -->
 * Operating system: Ubuntu
 * Installation method: pip <!-- e.g. pip, conda, system packages, ... -->

### Additional context



Discussion:
I can't reproduce the crash. It seems to run fine for me. Windows 11 / Python 3.10 / PyQt6 6.5.0 / numpy 1.24.3

Simplified version:
```python
import pyqtgraph as pg
import numpy as np

pg.mkQApp()
pw = pg.PlotWidget()
pw.setClipToView(True)
pw.enableAutoRange(False)
pw.setDownsampling(ds=None, auto=1, mode="subsample")
data = np.arange(10000000)+np.random.normal(0,10,10000000)
pw.plot(data, title="Simplest possible plotting example")
pw.show()
pg.exec()
```
Maybe try to resize the window. It doesn't happen for all sizes. I tested your code and it does the same, whether in 0.13.3 or 0.13.4-dev0 (still ubuntu). If I add ``` print("ds = ", ds)``` just  before ```if self.opts['clipToView']:``` in ```_getDisplayDataset```, I get  the following infinite loop : 
```ds =  1
ds =  1
ds =  1
ds =  1
ds =  3595
ds =  3595
ds =  2886
ds =  2936
ds =  2935
ds =  2936
ds =  2935
ds =  2936
ds =  2935
ds =  2936
...
```

I found a quick fix which is to only allow  discrete logarithmically spaced values for ds as explained here : https://github.com/pyqtgraph/pyqtgraph/discussions/2710. That avoids that loop between the automatic computation of ```ds``` and the range update.

Btw the fact that ```ds = 1``` at first is also a problem. If you have a large dataset you would like it to be auto downsampled from the beginning, if not the first  plot update will crash the program. The problem comes from the fact that the ```PlotDataItem``` gets updated before  the autodownsample or downsample properties get passed to it by the ```PlotItem```. Also gave a quick fix there https://github.com/pyqtgraph/pyqtgraph/discussions/2710 , in the section "PlotDataItem update problem bypass. I just pass the autodownsample argument to the ```PlotDataItem``` at the creation even if they are set for the ```PlotItem```. It must be possible to make that better.

Best




Okay, when running on Ubuntu 22.04 (WSL2), I got the infinite looping, alternating between 3658 and 3659. No window resizing necessary.
Would it help to change `ds = int(ds_float)` to `ds = round(ds_float)`?
I have created #2726 to hopefully fix this issue.

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py
--- a/pyqtgraph/graphicsItems/PlotDataItem.py
+++ b/pyqtgraph/graphicsItems/PlotDataItem.py
@@ -447,6 +447,7 @@ def setLogMode(self, xState, yState):
         self.opts['logMode'] = [xState, yState]
         self._datasetMapped  = None  # invalidate mapped data
         self._datasetDisplay = None  # invalidate display data
+        self._adsLastValue   = 1     # reset auto-downsample value
         self.updateItems(styleUpdate=False)
         self.informViewBoundsChanged()
 
@@ -461,6 +462,7 @@ def setDerivativeMode(self, state):
         self.opts['derivativeMode'] = state
         self._datasetMapped  = None  # invalidate mapped data
         self._datasetDisplay = None  # invalidate display data
+        self._adsLastValue   = 1     # reset auto-downsample value
         self.updateItems(styleUpdate=False)
         self.informViewBoundsChanged()
 
@@ -476,6 +478,7 @@ def setPhasemapMode(self, state):
         self.opts['phasemapMode'] = state
         self._datasetMapped  = None  # invalidate mapped data
         self._datasetDisplay = None  # invalidate display data
+        self._adsLastValue   = 1     # reset auto-downsample value
         self.updateItems(styleUpdate=False)
         self.informViewBoundsChanged()
 
@@ -622,6 +625,7 @@ def setDownsampling(self, ds=None, auto=None, method=None):
         if changed:
             self._datasetMapped  = None  # invalidata mapped data
             self._datasetDisplay = None  # invalidate display data
+            self._adsLastValue   = 1     # reset auto-downsample value
             self.updateItems(styleUpdate=False)
 
     def setClipToView(self, state):
@@ -822,6 +826,7 @@ def setData(self, *args, **kargs):
             self._dataset = PlotDataset( xData, yData )
         self._datasetMapped  = None  # invalidata mapped data , will be generated in getData() / _getDisplayDataset()
         self._datasetDisplay = None  # invalidate display data, will be generated in getData() / _getDisplayDataset()
+        self._adsLastValue   = 1     # reset auto-downsample value
 
         profiler('set data')
 
@@ -1004,6 +1009,12 @@ def _getDisplayDataset(self):
                             ds = int(ds_float)
                     ## downsampling is expensive; delay until after clipping.
 
+            # use the last computed value if our new value is not too different.
+            # this guards against an infinite cycle where the plot never stabilizes.
+            if math.isclose(ds, self._adsLastValue, rel_tol=0.01):
+                ds = self._adsLastValue
+            self._adsLastValue = ds
+
         if self.opts['clipToView']:
             if view is None or view.autoRangeEnabled()[0]:
                 pass # no ViewBox to clip to, or view will autoscale to data range.


Test patch: