NAP-9 — Multiple Canvases#
- Authors:
Ashley Anderson <aandersoniii@chanzuckerberg.com>, Wouter-Michiel Vierdag, Lorenzo Gaifas
- Created:
2023-08-04
- Status:
Draft
- Type:
Standards Track
Definitions#
In order to facilitate discussion - this NAP will use the following definitions.
Viewer - Currently maps basically 1:1 to the napari application main window, including canvas(es), dims sliders, layer list, layer controls, and dock widgets. Related is the ViewerModel
, a class in napari that maintains the state of the Viewer.
Canvas - The main napari data view (2D rectangle) where 2D or 3D slice data is displayed, and additional data views displayed within a single Viewer window. This NAP is in part an attempt to formaize this concept, and as noted proposes a related CanvasModel
class in napari to hold the state of a Canvas.
Layer - The base unit of the napari image data model. A ViewerModel
maintains an ordered list of Layers that it may display on its Canvas.
Layer Slice - A subset of data from a Layer, reduced to 2D or 3D (via slicing) for visualization. This again is a general conecpt that this NAP proposes to formalize as a napari class (or set of classes paired with the Layer types).
Visual - The corresponding visual representation of a Layer Slice displayed on a Canvas. The specific visual is determined by the Layer (and Layer Slice) type.
Note that VisPy (the current backend for all napari visuals) has its own specific definitions for some of these or related concepts, such as Canvas
and ViewBox
. Where necessary to refer to these concepts in this NAP (or discussion), such concepts will be qualified accordingly (for example: “a VisPy Canvas”).
Abstract#
Current napari architecture supports a single canvas (viewbox) per viewer (window). Simultaneously showing multiple views of the same data generally necessitates opening an entirely new napari viewer window or low-level work with Qt widgets and private napari APIs. This wastes resources (primarily memory) and complicates interaction.
Motivation and Scope#
The ability to view n-D data from multiple perspectives (or different data from the same perspective – for example side-by-side segmentations) is a common feature request, and has proved useful in many other tools for data exploration and analysis. Here is a sampling of issues requesting support and discussing potential implementations:
#5348 Multicanvas viewer
#2338 Multicanvas API Thoughts
#760 Linked multicanvas support
#662 Linked 2D views
#561 multicanvas grid display for layers in Napari
#1478 Orthogonal viewer plugin
Several plugins and examples have been created to address these limitations, for example:
This document is intended to cover what #5348 refers to as “True Multicanvas”.
Providing native support in napari would allow developers to more easily create these experiences, enable interoperability between such plugins, and improve performance.
Out of Scope#
Improvements to VisPy to support multiple views of the same
SceneGraph
(sharing data, saving VRAM) - for relevant discussion start with vispy/#1992.For now, canvas arrangement (for example: tiling behavior) will be handled in the view only (left to Qt or custom Qt widgets). Making this state (de)serializable is out of scope for this project, but may be relevant when implementing a “savable viewer state” feature.
Normalizing slice data for different layer types, though this may benefit in the course of this work.
Specific UI implementations will be explored as part of this work, but I expect UX and UI will be formalized later (possibly in a separate NAP).
Supporting alternative frontend (Qt) and backend (Vispy) frameworks. While this work should not make such tasks more difficult in the future, explicit consideration is out-of-scope until further progress is made in these areas.
Non-goals also in NAP-3 are related but also considered out-of-scope here
Separation of rendering information from the Layer(Data) model
Window state restoration
Detailed Description#
Requirements#
The application data model (
ViewerModel
+ Layers) shall support multiple canvases.The application shall natively display multiple canvases simultaneously.
There shall be a minimum of one canvas (current status) per viewer.
All canvases shall share a common layer list and (unsliced) layer data.
Each canvas shall have independent:
Data slicing
Camera (zoom, center)
Dimensionality (2D/3D display)
Layer visibility
The implementation should minimize changes to the existing public API.
The napari application (
ViewerModel
) shall maintain a concept of a single “active” (currently focused) canvas.Alternatively, there could be a “main” canvas that does not change (“main” and “active” could even be simultaneously supported).
There will be no possibility of a viewer with no canvases.
Users shall be able to add, remove, and (maybe[1]) rearrange canvases.
Design Considerations & Decisions#
Part of this design document is intended to capture the desired behavior and prevent scope creep. At the extreme “multiple canvases” can be achieved with “multiple viewers”. Therefore we need to draw a line somewhere to differentiate a “canvas” from a “viewer”. [2]
TODO: add rough CanvasModel definition here [name=Ashley A]
An important consideration is to minimize breaking changes to the public napari API. While napari is still pre-1.0, there is already a healthy developing ecosystem of plugins, scripts, and users. Changes to the API may be necessary and should be made if they constitute improvements, but should be minimized and well documented.
The concept of an “active” canvas will work in service of minimizing API changes. This will allow existing APIs on the main Viewer/ViewerModel to remain and simply delegate to the active canvas.
In addition to maintaining the model-view-controller (MVC) architecture of napari, this proposal aims to maintain or improve decoupling of the UI framework (currently Qt), the visualization library (currently VisPy), and the napari core code.
TODO: add a list open questions and key decisions here [name=Ashley A]
selection state
Architecture#
napari architecture is based on the MVC pattern. The model layer comprises a ViewerModel
and a list of Layer models (subclasses of a base Layer
). There are seven layer types, each with a corresponding view type. Currently models and views are paired 1:1, and the correlation is stored in a map (layer_to_visual
) on the VispyCanvas
. Figure 1 shows the class relationships for the base model types and the Image layer types (for brevity - other layer types have similar connectivity).
Figure 2 shows proposed changes (in orange) to the architecture to support multiple canvases. The new architecture is still following the MVC pattern. Again, this diagram only includes the Image layer type. Here is a summary of the planned changes:
Slice state will be moved off the layer as necessary, into new
LayerSlice
classes for each layer typeThis will be different for each Layer type - unifying the structure in the process may be a secondary benefit but is not the goal
Each VispyCanvas will hold a reference to a dedicated model class (
CanvasModel
)dims will move from
ViewerModel
->CanvasModel
camera will move from
ViewerModel
->CanvasModel
layer_to_slice
will map each Layer (global list) to aLayerSlice
(one perCanvasModel
)
ViewerModel
will own a list ofCanvasModel
objectsQtViewer
will own a list ofVispyCanvas
objectsVispyLayer
subclasses will hold references to theirLayer
(for rendering information) as well as aLayerSlice
(for data)The LayerSlicer will need to update the sync and async callbacks[3] to pass a canvas parameter where the resulting sliced data will be stored, rather than storing it on the layer itself. Slice task cancellation logic will need to be revisited accordingly.
async callback:
LayerSlicer._on_slice_done
sync callback:
Layer._slice_dims
Callbacks (interaction, events) will need to be specifically connected to individual
CanvasModel
objects where relevant (dims, camera) rather than theViewerModel
.
Note: in both diagrams, the
on_draw()
method on theVispyCanvas
breaks MVC convention (view layer talks directly to the model layer). This is a separate/known issue and I believe is mostly only true for multiscale image layers right now. Changing this is considered out of scope for this project at this time.
Slice state for each layer is currently stored on the Layer model. Again, see NAP-4 for previous discussion. This NAP proposes to move this state off the Layer instance, into a specific Layer Slice instance. This is what will allow multiple slices of a single layer to be visualized simultaneously. Table 1 lists the attributes related to slice state that will be moved in this work from each Layer class into corresponding Layer Slice classes.
Note: some layers include “seleciton” information (Points, Shapes, and Tracks). These will be considered as Layer-level concepts, unrelated to the proposed changes.
Table 1 - Layer attributes that hold slice data. These attributes will be moved from the Layer onto individual Layer Slice objects (one Layer Slice per Layer per Canvas).
TODO: make this a list or figure out a way to center it [name=Ashley A]
Layer Class |
Slice Attributes |
---|---|
Base |
|
|
|
Image, Labels |
|
|
|
Points |
|
|
|
Surface |
|
|
|
|
|
|
|
Vectors |
|
|
|
|
|
|
|
Tracks |
|
|
|
|
|
|
|
|
|
|
|
Shapes |
|
|
TODO: add rough class definition(s) for Layer Slice(s) Also - the various
_<Layer>SliceResponse
classes introduced by async slicing may already fill much of this role. Another option to consider here is to codify a protocol (typing.Protocol
) for these classes. Even this protocol may not be necessary - early prototypes use these classes as-is with minimal modifications. [name=Ashley A]
UI Design and Architecture#
Specific UI design and architecture remains to be determined. This will be explored as part of step 4 in the Implementation Plan. UI design needs additional refinement and exploration, and this is expected to continue after basic/core implementation propsed in this NAP is complete. UI changes may also be described in a separate NAP along with a discussion of convenience functions and affordances for common operations. Some placeholder or experimental code will be used in the meantime as a prototype implementation.
Some open questions here are (for example):
Should each canvas also have visible dims sliders, or can we keep one set of dims sliders that changes based on the active (selected) canvas?
What kind of cross-reference displays or tools should there be?
through-plane slice indicators
three-point slice definition
What kinds of camera-linking should be supported?
orthogonal
stereoscopic
Beyond showing a grid of canvases, it would be nice for individual canvases to be:
Resizable
Reorderable
Re-tileable (for example, changing number of rows and columns to tile)
Maybe: Maximized, stacked, and minimized (e.g. with tabs)
Here are some Qt classes that may provide a sound base for multicanvas UI implementation:
QDockWidget
, with the main window being modified to allow dock widget nesting (dockNestingEnabled
). This may require the fewest modifications to the existing Qt viewer. Allowing widgets to be undocked would make this extremely flexible, but possibly also confusing.QMdiArea
(“multiple document interface”) satisfies most of these requirements, and should be customizable to satisfy them all. This would offer extreme flexibility of layout.GridLayout
would likely provide a quite simple but otherwise inflexible solution. For example this may make independent resizing of canvases difficult.
Implementation#
Introduce minimally disruptive
_canvases
attribute on the ViewerModelJust a list (
EventedList
) ofCanvasModel
objectsOne canvas is “active”, relevant properties on ViewerModel (
camera
,dims
) are delegated to the active canvas“active” canvas is just index 0 (alternatively: use
SelectableEventedList
)
Only the active canvas is shown (minimal modifications to
QtViewer
andVispyCanvas
)A single
QtDims
view (slicing sliders) is shown, updates depending on the active canvasAdjust event callbacks such that interactions (slicing, camera movement, ndisplay toggle) only apply to the active canvas
Add public APIs to add/remove/rotate (change active)
_canvases
Add Layer Slice classes to reduce data reslicing when switching between canvases
Start with
Image
andLabels
layersMove/modify/replace
set_view_slice
and_update_slice_response
to set data on Layer Slice for associated canvas
Modify
VispyCanvas
Get camera and dims information from associated
CanvasModel
instead ofViewer
Obtain relevant data from Layer Slice instead of Layer
Modify
ViewerModel
andLayerSlicer
[3]Submit
CanvasModel
(or an ID) toLayerSlicer
instead of dims directlyEmit
CanvasModel
(or an ID) and slice data fromLayerSlicer.ready
and/orLayer.set_data
eventsVispyCanvas
will subscribe to relevant events, set data if corresponding to its ownCanvasModel
. Other canvases may also be interested in this event for example to update cross-reference overlays.
Update
QtViewer
andVispyCanvas
to support multiple canvasesStill only displaying one canvas at a time in the main widget
Update main widget as
ViewerModel
“active” canvas changes, storing additional canvases and swapping them out as necessary
Update
QtViewer
to show multiple canvases simultaneouslyThis is exploratory work at the moment, see UI Architecture section below
Backward Compatibility#
Maintaining the proxy API on the viewer via the concepts of a main and/or active canvas will make this work mostly backward-compatible, though there will inevitably be some breaking changes. There will likely be significant breaking changes to private APIs. For example if plugins are attempting to access slice data directly from a layer instance, it may no longer be as expected. If this is a large burden, it too may be mitigated by delegating from the layer to the slice corresponding to the main or active canvas.
Future Work#
The goal of this NAP is to cover the main architectural changes to enable multi-canvas work. Future work is expected in 1) user experience, design, and GUI implementation details; and 2) consistent, ergonomic, and documented public APIs for advanced interaction with multiple canvases.
Alternatives#
Users can open multiple napari viewers#
Using multiple napari viewers does not satisfy the core user needs for multiple canvases when processing or manipulating data. Multiple viewers also wastes system resources as viewers do not communicate or share memory.
Leave multicanvas to plugins and custom widgets#
Unifying an implementation and (eventually) providing a stable multi-canvas API will save work for plugin authors, and allow more plugins to interoperate.
Implement slices using shallow Layer copies#
This is a good and reasonable alternative to the proposed implementation, and is how the multiple viewer widgets example is implemented. This implementation also makes it easier to configure rendering/appearance per-canvas (layer visibility, colormap, etc.). However this implementation relies more on careful bookkeeping than data modeling. If this is desired functionality, layer data should be fully separated from the data view (slice and view state). Ultimately this implementation is similar to that proposed in this NAP, and could be considered along a continuum of separating layer data, slice data, and rendering configuration.
Discussion#
#5348 Multicanvas viewer
This is the most recent and thorough discussion multi-canvas prior to this NAP
Copyright#
This document is dedicated to the public domain with the Creative Commons CC0 license [4]. Attribution to this source is encouraged where appropriate, as per CC0+BY [5].