Skip to content

ndv.controllers #

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

Classes:

  • ArrayViewer

    Viewer dedicated to displaying a single n-dimensional array.

  • ImageStats

    Result of computing image statistics.

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 view 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.

  • refresh_stats

    Force re-emit stats for all channels with existing data.

  • show

    Show the viewer.

  • widget

    Return the native front-end widget.

Attributes:

Source code in src/ndv/controllers/_array_viewer.py
 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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._display_model = display_model
    self._data_wrapper: DataWrapper | None = wrapper
    self._resolved = EMPTY_STATE

    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
    # maps pending futures to their request generation (for stale detection)
    self._gen_counter = count()
    self._current_gen: int = 0
    self._futures: dict[Future[DataResponse], int] = {}

    # 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._shared_histogram: SharedHistogramCanvas | None = None
    self._shared_histogram_links: dict[ChannelKey, _SharedHistogramLink] = {}
    self._view = frontend_cls(self._canvas.frontend_widget(), self._viewer_model)

    self._roi_view: RectangularROIHandle | None = None

    self._set_model_connected(self._display_model)
    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.sharedHistogramRequested.connect(self._add_shared_histogram)
    self._view.channelModeChanged.connect(self._on_view_channel_mode_changed)
    self._view.ndimToggleRequested.connect(self._on_view_ndim_toggle_requested)

    self._highlight_pos: tuple[float, float] | None = None
    self._canvas.mouseMoved.connect(self._on_canvas_mouse_moved)
    self._canvas.mouseLeft.connect(self._on_canvas_mouse_left)

    self._focused_slider_axis: AxisKey | None = None
    self._disconnect_key_events = _app.filter_key_events(
        self._view.frontend_widget(), self._view
    )
    self._view.keyPressed.connect(self._on_key_pressed)

    if self._data_wrapper is not None:
        self._fully_synchronize_view()

data property writable #

data: Any

Return data being displayed (the actual data, not the wrapper).

data_wrapper property #

data_wrapper: Any

Return the data wrapper object being used to interface with the data.

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
246
247
248
249
250
251
252
253
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_wrapper, display_model=self.display_model)

close #

close() -> None

Close the viewer.

Source code in src/ndv/controllers/_array_viewer.py
241
242
243
244
def close(self) -> None:
    """Close the viewer."""
    self._disconnect_key_events()
    self._view.set_visible(False)

hide #

hide() -> None

Hide the viewer.

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

refresh_stats #

refresh_stats() -> None

Force re-emit stats for all channels with existing data.

This will mostly be used by external listeners that want the initial data, before any interaction has occurred.

Source code in src/ndv/controllers/_array_viewer.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def refresh_stats(self) -> None:
    """Force re-emit stats for all channels with existing data.

    This will mostly be used by external listeners that want the initial data,
    before any interaction has occurred.
    """
    if not len(self.stats_updated):
        return
    sig_bits = wrp.significant_bits if (wrp := self._data_wrapper) else None
    for key, ctrl in self._lut_controllers.items():
        if ctrl.handles:
            stats = compute_image_stats(
                ctrl.handles[0].data(),
                ctrl.lut_model.clims,
                need_histogram=True,
                significant_bits=sig_bits,
            )
            self.stats_updated.emit(key, stats)

show #

show() -> None

Show the viewer.

Source code in src/ndv/controllers/_array_viewer.py
233
234
235
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
160
161
162
163
164
165
166
167
168
169
170
171
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()

ImageStats dataclass #

ImageStats(
    counts: ndarray | None,
    bin_edges: ndarray | None,
    clims: tuple[float, float],
)

Result of computing image statistics.