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:

  • call_later

    Call func after msec milliseconds.

  • imshow

    Display an array or DataWrapper in a new ArrayViewer window.

  • process_events

    Force processing of events for the application.

  • run_app

    Start the active GUI application event loop.

  • set_canvas_backend

    Sets the preferred canvas backend. Cannot be set after the GUI is running.

  • set_gui_backend

    Sets the preferred GUI backend. Cannot be set after the GUI is running.

ArrayViewer #

ArrayViewer(
    data: Any | DataWrapper = None,
    /,
    *,
    viewer_options: ArrayViewerModel
    | ArrayViewerModelKwargs
    | None = 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
 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
122
123
124
125
126
127
128
129
130
131
def __init__(
    self,
    data: Any | DataWrapper = None,
    /,
    *,
    viewer_options: ArrayViewerModel | ArrayViewerModelKwargs | None = 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(
        display=display_model, data_wrapper=wrapper
    )
    self._connect_datawrapper(None, wrapper)

    self._viewer_model = ArrayViewerModel.model_validate(viewer_options or {})
    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._highlight_pos: tuple[int, int] | None = None
    self._canvas.mouseMoved.connect(self._on_canvas_mouse_moved)
    self._canvas.mouseLeft.connect(self._on_canvas_mouse_left)

    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
229
230
231
232
233
234
235
236
237
238
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
225
226
227
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
221
222
223
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
217
218
219
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
135
136
137
138
139
140
141
142
143
144
145
146
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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."
        )
    self.dims_changed.connect(self.clear_cache)

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.

data_changed class-attribute instance-attribute #

data_changed = Signal()

Signal emitted when the data changes.

NOTE: It is up to data wrappers, or even end-users to emit this signal when the data object changes. We do not currently use object proxies to spy on mutation of the underlying data.

dims property #

dims: tuple[Hashable, ...]

Return the dimension labels for the data.

dims_changed class-attribute instance-attribute #

dims_changed = Signal()

Signal to emit when the dimensions of the data change.

NOTE: It is up to data wrappers, or even end-users to emit this signal when the dimensions/shape of the wrapped _data object changes.

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
315
316
317
318
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
@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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
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()
    if len(sizes) < 3 or min(sizes.values()) > self.MAX_CHANNELS:
        return None

    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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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
147
148
149
150
151
152
153
154
155
156
157
158
159
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
303
304
305
306
307
308
309
310
311
312
313
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
233
234
235
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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
120
121
122
123
124
125
126
127
@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.
    """

call_later #

call_later(msec: int, func: Callable[[], None]) -> None

Call func after msec milliseconds.

This can be used to enqueue a function to be called after the current event loop iteration. For example, before calling run_app(), to ensure that the event loop is running before the function is called.

Parameters:

  • msec #

    (int) –

    The number of milliseconds to wait before calling func.

  • func #

    (Callable[[], None]) –

    The function to call.

Source code in src/ndv/views/_app.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def call_later(msec: int, func: Callable[[], None]) -> None:
    """Call `func` after `msec` milliseconds.

    This can be used to enqueue a function to be called after the current event loop
    iteration.  For example, before calling `run_app()`, to ensure that the event
    loop is running before the function is called.

    Parameters
    ----------
    msec : int
        The number of milliseconds to wait before calling `func`.
    func : Callable[[], None]
        The function to call.
    """
    ndv_app().call_later(msec, func)

imshow #

imshow(
    data: Any | DataWrapper,
    /,
    *,
    viewer_options: ArrayViewerModel
    | ArrayViewerModelKwargs
    | None = ...,
    display_model: ArrayDisplayModel = ...,
) -> ArrayViewer
imshow(
    data: Any | DataWrapper,
    /,
    *,
    viewer_options: ArrayViewerModel
    | ArrayViewerModelKwargs
    | None = ...,
    **display_kwargs: Unpack[ArrayDisplayModelKwargs],
) -> ArrayViewer
imshow(
    data: Any | DataWrapper,
    /,
    *,
    viewer_options: ArrayViewerModel
    | ArrayViewerModelKwargs
    | None = None,
    display_model: ArrayDisplayModel | None = None,
    **display_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.

  • viewer_options #

    (ArrayViewerModel | ArrayViewerModelKwargs | None, default: None ) –

    Either a ArrayViewerModel or a dictionary of keyword arguments used to create one. See docs for ArrayViewerModel for options.

  • **display_kwargs #

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

    Additional keyword arguments used to create the ArrayDisplayModel. (Generally, this is used instead of passing a display_model directly.)

Returns:

Source code in src/ndv/util.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def imshow(
    data: Any | DataWrapper,
    /,
    *,
    viewer_options: ArrayViewerModel | ArrayViewerModelKwargs | None = None,
    display_model: ArrayDisplayModel | None = None,
    **display_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.
    viewer_options: ArrayViewerModel | ArrayViewerModelKwargs, optional
        Either a [`ArrayViewerModel`][ndv.models.ArrayViewerModel] or a dictionary of
        keyword arguments used to create one.
        See docs for [`ArrayViewerModel`][ndv.models.ArrayViewerModel] for options.
    **display_kwargs : Unpack[ArrayDisplayModelKwargs]
        Additional keyword arguments used to create the
        [`ArrayDisplayModel`][ndv.models.ArrayDisplayModel]. (Generally, this is
        used instead of passing a `display_model` directly.)

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

    run_app()
    return viewer

process_events #

process_events() -> None

Force processing of events for the application.

Source code in src/ndv/views/_app.py
342
343
344
def process_events() -> None:
    """Force processing of events for the application."""
    ndv_app().process_events()

run_app #

run_app() -> None

Start the active GUI application event loop.

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

set_canvas_backend #

set_canvas_backend(
    backend: Literal["pygfx", "vispy"] | None = None,
) -> None

Sets the preferred canvas backend. Cannot be set after the GUI is running.

Source code in src/ndv/views/_app.py
227
228
229
230
231
232
233
234
def set_canvas_backend(backend: Literal["pygfx", "vispy"] | None = None) -> None:
    """Sets the preferred canvas backend. Cannot be set after the GUI is running."""
    if _APP:
        raise RuntimeError("Cannot change the backend once the app is running")
    if backend is None:
        os.environ.pop(CANVAS_ENV_VAR)
    else:
        os.environ[CANVAS_ENV_VAR] = CanvasBackend(backend).value  # validate

set_gui_backend #

set_gui_backend(
    backend: Literal["jupyter", "qt", "wx"] | None = None,
) -> None

Sets the preferred GUI backend. Cannot be set after the GUI is running.

Source code in src/ndv/views/_app.py
237
238
239
240
241
242
243
244
def set_gui_backend(backend: Literal["jupyter", "qt", "wx"] | None = None) -> None:
    """Sets the preferred GUI backend. Cannot be set after the GUI is running."""
    if _APP:
        raise RuntimeError("Cannot change the backend once the app is running")
    if backend is None:
        os.environ.pop(GUI_ENV_VAR)
    else:
        os.environ[GUI_ENV_VAR] = GuiFrontend(backend).value  # validate