nwb-video-widgets 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,9 +32,19 @@ class NWBDANDIVideoPlayer(anywidget.AnyWidget):
32
32
  nwbfile : pynwb.NWBFile, optional
33
33
  Pre-loaded NWB file to avoid re-loading. If not provided, the widget
34
34
  will load the NWB file via streaming.
35
+ video_grid : list[list[str]], optional
36
+ A 2D grid layout specifying which videos to display and where.
37
+ Each inner list represents a row of videos. When provided, bypasses
38
+ the interactive settings panel and displays videos in the specified
39
+ grid arrangement. Video names that don't exist are silently skipped.
40
+ video_labels : dict[str, str], optional
41
+ Mapping of video names to custom display labels. If a video name is
42
+ not in the dictionary, the original video name is displayed.
35
43
 
36
44
  Example
37
45
  -------
46
+ Interactive mode (default):
47
+
38
48
  >>> from dandi.dandiapi import DandiAPIClient
39
49
  >>> client = DandiAPIClient()
40
50
  >>> dandiset = client.get_dandiset("000409", "draft")
@@ -48,6 +58,23 @@ class NWBDANDIVideoPlayer(anywidget.AnyWidget):
48
58
  ... asset=asset,
49
59
  ... nwbfile=already_loaded_nwbfile,
50
60
  ... )
61
+
62
+ Fixed grid mode (single row):
63
+
64
+ >>> widget = NWBDANDIVideoPlayer(
65
+ ... asset=asset,
66
+ ... video_grid=[["VideoLeftCamera", "VideoBodyCamera", "VideoRightCamera"]]
67
+ ... )
68
+
69
+ Fixed grid mode (2x2 grid):
70
+
71
+ >>> widget = NWBDANDIVideoPlayer(
72
+ ... asset=asset,
73
+ ... video_grid=[
74
+ ... ["VideoLeftCamera", "VideoRightCamera"],
75
+ ... ["VideoBodyCamera"],
76
+ ... ]
77
+ ... )
51
78
  """
52
79
 
53
80
  video_urls = traitlets.Dict({}).tag(sync=True)
@@ -56,6 +83,8 @@ class NWBDANDIVideoPlayer(anywidget.AnyWidget):
56
83
  selected_videos = traitlets.List([]).tag(sync=True)
57
84
  layout_mode = traitlets.Unicode("row").tag(sync=True)
58
85
  settings_open = traitlets.Bool(False).tag(sync=True)
86
+ grid_layout = traitlets.List([]).tag(sync=True)
87
+ video_labels = traitlets.Dict({}).tag(sync=True)
59
88
 
60
89
  _esm = pathlib.Path(__file__).parent / "video_widget.js"
61
90
  _css = pathlib.Path(__file__).parent / "video_widget.css"
@@ -64,6 +93,8 @@ class NWBDANDIVideoPlayer(anywidget.AnyWidget):
64
93
  self,
65
94
  asset: RemoteAsset,
66
95
  nwbfile: Optional[NWBFile] = None,
96
+ video_grid: Optional[list[list[str]]] = None,
97
+ video_labels: Optional[dict[str, str]] = None,
67
98
  **kwargs,
68
99
  ):
69
100
  # Load NWB file if not provided
@@ -73,17 +104,43 @@ class NWBDANDIVideoPlayer(anywidget.AnyWidget):
73
104
  video_urls = self.get_video_urls_from_dandi(nwbfile, asset)
74
105
  video_timestamps = get_video_timestamps(nwbfile)
75
106
  available_videos = get_video_info(nwbfile)
76
-
77
- # Start with no videos selected to avoid initial buffering
78
- super().__init__(
79
- video_urls=video_urls,
80
- video_timestamps=video_timestamps,
81
- available_videos=available_videos,
82
- selected_videos=[],
83
- layout_mode="grid",
84
- settings_open=True,
85
- **kwargs,
86
- )
107
+ video_labels = video_labels or {}
108
+
109
+ if video_grid is not None and len(video_grid) > 0:
110
+ # Fixed grid mode - bypass settings panel
111
+ # Filter to only include videos that exist in video_urls
112
+ filtered_grid = [
113
+ [v for v in row if v in video_urls]
114
+ for row in video_grid
115
+ ]
116
+ # Remove empty rows
117
+ filtered_grid = [row for row in filtered_grid if row]
118
+ # Flatten grid to get selected videos (preserving order)
119
+ selected = [v for row in filtered_grid for v in row]
120
+ super().__init__(
121
+ video_urls=video_urls,
122
+ video_timestamps=video_timestamps,
123
+ available_videos=available_videos,
124
+ selected_videos=selected,
125
+ layout_mode="grid",
126
+ settings_open=False,
127
+ grid_layout=filtered_grid,
128
+ video_labels=video_labels,
129
+ **kwargs,
130
+ )
131
+ else:
132
+ # Interactive mode (current behavior)
133
+ super().__init__(
134
+ video_urls=video_urls,
135
+ video_timestamps=video_timestamps,
136
+ available_videos=available_videos,
137
+ selected_videos=[],
138
+ layout_mode="grid",
139
+ settings_open=True,
140
+ grid_layout=[],
141
+ video_labels=video_labels,
142
+ **kwargs,
143
+ )
87
144
 
88
145
  @staticmethod
89
146
  def _load_nwbfile_from_dandi(asset: RemoteAsset) -> NWBFile:
@@ -1,7 +1,10 @@
1
1
  """Local NWB video player widget."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import pathlib
4
6
  from pathlib import Path
7
+ from typing import Optional
5
8
 
6
9
  import anywidget
7
10
  import traitlets
@@ -28,15 +31,42 @@ class NWBLocalVideoPlayer(anywidget.AnyWidget):
28
31
  nwbfile : pynwb.NWBFile
29
32
  NWB file containing video ImageSeries in acquisition. Must have been
30
33
  loaded from disk (i.e., nwbfile.read_io must not be None).
34
+ video_grid : list[list[str]], optional
35
+ A 2D grid layout specifying which videos to display and where.
36
+ Each inner list represents a row of videos. When provided, bypasses
37
+ the interactive settings panel and displays videos in the specified
38
+ grid arrangement. Video names that don't exist are silently skipped.
39
+ video_labels : dict[str, str], optional
40
+ Mapping of video names to custom display labels. If a video name is
41
+ not in the dictionary, the original video name is displayed.
31
42
 
32
43
  Example
33
44
  -------
45
+ Interactive mode (default):
46
+
34
47
  >>> from pynwb import NWBHDF5IO
35
48
  >>> with NWBHDF5IO("experiment.nwb", "r") as io:
36
49
  ... nwbfile = io.read()
37
50
  ... widget = NWBLocalVideoPlayer(nwbfile)
38
51
  ... display(widget)
39
52
 
53
+ Fixed grid mode (single row):
54
+
55
+ >>> widget = NWBLocalVideoPlayer(
56
+ ... nwbfile,
57
+ ... video_grid=[["VideoLeftCamera", "VideoBodyCamera", "VideoRightCamera"]]
58
+ ... )
59
+
60
+ Fixed grid mode (2x2 grid):
61
+
62
+ >>> widget = NWBLocalVideoPlayer(
63
+ ... nwbfile,
64
+ ... video_grid=[
65
+ ... ["VideoLeftCamera", "VideoRightCamera"],
66
+ ... ["VideoBodyCamera"],
67
+ ... ]
68
+ ... )
69
+
40
70
  Raises
41
71
  ------
42
72
  ValueError
@@ -49,6 +79,8 @@ class NWBLocalVideoPlayer(anywidget.AnyWidget):
49
79
  selected_videos = traitlets.List([]).tag(sync=True)
50
80
  layout_mode = traitlets.Unicode("row").tag(sync=True)
51
81
  settings_open = traitlets.Bool(False).tag(sync=True)
82
+ grid_layout = traitlets.List([]).tag(sync=True)
83
+ video_labels = traitlets.Dict({}).tag(sync=True)
52
84
 
53
85
  _esm = pathlib.Path(__file__).parent / "video_widget.js"
54
86
  _css = pathlib.Path(__file__).parent / "video_widget.css"
@@ -56,22 +88,50 @@ class NWBLocalVideoPlayer(anywidget.AnyWidget):
56
88
  def __init__(
57
89
  self,
58
90
  nwbfile: NWBFile,
91
+ video_grid: Optional[list[list[str]]] = None,
92
+ video_labels: Optional[dict[str, str]] = None,
59
93
  **kwargs,
60
94
  ):
61
95
  video_urls = self.get_video_urls_from_local(nwbfile)
62
96
  video_timestamps = get_video_timestamps(nwbfile)
63
97
  available_videos = get_video_info(nwbfile)
64
-
65
- # Start with no videos selected to avoid initial buffering
66
- super().__init__(
67
- video_urls=video_urls,
68
- video_timestamps=video_timestamps,
69
- available_videos=available_videos,
70
- selected_videos=[],
71
- layout_mode="grid",
72
- settings_open=True,
73
- **kwargs,
74
- )
98
+ video_labels = video_labels or {}
99
+
100
+ if video_grid is not None and len(video_grid) > 0:
101
+ # Fixed grid mode - bypass settings panel
102
+ # Filter to only include videos that exist in video_urls
103
+ filtered_grid = [
104
+ [v for v in row if v in video_urls]
105
+ for row in video_grid
106
+ ]
107
+ # Remove empty rows
108
+ filtered_grid = [row for row in filtered_grid if row]
109
+ # Flatten grid to get selected videos (preserving order)
110
+ selected = [v for row in filtered_grid for v in row]
111
+ super().__init__(
112
+ video_urls=video_urls,
113
+ video_timestamps=video_timestamps,
114
+ available_videos=available_videos,
115
+ selected_videos=selected,
116
+ layout_mode="grid",
117
+ settings_open=False,
118
+ grid_layout=filtered_grid,
119
+ video_labels=video_labels,
120
+ **kwargs,
121
+ )
122
+ else:
123
+ # Interactive mode (current behavior)
124
+ super().__init__(
125
+ video_urls=video_urls,
126
+ video_timestamps=video_timestamps,
127
+ available_videos=available_videos,
128
+ selected_videos=[],
129
+ layout_mode="grid",
130
+ settings_open=True,
131
+ grid_layout=[],
132
+ video_labels=video_labels,
133
+ **kwargs,
134
+ )
75
135
 
76
136
  @staticmethod
77
137
  def get_video_urls_from_local(nwbfile: NWBFile) -> dict[str, str]:
@@ -394,6 +394,17 @@ function render({ model, el }) {
394
394
  const urls = model.get("video_urls");
395
395
  const selectedVideos = model.get("selected_videos") || [];
396
396
  const layoutMode = model.get("layout_mode") || "row";
397
+ const gridLayout = model.get("grid_layout") || [];
398
+
399
+ // Check if we're in fixed grid mode (grid_layout is non-empty)
400
+ const isFixedGridMode = gridLayout.length > 0;
401
+
402
+ // Hide/show settings button based on mode
403
+ if (isFixedGridMode) {
404
+ settingsBtn.style.display = "none";
405
+ } else {
406
+ settingsBtn.style.display = "";
407
+ }
397
408
 
398
409
  // Filter to only selected videos that have URLs
399
410
  const videosToShow = selectedVideos.filter((name) => urls[name]);
@@ -406,25 +417,47 @@ function render({ model, el }) {
406
417
  return;
407
418
  }
408
419
 
409
- // Calculate grid dimensions based on layout mode
410
- const { rows: numRows, cols: numCols } = calculateGridDimensions(
411
- layoutMode,
412
- videosToShow.length
413
- );
420
+ let numRows, numCols;
421
+ let videoPositions = []; // Array of {name, rowIdx, colIdx}
422
+
423
+ if (isFixedGridMode) {
424
+ // Fixed grid mode - use 2D layout directly
425
+ numRows = gridLayout.length;
426
+ numCols = Math.max(...gridLayout.map((row) => row.length));
427
+
428
+ // Build positions from grid_layout
429
+ gridLayout.forEach((row, rowIdx) => {
430
+ row.forEach((name, colIdx) => {
431
+ if (urls[name]) {
432
+ videoPositions.push({ name, rowIdx, colIdx });
433
+ }
434
+ });
435
+ });
436
+ } else {
437
+ // Interactive mode - use selected_videos + layout_mode
438
+ const dims = calculateGridDimensions(layoutMode, videosToShow.length);
439
+ numRows = dims.rows;
440
+ numCols = dims.cols;
441
+
442
+ // Build positions from selected_videos order
443
+ videosToShow.forEach((name, index) => {
444
+ const rowIdx = Math.floor(index / numCols);
445
+ const colIdx = index % numCols;
446
+ videoPositions.push({ name, rowIdx, colIdx });
447
+ });
448
+ }
414
449
 
415
450
  gridContainer.style.gridTemplateColumns = "repeat(" + numCols + ", auto)";
416
451
  gridContainer.style.gridTemplateRows = "repeat(" + numRows + ", auto)";
417
452
 
418
- // Place videos in grid cells based on layout mode
419
- videosToShow.forEach((name, index) => {
453
+ // Place videos in grid cells
454
+ videoPositions.forEach(({ name, rowIdx, colIdx }) => {
420
455
  const url = urls[name];
421
- const rowIdx = Math.floor(index / numCols);
422
- const colIdx = index % numCols;
423
456
 
424
457
  const videoCell = document.createElement("div");
425
458
  videoCell.classList.add("video-widget__video-cell");
426
- videoCell.style.gridRow = rowIdx + 1;
427
- videoCell.style.gridColumn = colIdx + 1;
459
+ videoCell.style.gridRow = String(rowIdx + 1);
460
+ videoCell.style.gridColumn = String(colIdx + 1);
428
461
 
429
462
  const videoContainer = document.createElement("div");
430
463
  videoContainer.classList.add("video-widget__video-container");
@@ -463,8 +496,9 @@ function render({ model, el }) {
463
496
  videoContainer.appendChild(video);
464
497
  videoContainer.appendChild(loadingDiv);
465
498
 
499
+ const videoLabels = model.get("video_labels") || {};
466
500
  const label = document.createElement("p");
467
- label.textContent = name.replace("Video", "").replace("Camera", "");
501
+ label.textContent = videoLabels[name] || name;
468
502
  label.classList.add("video-widget__video-label");
469
503
 
470
504
  videoCell.appendChild(videoContainer);
@@ -544,6 +578,7 @@ function render({ model, el }) {
544
578
  updateSettingsPanel();
545
579
  });
546
580
  model.on("change:layout_mode", updateVideos);
581
+ model.on("change:grid_layout", updateVideos);
547
582
  model.on("change:settings_open", updateSettingsPanel);
548
583
  model.on("change:available_videos", updateSettingsPanel);
549
584
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwb-video-widgets
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Interactive Jupyter widgets for NWB video and pose visualization
5
5
  Project-URL: Homepage, https://github.com/catalystneuro/nwb-video-widgets
6
6
  Project-URL: Repository, https://github.com/catalystneuro/nwb-video-widgets
@@ -12,11 +12,12 @@ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Framework :: Jupyter
13
13
  Classifier: Intended Audience :: Science/Research
14
14
  Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: JavaScript
15
16
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
- Requires-Python: >=3.10
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: <3.14,>=3.10
20
21
  Requires-Dist: anywidget>=0.9.0
21
22
  Requires-Dist: matplotlib
22
23
  Requires-Dist: numpy
@@ -25,10 +26,6 @@ Provides-Extra: dandi
25
26
  Requires-Dist: dandi>=0.60.0; extra == 'dandi'
26
27
  Requires-Dist: h5py; extra == 'dandi'
27
28
  Requires-Dist: remfile>=0.1.13; extra == 'dandi'
28
- Provides-Extra: test
29
- Requires-Dist: opencv-python-headless; extra == 'test'
30
- Requires-Dist: pytest-cov; extra == 'test'
31
- Requires-Dist: pytest>=7.0; extra == 'test'
32
29
  Description-Content-Type: text/markdown
33
30
 
34
31
  # nwb-video-widgets
@@ -100,6 +97,70 @@ widget = NWBLocalVideoPlayer(nwbfile)
100
97
  widget
101
98
  ```
102
99
 
100
+ ### Fixed Grid Layout
101
+
102
+ When you know exactly which videos you want to display and how to arrange them, use the `video_grid` parameter to bypass the interactive settings panel. This is useful for:
103
+
104
+ - Reproducible notebooks where you want consistent output
105
+ - Presentations or demos with predetermined layouts
106
+ - Embedding widgets in dashboards or reports
107
+
108
+ The `video_grid` parameter accepts a 2D list where each inner list represents a row of videos:
109
+
110
+ ```python
111
+ # Single row of three cameras
112
+ widget = NWBLocalVideoPlayer(
113
+ nwbfile,
114
+ video_grid=[["VideoLeftCamera", "VideoBodyCamera", "VideoRightCamera"]]
115
+ )
116
+
117
+ # 2x2 grid layout
118
+ widget = NWBLocalVideoPlayer(
119
+ nwbfile,
120
+ video_grid=[
121
+ ["VideoLeftCamera", "VideoRightCamera"],
122
+ ["VideoBodyCamera", "VideoTopCamera"],
123
+ ]
124
+ )
125
+
126
+ # Asymmetric grid (2 videos on top, 1 on bottom)
127
+ widget = NWBLocalVideoPlayer(
128
+ nwbfile,
129
+ video_grid=[
130
+ ["VideoLeftCamera", "VideoRightCamera"],
131
+ ["VideoBodyCamera"],
132
+ ]
133
+ )
134
+ ```
135
+
136
+ The same parameter works with `NWBDANDIVideoPlayer`:
137
+
138
+ ```python
139
+ widget = NWBDANDIVideoPlayer(
140
+ asset=asset,
141
+ video_grid=[["VideoLeftCamera", "VideoRightCamera"]]
142
+ )
143
+ ```
144
+
145
+ Video names that don't exist in the NWB file are silently skipped.
146
+
147
+ ### Custom Video Labels
148
+
149
+ By default, the video name from the NWB file is displayed under each video. Use the `video_labels` parameter to provide custom display names:
150
+
151
+ ```python
152
+ widget = NWBLocalVideoPlayer(
153
+ nwbfile,
154
+ video_grid=[["VideoLeftCamera", "VideoRightCamera"]],
155
+ video_labels={
156
+ "VideoLeftCamera": "Left",
157
+ "VideoRightCamera": "Right",
158
+ }
159
+ )
160
+ ```
161
+
162
+ Videos not in the dictionary will display their original name.
163
+
103
164
  ---
104
165
 
105
166
  ## Pose Estimation Widgets
@@ -1,15 +1,15 @@
1
1
  nwb_video_widgets/__init__.py,sha256=4m-AlratOMO31jFrnWQSRqU2duXHMs7ZI3Iogs-hbAs,616
2
2
  nwb_video_widgets/_utils.py,sha256=kXcgjbq8mdgR0P4drPRy_Wrag3F5Od_7Ysikrr_VQMk,10566
3
3
  nwb_video_widgets/dandi_pose_widget.py,sha256=Tk_ZnqZIRIJzIAghuc1OEh0tmP9tMDP8UVDzZ4CoySg,13511
4
- nwb_video_widgets/dandi_video_widget.py,sha256=OaaJrNe0IoQ8X-fkDddIa6GfpCjsYZoso3-OAJEruTA,5020
4
+ nwb_video_widgets/dandi_video_widget.py,sha256=Jnn1-AXjZMNwVf9oL-0pZREjqdO6IS3YT5X_F2-4_Us,7342
5
5
  nwb_video_widgets/local_pose_widget.py,sha256=6yrm3qoRf0C1s2CFQXTv9xCUG9NrDm4_mEv2Z8_QYww,12774
6
- nwb_video_widgets/local_video_widget.py,sha256=c1RRTGkOdbtEBFHUw8FzHbpezWCdRPGauhXW72sTozM,4325
6
+ nwb_video_widgets/local_video_widget.py,sha256=V0JK9C04DLfa1fLFWxrikNML5Xs7vFNP1BC-VKqPeNs,6703
7
7
  nwb_video_widgets/pose_widget.css,sha256=nTaPfxr-6TC2IlcjNf0jVHDEqG3dx0i5jF1mtf1tzPo,15172
8
8
  nwb_video_widgets/pose_widget.js,sha256=zoXoLI1j63BOvngQ-Riufy7ae297VUidXW1KA7Z2TjI,26888
9
9
  nwb_video_widgets/video_widget.css,sha256=h6G67yoRNaVR9obRZWJ8xgo6KBKbzuRfUgDPKCYlBFo,11893
10
- nwb_video_widgets/video_widget.js,sha256=chqvXBD4p_9GEOvakVZHQqEz7WZwlzfNo0-toI_ix2Y,20235
10
+ nwb_video_widgets/video_widget.js,sha256=BV0lNXCvA_zIGtgWgL9rEbAJOfS7HCthJmjUl9cB6b0,21401
11
11
  nwb_video_widgets/video_widget.py,sha256=glRQOBquOHDFeKC_aW_fMvJGqqh90AsMRWzMGSokonA,6252
12
- nwb_video_widgets-0.1.0.dist-info/METADATA,sha256=wh2Hie2ItnAEnHc31M0Uf8KRL3g7KsjMGUGecd9Xll4,5177
13
- nwb_video_widgets-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- nwb_video_widgets-0.1.0.dist-info/licenses/LICENSE,sha256=9Xuwwu2OWDAyD-b0_T2W-E13CysuHPPHYu7aNSOtIMI,1070
15
- nwb_video_widgets-0.1.0.dist-info/RECORD,,
12
+ nwb_video_widgets-0.1.2.dist-info/METADATA,sha256=gtwuUaCbKUTbmdbMCg2paqui5KnQseX480wgRXhFgKc,6751
13
+ nwb_video_widgets-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ nwb_video_widgets-0.1.2.dist-info/licenses/LICENSE,sha256=9Xuwwu2OWDAyD-b0_T2W-E13CysuHPPHYu7aNSOtIMI,1070
15
+ nwb_video_widgets-0.1.2.dist-info/RECORD,,