Skip to content

Commit 18f2e16

Browse files
authored
docs: more updates to API documentation (#103)
* docs: more docs cleanup * add tldr to install page
1 parent 2233c97 commit 18f2e16

10 files changed

+120
-44
lines changed

docs/env_var.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515

1616
## Framework selection
1717

18-
If you have multiple supported GUI or graphics libraries installed, you can
19-
control which ones `ndv` uses with **`NDV_CANVAS_BACKEND`** and **`NDV_GUI_FRONTEND`**,
18+
Depending on how you've [installed ndv](./install.md), you may end up with
19+
multiple supported GUI or graphics libraries installed. You can control which
20+
ones `ndv` uses with **`NDV_CANVAS_BACKEND`** and **`NDV_GUI_FRONTEND`**,
2021
respectively, as described above. Note that currently, only one GUI framework
2122
can be used per session.
2223

docs/install.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Installation
22

3+
!!! tip "TLDR;"
4+
5+
=== "For a desktop usage"
6+
7+
```python
8+
pip install "ndv[vispy,pyqt]"
9+
```
10+
11+
=== "For Jupyter notebook/lab"
12+
13+
```python
14+
pip install "ndv[vispy,jupyter]"
15+
```
16+
317
`ndv` can be used in a variety of contexts. It supports various **GUI
418
frameworks**, including [PyQt](https://riverbankcomputing.com/software/pyqt),
519
[PySide](https://wiki.qt.io/Qt_for_Python), [wxPython](https://wxpython.org),
@@ -14,7 +28,7 @@ and GUI libraries you want to use:
1428
<!-- logic for this table in install-table.js -->
1529
<div id="install-table"></div>
1630

17-
!!! tip
31+
!!! note "Framework Selection"
1832

1933
If you have *multiple* supported GUI or graphics libraries installed, you can
2034
select which ones `ndv` uses with

docs/motivation.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ display data.
2222
amount of time. "Reasonable" is of course relative and subjective, but we
2323
aim for less than 1 second on a modern laptop (currently at <100ms).
2424
- [x] **Broad GUI Compatibility**: A common feature request for `napari` is to support
25-
Jupyter notebooks. `ndv` [can work with](./env_var.md#framework-selection) Qt,
25+
Jupyter notebooks. `ndv` [can work with](./install.md) Qt,
2626
wxPython, *and* Jupyter.
2727
- [x] **Flexible Graphics Providers**: `ndv` works with VisPy in a classical OpenGL
2828
context, but has an abstracting layer that allows for other graphics engines.
2929
We currently also support `pygfx`, a WGPU-based graphics engine.
3030
- [x] **Model/View architecture**: `ndv` should have a clear separation between the
3131
data model and the view. The model should be serializable and easily
32-
transferable between different views.
32+
transferable between different views. (The primary model is currently
33+
[`ArrayDisplayModel`][ndv.models.ArrayDisplayModel])
3334
- [x] **Asynchronous first**: `ndv` should be asynchronous by default: meaning
3435
that the data request/response process happens in the background, and the
3536
GUI remains responsive. (Optimization of remote, multi-resolution data is on

mkdocs.yml

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ markdown_extensions:
7373
- pymdownx.details
7474
- pymdownx.keys
7575
- pymdownx.tilde
76+
- pymdownx.tabbed:
77+
alternate_style: true
7678
- pymdownx.emoji:
7779
emoji_index: !!python/name:material.extensions.emoji.twemoji
7880
emoji_generator: !!python/name:material.extensions.emoji.to_svg
@@ -110,6 +112,7 @@ plugins:
110112
python:
111113
import:
112114
- https://docs.python.org/3/objects.inv
115+
- https://numpy.org/doc/stable/objects.inv
113116
- https://docs.pydantic.dev/latest/objects.inv
114117
- https://cmap-docs.readthedocs.io/objects.inv
115118
- https://psygnal.readthedocs.io/en/latest/objects.inv

src/ndv/models/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
from ._array_display_model import ArrayDisplayModel, ChannelMode
44
from ._base_model import NDVModel
55
from ._data_wrapper import DataWrapper
6-
from ._lut_model import ClimPolicy, ClimsManual, ClimsMinMax, ClimsPercentile, LUTModel
6+
from ._lut_model import (
7+
ClimPolicy,
8+
ClimsManual,
9+
ClimsMinMax,
10+
ClimsPercentile,
11+
ClimsStdDev,
12+
LUTModel,
13+
)
714

815
__all__ = [
916
"ArrayDisplayModel",
@@ -12,6 +19,7 @@
1219
"ClimsManual",
1320
"ClimsMinMax",
1421
"ClimsPercentile",
22+
"ClimsStdDev",
1523
"DataWrapper",
1624
"LUTModel",
1725
"NDVModel",

src/ndv/models/_array_display_model.py

+44-22
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@
1515
from ._reducer import ReducerType
1616

1717
if TYPE_CHECKING:
18-
from collections.abc import Mapping
18+
from collections.abc import Hashable, Mapping # noqa: F401
19+
from typing import Callable # noqa: F401
1920

2021
import cmap
22+
import numpy.typing as npt # noqa: F401 # used for mkdocstrings
2123

2224
from ._lut_model import AutoscaleType
2325

2426
class LutModelKwargs(TypedDict, total=False):
27+
"""Keyword arguments for `LUTModel`."""
28+
2529
visible: bool
2630
cmap: cmap.Colormap | cmap._colormap.ColorStopsLike
2731
clims: tuple[float, float] | None
2832
gamma: float
2933
autoscale: AutoscaleType
3034

3135
class ArrayDisplayModelKwargs(TypedDict, total=False):
36+
"""Keyword arguments for `ArrayDisplayModel`."""
37+
3238
visible_axes: tuple[AxisKey, AxisKey, AxisKey] | tuple[AxisKey, AxisKey]
3339
current_index: Mapping[AxisKey, Union[int, slice]]
3440
channel_mode: "ChannelMode" | Literal["grayscale", "composite", "color", "rgba"]
@@ -106,40 +112,50 @@ def is_multichannel(self) -> bool:
106112
class ArrayDisplayModel(NDVModel):
107113
"""Model of how to display an array.
108114
109-
In the following types, `AxisKey` can be either an integer index or a string label.
115+
An `ArrayDisplayModel` is used to specify how to display a multi-dimensional array.
116+
It specifies which axes are visible, how to reduce along axes that are not visible,
117+
how to display channels, and how to apply lookup tables to channels. It is
118+
typically paired with a [`ndv.DataWrapper`][] in order to resolve axis keys and
119+
slice data.
120+
121+
!!! info
122+
123+
In the following types, `Hashable` is used to refer to a type that will
124+
typically be either an integer index or a string label for an axis.
110125
111126
Attributes
112127
----------
113-
visible_axes : tuple[AxisKey, ...]
128+
visible_axes : tuple[Hashable, ...]
114129
Ordered list of axes to visualize, from slowest to fastest.
115-
e.g. ('z', -2, -1)
116-
current_index : Mapping[AxisKey, int | Slice]
130+
e.g. `('Z', -2, -1)`
131+
current_index : Mapping[Hashable, int | slice]
117132
The currently displayed position/slice along each dimension.
118133
e.g. {0: 0, 'time': slice(10, 20)}
119134
Not all axes need be present, and axes not present are assumed to
120135
be slice(None), meaning it is up to the controller of this model to restrict
121136
indices to an efficient range for retrieval.
122137
If the number of non-singleton axes is greater than `n_visible_axes`,
123-
then reducers are used to reduce the data along the remaining axes.
124-
NOTE: In terms of requesting data, there is a slight "delocalization" of state
125-
here in that we probably also want to avoid requesting data for channel
126-
positions that are not visible.
127-
reducers : Mapping[AxisKey | None, ReducerType]
128-
Callable to reduce data along axes remaining after slicing.
138+
then `reducers` are used to reduce the data along the remaining axes.
139+
reducers : Mapping[Hashable | None, numpy.ufunc]
140+
Function used to reduce data along axes remaining after slicing.
129141
Ideally, the ufunc should accept an `axis` argument.
130-
(TODO: what happens if not?)
142+
*(TODO: what happens if not?)*
143+
default_reducer : numpy.ufunc
144+
Default reducer to use when no reducer is specified for an axis. By default,
145+
this is [`numpy.max`][].
131146
channel_mode : ChannelMode
132147
How to display channel information:
133-
- `GRAYSCALE`: ignore channel axis, use `default_lut`
134-
- `COMPOSITE`: display all channels as a composite image, using `luts`
135-
- `COLOR`: display a single channel at a time, using `luts`
136-
- `RGBA`: display as an RGB image, using `default_lut` (except for cmap)
148+
149+
- `GRAYSCALE`: ignore channel axis, use `default_lut`
150+
- `COMPOSITE`: display all channels as a composite image, using `luts`
151+
- `COLOR`: display a single channel at a time, using `luts`
152+
- `RGBA`: display as an RGB image, using `default_lut` (except for cmap)
137153
138154
If `channel_mode` is set to anything other than `GRAYSCALE`, then `channel_axis`
139155
must be set to a valid axis; if no `channel_axis` is set, at the time of
140-
display, the `DataWrapper` MAY guess the `channel_axis`, and set it on the
141-
model.
142-
channel_axis : AxisKey | None
156+
display, the [`DataWrapper`][ndv.DataWrapper] MAY guess the `channel_axis`,
157+
and set it on the model.
158+
channel_axis : Hashable | None
143159
The dimension index or name of the channel dimension.
144160
The implication of setting channel_axis is that *all* elements along the channel
145161
dimension are shown, with different LUTs applied to each channel.
@@ -151,18 +167,24 @@ class ArrayDisplayModel(NDVModel):
151167
Values are `LUT` objects that specify how to display the channel.
152168
The special key `None` is used to represent a fallback LUT for all channels,
153169
and is used when `channel_axis` is None. It should always be present
170+
default_lut : LUTModel
171+
Default lookup table to use when no `LUTModel` is specified for a channel in
172+
`luts`.
154173
"""
155174

156175
visible_axes: TwoOrThreeAxisTuple = (-2, -1)
176+
# NOTE: In terms of requesting data, there is a slight "delocalization" of state
177+
# here in that we probably also want to avoid requesting data for channel
178+
# positions that are not visible.
157179
current_index: IndexMap = Field(default_factory=IndexMap, frozen=True)
158180

159-
channel_mode: ChannelMode = ChannelMode.GRAYSCALE
160-
channel_axis: Optional[AxisKey] = None
161-
162181
# map of axis to reducer (function that can reduce dimensionality along that axis)
163182
reducers: Reducers = Field(default_factory=Reducers, frozen=True)
164183
default_reducer: ReducerType = "numpy.max" # type: ignore [assignment] # FIXME
165184

185+
channel_mode: ChannelMode = ChannelMode.GRAYSCALE
186+
channel_axis: Optional[AxisKey] = None
187+
166188
# map of index along channel axis to LUTModel object
167189
luts: LutMap = Field(default_factory=_default_luts)
168190
default_lut: LUTModel = Field(default_factory=LUTModel, frozen=True)

src/ndv/models/_data_wrapper.py

+26-7
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ def _recurse_subclasses(cls: _T) -> Iterator[_T]:
5656
class DataWrapper(Generic[ArrayT], ABC):
5757
"""Interface for wrapping different array-like data types.
5858
59-
`DataWrapper.create` is a factory method that returns a DataWrapper instance
60-
for the given data type. If your datastore type is not supported, you may implement
61-
a new DataWrapper subclass to handle your data type. To do this, import and
62-
subclass DataWrapper, and (minimally) implement the supports and isel methods.
63-
Ensure that your class is imported before the DataWrapper.create method is called,
64-
and it will be automatically detected and used to wrap your data.
59+
[`DataWrapper.create()`][ndv.DataWrapper.create] is a factory method that returns a
60+
`DataWrapper` instance for the given data type. If your datastore type is not
61+
supported, you may implement a new `DataWrapper` subclass to handle your data type.
62+
To do this, import and subclass `DataWrapper`, and (minimally) implement the
63+
supports and isel methods. Ensure that your class is imported before the
64+
`DataWrapper.create` method is called, and it will be automatically detected and
65+
used to wrap your data.
6566
"""
6667

6768
# Order in which subclasses are checked for support.
@@ -102,7 +103,10 @@ def coords(self) -> Mapping[Hashable, Sequence]:
102103

103104
@abstractmethod
104105
def isel(self, index: Mapping[int, int | slice]) -> np.ndarray:
105-
"""Return a slice of the data as a numpy array."""
106+
"""Return a slice of the data as a numpy array.
107+
108+
`index` will look like (e.g.) `{0: slice(0, 10), 1: 5}`.
109+
"""
106110

107111
def save_as_zarr(self, path: str) -> None:
108112
raise NotImplementedError("Saving as zarr is not supported for this data type")
@@ -122,6 +126,21 @@ def dtype(self) -> np.dtype:
122126

123127
@classmethod
124128
def create(cls, data: ArrayT) -> DataWrapper[ArrayT]:
129+
"""Create a DataWrapper instance for the given data.
130+
131+
This method will detect all subclasses of DataWrapper and check them in order of
132+
their `PRIORITY` class variable. The first subclass that
133+
[`supports`][ndv.DataWrapper.supports] the given data will be used to wrap it.
134+
135+
!!! tip
136+
137+
This means that you can subclass DataWrapper to handle new data types.
138+
Just make sure that your subclass is imported before calling `create`.
139+
140+
If no subclasses support the data, a `NotImplementedError` is raised.
141+
142+
If an instance of `DataWrapper` is passed in, it will be returned as-is.
143+
"""
125144
if isinstance(data, DataWrapper):
126145
return data
127146

src/ndv/models/_lut_model.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,17 @@ class LUTModel(NDVModel):
136136
visible : bool
137137
Whether to display this channel.
138138
NOTE: This has implications for data retrieval, as we may not want to request
139-
channels that are not visible. See current_index above.
139+
channels that are not visible.
140+
See [`ArrayDisplayModel.current_index`][ndv.models.ArrayDisplayModel].
140141
cmap : cmap.Colormap
141-
Colormap to use for this channel.
142-
clims : Union[ManualClims, PercentileClims, StdDevClims, MinMaxClims]
143-
Method for determining the contrast limits for this channel.
142+
[`cmap.Colormap`](https://cmap-docs.readthedocs.io/colormaps/) to use for this
143+
channel. This can be expressed as any channel. This can be expressed as any
144+
["colormap like" object](https://cmap-docs.readthedocs.io/en/latest/colormaps/#colormaplike-objects)
145+
clims : Union[ClimsManual, ClimsPercentile, ClimsStdDev, ClimsMinMax]
146+
Method for determining the contrast limits for this channel. If a 2-element
147+
`tuple` or `list` is provided, it is interpreted as a manual contrast limit.
144148
gamma : float
145-
Gamma correction for this channel. By default, 1.0.
149+
Gamma applied to the data before applying the colormap. By default, `1.0`.
146150
"""
147151

148152
visible: bool = True

src/ndv/util.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ def imshow(
2828
display_model: ArrayDisplayModel | None = None,
2929
**kwargs: Unpack[ArrayDisplayModelKwargs],
3030
) -> ArrayViewer:
31-
"""Display an array or DataWrapper in a new NDViewer window.
31+
"""Display an array or DataWrapper in a new `ArrayViewer` window.
32+
33+
This convenience function creates an `ArrayViewer` instance populated with `data`,
34+
calls `show()` on it, and then runs the application.
3235
3336
Parameters
3437
----------
@@ -37,12 +40,13 @@ def imshow(
3740
display_model: ArrayDisplayModel, optional
3841
The display model to use. If not provided, a new one will be created.
3942
kwargs : Unpack[ArrayDisplayModelKwargs]
40-
Additional keyword arguments to pass to the NDViewer
43+
Additional keyword arguments used to create the
44+
[`ArrayDisplayModel`][ndv.models.ArrayDisplayModel].
4145
4246
Returns
4347
-------
4448
ArrayViewer
45-
The viewer window.
49+
The `ArrayViewer` instance.
4650
"""
4751
viewer = ArrayViewer(data, display_model, **kwargs)
4852
viewer.show()

src/ndv/views/_app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ def filter_mouse_events(canvas: Any, receiver: Mouseable) -> Callable[[], None]:
586586

587587

588588
def run_app() -> None:
589-
"""Start the GUI application event loop."""
589+
"""Start the active GUI application event loop."""
590590
GUI_PROVIDERS[gui_frontend()].exec()
591591

592592

0 commit comments

Comments
 (0)