Skip to content

Commit 10abe13

Browse files
authored
fix: make sure it's ok to set_data to None and generally have an empty viewer (#43)
* fix: allow Null data * clear current index
1 parent 8f9212d commit 10abe13

File tree

3 files changed

+40
-7
lines changed

3 files changed

+40
-7
lines changed

src/ndv/viewer/_dims_slider.py

+7
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,13 @@ def remove_dimension(self, key: DimKey) -> None:
507507
cast("QVBoxLayout", self.layout()).removeWidget(slider)
508508
slider.deleteLater()
509509

510+
def clear(self) -> None:
511+
"""Remove all dimensions from the DimsSliders widget."""
512+
for key in list(self._sliders):
513+
self.remove_dimension(key)
514+
self._current_index = {}
515+
self._invisible_dims = set()
516+
510517
def _on_dim_slider_value_changed(self, key: DimKey, value: Index) -> None:
511518
self._current_index[key] = value
512519
self.valueChanged.emit(self.value())

src/ndv/viewer/_viewer.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def __init__(
122122
super().__init__(parent=parent)
123123

124124
# ATTRIBUTES ----------------------------------------------------
125+
self._data_wrapper: DataWrapper | None = None
125126

126127
# mapping of key to a list of objects that control image nodes in the canvas
127128
self._img_handles: defaultdict[ImgKey, list[PImageHandle]] = defaultdict(list)
@@ -235,8 +236,7 @@ def __init__(
235236
# SETUP ------------------------------------------------------
236237

237238
self.set_channel_mode(channel_mode)
238-
if data is not None:
239-
self.set_data(data)
239+
self.set_data(data)
240240

241241
# ------------------- PUBLIC API ----------------------------
242242
@property
@@ -245,13 +245,15 @@ def dims_sliders(self) -> DimsSliders:
245245
return self._dims_sliders
246246

247247
@property
248-
def data_wrapper(self) -> DataWrapper:
248+
def data_wrapper(self) -> DataWrapper | None:
249249
"""Return the DataWrapper object around the datastore."""
250250
return self._data_wrapper
251251

252252
@property
253253
def data(self) -> Any:
254254
"""Return the data backing the view."""
255+
if self._data_wrapper is None:
256+
return None
255257
return self._data_wrapper.data
256258

257259
@data.setter
@@ -277,6 +279,14 @@ def set_data(
277279
or slices that define the slice of the data to display. If not provided,
278280
the initial index will be set to the middle of the data.
279281
"""
282+
# clear current data
283+
if data is None:
284+
self._data_wrapper = None
285+
self._clear_images()
286+
self._dims_sliders.clear()
287+
self._data_info_label.setText("")
288+
return
289+
280290
# store the data
281291
self._data_wrapper = DataWrapper.create(data)
282292

@@ -351,6 +361,9 @@ def set_ndim(self, ndim: Literal[2, 3]) -> None:
351361
self._ndims = ndim
352362
self._canvas.set_ndim(ndim)
353363

364+
if self._data_wrapper is None:
365+
return
366+
354367
# set the visibility of the last non-channel dimension
355368
sizes = list(self._data_wrapper.sizes())
356369
if self._channel_axis is not None:
@@ -360,8 +373,7 @@ def set_ndim(self, ndim: Literal[2, 3]) -> None:
360373
self._dims_sliders.set_dimension_visible(dim3, True if ndim == 2 else False)
361374

362375
# clear image handles and redraw
363-
if self._img_handles:
364-
self.refresh()
376+
self.refresh()
365377

366378
def set_channel_mode(self, mode: ChannelMode | str | None = None) -> None:
367379
"""Set the mode for displaying the channels.
@@ -393,8 +405,7 @@ def set_channel_mode(self, mode: ChannelMode | str | None = None) -> None:
393405
self._channel_axis, mode != ChannelMode.COMPOSITE
394406
)
395407

396-
if self._img_handles:
397-
self.refresh()
408+
self.refresh()
398409

399410
def refresh(self) -> None:
400411
"""Refresh the canvas."""
@@ -439,6 +450,9 @@ def _update_slider_ranges(self) -> None:
439450
440451
If `sizes` is not provided, sizes will be inferred from the datastore.
441452
"""
453+
if self._data_wrapper is None:
454+
return
455+
442456
maxes = self._data_wrapper.sizes()
443457
self._dims_sliders.setMaxima({k: v - 1 for k, v in maxes.items()})
444458

@@ -472,6 +486,8 @@ def _update_data_for_index(self, index: Indices) -> None:
472486
makes a request for the new data slice and queues _on_data_future_done to be
473487
called when the data is ready.
474488
"""
489+
if self._data_wrapper is None:
490+
return
475491
if (
476492
self._channel_axis is not None
477493
and self._channel_mode == ChannelMode.COMPOSITE

tests/test_nd_viewer.py

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ def allow_linux_widget_leaks(func: Any) -> Any:
2727
BACKENDS.append("pygfx")
2828

2929

30+
def test_empty_viewer(qtbot: QtBot) -> None:
31+
viewer = NDViewer()
32+
qtbot.add_widget(viewer)
33+
viewer.refresh()
34+
viewer.set_data(np.random.rand(4, 3, 32, 32))
35+
assert isinstance(viewer.data, np.ndarray)
36+
viewer.set_data(None)
37+
assert viewer.data is None
38+
39+
3040
@allow_linux_widget_leaks
3141
@pytest.mark.parametrize("backend", BACKENDS)
3242
def test_ndviewer(qtbot: QtBot, backend: str, monkeypatch: pytest.MonkeyPatch) -> None:

0 commit comments

Comments
 (0)