Repository: pyqtgraph/pyqtgraph

Issue:
ErrorBarItem does not plot for values smaller than 1e-12
### Short description
When I try to plot with error bars having values smaller than 1e-12, they do not show up. No error or warning.

### Code to reproduce
```python
import pyqtgraph, numpy

ey = numpy.array([2e-12, 1.1e-12, 1e-12, 0.9e-12])
p = pyqtgraph.plot()
p.addItem(pyqtgraph.ErrorBarItem(x=[0,1,2,3], y=[0,0,0,0], top=ey))
```

### Expected behavior
We should see 4 bars of diminishing height.

### Real behavior
We see only the first two.

### Tested environment(s)

 * PyQtGraph version: 0.10.0
 * Qt Python binding: PyQt5 5.9.2 Qt 5.9.6
 * Python version: 3.7
 * NumPy version: 1.15.4
 * Operating system: Linux Mint 19
 * Installation method: conda

### Additional note
PlotCurveItems appear with crazy small values, well below 1e-12, but their error bars don't. This confused me for a long time!

Thanks in advance, and sorry if this issue is fixed in 0.11 already.



Discussion:
Seems to be on the Qt side -- here's a pure Qt example with the same problem:

```python
from PyQt5 import QtGui, QtWidgets

y = 1.0e-12  # set to 1.1e-12 and all three lines show up

app = QtWidgets.QApplication([])

win = QtWidgets.QMainWindow()
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
win.setCentralWidget(view)

view.scale(10, -10/y)

for i, yval, color in zip(range(3), [y, 1.1*y, 1.2*y], ['red', 'green', 'blue']):
    path = QtGui.QPainterPath()
    path.moveTo(i, 0)
    path.lineTo(i, yval)

    item = QtWidgets.QGraphicsPathItem(path)
    pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(color)), 0)
    item.setPen(pen)
    scene.addItem(item)

win.show()
app.exec_()
```
Surprising, because the regular PlotCurveItems do work for small numbers. Can pyqtgraph simply switch the way it draws ErrorBarItems to match the way it draws PlotCurveItems?
That's a great point. `PlotCurveItem` uses `arrayToQPath` to generate the `QPainterPath` -- I quickly hacked in usage of that function (with `connect='pairs'`) for `ErrorBarItem` instead of `moveTo`/`lineTo` calls and your example now works. It's not immediately obvious to me why that'd be the case, but at least there's a potential workaround.
Great. Hope this will be in the conda release of v0.11
________________________________
From: Kenneth Lyons <notifications@github.com>
Sent: Thursday, April 2, 2020 1:21 AM
To: pyqtgraph/pyqtgraph <pyqtgraph@noreply.github.com>
Cc: Jack Sankey <jack.sankey@gmail.com>; Author <author@noreply.github.com>
Subject: Re: [pyqtgraph/pyqtgraph] ErrorBarItem does not plot for values smaller than 1e-12 (#1149)


That's a great point. PlotCurveItem uses arrayToQPath to generate the QPainterPath -- I quickly hacked in usage of that function (with connect='pairs') for ErrorBarItem instead of moveTo/lineTo calls and your example now works. It's not immediately obvious to me why that'd be the case, but at least there's a potential workaround.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub<https://github.com/pyqtgraph/pyqtgraph/issues/1149#issuecomment-607627297>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AB7ELJ6H3UW6DNZ5XQ5UMKDRKQOEHANCNFSM4LZE7UQA>.

I just ran into this issue while replicating issue #1457 this is only an issue on older Qt versions, if you use the conda version of pyside2, you get Qt 5.12.x; which is not susceptible for this issue.

That said, we should consider using `connect='pairs'` for drawing anyway...
Confirming this issue is still present in Qt6
And here is the issue reported in the Qt BugTracker: https://bugreports.qt.io/browse/QTBUG-75630

Resolving patch:
diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py
--- a/pyqtgraph/functions.py
+++ b/pyqtgraph/functions.py
@@ -662,6 +662,35 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
         return output
 
 
+def interweaveArrays(*args):
+    """
+    Parameters
+    ----------
+
+    args : numpy.ndarray
+           series of 1D numpy arrays of the same length and dtype
+    
+    Returns
+    -------
+    numpy.ndarray
+        A numpy array with all the input numpy arrays interwoven
+
+    Examples
+    --------
+
+    >>> result = interweaveArrays(numpy.ndarray([0, 2, 4]), numpy.ndarray([1, 3, 5]))
+    >>> result
+    array([0, 1, 2, 3, 4, 5])
+    """
+
+    size = sum(x.size for x in args)
+    result = np.empty((size,), dtype=args[0].dtype)
+    n = len(args)
+    for index, array in enumerate(args):
+        result[index::n] = array
+    return result
+
+
 def interpolateArray(data, x, default=0.0, order=1):
     """
     N-dimensional interpolation similar to scipy.ndimage.map_coordinates.
diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py
--- a/pyqtgraph/graphicsItems/ErrorBarItem.py
+++ b/pyqtgraph/graphicsItems/ErrorBarItem.py
@@ -2,6 +2,7 @@
 from .GraphicsObject import GraphicsObject
 from .. import getConfigOption
 from .. import functions as fn
+import numpy as np
 
 __all__ = ['ErrorBarItem']
 
@@ -66,7 +67,6 @@ def drawPath(self):
         
         beam = self.opts['beam']
         
-        
         height, top, bottom = self.opts['height'], self.opts['top'], self.opts['bottom']
         if height is not None or top is not None or bottom is not None:
             ## draw vertical error bars
@@ -82,23 +82,27 @@ def drawPath(self):
                     y2 = y
                 else:
                     y2 = y + top
-            
-            for i in range(len(x)):
-                p.moveTo(x[i], y1[i])
-                p.lineTo(x[i], y2[i])
-                
+
+            xs = fn.interweaveArrays(x, x)
+            y1_y2 = fn.interweaveArrays(y1, y2)
+            verticalLines = fn.arrayToQPath(xs, y1_y2, connect="pairs")
+            p.addPath(verticalLines)
+
             if beam is not None and beam > 0:
                 x1 = x - beam/2.
                 x2 = x + beam/2.
+
+                x1_x2 = fn.interweaveArrays(x1, x2)
                 if height is not None or top is not None:
-                    for i in range(len(x)):
-                        p.moveTo(x1[i], y2[i])
-                        p.lineTo(x2[i], y2[i])
+                    y2s = fn.interweaveArrays(y2, y2)
+                    topEnds = fn.arrayToQPath(x1_x2, y2s, connect="pairs")
+                    p.addPath(topEnds)
+
                 if height is not None or bottom is not None:
-                    for i in range(len(x)):
-                        p.moveTo(x1[i], y1[i])
-                        p.lineTo(x2[i], y1[i])
-        
+                    y1s = fn.interweaveArrays(y1, y1)
+                    bottomEnds = fn.arrayToQPath(x1_x2, y1s, connect="pairs")
+                    p.addPath(bottomEnds)
+
         width, right, left = self.opts['width'], self.opts['right'], self.opts['left']
         if width is not None or right is not None or left is not None:
             ## draw vertical error bars
@@ -115,21 +119,24 @@ def drawPath(self):
                 else:
                     x2 = x + right
             
-            for i in range(len(x)):
-                p.moveTo(x1[i], y[i])
-                p.lineTo(x2[i], y[i])
-                
+            ys = fn.interweaveArrays(y, y)
+            x1_x2 = fn.interweaveArrays(x1, x2)
+            ends = fn.arrayToQPath(x1_x2, ys, connect='pairs')
+            p.addPath(ends)
+
             if beam is not None and beam > 0:
                 y1 = y - beam/2.
                 y2 = y + beam/2.
+                y1_y2 = fn.interweaveArrays(y1, y2)
                 if width is not None or right is not None:
-                    for i in range(len(x)):
-                        p.moveTo(x2[i], y1[i])
-                        p.lineTo(x2[i], y2[i])
+                    x2s = fn.interweaveArrays(x2, x2)
+                    rightEnds = fn.arrayToQPath(x2s, y1_y2, connect="pairs")
+                    p.addPath(rightEnds)
+
                 if width is not None or left is not None:
-                    for i in range(len(x)):
-                        p.moveTo(x1[i], y1[i])
-                        p.lineTo(x1[i], y2[i])
+                    x1s = fn.interweaveArrays(x1, x1)
+                    leftEnds = fn.arrayToQPath(x1s, y1_y2, connect="pairs")
+                    p.addPath(leftEnds)
                     
         self.path = p
         self.prepareGeometryChange()


Test patch: