Repository: napari/napari

Issue:
contrast limits slider broken with very small max value
## 🐛 Bug

I was working with an image today with a very small max value, 2.6e-17, but a lot of richness around 10-20 etc, and found that the contrast limits slider is not adjustable, i.e. when i click on it I can't drag it around.

## To Reproduce

```python
v = napari.Viewer()
v.add_image(np.random.random((10, 10))*2.6e-17)
```

## Expected behavior

I should be able to adjust contrast limits value

## Environment

Current main, and latest superqt



Discussion:
If you can produce this with only superqt, can you open an issue there instead?

Resolving patch:
diff --git a/napari/_qt/layer_controls/qt_image_controls_base.py b/napari/_qt/layer_controls/qt_image_controls_base.py
--- a/napari/_qt/layer_controls/qt_image_controls_base.py
+++ b/napari/_qt/layer_controls/qt_image_controls_base.py
@@ -89,8 +89,11 @@ def __init__(self, layer: Image):
 
         # Create contrast_limits slider
         self.contrastLimitsSlider = _QDoubleRangeSlider(Qt.Horizontal, self)
-        self.contrastLimitsSlider.setSingleStep(0.01)
+        decimals = range_to_decimals(
+            self.layer.contrast_limits_range, self.layer.dtype
+        )
         self.contrastLimitsSlider.setRange(*self.layer.contrast_limits_range)
+        self.contrastLimitsSlider.setSingleStep(10 ** -decimals)
         self.contrastLimitsSlider.setValue(self.layer.contrast_limits)
         self.contrastLimitsSlider.setToolTip(
             trans._('Right click for detailed slider popup.')
@@ -214,18 +217,7 @@ class QContrastLimitsPopup(QRangeSliderPopup):
     def __init__(self, layer: Image, parent=None):
         super().__init__(parent)
 
-        if np.issubdtype(layer.dtype, np.integer):
-            decimals = 0
-        else:
-            # scale precision with the log of the data range order of magnitude
-            # eg.   0 - 1   (0 order of mag)  -> 3 decimal places
-            #       0 - 10  (1 order of mag)  -> 2 decimals
-            #       0 - 100 (2 orders of mag) -> 1 decimal
-            #       ≥ 3 orders of mag -> no decimals
-            # no more than 6 decimals
-            d_range = np.subtract(*layer.contrast_limits_range[::-1])
-            decimals = min(6, max(int(3 - np.log10(d_range)), 0))
-
+        decimals = range_to_decimals(layer.contrast_limits_range, layer.dtype)
         self.slider.setRange(*layer.contrast_limits_range)
         self.slider.setDecimals(decimals)
         self.slider.setSingleStep(10 ** -decimals)
@@ -259,3 +251,37 @@ def reset():
             range_btn.setFixedWidth(65)
             range_btn.clicked.connect(layer.reset_contrast_limits_range)
             self._layout.addWidget(range_btn, alignment=Qt.AlignBottom)
+
+
+def range_to_decimals(range_, dtype):
+    """Convert a range to decimals of precision.
+
+    Parameters
+    ----------
+    range : tuple
+        Slider range, min and then max values.
+    dtype : np.dtype
+        Data type of the layer. Integers layers are given integer.
+        step sizes.
+
+    Returns
+    -------
+    int
+        Decimals of precision.
+    """
+
+    if hasattr(dtype, 'numpy_dtype'):
+        # retrieve the corresponding numpy.dtype from a tensorstore.dtype
+        dtype = dtype.numpy_dtype
+
+    if np.issubdtype(dtype, np.integer):
+        return 0
+    else:
+        # scale precision with the log of the data range order of magnitude
+        # eg.   0 - 1   (0 order of mag)  -> 3 decimal places
+        #       0 - 10  (1 order of mag)  -> 2 decimals
+        #       0 - 100 (2 orders of mag) -> 1 decimal
+        #       ≥ 3 orders of mag -> no decimals
+        # no more than 64 decimals
+        d_range = np.subtract(*range_[::-1])
+        return min(64, max(int(3 - np.log10(d_range)), 0))


Test patch:
diff --git a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
--- a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
@@ -10,6 +10,7 @@
     QContrastLimitsPopup,
     QRangeSliderPopup,
     QtBaseImageControls,
+    range_to_decimals,
 )
 from napari.layers import Image, Surface
 
@@ -101,10 +102,13 @@ def test_clim_slider_step_size_and_precision(qtbot, mag):
     popup = QContrastLimitsPopup(layer)
     qtbot.addWidget(popup)
 
-    # the range slider popup labels should have a number of decimal points that
-    # is inversely proportional to the order of magnitude of the range of data,
-    # but should never be greater than 5 or less than 0
-    decimals = min(6, max(int(3 - mag), 0))
+    # scale precision with the log of the data range order of magnitude
+    # eg.   0 - 1   (0 order of mag)  -> 3 decimal places
+    #       0 - 10  (1 order of mag)  -> 2 decimals
+    #       0 - 100 (2 orders of mag) -> 1 decimal
+    #       ≥ 3 orders of mag -> no decimals
+    # no more than 64 decimals
+    decimals = range_to_decimals(layer.contrast_limits, layer.dtype)
     assert popup.slider.decimals() == decimals
 
     # the slider step size should also be inversely proportional to the data