Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Oct 17, 2024
1 parent 069fdc8 commit 0e65f93
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 6 deletions.
13 changes: 13 additions & 0 deletions mvc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import numpy as np
from qtpy.QtWidgets import QApplication

from ndv.v2ctl import ViewerController
from ndv.v2view import ViewerView

app = QApplication([])

viewer = ViewerController(ViewerView()) # ultimately, this will be the public api
model = viewer.model
viewer.data = np.random.rand(96, 64, 128).astype(np.float32)
viewer.view.show() # temp
app.exec()
152 changes: 152 additions & 0 deletions src/ndv/v2ctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from collections.abc import Container, Hashable, Mapping, Sequence
from typing import Any, Protocol

Check warning on line 2 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L1-L2

Added lines #L1 - L2 were not covered by tests

from psygnal import SignalInstance

Check warning on line 4 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L4

Added line #L4 was not covered by tests

from .models._array_display_model import ArrayDisplayModel, AxisKey
from .viewer._backends._protocols import PImageHandle
from .viewer._data_wrapper import DataWrapper

Check warning on line 8 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L6-L8

Added lines #L6 - L8 were not covered by tests


class ViewP(Protocol):
currentIndexChanged: SignalInstance

Check warning on line 12 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L11-L12

Added lines #L11 - L12 were not covered by tests

def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: ...
def current_index(self) -> Mapping[AxisKey, int]: ...
def set_current_index(self, value: Mapping[AxisKey, int | slice]) -> None: ...
def add_image_to_canvas(self, data: Any) -> PImageHandle: ...
def hide_sliders(
self, axes_to_hide: Container[Hashable], *, show_remainder: bool = ...
) -> None: ...


class ViewerController:
_data_wrapper: DataWrapper | None
_display_model: ArrayDisplayModel

Check warning on line 25 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L23-L25

Added lines #L23 - L25 were not covered by tests

def __init__(self, view: ViewP, model: ArrayDisplayModel | None = None) -> None:
self.view = view
self.model = model or ArrayDisplayModel()
self._data_wrapper = None
self.view.currentIndexChanged.connect(self.on_slider_value_changed)

Check warning on line 31 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L27-L31

Added lines #L27 - L31 were not covered by tests

@property
def model(self) -> ArrayDisplayModel:

Check warning on line 34 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L33-L34

Added lines #L33 - L34 were not covered by tests
"""Return the display model for the viewer."""
return self._display_model

Check warning on line 36 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L36

Added line #L36 was not covered by tests

@model.setter
def model(self, display_model: ArrayDisplayModel) -> None:

Check warning on line 39 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L38-L39

Added lines #L38 - L39 were not covered by tests
"""Set the display model for the viewer."""
display_model = ArrayDisplayModel.model_validate(display_model)
previous_model: ArrayDisplayModel | None = getattr(self, "_display_model", None)
if previous_model is not None:
self._set_model_connected(previous_model, False)

Check warning on line 44 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L41-L44

Added lines #L41 - L44 were not covered by tests

self._display_model = display_model
self._set_model_connected(display_model)

Check warning on line 47 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L46-L47

Added lines #L46 - L47 were not covered by tests

def _set_model_connected(

Check warning on line 49 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L49

Added line #L49 was not covered by tests
self, model: ArrayDisplayModel, connect: bool = True
) -> None:
"""Connect or disconnect the model to/from the viewer.
We do this in a single method so that we are sure to connect and disconnect
the same events in the same order.
"""
_connect = "connect" if connect else "disconnect"

Check warning on line 57 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L57

Added line #L57 was not covered by tests

for obj, callback in [
# (model.events.visible_axes, self._on_visible_axes_changed),
# the current_index attribute itself is immutable
(model.current_index.value_changed, self._on_current_index_changed),
# (model.events.channel_axis, self._on_channel_axis_changed),
# TODO: lut values themselves are mutable evented objects...
# so we need to connect to their events as well
# (model.luts.value_changed, self._on_luts_changed),
]:
getattr(obj, _connect)(callback)

def _on_current_index_changed(self) -> None:
value = self.model.current_index
self.view.set_current_index(value)
self._update_canvas()

Check warning on line 73 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L70-L73

Added lines #L70 - L73 were not covered by tests

@property
def data(self) -> Any:

Check warning on line 76 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L75-L76

Added lines #L75 - L76 were not covered by tests
"""Return data being displayed."""
if self._data_wrapper is None:
return None
return self._data_wrapper.data

Check warning on line 80 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L78-L80

Added lines #L78 - L80 were not covered by tests

@data.setter
def data(self, data: Any) -> None:

Check warning on line 83 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L82-L83

Added lines #L82 - L83 were not covered by tests
"""Set the data to be displayed."""
if data is None:
self._data_wrapper = None
return
self._data_wrapper = DataWrapper.create(data)
dims = self._data_wrapper.dims
coords = {

Check warning on line 90 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L85-L90

Added lines #L85 - L90 were not covered by tests
self._canonicalize_axis_key(ax, dims): c
for ax, c in self._data_wrapper.coords.items()
}
self.view.create_sliders(coords)
self._update_visible_sliders()
self._update_canvas()

Check warning on line 96 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L94-L96

Added lines #L94 - L96 were not covered by tests

def on_slider_value_changed(self) -> None:

Check warning on line 98 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L98

Added line #L98 was not covered by tests
"""Update the model when slider value changes."""
slider_values = self.view.current_index()
self.model.current_index.update(slider_values)
return

Check warning on line 102 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L100-L102

Added lines #L100 - L102 were not covered by tests
self._update_canvas()

def _update_canvas(self) -> None:
if not self._data_wrapper:
return
idx_request = self._current_index_request()
data = self._data_wrapper.isel(idx_request)
if hdl := getattr(self, "_handle", None):
hdl.remove()
self._handle = self.view.add_image_to_canvas(data)

Check warning on line 112 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L105-L112

Added lines #L105 - L112 were not covered by tests

def _current_index_request(self) -> Mapping[int, int | slice]:

Check warning on line 114 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L114

Added line #L114 was not covered by tests
# Generate cannocalized index request
if self._data_wrapper is None:
return {}

Check warning on line 117 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L116-L117

Added lines #L116 - L117 were not covered by tests

dims = self._data_wrapper.dims
idx_request = {

Check warning on line 120 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L119-L120

Added lines #L119 - L120 were not covered by tests
self._canonicalize_axis_key(ax, dims): v
for ax, v in self.model.current_index.items()
}
for ax in self.model.visible_axes:
ax_ = self._canonicalize_axis_key(ax, dims)
if not isinstance(idx_request.get(ax_), slice):
idx_request[ax_] = slice(None)
return idx_request

Check warning on line 128 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L124-L128

Added lines #L124 - L128 were not covered by tests

def _update_visible_sliders(self) -> None:

Check warning on line 130 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L130

Added line #L130 was not covered by tests
"""Update which sliders are visible based on the current model."""
dims = self._data_wrapper.dims
visible_axes = {

Check warning on line 133 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L132-L133

Added lines #L132 - L133 were not covered by tests
self._canonicalize_axis_key(ax, dims) for ax in self.model.visible_axes
}
self.view.hide_sliders(visible_axes, show_remainder=True)

Check warning on line 136 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L136

Added line #L136 was not covered by tests

def _canonicalize_axis_key(self, axis: AxisKey, dims: Sequence[Hashable]) -> int:

Check warning on line 138 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L138

Added line #L138 was not covered by tests
"""Return positive index for AxisKey (which can be +/- int or label)."""
# TODO: improve performance by indexing ahead of time
if isinstance(axis, int):
ndims = len(dims)
ax = axis if axis >= 0 else len(dims) + axis
if ax >= ndims:
raise IndexError(

Check warning on line 145 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L141-L145

Added lines #L141 - L145 were not covered by tests
f"Axis index {axis} out of bounds for data with {ndims} dimensions"
)
return ax
try:
return dims.index(axis)
except ValueError as e:
raise IndexError(f"Axis label {axis} not found in data dimensions") from e

Check warning on line 152 in src/ndv/v2ctl.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2ctl.py#L148-L152

Added lines #L148 - L152 were not covered by tests
66 changes: 66 additions & 0 deletions src/ndv/v2view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from collections.abc import Container, Hashable, Mapping, Sequence
from typing import Any

Check warning on line 2 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L1-L2

Added lines #L1 - L2 were not covered by tests

from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QFormLayout, QVBoxLayout, QWidget
from superqt import QLabeledSlider

Check warning on line 6 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L4-L6

Added lines #L4 - L6 were not covered by tests

from .models._array_display_model import AxisKey
from .viewer._backends import get_canvas_class
from .viewer._backends._protocols import PImageHandle

Check warning on line 10 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L8-L10

Added lines #L8 - L10 were not covered by tests


class ViewerView(QWidget):
currentIndexChanged = Signal()

Check warning on line 14 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L13-L14

Added lines #L13 - L14 were not covered by tests

def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self._sliders: dict[Hashable, QLabeledSlider] = {}
self._canvas = get_canvas_class()()
self._canvas.set_ndim(2)
layout = QVBoxLayout(self)
self._slider_layout = QFormLayout()
self._slider_layout.setFieldGrowthPolicy(

Check warning on line 23 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L16-L23

Added lines #L16 - L23 were not covered by tests
QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow
)
layout.addWidget(self._canvas.qwidget())
layout.addLayout(self._slider_layout)

Check warning on line 27 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L26-L27

Added lines #L26 - L27 were not covered by tests

def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None:

Check warning on line 29 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L29

Added line #L29 was not covered by tests
"""Update sliders with the given coordinate ranges."""
for axis, _coords in coords.items():
sld = QLabeledSlider(Qt.Orientation.Horizontal)
sld.valueChanged.connect(self.currentIndexChanged.emit)
if isinstance(_coords, range):
sld.setRange(_coords.start, _coords.stop - 1)
sld.setSingleStep(_coords.step)
self._slider_layout.addRow(str(axis), sld)
self._sliders[axis] = sld
self.currentIndexChanged.emit()

Check warning on line 39 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L31-L39

Added lines #L31 - L39 were not covered by tests

def add_image_to_canvas(self, data: Any) -> PImageHandle:

Check warning on line 41 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L41

Added line #L41 was not covered by tests
"""Add image data to the canvas."""
hdl = self._canvas.add_image(data)
self._canvas.set_range()
return hdl

Check warning on line 45 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L43-L45

Added lines #L43 - L45 were not covered by tests

def hide_sliders(

Check warning on line 47 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L47

Added line #L47 was not covered by tests
self, axes_to_hide: Container[Hashable], show_remainder: bool = True
) -> None:
"""Hide sliders based on visible axes."""
for ax, slider in self._sliders.items():
if ax in axes_to_hide:
self._slider_layout.setRowVisible(slider, False)
elif show_remainder:
self._slider_layout.setRowVisible(slider, True)

Check warning on line 55 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L51-L55

Added lines #L51 - L55 were not covered by tests

def current_index(self) -> Mapping[AxisKey, int | slice]:

Check warning on line 57 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L57

Added line #L57 was not covered by tests
"""Return the current value of the sliders."""
return {axis: slider.value() for axis, slider in self._sliders.items()}

Check warning on line 59 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L59

Added line #L59 was not covered by tests

def set_current_index(self, value: Mapping[AxisKey, int | slice]) -> None:

Check warning on line 61 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L61

Added line #L61 was not covered by tests
"""Set the current value of the sliders."""
for axis, val in value.items():
if isinstance(val, slice):

Check warning on line 64 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L63-L64

Added lines #L63 - L64 were not covered by tests
raise NotImplementedError("Slices are not supported yet")
self._sliders[axis].setValue(val)

Check warning on line 66 in src/ndv/v2view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/v2view.py#L66

Added line #L66 was not covered by tests
3 changes: 3 additions & 0 deletions src/ndv/viewer/_backends/_vispy.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,9 @@ def set_range(
When called with no arguments, the range is set to the full extent of the data.
"""
# temporary
self._camera.set_range()
return
_x = [0.0, 0.0]
_y = [0.0, 0.0]
_z = [0.0, 0.0]
Expand Down
2 changes: 1 addition & 1 deletion src/ndv/viewer_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,10 @@ def _update_canvas(self) -> None:
return
idx_request = self._current_index_request()
data = self._data_wrapper.isel(idx_request)

if hdl := getattr(self, "_handle", None):
hdl.remove()
self._handle = self._canvas.add_image(data)
self._canvas.set_range()

Check warning on line 219 in src/ndv/viewer_v2.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/viewer_v2.py#L212-L219

Added lines #L212 - L219 were not covered by tests

def _on_channel_axis_changed(self, value: AxisKey) -> None:
print("Channel axis changed:", value)

Check warning on line 222 in src/ndv/viewer_v2.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/viewer_v2.py#L221-L222

Added lines #L221 - L222 were not covered by tests
Expand Down
8 changes: 3 additions & 5 deletions x.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import numpy as np
from qtpy.QtWidgets import QApplication
from rich import print

from ndv.viewer_v2 import Viewer

app = QApplication([])

v = Viewer()
v.data = np.random.rand(8, 64, 128)
v.data = np.random.rand(96, 64, 128).astype(np.float32)
v.model.luts[1] = "viridis"

# v.model.visible_axes = (0, 1)
print(v.model)
v.model.visible_axes = (-2, -1)
# print(v.model)
v.show()
v.model.current_index.update({0: 3, 1: 32, 2: 12})
app.exec()

0 comments on commit 0e65f93

Please sign in to comment.