Skip to content

Commit 3a8261a

Browse files
authored
Merge pull request #208 from dstansby/fix-slicing
Improve slicing experience
2 parents d7e88a9 + 9eb43ee commit 3a8261a

File tree

3 files changed

+80
-39
lines changed

3 files changed

+80
-39
lines changed

Diff for: docs/changelog.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@ Changelog
22
=========
33
1.0.3
44
-----
5+
Changes
6+
~~~~~~~
7+
- The slice widget is now limited to slicing along the x/y dimensions. Support
8+
for slicing along z has been removed for now to make the code simpler.
9+
- The slice widget now uses a slider to select the slice value.
10+
511
Bug fixes
612
~~~~~~~~~
713
- Fixed creating 1D slices of 2D images.
14+
- Removed the limitation that only the first 99 indices could be sliced using
15+
the slice widget.
816

917
1.0.2
1018
-----

Diff for: src/napari_matplotlib/slice.py

+48-39
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
from typing import Any, Dict, List, Optional, Tuple
1+
from typing import Any, List, Optional, Tuple
22

33
import matplotlib.ticker as mticker
44
import napari
55
import numpy as np
66
import numpy.typing as npt
7-
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSpinBox, QWidget
7+
from qtpy.QtCore import Qt
8+
from qtpy.QtWidgets import (
9+
QComboBox,
10+
QLabel,
11+
QSlider,
12+
QVBoxLayout,
13+
QWidget,
14+
)
815

916
from .base import SingleAxesWidget
1017
from .util import Interval
1118

1219
__all__ = ["SliceWidget"]
1320

14-
_dims_sel = ["x", "y"]
15-
1621

1722
class SliceWidget(SingleAxesWidget):
1823
"""
@@ -30,28 +35,46 @@ def __init__(
3035
# Setup figure/axes
3136
super().__init__(napari_viewer, parent=parent)
3237

33-
button_layout = QHBoxLayout()
34-
self.layout().addLayout(button_layout)
35-
3638
self.dim_selector = QComboBox()
39+
self.dim_selector.addItems(["x", "y"])
40+
41+
self.slice_selector = QSlider(orientation=Qt.Orientation.Horizontal)
42+
43+
# Create widget layout
44+
button_layout = QVBoxLayout()
3745
button_layout.addWidget(QLabel("Slice axis:"))
3846
button_layout.addWidget(self.dim_selector)
39-
self.dim_selector.addItems(["x", "y", "z"])
40-
41-
self.slice_selectors = {}
42-
for d in _dims_sel:
43-
self.slice_selectors[d] = QSpinBox()
44-
button_layout.addWidget(QLabel(f"{d}:"))
45-
button_layout.addWidget(self.slice_selectors[d])
47+
button_layout.addWidget(self.slice_selector)
48+
self.layout().addLayout(button_layout)
4649

4750
# Setup callbacks
48-
# Re-draw when any of the combon/spin boxes are updated
51+
# Re-draw when any of the combo/slider is updated
4952
self.dim_selector.currentTextChanged.connect(self._draw)
50-
for d in _dims_sel:
51-
self.slice_selectors[d].textChanged.connect(self._draw)
53+
self.slice_selector.valueChanged.connect(self._draw)
5254

5355
self._update_layers(None)
5456

57+
def on_update_layers(self) -> None:
58+
"""
59+
Called when layer selection is updated.
60+
"""
61+
if not len(self.layers):
62+
return
63+
if self.current_dim_name == "x":
64+
max = self._layer.data.shape[-2]
65+
elif self.current_dim_name == "y":
66+
max = self._layer.data.shape[-1]
67+
else:
68+
raise RuntimeError("dim name must be x or y")
69+
self.slice_selector.setRange(0, max - 1)
70+
71+
@property
72+
def _slice_width(self) -> int:
73+
"""
74+
Width of the slice being plotted.
75+
"""
76+
return self._layer.data.shape[self.current_dim_index]
77+
5578
@property
5679
def _layer(self) -> napari.layers.Layer:
5780
"""
@@ -73,7 +96,7 @@ def current_dim_index(self) -> int:
7396
"""
7497
# Note the reversed list because in napari the z-axis is the first
7598
# numpy axis
76-
return self._dim_names[::-1].index(self.current_dim_name)
99+
return self._dim_names.index(self.current_dim_name)
77100

78101
@property
79102
def _dim_names(self) -> List[str]:
@@ -82,45 +105,31 @@ def _dim_names(self) -> List[str]:
82105
dimensionality of the currently selected data.
83106
"""
84107
if self._layer.data.ndim == 2:
85-
return ["x", "y"]
108+
return ["y", "x"]
86109
elif self._layer.data.ndim == 3:
87-
return ["x", "y", "z"]
110+
return ["z", "y", "x"]
88111
else:
89112
raise RuntimeError("Don't know how to handle ndim != 2 or 3")
90113

91-
@property
92-
def _selector_values(self) -> Dict[str, int]:
93-
"""
94-
Values of the slice selectors.
95-
96-
Mapping from dimension name to value.
97-
"""
98-
return {d: self.slice_selectors[d].value() for d in _dims_sel}
99-
100114
def _get_xy(self) -> Tuple[npt.NDArray[Any], npt.NDArray[Any]]:
101115
"""
102116
Get data for plotting.
103117
"""
104-
dim_index = self.current_dim_index
105-
if self._layer.data.ndim == 2:
106-
dim_index -= 1
107-
x = np.arange(self._layer.data.shape[dim_index])
108-
109-
vals = self._selector_values
110-
vals.update({"z": self.current_z})
118+
val = self.slice_selector.value()
111119

112120
slices = []
113121
for dim_name in self._dim_names:
114122
if dim_name == self.current_dim_name:
115123
# Select all data along this axis
116124
slices.append(slice(None))
125+
elif dim_name == "z":
126+
# Only select the currently viewed z-index
127+
slices.append(slice(self.current_z, self.current_z + 1))
117128
else:
118129
# Select specific index
119-
val = vals[dim_name]
120130
slices.append(slice(val, val + 1))
121131

122-
# Reverse since z is the first axis in napari
123-
slices = slices[::-1]
132+
x = np.arange(self._slice_width)
124133
y = self._layer.data[tuple(slices)].ravel()
125134

126135
return x, y

Diff for: src/napari_matplotlib/tests/test_slice.py

+24
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,27 @@ def test_slice_2D(make_napari_viewer, astronaut_data):
3737
# Need to return a copy, as original figure is too eagerley garbage
3838
# collected by the widget
3939
return deepcopy(fig)
40+
41+
42+
def test_slice_axes(make_napari_viewer, astronaut_data):
43+
viewer = make_napari_viewer()
44+
viewer.theme = "light"
45+
46+
# Take first RGB channel
47+
data = astronaut_data[0][:256, :, 0]
48+
# Shape:
49+
# x: 0 > 512
50+
# y: 0 > 256
51+
assert data.ndim == 2, data.shape
52+
# Make sure data isn't square for later tests
53+
assert data.shape[0] != data.shape[1]
54+
viewer.add_image(data)
55+
56+
widget = SliceWidget(viewer)
57+
assert widget._dim_names == ["y", "x"]
58+
assert widget.current_dim_name == "x"
59+
assert widget.slice_selector.value() == 0
60+
assert widget.slice_selector.minimum() == 0
61+
assert widget.slice_selector.maximum() == data.shape[0] - 1
62+
# x/y are flipped in napari
63+
assert widget._slice_width == data.shape[1]

0 commit comments

Comments
 (0)