Repository: pyqtgraph/pyqtgraph

Issue:
ColorBarItem lower axis value stuck
<!-- In the following, please describe your issue in detail! -->
<!-- If some of the sections do not apply, just remove them. -->

### Short description
<!-- This should summarize the issue. -->

After dragging lower handle down till lower limit is hit on the axis, dragging it up no longer shifts the axis values.

### 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. -->

1) Run the following script
2) Drag the lower handle downwards. The axis lower bound gets locked at -0.2.
3) Drag the lower handle upwards. The axis lower bound stays locked at -0.2.

```python
import pyqtgraph as pg
import numpy as np

pg.mkQApp()

pw = pg.PlotWidget()
pw.show()
imgitem = pg.ImageItem(axisOrder='row-major')
pw.addItem(imgitem)
xy = np.linspace(-3, 3, 1000)
data = np.sinc(xy)[:,None] * np.sinc(xy)
cmap = pg.colormap.get('viridis')
limits = data.min(), data.max()
bar = pg.ColorBarItem(values=limits, colorMap=cmap, limits=limits, rounding=0.1)
bar.setImageItem(imgitem, insert_in=pw.getPlotItem() )
imgitem.setImage(data)

pg.exec()
```

### Expected behavior
<!-- What should happen? -->
The axis lower bound value should not get stuck at -0.2

### Real behavior
<!-- What happens? -->
The axis lower bound value gets stuck at 0.2


### Tested environment(s)

 * PyQtGraph version: pyqtgraph 0.12.3<!-- output of pyqtgraph.__version__ -->
 * Qt Python binding: <!-- output of pyqtgraph.Qt.VERSION_INFO -->
 * Python version: 3.8.10
 * NumPy version: 1.21.2 <!-- output of numpy.__version__ -->
 * Operating system: Windows 10
 * Installation method: pip install -e . <!-- e.g. pip, conda, system packages, ... -->

### Additional context



Discussion:

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/ColorBarItem.py b/pyqtgraph/graphicsItems/ColorBarItem.py
--- a/pyqtgraph/graphicsItems/ColorBarItem.py
+++ b/pyqtgraph/graphicsItems/ColorBarItem.py
@@ -72,11 +72,15 @@ def __init__(self, values=(0,1), width=25, colorMap=None, label=None,
         self.horizontal = bool( orientation in ('h', 'horizontal') )
 
         self.lo_prv, self.hi_prv = self.values # remember previous values while adjusting range
-        if limits is None:
-            self.lo_lim = None
-            self.hi_lim = None
-        else:
+        self.lo_lim = None
+        self.hi_lim = None
+        if limits is not None:
             self.lo_lim, self.hi_lim = limits
+            # slightly expand the limits to match the rounding steps:
+            if self.lo_lim is not None:
+                self.lo_lim = self.rounding * math.floor( self.lo_lim/self.rounding )
+            if self.hi_lim is not None:
+                self.hi_lim = self.rounding * math.ceil( self.hi_lim/self.rounding )
 
         self.disableAutoRange()
         self.hideButtons()
@@ -259,32 +263,33 @@ def _regionChanging(self):
         # These are the new values if adjuster is released now, rate of change depends on original separation
         span_prv = self.hi_prv - self.lo_prv # previous span of values
         hi_new = self.hi_prv + (span_prv + 2*self.rounding) * top # make sure that we can always
-        lo_new = self.lo_prv + (span_prv + 2*self.rounding) * bot #   reach 2x the minimal step
+        lo_new = self.lo_prv + (span_prv + 2*self.rounding) * bot # reach 2x the minimal step
+
         # Alternative model with speed depending on level magnitude:
         # mean_val = abs(self.lo_prv) + abs(self.hi_prv) / 2
         # hi_new = self.hi_prv + (mean_val + 2*self.rounding) * top # make sure that we can always
         # lo_new = self.lo_prv + (mean_val + 2*self.rounding) * bot #    reach 2x the minimal step
 
-        if self.hi_lim is not None and hi_new > self.hi_lim: # limit maximum value
-            # print('lim +')
-            hi_new = self.hi_lim
-            if lo_new > hi_new - span_prv: # avoid collapsing the span against top or bottom limits
-                lo_new = hi_new - span_prv
-        if self.lo_lim is not None and lo_new < self.lo_lim: # limit minimum value
-            # print('lim -')
-            lo_new = self.lo_lim
-            if hi_new < lo_new + span_prv: # avoid collapsing the span against top or bottom limits
-                hi_new = lo_new + span_prv
-        if lo_new + self.rounding > hi_new: # do not allow less than one "rounding" unit of span
-            # print('lim X')
+        if self.hi_lim is not None:
+            if hi_new > self.hi_lim: # limit maximum value
+                hi_new = self.hi_lim 
+                if top!=0 and bot!=0:          # moving entire region?
+                    lo_new = hi_new - span_prv # avoid collapsing the span against top limit
+        if self.lo_lim is not None:
+            if lo_new < self.lo_lim: # limit minimum value
+                lo_new = self.lo_lim 
+                if top!=0 and bot!=0:          # moving entire region?
+                    hi_new = lo_new + span_prv # avoid collapsing the span against bottom limit
+        if hi_new-lo_new < self.rounding: # do not allow less than one "rounding" unit of span 
             if   bot == 0: hi_new = lo_new + self.rounding
             elif top == 0: lo_new = hi_new - self.rounding
-            else:
-                lo_new = (lo_new + hi_new - self.rounding) / 2
-                hi_new = lo_new + self.rounding
+            else: # this should never happen, but let's try to recover if it does:
+                mid = (hi_new + lo_new) / 2
+                hi_new = mid + self.rounding / 2
+                lo_new = mid - self.rounding / 2
+
         lo_new = self.rounding * round( lo_new/self.rounding )
         hi_new = self.rounding * round( hi_new/self.rounding )
-        # if hi_new == lo_new: hi_new = lo_new + self.rounding # hack solution if axis span still collapses
         self.values = (lo_new, hi_new)
         self._update_items()
         self.sigLevelsChanged.emit(self)


Test patch: