Architecture¶
This document details the internal structure, frontend-backend synchronization patterns, and communication protocols in IPyNiiVue.
System Overview¶
Python Side: Users interact with the
NiiVueclass to load and manipulate neuroimaging data.JavaScript Side: Handles WebGL rendering and user interactions in the browser.
Communication Layer: Traitlets synchronize state between Python and JavaScript via WebSocket (ex: JupyterLab) or HTTP (ex: Marimo).
Architecture Layers¶
1. Python Backend (Kernel)¶
Runs in the notebook kernel process and contains:
NiiVue,Volume,Mesh, andMeshLayerclasses.State management through traitlets.
Data processing and computational logic.
Chunked data handling for large data from the frontend (to overcome Tornado’s 10MB limit).
2. Communication Layer¶
Provided by the anywidget framework:
Maintains synchronized widget models between kernel and browser.
Handles state synchronization via traitlets.
Manages WebSocket/HTTP communication.
Serializes/deserializes data between Python and JavaScript.
3. JavaScript Frontend (Browser)¶
Runs in the browser and includes:
WebGL rendering via the NiiVue library.
UI event handling (mouse, keyboard interactions).
Visual output in notebook cells.
High-Level Data Flow¶
flowchart LR
subgraph "Kernel"
A[Python Backend<br/>NiiVue class]
end
subgraph "Communication Layer"
B[Widget Bridge<br/>Traitlets sync<br/>WebSocket/HTTP]
end
subgraph "Browser"
C[JavaScript Frontend<br/>WebGL rendering<br/>User interactions]
D[Notebook Output Cell<br/>Visual display]
end
A <--> B
B <--> C
C <--> D
Implementation Details¶
The following sections detail the specific class structures, binary optimization strategies, and synchronization logic used to implement the architecture described above.
1. Class Hierarchy¶
The project extends anywidget to bridge Python and JavaScript. The hierarchy isolates binary handling logic in a base class, separating it from specific widget implementations.
anywidget.AnyWidget
└── BaseAnyWidget (src/ipyniivue/widget.py)
├── NiiVue
├── Volume
├── Mesh
└── MeshLayer
traitlets.HasTraits
├── ConfigOptions (src/ipyniivue/config_options.py)
├── Scene (src/ipyniivue/traits.py)
├── Graph (src/ipyniivue/traits.py)
├── ColorMap (src/ipyniivue/traits.py)
├── LUT (src/ipyniivue/traits.py)
└── NIFTI1Hdr (src/ipyniivue/traits.py)
2. BaseAnyWidget¶
Located in src/ipyniivue/widget.py, BaseAnyWidget overrides some methods in AnyWidget and defines new methods and attributes used in data transfers.
Method / Attribute |
Purpose |
Description |
|---|---|---|
|
Override |
Intercepts state keys starting with |
|
Hook |
Subclasses must override this to list synced binary traits (e.g., |
|
Mapping |
Subclasses can override this to map Python trait names to JS property names for binary attributes (e.g., |
|
Observer |
Automatically attached to traits in |
3. Binary Data Transfer Protocol¶
Logic in js/lib.ts and src/ipyniivue/utils.py handles binary payloads that exceed standard limits (> 10MB).
Chunking:
lib.sendChunkedData(JS) splits buffers into 5MB chunks. On the Python side,set_state(viaChunkedDataHandler) reassembles them.Diffing (Py → JS):
_handle_binary_trait_changesendsbuffer_updatemessages containingindicesandvaluesarrays if the type is the same (if the type is different, abuffer_changemessage is sent with the full data buffer). The frontendhandleBufferMsgutilizesapplyDifferencesToTypedArrayto patch the existing buffer rather than reloading it.
4. Frontend/Backend Sync¶
The Render Loop¶
When the volumes or meshes list changes in Python, the frontend executes render_volumes or render_meshes.
Logic Flow:
(js/volume.ts & js/mesh.ts)
Gather Models: The frontend resolves the list of Model IDs provided by Python into actual
AnyModelobjects.Generate Maps: Two maps are created:
backend_map(ID → Model) andfrontend_map(ID → NVObject).- Update:
Create: If an ID exists in the Backend but not the Frontend → Call
create_volume.Dispose: If an ID exists in the Frontend but not the Backend → Call
nv.removeVolumeand triggerdisposer.dispose(id).Reorder: The
nv.volumesarray is sorted to match the order of the Python list.
5. Communication Flows¶
The system handles object creation differently depending on where the volume / mesh / mesh layer is added.
ID Gen:
Backend: When a volume is created in Python, the
Volumeconstructor generates a UUID suffixed with_pyto serve as the identifier.Frontend: When a volume is loaded via the browser (e.g., drag-and-drop), the ID is generated internally by the Niivue JavaScript library.
Flow A: Backend-Initiated Creation¶
User adds a volume via Python code (e.g., `nv.add_volume()`).
Step |
Layer |
Action |
Source Ref |
|---|---|---|---|
1 |
Python |
User calls |
|
2 |
Bridge |
|
N/A |
3 |
JS |
|
|
4 |
JS |
|
|
5 |
JS |
Listeners are attached to the new volume’s properties (opacity, colormap) to handle future updates. |
|
Flow B: Frontend-Initiated Creation¶
User drags a file directly onto the canvas in the browser.
Step |
Layer |
Action |
Source Ref |
|---|---|---|---|
1 |
JS |
|
|
2 |
JS |
|
|
3 |
JS |
If new, |
|
4 |
Bridge |
Message travels over Bridge to Kernel. |
N/A |
5 |
Python |
|
|
6 |
Python |
A new |
|
7 |
JS |
|
|
8 |
JS |
|
|
6. Event & Message Routing¶
Standard attributes sync via Traitlets. Specific actions use custom messages.
Custom Message Protocol (msg:custom)¶
Python:
uses _handle_custom_msg
# Logic Flow in _handle_custom_msg
if event == "add_volume":
_add_volume_from_frontend(data)
elif event == "hover_idx_change":
callback(data) # Triggers user-defined Python callbacks
elif event == "image_loaded":
# Waits for traits (img, hdr) to be fully synced before firing callback
handler(volume)
JS:
uses model.on("msg:custom")
Some example use-cases:
Message Type |
Action |
Data Payload |
|---|---|---|
|
Calls |
Filename string |
|
Updates gamma levels |
Float value |
|
Runs segmentation |
Integer levels |
|
Loads binary drawing |
URL string or Binary Buffer |
Nested State Synchronization¶
Nested objects do not automatically trigger Traitlet updates when their internal keys change.
To address this limitation while keeping the ipyniivue API intuitive, ipyniivue implements a propagation mechanism that forwards updates to the parent class.
For example, executing nv.scene.gamma = 1 triggers the internal observer _propagate_parent_change within the Scene class. This observer invokes _notify_scene_changed on the parent NiiVue instance, ensuring the update is properly serialized and synchronized with the frontend.
Appendix: File Organization¶
Python Components (src/ipyniivue/)¶
__init__.py: Package initialization and exportswidget.py: CoreNiiVue,Volume,Meshclassestraits.py: Custom traitlet classes (Scene,Graph,ColorMap)serializers.py: Custom serializers and deserializers for complex types and Enumsconfig_options.py: Auto-generated mappings for NiiVue configuration optionsconstants.py: Enumerations for slice types, drag modes, render settingsutils.py: General utilitiesdownload_dataset.py: Utility for fetching data
JavaScript Bundle (js/)¶
widget.ts: Main entry pointvolume.ts: Volume handlingmesh.ts: Mesh and MeshLayer handlinglib.ts: General utilitiestypes.ts/types/*.d.ts: Type declarations for models, messages, and external dependencies.
Scripts (scripts/)¶
generate_options_mixin.py: For automatically generating code insrc/ipyniivue/config_options.py. Modify this file and runpython scripts/generate_options_mixin.pyfrom the root directory instead of modifyingsrc/ipyniivue/config_options.pydirectly.
Documentation & Tests¶
docs/: Sphinx documentation source (.rst), configuration (conf.py), and build scripts (Makefile,make.bat).tests/: Pytest unit tests.