Skip to content

ndv #

Fast and flexible n-dimensional data viewer.

Modules:

  • controllers

    Controllers are the primary public interfaces that wrap models & views.

  • data

    Sample data for testing and examples.

  • models

    Models for ndv.

  • util

    Utility and convenience functions.

  • views

    Wrappers around GUI & graphics frameworks.

Classes:

  • ArrayViewer

    Viewer dedicated to displaying a single n-dimensional array.

  • DataWrapper

    Interface for wrapping different array-like data types.

Functions:

  • imshow

    Display an array or DataWrapper in a new ArrayViewer window.

  • run_app

    Start the active GUI application event loop.

ArrayViewer #

ArrayViewer(
    data: Any | DataWrapper = None,
    /,
    display_model: ArrayDisplayModel | None = None,
    **kwargs: Unpack[ArrayDisplayModelKwargs],
)

Viewer dedicated to displaying a single n-dimensional array.

This wraps a model and sview into a single object, and defines the public API.

See also

ndv.imshow - a convenience function that constructs and shows an ArrayViewer.

Future plans

In the future, ndv would like to support multiple, layered data sources with coordinate transforms. We reserve the name Viewer for a more fully featured viewer. ArrayViewer assumes you're viewing a single array.

Parameters:

  • data #

    ( DataWrapper | Any, default: None ) –

    Data to be displayed.

  • display_model #

    (ArrayDisplayModel, default: None ) –

    Just the display model to use. If provided, data_or_model must be an array or DataWrapper... and kwargs will be ignored.

  • **kwargs #

    (Unpack[ArrayDisplayModelKwargs], default: {} ) –

    Keyword arguments to pass to the ArrayDisplayModel constructor. If display_model is provided, these will be ignored.

Methods:

  • clone

    Return a new ArrayViewer instance with the same data and display model.

  • close

    Close the viewer.

  • hide

    Hide the viewer.

  • show

    Show the viewer.

  • widget

    Return the native front-end widget.

Attributes:

Source code in src/ndv/controllers/_array_viewer.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def __init__(
    self,
    data: Any | DataWrapper = None,
    /,
    display_model: ArrayDisplayModel | None = None,
    **kwargs: Unpack[ArrayDisplayModelKwargs],
) -> None:
    wrapper = None if data is None else DataWrapper.create(data)
    if display_model is None:
        display_model = self._default_display_model(wrapper, **kwargs)
    elif kwargs:
        warnings.warn(
            "When display_model is provided, kwargs are be ignored.",
            stacklevel=2,
        )

    self._data_model = _ArrayDataDisplayModel(
        data_wrapper=wrapper, display=display_model
    )
    self._viewer_model = ArrayViewerModel()
    self._viewer_model.events.interaction_mode.connect(
        self._on_interaction_mode_changed
    )
    self._roi_model: RectangularROIModel | None = None

    app = _app.gui_frontend()

    # whether to fetch data asynchronously.  Not publicly exposed yet...
    # but can use 'NDV_SYNCHRONOUS' env var to set globally
    # jupyter doesn't need async because it's already async (in that the
    # GUI is already running in JS)
    NDV_SYNCHRONOUS = os.getenv("NDV_SYNCHRONOUS", "0") in {"1", "True", "true"}
    self._async = not NDV_SYNCHRONOUS and app != _app.GuiFrontend.JUPYTER
    # set of futures for data requests
    self._futures: set[Future[DataResponse]] = set()

    # mapping of channel keys to their respective controllers
    # where None is the default channel
    self._lut_controllers: dict[ChannelKey, ChannelController] = {}

    # get and create the front-end and canvas classes
    frontend_cls = _app.get_array_view_class()
    canvas_cls = _app.get_array_canvas_class()
    self._canvas = canvas_cls(self._viewer_model)

    # TODO: Is this necessary?
    self._histograms: dict[ChannelKey, HistogramCanvas] = {}
    self._view = frontend_cls(
        self._canvas.frontend_widget(), self._data_model, self._viewer_model
    )

    self._roi_view: RectangularROIHandle | None = None

    self._set_model_connected(self._data_model.display)
    self._canvas.set_ndim(self.display_model.n_visible_axes)

    self._view.currentIndexChanged.connect(self._on_view_current_index_changed)
    self._view.resetZoomClicked.connect(self._on_view_reset_zoom_clicked)
    self._view.histogramRequested.connect(self._add_histogram)
    self._view.channelModeChanged.connect(self._on_view_channel_mode_changed)
    self._view.visibleAxesChanged.connect(self._on_view_visible_axes_changed)

    self._canvas.mouseMoved.connect(self._on_canvas_mouse_moved)

    if self._data_model.data_wrapper is not None:
        self._fully_synchronize_view()

data property writable #

data: Any

Return data being displayed.

data_wrapper property #

data_wrapper: Any

Return data being displayed.

display_model property writable #

display_model: ArrayDisplayModel

Return the current ArrayDisplayModel.

roi property writable #

roi: RectangularROIModel | None

Return ROI being displayed.

clone #

clone() -> ArrayViewer

Return a new ArrayViewer instance with the same data and display model.

Currently, this is a shallow copy. Modifying one viewer will affect the state of the other.

Source code in src/ndv/controllers/_array_viewer.py
212
213
214
215
216
217
218
219
220
221
def clone(self) -> ArrayViewer:
    """Return a new ArrayViewer instance with the same data and display model.

    Currently, this is a shallow copy.  Modifying one viewer will affect the state
    of the other.
    """
    # TODO: provide deep_copy option
    return ArrayViewer(
        self._data_model.data_wrapper, display_model=self.display_model
    )

close #

close() -> None

Close the viewer.

Source code in src/ndv/controllers/_array_viewer.py
208
209
210
def close(self) -> None:
    """Close the viewer."""
    self._view.set_visible(False)

hide #

hide() -> None

Hide the viewer.

Source code in src/ndv/controllers/_array_viewer.py
204
205
206
def hide(self) -> None:
    """Hide the viewer."""
    self._view.set_visible(False)

show #

show() -> None

Show the viewer.

Source code in src/ndv/controllers/_array_viewer.py
200
201
202
def show(self) -> None:
    """Show the viewer."""
    self._view.set_visible(True)

widget #

widget() -> Any

Return the native front-end widget.

Warning

If you directly manipulate the frontend widget, you're on your own 😄. No guarantees can be made about synchronization with the model. It is exposed for embedding in an application, and for experimentation and custom use cases. Please open an issue if you have questions.

Source code in src/ndv/controllers/_array_viewer.py
129
130
131
132
133
134
135
136
137
138
139
140
def widget(self) -> Any:
    """Return the native front-end widget.

    !!! Warning

        If you directly manipulate the frontend widget, you're on your own :smile:.
        No guarantees can be made about synchronization with the model.  It is
        exposed for embedding in an application, and for experimentation and custom
        use cases.  Please [open an
        issue](https://github.com/pyapp-kit/ndv/issues/new) if you have questions.
    """
    return self._view.frontend_widget()

DataWrapper #

DataWrapper(data: ArrayT)

Bases: Generic[ArrayT], ABC

Interface for wrapping different array-like data types.

DataWrapper.create() is a factory method that returns a DataWrapper instance for the given data type. If your datastore type is not supported, you may implement a new DataWrapper subclass to handle your data type. To do this, import and subclass DataWrapper, and (minimally) implement the supports and isel methods. Ensure that your class is imported before the DataWrapper.create method is called, and it will be automatically detected and used to wrap your data.

This base class provides basic support for numpy-like array types. If the data supports getitem and shape attributes, it will work. If the data does not support getitem, the subclass MUST implement the isel method. If the data does not have a shape attribute, the subclass MUST implement the dims and coords properties.

Methods:

  • clear_cache

    Clear any cached properties.

  • create

    Create a DataWrapper instance for the given data.

  • guess_channel_axis

    Return the (best guess) axis name for the channel dimension.

  • guess_z_axis

    Return the (best guess) axis name for the z (3rd spatial) dimension.

  • isel

    Return a slice of the data as a numpy array.

  • normalize_axis_key

    Return positive index for axis (which can be +/- int or str label).

  • sizes

    Return the sizes of the dimensions.

  • summary_info

    Return info label with information about the data.

  • supports

    Return True if this wrapper can handle the given object.

Attributes:

Source code in src/ndv/models/_data_wrapper.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(self, data: ArrayT) -> None:
    self._data = data
    if not hasattr(self._data, "__getitem__") and "isel" not in type(self).__dict__:
        raise NotImplementedError(
            "DataWrapper subclass MUST implement `isel` method if data does not "
            "support __getitem__."
        )

    has_shape = hasattr(self._data, "shape") and isinstance(self._data.shape, tuple)
    has_methods = "dims" in type(self).__dict__ and "coords" in type(self).__dict__
    if not has_shape and not has_methods:
        raise NotImplementedError(
            "DataWrapper subclass MUST implement `dims` and `coords` properties"
            " if data does not have a `shape` attribute or if the shape is not "
            "a tuple."
        )

axis_map cached property #

axis_map: Mapping[Hashable, int]

Mapping of ALL valid axis keys to normalized, positive integer keys.

coords property #

Return the coordinates for the data.

data property #

data: ArrayT

Return the data being wrapped.

dims property #

dims: tuple[Hashable, ...]

Return the dimension labels for the data.

dtype property #

dtype: dtype

Return the dtype for the data.

clear_cache #

clear_cache() -> None

Clear any cached properties.

Source code in src/ndv/models/_data_wrapper.py
288
289
290
291
def clear_cache(self) -> None:
    """Clear any cached properties."""
    if hasattr(self, "axis_map"):
        del self.axis_map

create classmethod #

create(data: ArrayT) -> DataWrapper[ArrayT]

Create a DataWrapper instance for the given data.

This method will detect all subclasses of DataWrapper and check them in order of their PRIORITY class variable. The first subclass that supports the given data will be used to wrap it.

Tip

This means that you can subclass DataWrapper to handle new data types. Just make sure that your subclass is imported before calling create.

If no subclasses support the data, a NotImplementedError is raised.

If an instance of DataWrapper is passed in, it will be returned as-is.

Source code in src/ndv/models/_data_wrapper.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@classmethod
def create(cls, data: ArrayT) -> DataWrapper[ArrayT]:
    """Create a DataWrapper instance for the given data.

    This method will detect all subclasses of DataWrapper and check them in order of
    their `PRIORITY` class variable. The first subclass that
    [`supports`][ndv.DataWrapper.supports] the given data will be used to wrap it.

    !!! tip

        This means that you can subclass DataWrapper to handle new data types.
        Just make sure that your subclass is imported before calling `create`.

    If no subclasses support the data, a `NotImplementedError` is raised.

    If an instance of `DataWrapper` is passed in, it will be returned as-is.
    """
    if isinstance(data, DataWrapper):
        return data

    # check subclasses for support
    # This allows users to define their own DataWrapper subclasses which will
    # be automatically detected (assuming they have been imported by this point)
    for subclass in sorted(_recurse_subclasses(cls), key=lambda x: x.PRIORITY):
        try:
            if subclass.supports(data):
                logging.debug(f"Using {subclass.__name__} to wrap {type(data)}")
                return subclass(data)
        except Exception as e:
            warnings.warn(
                f"Error checking DataWrapper subclass {subclass.__name__}: {e}",
                RuntimeWarning,
                stacklevel=2,
            )
    raise NotImplementedError(f"Don't know how to wrap type {type(data)}")

guess_channel_axis #

guess_channel_axis() -> Hashable | None

Return the (best guess) axis name for the channel dimension.

Source code in src/ndv/models/_data_wrapper.py
215
216
217
218
219
220
221
222
223
224
225
226
def guess_channel_axis(self) -> Hashable | None:
    """Return the (best guess) axis name for the channel dimension."""
    # for arrays with labeled dimensions,
    # see if any of the dimensions are named "channel"
    sizes = self.sizes()
    for dimkey, val in sizes.items():
        if str(dimkey).lower() in self.COMMON_CHANNEL_NAMES:
            if val <= self.MAX_CHANNELS:
                return self.normalize_axis_key(dimkey)

    # otherwise use the smallest dimension as the channel axis
    return min(sizes, key=sizes.get)  # type: ignore [arg-type]

guess_z_axis #

guess_z_axis() -> Hashable | None

Return the (best guess) axis name for the z (3rd spatial) dimension.

Source code in src/ndv/models/_data_wrapper.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def guess_z_axis(self) -> Hashable | None:
    """Return the (best guess) axis name for the z (3rd spatial) dimension."""
    sizes = self.sizes()
    ch = self.guess_channel_axis()
    for dimkey in sizes:
        if str(dimkey).lower() in self.COMMON_Z_AXIS_NAMES:
            if (normed := self.normalize_axis_key(dimkey)) != ch:
                return normed

    # otherwise return the LAST axis that is neither in the last two dimensions
    # or the channel axis guess
    return next(
        (self.normalize_axis_key(x) for x in reversed(self.dims[:-2]) if x != ch),
        None,
    )

isel #

isel(index: Mapping[int, int | slice]) -> ndarray

Return a slice of the data as a numpy array.

index will look like (e.g.) {0: slice(0, 10), 1: 5}. The default implementation converts the index to a tuple of the same length as the self.dims, populating missing keys with slice(None), and then slices the data array using getitem.

Source code in src/ndv/models/_data_wrapper.py
122
123
124
125
126
127
128
129
130
131
132
133
134
def isel(self, index: Mapping[int, int | slice]) -> np.ndarray:
    """Return a slice of the data as a numpy array.

    `index` will look like (e.g.) `{0: slice(0, 10), 1: 5}`.
    The default implementation converts the index to a tuple of the same length as
    the self.dims, populating missing keys with `slice(None)`, and then slices the
    data array using __getitem__.
    """
    idx = tuple(index.get(k, slice(None)) for k in range(len(self.dims)))
    # this type ignore is asserted in the __init__ method
    # if the data does not support __getitem__, then the DataWrapper subclass will
    # fail to initialize
    return self._asarray(self._data[idx])  # type: ignore [index]

normalize_axis_key #

normalize_axis_key(axis: Hashable) -> int

Return positive index for axis (which can be +/- int or str label).

Source code in src/ndv/models/_data_wrapper.py
276
277
278
279
280
281
282
283
284
285
286
def normalize_axis_key(self, axis: Hashable) -> int:
    """Return positive index for `axis` (which can be +/- int or str label)."""
    try:
        return self.axis_map[axis]
    except KeyError as e:
        ndims = len(self.dims)
        if isinstance(axis, int):
            raise IndexError(
                f"Axis index {axis} out of bounds for data with {ndims} dimensions"
            ) from e
        raise IndexError(f"Axis label {axis} not found in data dimensions") from e

sizes #

sizes() -> Mapping[Hashable, int]

Return the sizes of the dimensions.

Source code in src/ndv/models/_data_wrapper.py
208
209
210
def sizes(self) -> Mapping[Hashable, int]:
    """Return the sizes of the dimensions."""
    return {dim: len(self.coords[dim]) for dim in self.dims}

summary_info #

summary_info() -> str

Return info label with information about the data.

Source code in src/ndv/models/_data_wrapper.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def summary_info(self) -> str:
    """Return info label with information about the data."""
    package = getattr(self._data, "__module__", "").split(".")[0]
    info = f"{package}.{getattr(type(self._data), '__qualname__', '')}"

    if sizes := self.sizes():
        # if all of the dimension keys are just integers, omit them from size_str
        if all(isinstance(x, int) for x in sizes):
            size_str = repr(tuple(sizes.values()))
        # otherwise, include the keys in the size_str
        else:
            size_str = ", ".join(f"{k}:{v}" for k, v in sizes.items())
            size_str = f"({size_str})"
        info += f" {size_str}"
    if dtype := getattr(self._data, "dtype", ""):
        info += f", {dtype}"
    if nbytes := getattr(self._data, "nbytes", 0):
        info += f", {_human_readable_size(nbytes)}"
    return info

supports abstractmethod classmethod #

supports(obj: Any) -> TypeGuard[Any]

Return True if this wrapper can handle the given object.

Any exceptions raised by this method will be suppressed, so it is safe to directly import necessary dependencies without a try/except block.

Source code in src/ndv/models/_data_wrapper.py
100
101
102
103
104
105
106
107
@classmethod
@abstractmethod
def supports(cls, obj: Any) -> TypeGuard[Any]:
    """Return True if this wrapper can handle the given object.

    Any exceptions raised by this method will be suppressed, so it is safe to
    directly import necessary dependencies without a try/except block.
    """

imshow #

imshow(
    data: Any | DataWrapper,
    /,
    **kwargs: Unpack[ArrayDisplayModelKwargs],
) -> ArrayViewer
imshow(
    data: Any | DataWrapper,
    /,
    display_model: ArrayDisplayModel | None = None,
    **kwargs: Unpack[ArrayDisplayModelKwargs],
) -> ArrayViewer

Display an array or DataWrapper in a new ArrayViewer window.

This convenience function creates an ArrayViewer instance populated with data, calls show() on it, and then runs the application.

Parameters:

  • data #

    (Any | DataWrapper) –

    The data to be displayed. Any ArrayLike object or an ndv.DataWrapper.

  • display_model #

    (ArrayDisplayModel | None, default: None ) –

    The display model to use. If not provided, a new one will be created.

  • kwargs #

    (Unpack[ArrayDisplayModelKwargs], default: {} ) –

    Additional keyword arguments used to create the ArrayDisplayModel.

Returns:

Source code in src/ndv/util.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def imshow(
    data: Any | DataWrapper,
    /,
    display_model: ArrayDisplayModel | None = None,
    **kwargs: Unpack[ArrayDisplayModelKwargs],
) -> ArrayViewer:
    """Display an array or DataWrapper in a new `ArrayViewer` window.

    This convenience function creates an `ArrayViewer` instance populated with `data`,
    calls `show()` on it, and then runs the application.

    Parameters
    ----------
    data : Any | DataWrapper
        The data to be displayed. Any ArrayLike object or an `ndv.DataWrapper`.
    display_model: ArrayDisplayModel, optional
        The display model to use. If not provided, a new one will be created.
    kwargs : Unpack[ArrayDisplayModelKwargs]
        Additional keyword arguments used to create the
        [`ArrayDisplayModel`][ndv.models.ArrayDisplayModel].

    Returns
    -------
    ArrayViewer
        The `ArrayViewer` instance.
    """
    viewer = ArrayViewer(data, display_model, **kwargs)
    viewer.show()

    run_app()
    return viewer

run_app #

run_app() -> None

Start the active GUI application event loop.

Source code in src/ndv/views/_app.py
312
313
314
def run_app() -> None:
    """Start the active GUI application event loop."""
    ndv_app().run()