Repository: napari/napari

Issue:
contrast limits slider increment is static and too large for some images
## 🐛 Bug

When I open images with intensities occupying the low end of [0,1] range, the contrast limits slider  increments of 0.01 are too large to be useful.  Like I only get 2-3 positions for some of my recent images.

## To Reproduce

Steps to reproduce the behavior:

1. open an image with intensity values _val_ s.t.   0 < _val_ < 0.05 
2. adjust the clims
3. frown

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior
The user shouldn't see slider increments unless the underlying control is of something with a few discrete values. In this case, I suggest they should scale to ~5% of the range for the small sliders and < 1% for the big sliders.

## Environment
napari==0.2.10+4.gb45a26d


## In other news:

I like the expando-slider and support their further expansion across the landscape of napari!


Discussion:
Hey @berl! Would being able to expand the slider and manually set the values help (see #837)? The behavior isn't super discoverable, but you can right click on the clim slider to get the expanded UI for setting values, ranges, etc.
unfortunately, the popup slider probably won't help in this case.  Try for example:
```python
import numpy as np
import napari                                                           

with napari.gui_qt(): 
    napari.view_image(np.random.rand(10,10,10)/20) 
```
I have an idea how this can be solved pretty easily and will submit a PR shortly

Resolving patch:
diff --git a/napari/_qt/layers/qt_image_base_layer.py b/napari/_qt/layers/qt_image_base_layer.py
--- a/napari/_qt/layers/qt_image_base_layer.py
+++ b/napari/_qt/layers/qt_image_base_layer.py
@@ -153,11 +153,21 @@ def create_range_popup(layer, attr, parent=None):
         )
     is_integer_type = np.issubdtype(layer.dtype, np.integer)
 
+    d_range = getattr(layer, range_attr)
     popup = QRangeSliderPopup(
         initial_values=getattr(layer, attr),
-        data_range=getattr(layer, range_attr),
+        data_range=d_range,
         collapsible=False,
-        precision=(0 if is_integer_type else 2),
+        precision=(
+            0
+            if is_integer_type
+            # 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
+            else int(max(3 - np.log10(max(d_range[1] - d_range[0], 0.01)), 0))
+        ),
         parent=parent,
     )
 
diff --git a/napari/_qt/qt_range_slider.py b/napari/_qt/qt_range_slider.py
--- a/napari/_qt/qt_range_slider.py
+++ b/napari/_qt/qt_range_slider.py
@@ -77,7 +77,13 @@ def __init__(
 
         self.setRange((0, 100) if data_range is None else data_range)
         self.setValues((20, 80) if initial_values is None else initial_values)
-        self.setStep(0.01 if step_size is None else step_size)
+        if step_size is None:
+            # pick an appropriate slider step size based on the data range
+            if data_range is not None:
+                step_size = (data_range[1] - data_range[0]) / 1000
+            else:
+                step_size = 0.001
+        self.setStep(step_size)
         if not parent:
             if 'HRange' in self.__class__.__name__:
                 self.setGeometry(200, 200, 200, 20)


Test patch:
diff --git a/napari/_qt/layers/tests/test_qt_image_base_layer_.py b/napari/_qt/layers/tests/test_qt_image_base_layer_.py
--- a/napari/_qt/layers/tests/test_qt_image_base_layer_.py
+++ b/napari/_qt/layers/tests/test_qt_image_base_layer_.py
@@ -6,7 +6,10 @@
 from qtpy.QtCore import Qt
 from qtpy.QtWidgets import QPushButton
 
-from napari._qt.layers.qt_image_base_layer import QtBaseImageControls
+from napari._qt.layers.qt_image_base_layer import (
+    QtBaseImageControls,
+    create_range_popup,
+)
 from napari.layers import Image, Surface
 
 _IMAGE = np.arange(100).astype(np.uint16).reshape((10, 10))
@@ -72,3 +75,22 @@ def test_range_popup_clim_buttons(qtbot, layer):
         assert tuple(qtctrl.contrastLimitsSlider.range()) == (0, 2 ** 16 - 1)
     else:
         assert rangebtn is None
+
+
+@pytest.mark.parametrize('mag', [-12, -9, -3, 0, 2, 4, 6])
+def test_clim_slider_step_size_and_precision(qtbot, mag):
+    """Make sure the slider has a reasonable step size and precision.
+
+    ...across a broad range of orders of magnitude.
+    """
+    layer = Image(np.random.rand(20, 20) / 10 ** mag)
+    popup = create_range_popup(layer, 'contrast_limits')
+
+    # 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
+    assert popup.precision == max(min(mag + 3, 5), 0)
+
+    # the slider step size should also be inversely proportional to the data
+    # range, with 1000 steps across the data range
+    assert np.ceil(popup.slider._step * 10 ** (mag + 4)) == 10