Repository: pyqtgraph/pyqtgraph

Issue:
`arrayToQPath` fails when drawing an empty path and boolean array `connect` argument
(pypi version 0.12.2 release)
```python
import numpy as np
import pyqtgraph as pg
x = np.zeros((0, 2))
isfinite = np.ones(x.shape[0], bool)
pg.arrayToQPath(*x.T, connect=isfinite)
```

Traceback:
```
Traceback (most recent call last):
  File "C:\Users\njessurun\Miniconda3\envs\py39\lib\site-packages\IPython\core\interactiveshell.py", line 3441, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-2-28fd2e751002>", line 5, in <module>
    pg.arrayToQPath(*x.T, connect=isfinite)
  File "C:\Users\njessurun\Miniconda3\envs\py39\lib\site-packages\pyqtgraph\functions.py", line 2047, in arrayToQPath
    arr[0]['c'] = 0  # the first vertex has no previous vertex to connect
IndexError: index 0 is out of bounds for axis 0 with size 0
```

`arrayToQPath` fails when drawing an empty path and boolean array `connect` argument
(pypi version 0.12.2 release)
```python
import numpy as np
import pyqtgraph as pg
x = np.zeros((0, 2))
isfinite = np.ones(x.shape[0], bool)
pg.arrayToQPath(*x.T, connect=isfinite)
```

Traceback:
```
Traceback (most recent call last):
  File "C:\Users\njessurun\Miniconda3\envs\py39\lib\site-packages\IPython\core\interactiveshell.py", line 3441, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-2-28fd2e751002>", line 5, in <module>
    pg.arrayToQPath(*x.T, connect=isfinite)
  File "C:\Users\njessurun\Miniconda3\envs\py39\lib\site-packages\pyqtgraph\functions.py", line 2047, in arrayToQPath
    arr[0]['c'] = 0  # the first vertex has no previous vertex to connect
IndexError: index 0 is out of bounds for axis 0 with size 0
```



Discussion:
I was able to solve this by changing this:

https://github.com/pyqtgraph/pyqtgraph/blob/6b4385ce0d0f9078aa22e2e27aa5307271e95ae1/pyqtgraph/functions.py#L2047

To

`arr[0:1]['c'] = 0`

... I'm not sure whether that incurs more or less overhead than just `if len(arr): arr[0]['c'] = 0`
I was able to solve this by changing this:

https://github.com/pyqtgraph/pyqtgraph/blob/6b4385ce0d0f9078aa22e2e27aa5307271e95ae1/pyqtgraph/functions.py#L2047

To

`arr[0:1]['c'] = 0`

... I'm not sure whether that incurs more or less overhead than just `if len(arr): arr[0]['c'] = 0`

Resolving patch:
diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py
--- a/pyqtgraph/functions.py
+++ b/pyqtgraph/functions.py
@@ -1925,6 +1925,8 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True):
 
     path = QtGui.QPainterPath()
     n = x.shape[0]
+    if n == 0:
+      return path
 
     connect_array = None
     if isinstance(connect, np.ndarray):


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 from collections import OrderedDict
 from copy import deepcopy
-from contextlib import suppress
 from pyqtgraph.functions import arrayToQPath, eq
 
 import numpy as np
@@ -264,37 +263,66 @@ def test_CIELab_reconversion():
 
 MoveToElement = pg.QtGui.QPainterPath.ElementType.MoveToElement
 LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement
+_dtypes = []
+for bits in 32, 64:
+    for base in 'int', 'float', 'uint':
+        _dtypes.append(f'{base}{bits}')
+_dtypes.extend(['uint8', 'uint16'])
+
+def _handle_underflow(dtype, *elements):
+    """Wrapper around path description which converts underflow into proper points"""
+    out = []
+    for el in elements:
+        newElement = [el[0]]
+        for ii in range(1, 3):
+            coord = el[ii]
+            if coord < 0:
+                coord = np.array(coord, dtype=dtype)
+            newElement.append(float(coord))
+        out.append(tuple(newElement))
+    return out
+
 @pytest.mark.parametrize(
     "xs, ys, connect, expected", [
-        (
-            np.arange(6), np.arange(0, -6, step=-1), 'all', (
-                (MoveToElement, 0.0, 0.0),
-                (LineToElement, 1.0, -1.0),
-                (LineToElement, 2.0, -2.0),
-                (LineToElement, 3.0, -3.0),
-                (LineToElement, 4.0, -4.0),
-                (LineToElement, 5.0, -5.0),
-            )
-        ), 
-        (
-            np.arange(6), np.arange(0, -6, step=-1), 'pairs', (
-                (MoveToElement, 0.0, 0.0),
-                (LineToElement, 1.0, -1.0),
-                (MoveToElement, 2.0, -2.0),
-                (LineToElement, 3.0, -3.0),
-                (MoveToElement, 4.0, -4.0),
-                (LineToElement, 5.0, -5.0),
-            )
+        *(
+            (
+                np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'all',
+                _handle_underflow(dtype,
+                                  (MoveToElement, 0.0, 0.0),
+                                  (LineToElement, 1.0, -1.0),
+                                  (LineToElement, 2.0, -2.0),
+                                  (LineToElement, 3.0, -3.0),
+                                  (LineToElement, 4.0, -4.0),
+                                  (LineToElement, 5.0, -5.0)
+                                  )
+            ) for dtype in _dtypes
         ),
-        (
-            np.arange(5), np.arange(0, -5, step=-1), 'pairs', (
-                (MoveToElement, 0.0, 0.0),
-                (LineToElement, 1.0, -1.0),
-                (MoveToElement, 2.0, -2.0),
-                (LineToElement, 3.0, -3.0),
-                (MoveToElement, 4.0, -4.0)
-            ) 
+        *(
+            (
+                np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'pairs',
+                _handle_underflow(dtype,
+                                  (MoveToElement, 0.0, 0.0),
+                                  (LineToElement, 1.0, -1.0),
+                                  (MoveToElement, 2.0, -2.0),
+                                  (LineToElement, 3.0, -3.0),
+                                  (MoveToElement, 4.0, -4.0),
+                                  (LineToElement, 5.0, -5.0),
+                                  )
+            ) for dtype in _dtypes
+        ),
+        *(
+            (
+                np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), 'pairs',
+                _handle_underflow(dtype,
+                                  (MoveToElement, 0.0, 0.0),
+                                  (LineToElement, 1.0, -1.0),
+                                  (MoveToElement, 2.0, -2.0),
+                                  (LineToElement, 3.0, -3.0),
+                                  (MoveToElement, 4.0, -4.0)
+                                  )
+            ) for dtype in _dtypes
         ),
+        # NaN types don't coerce to integers, don't test for all types since that doesn't make sense
         (
             np.arange(5), np.array([0, -1, np.NaN, -3, -4]), 'finite', (
                 (MoveToElement, 0.0, 0.0),
@@ -313,25 +341,34 @@ def test_CIELab_reconversion():
                 (LineToElement, 4.0, -4.0)
             )
         ),
-        (
-            np.arange(5), np.arange(0, -5, step=-1), np.array([0, 1, 0, 1, 0]), (
-                (MoveToElement, 0.0, 0.0),
-                (MoveToElement, 1.0, -1.0),
-                (LineToElement, 2.0, -2.0),
-                (MoveToElement, 3.0, -3.0),
-                (LineToElement, 4.0, -4.0)
-            )
-        )
+        *(
+            (
+                np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), np.array([0, 1, 0, 1, 0]),
+                _handle_underflow(dtype,
+                                  (MoveToElement, 0.0, 0.0),
+                                  (MoveToElement, 1.0, -1.0),
+                                  (LineToElement, 2.0, -2.0),
+                                  (MoveToElement, 3.0, -3.0),
+                                  (LineToElement, 4.0, -4.0)
+                                  )
+            ) for dtype in _dtypes
+        ),
+        # Empty path with all types of connection
+        *(
+            (
+                np.arange(0), np.arange(0, dtype=dtype), conn, ()
+            ) for conn in ['all', 'pairs', 'finite', np.array([])] for dtype in _dtypes
+        ),
     ]
 )
 def test_arrayToQPath(xs, ys, connect, expected):
     path = arrayToQPath(xs, ys, connect=connect)
+    element = None
     for i in range(path.elementCount()):
-        with suppress(NameError):
-            # nan elements add two line-segments, for simplicity of test config
-            # we can ignore the second segment
-            if (eq(element.x, np.nan) or eq(element.y, np.nan)):
-                continue
+        # nan elements add two line-segments, for simplicity of test config
+        # we can ignore the second segment
+        if element is not None and (eq(element.x, np.nan) or eq(element.y, np.nan)):
+            continue
         element = path.elementAt(i)
         assert eq(expected[i], (element.type, element.x, element.y))