diff --git a/napari/_qt/layers/qt_image_base_layer.py b/napari/_qt/layers/qt_image_base_layer.py index 6a728b4f172..b0ea742e6bc 100644 --- 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/layers/tests/test_qt_image_base_layer_.py b/napari/_qt/layers/tests/test_qt_image_base_layer_.py index c84c70ee74c..50959f02768 100644 --- 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 diff --git a/napari/_qt/qt_range_slider.py b/napari/_qt/qt_range_slider.py index 56df381c51c..27facc007b6 100644 --- 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)