Repository: pyqtgraph/pyqtgraph

Issue:
pg.PColorMeshItem crashes when x, y array shapes are changed (setData(x, y, z...)).
I am creating a plot using the **PColorMeshItem** over time (i have a timer loop in pyside6), if i change the oversampling of my datas, then the x, y and z array shapes are changing and the python code generates an error when setting the data using the setData(x, y, z) function : 

Traceback (most recent call last):
  File "D:\3-Python_Programs\...\Radar_Olivier.py", line 203, in updatePlot
    self.img4.setData(x_vals, y_vals, np.flipud(np.abs(angle_arr.T)))
  File "D:\3-Python_Programs\...\venv\lib\site-packages\pyqtgraph\graphicsItems\PColorMeshItem.py", line 243, in setData
    if np.any(self.x != args[0]) or np.any(self.y != args[1]):
ValueError: operands could not be broadcast together with shapes (129,801) (65,401) 

I found the problem... 
In the "PColorMeshItem.py" file on the line **242** : 
replace:
      if np.any(self.x != args[0]) or np.any(self.y != args[1]): 
with : 
      **if np.any(self.x != args[0].shape[0]) or np.any(self.y != args[1].shape[1]):**

Then the problem is solved.
Thank you !


Discussion:
I think both branches below that are testing for "shape changed" are problematic in their own way:
https://github.com/pyqtgraph/pyqtgraph/blob/fa26ecef59be4a67164d0e27924fb65f4bb188bc/pyqtgraph/graphicsItems/PColorMeshItem.py#L228-L233

The 3-argument branch, which is the one concerned in this issue, could be fixed by using `functions.eq()`:
https://github.com/pyqtgraph/pyqtgraph/blob/fa26ecef59be4a67164d0e27924fb65f4bb188bc/pyqtgraph/functions.py#L642-L644
The existing "shapeChanged" logic was before "dataBounds" was implemented.
What is really wanted is to check if the data bounds have changed. i.e. "shapeChanged" is actually a misnomer.

```python
old_bounds = self._dataBounds
self._prepareData(args)
shapeChanged = old_bounds != self._dataBounds
```

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/PColorMeshItem.py b/pyqtgraph/graphicsItems/PColorMeshItem.py
--- a/pyqtgraph/graphicsItems/PColorMeshItem.py
+++ b/pyqtgraph/graphicsItems/PColorMeshItem.py
@@ -91,11 +91,11 @@ def __init__(self, *args, **kwargs):
             Sets the minimum and maximum values to be represented by the colormap (min, max). 
             Values outside this range will be clipped to the colors representing min or max.
             ``None`` disables the limits, meaning that the colormap will autoscale 
-            each time ``setData()`` is called - unless ``enableAutoLevels=False``.
+            the next time ``setData()`` is called with new data.
         enableAutoLevels: bool, optional, default True
             Causes the colormap levels to autoscale whenever ``setData()`` is called. 
-            When enableAutoLevels is set to True, it is still possible to disable autoscaling
-            on a per-change-basis by using ``autoLevels=False`` when calling ``setData()``.
+            It is possible to override this value on a per-change-basis by using the
+            ``autoLevels`` keyword argument when calling ``setData()``.
             If ``enableAutoLevels==False`` and ``levels==None``, autoscaling will be 
             performed once when the first z data is supplied. 
         edgecolors : dict, optional
@@ -125,7 +125,7 @@ def __init__(self, *args, **kwargs):
             self.edgecolors.setCosmetic(True)
         self.antialiasing = kwargs.get('antialiasing', False)
         self.levels = kwargs.get('levels', None)
-        self.enableautolevels = kwargs.get('enableAutoLevels', True)
+        self._defaultAutoLevels = kwargs.get('enableAutoLevels', True)
         
         if 'colorMap' in kwargs:
             cmap = kwargs.get('colorMap')
@@ -213,38 +213,39 @@ def setData(self, *args, **kwargs):
 
             "ASCII from: <https://matplotlib.org/3.2.1/api/_as_gen/
                          matplotlib.pyplot.pcolormesh.html>".
-        autoLevels: bool, optional, default True
-            When set to True, PColorMeshItem will automatically select levels
-            based on the minimum and maximum values encountered in the data along the z axis.
-            The minimum and maximum levels are mapped to the lowest and highest colors 
-            in the colormap. The autoLevels parameter is ignored if ``enableAutoLevels is False`` 
+        autoLevels: bool, optional
+            If set, overrides the value of ``enableAutoLevels``
         """
-        autoLevels = kwargs.get('autoLevels', True)
+        old_bounds = self._dataBounds
+        self._prepareData(args)
+        boundsChanged = old_bounds != self._dataBounds
 
-        # Has the view bounds changed
-        shapeChanged = False
-        if self.qpicture is None:
-            shapeChanged = True
-        elif len(args)==1:
-            if args[0].shape[0] != self.x[:,1][-1] or args[0].shape[1] != self.y[0][-1]:
-                shapeChanged = True
-        elif len(args)==3:
-            if np.any(self.x != args[0]) or np.any(self.y != args[1]):
-                shapeChanged = True
+        self._rerender(
+            autoLevels=kwargs.get('autoLevels', self._defaultAutoLevels)
+        )
 
-        if len(args)==0:
-            # No data was received.
-            if self.z is None:
-                # No data is currently displayed, 
-                # so other settings (like colormap) can not be updated
-                return
-        else:
-            # Got new data. Prepare it for plotting
-            self._prepareData(args)
+        if boundsChanged:
+            self.prepareGeometryChange()
+            self.informViewBoundsChanged()
 
+        self.update()
 
-        self.qpicture = QtGui.QPicture()
-        painter = QtGui.QPainter(self.qpicture)
+    def _rerender(self, *, autoLevels):
+        self.qpicture = None
+        if self.z is not None:
+            if (self.levels is None) or autoLevels:
+                # Autoscale colormap
+                z_min = self.z.min()
+                z_max = self.z.max()
+                self.setLevels( (z_min, z_max), update=False)
+            self.qpicture = self._drawPicture()
+
+    def _drawPicture(self) -> QtGui.QPicture:
+        # on entry, the following members are all valid: x, y, z, levels
+        # this function does not alter any state (besides using self.quads)
+
+        picture = QtGui.QPicture()
+        painter = QtGui.QPainter(picture)
         # We set the pen of all polygons once
         if self.edgecolors is None:
             painter.setPen(QtCore.Qt.PenStyle.NoPen)
@@ -252,28 +253,17 @@ def setData(self, *args, **kwargs):
             painter.setPen(self.edgecolors)
             if self.antialiasing:
                 painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
-                
 
         ## Prepare colormap
         # First we get the LookupTable
         lut = self.lut_qcolor
         # Second we associate each z value, that we normalize, to the lut
         scale = len(lut) - 1
-        # Decide whether to autoscale the colormap or use the same levels as before
-        if (self.levels is None) or (self.enableautolevels and autoLevels):
-            # Autoscale colormap 
-            z_min = self.z.min()
-            z_max = self.z.max()
-            self.setLevels( (z_min, z_max), update=False)
-        else:
-            # Use consistent colormap scaling
-            z_min = self.levels[0]
-            z_max = self.levels[1]
-        rng = z_max - z_min
+        lo, hi = self.levels[0], self.levels[1]
+        rng = hi - lo
         if rng == 0:
             rng = 1
-        norm = fn.rescaleData(self.z, scale / rng, z_min,
-            dtype=int, clip=(0, len(lut)-1))
+        norm = fn.rescaleData(self.z, scale / rng, lo, dtype=int, clip=(0, len(lut)-1))
 
         if Qt.QT_LIB.startswith('PyQt'):
             drawConvexPolygon = lambda x : painter.drawConvexPolygon(*x)
@@ -299,24 +289,7 @@ def setData(self, *args, **kwargs):
                 drawConvexPolygon(polys[idx])
 
         painter.end()
-        self.update()
-
-        self.prepareGeometryChange()
-        if shapeChanged:
-            self.informViewBoundsChanged()
-
-
-
-    def _updateDisplayWithCurrentState(self, *args, **kargs):
-        ## Used for re-rendering mesh from self.z.
-        ## For example when a new colormap is applied, or the levels are adjusted
-
-        defaults = {
-            'autoLevels': False,
-        }
-        defaults.update(kargs)
-        return self.setData(*args, **defaults)
-
+        return picture
 
 
     def setLevels(self, levels, update=True):
@@ -334,9 +307,8 @@ def setLevels(self, levels, update=True):
         self.levels = levels
         self.sigLevelsChanged.emit(levels)
         if update:
-            self._updateDisplayWithCurrentState()
-
-
+            self._rerender(autoLevels=False)
+            self.update()
 
     def getLevels(self):
         """
@@ -351,9 +323,8 @@ def setLookupTable(self, lut, update=True):
         self.cmap = None    # invalidate since no longer consistent with lut
         self.lut_qcolor = lut[:]
         if update:
-            self._updateDisplayWithCurrentState()
-
-
+            self._rerender(autoLevels=False)
+            self.update()
 
     def getColorMap(self):
         return self.cmap
@@ -363,27 +334,14 @@ def setColorMap(self, cmap):
         self.cmap = cmap
 
     def enableAutoLevels(self):
-        self.enableautolevels = True
-
-
+        self._defaultAutoLevels = True
 
     def disableAutoLevels(self):
-        self.enableautolevels = False
-
-
+        self._defaultAutoLevels = False
 
     def paint(self, p, *args):
-        if self.z is None:
-            return
-
-        p.drawPicture(0, 0, self.qpicture)
-
-
-    def setBorder(self, b):
-        self.border = fn.mkPen(b)
-        self.update()
-
-
+        if self.qpicture is not None:
+            p.drawPicture(0, 0, self.qpicture)
 
     def width(self):
         if self._dataBounds is None:


Test patch: