pyviewarr 0.3.4__tar.gz → 0.4.0__tar.gz
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.
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/.gitignore +3 -0
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/PKG-INFO +2 -2
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/src/pyviewarr/__init__.py +82 -33
- pyviewarr-0.4.0/src/pyviewarr/static/widget.css +1 -0
- pyviewarr-0.4.0/src/pyviewarr/static/widget.js +32 -0
- pyviewarr-0.3.4/src/pyviewarr/static/widget.css +0 -1
- pyviewarr-0.3.4/src/pyviewarr/static/widget.js +0 -48
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/LICENSE +0 -0
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/README.md +0 -0
- {pyviewarr-0.3.4 → pyviewarr-0.4.0}/pyproject.toml +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
2
|
import pathlib
|
|
3
|
+
import warnings
|
|
3
4
|
from IPython.display import display
|
|
4
5
|
from dataclasses import asdict, dataclass
|
|
5
6
|
from typing import Any, Callable, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Union
|
|
@@ -310,18 +311,12 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
310
311
|
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
|
|
311
312
|
_css = pathlib.Path(__file__).parent / "static" / "widget.css"
|
|
312
313
|
|
|
313
|
-
#
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# Image dimensions
|
|
314
|
+
# Image dimensions of the currently shown slice (informational / introspection;
|
|
315
|
+
# the actual pixels are sent on demand over the custom message channel).
|
|
317
316
|
image_width = traitlets.Int(0).tag(sync=True)
|
|
318
317
|
image_height = traitlets.Int(0).tag(sync=True)
|
|
319
318
|
|
|
320
|
-
#
|
|
321
|
-
# Frontend applies setImageData when it observes a new token value.
|
|
322
|
-
_image_update_token = traitlets.Int(0).tag(sync=True)
|
|
323
|
-
|
|
324
|
-
# Data type string for viewarr (e.g., "f32", "u16")
|
|
319
|
+
# Data type string for viewarr (e.g., "f32", "u16") of the current slice.
|
|
325
320
|
dtype = traitlets.Unicode("f64").tag(sync=True)
|
|
326
321
|
|
|
327
322
|
# Widget display dimensions (CSS pixels)
|
|
@@ -331,7 +326,8 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
331
326
|
# Array shape (list of dimensions)
|
|
332
327
|
shape = traitlets.List(traitlets.Int()).tag(sync=True)
|
|
333
328
|
|
|
334
|
-
# Current slice indices for leading axes (empty for 2D arrays)
|
|
329
|
+
# Current slice indices for leading axes (empty for 2D arrays). Informational:
|
|
330
|
+
# navigation is driven by the frontend via the "request_slice" message.
|
|
335
331
|
current_slice_indices = traitlets.List(traitlets.Int()).tag(sync=True)
|
|
336
332
|
|
|
337
333
|
# Optional initial viewer state object (mapped to JS state keys)
|
|
@@ -426,8 +422,11 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
426
422
|
super().__init__(**kwargs)
|
|
427
423
|
self._on_shift_click = shift_click_callback
|
|
428
424
|
self._array = None
|
|
429
|
-
self.observe(self._on_slice_indices_changed, names=["current_slice_indices"])
|
|
430
425
|
self.observe(self._on_shift_click_event, names=["_shift_click_event"])
|
|
426
|
+
# Slices are served on demand over a custom message channel rather than
|
|
427
|
+
# via synced traits, so every request produces a fresh slice (synced List
|
|
428
|
+
# traits would dedup an unchanged value and stall a play-mode prefetch).
|
|
429
|
+
self.on_msg(self._handle_frontend_msg)
|
|
431
430
|
|
|
432
431
|
def set_shift_click_callback(
|
|
433
432
|
self, callback: Optional[ShiftClickCallback]
|
|
@@ -447,33 +446,84 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
447
446
|
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
|
|
448
447
|
callback(float(x), float(y))
|
|
449
448
|
|
|
450
|
-
def
|
|
451
|
-
"""
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def _update_slice(self):
|
|
455
|
-
"""Compute the current 2D slice and update traits."""
|
|
456
|
-
if self._array is None:
|
|
449
|
+
def _handle_frontend_msg(self, _widget, content, _buffers):
|
|
450
|
+
"""Handle a custom message from the frontend (e.g. a slice request)."""
|
|
451
|
+
if not isinstance(content, dict):
|
|
457
452
|
return
|
|
458
|
-
|
|
453
|
+
if content.get("type") == "request_slice":
|
|
454
|
+
try:
|
|
455
|
+
indices = [int(i) for i in content.get("indices", [])]
|
|
456
|
+
except (TypeError, ValueError):
|
|
457
|
+
return
|
|
458
|
+
# Every request must get an answer. The viewer only has one slice
|
|
459
|
+
# request in flight at a time, so a request that goes unanswered --
|
|
460
|
+
# because the array is gone, the indices are stale, or reading the
|
|
461
|
+
# slice raised -- would leave its scrubber dead for the rest of the
|
|
462
|
+
# session. Report the failure instead of letting it escape here.
|
|
463
|
+
try:
|
|
464
|
+
self._send_slice(indices)
|
|
465
|
+
except Exception as exc: # noqa: BLE001 - must not wedge the viewer
|
|
466
|
+
self._send_slice_error(indices, exc)
|
|
467
|
+
|
|
468
|
+
def _compute_slice(self, indices):
|
|
469
|
+
"""Return the 2D slice at ``indices`` as a contiguous little-endian array."""
|
|
459
470
|
arr = self._array
|
|
460
|
-
indices = self.current_slice_indices
|
|
461
|
-
|
|
462
|
-
# Slice the array: leading axes use indices, last two are full slices
|
|
463
471
|
if len(indices) > 0:
|
|
464
472
|
slice_obj = tuple(indices) + (slice(None), slice(None))
|
|
465
473
|
slice_arr = arr[slice_obj]
|
|
466
474
|
else:
|
|
467
475
|
slice_arr = arr
|
|
468
|
-
|
|
469
|
-
# Ensure slice is contiguous and little-endian
|
|
470
476
|
slice_arr = np.ascontiguousarray(slice_arr)
|
|
471
|
-
|
|
477
|
+
return slice_arr.astype(slice_arr.dtype.newbyteorder("<"))
|
|
472
478
|
|
|
473
|
-
|
|
479
|
+
def _refresh_slice_state(self, indices):
|
|
480
|
+
"""Compute the slice at ``indices`` and update the informational traits."""
|
|
481
|
+
slice_arr = self._compute_slice(indices)
|
|
482
|
+
self.current_slice_indices = list(indices)
|
|
474
483
|
self.image_height, self.image_width = slice_arr.shape
|
|
475
|
-
self.
|
|
476
|
-
|
|
484
|
+
self.dtype = _numpy_dtype_to_viewarr(slice_arr.dtype)
|
|
485
|
+
return slice_arr
|
|
486
|
+
|
|
487
|
+
def _send_slice(self, indices):
|
|
488
|
+
"""Serve a slice to the frontend over the custom message channel.
|
|
489
|
+
|
|
490
|
+
Always produces a slice for the requested indices (no dedup), so a
|
|
491
|
+
play-mode prefetch that re-requests the slice the kernel already holds
|
|
492
|
+
still completes.
|
|
493
|
+
"""
|
|
494
|
+
if self._array is None:
|
|
495
|
+
raise RuntimeError("no array is set on this viewer")
|
|
496
|
+
slice_arr = self._refresh_slice_state(indices)
|
|
497
|
+
self.send(
|
|
498
|
+
{
|
|
499
|
+
"type": "slice",
|
|
500
|
+
"indices": list(indices),
|
|
501
|
+
"width": int(self.image_width),
|
|
502
|
+
"height": int(self.image_height),
|
|
503
|
+
"dtype": self.dtype,
|
|
504
|
+
},
|
|
505
|
+
buffers=[slice_arr.tobytes()],
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def _send_slice_error(self, indices, exc):
|
|
509
|
+
"""Tell the frontend a slice request could not be served.
|
|
510
|
+
|
|
511
|
+
The frontend hands this to the viewer so it stops waiting on the reply
|
|
512
|
+
and the slider keeps working; without it one failed read disables
|
|
513
|
+
slice navigation until the widget is recreated.
|
|
514
|
+
"""
|
|
515
|
+
warnings.warn(
|
|
516
|
+
f"pyviewarr could not serve slice {list(indices)}: {exc}",
|
|
517
|
+
RuntimeWarning,
|
|
518
|
+
stacklevel=2,
|
|
519
|
+
)
|
|
520
|
+
self.send(
|
|
521
|
+
{
|
|
522
|
+
"type": "slice_error",
|
|
523
|
+
"indices": list(indices),
|
|
524
|
+
"message": f"{type(exc).__name__}: {exc}",
|
|
525
|
+
}
|
|
526
|
+
)
|
|
477
527
|
|
|
478
528
|
def set_array(self, arr: np.ndarray) -> None:
|
|
479
529
|
"""Set the array data to display.
|
|
@@ -493,12 +543,11 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
493
543
|
# Set shape
|
|
494
544
|
self.shape = list(arr.shape)
|
|
495
545
|
|
|
496
|
-
# Initialize slice
|
|
546
|
+
# Initialize on the first slice. We only refresh the informational traits
|
|
547
|
+
# here; the frontend pulls the actual pixels on demand (on viewer-ready
|
|
548
|
+
# and whenever the shape changes) via the "request_slice" message.
|
|
497
549
|
num_leading_axes = arr.ndim - 2
|
|
498
|
-
self.
|
|
499
|
-
|
|
500
|
-
# Update the displayed slice
|
|
501
|
-
self._update_slice()
|
|
550
|
+
self._refresh_slice_state([0] * num_leading_axes)
|
|
502
551
|
|
|
503
552
|
def get_current_slice(self) -> np.ndarray:
|
|
504
553
|
"""Get the current 2D slice being displayed.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.pyviewarr{display:inline-block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pyviewarr-container{background-color:transparent;border:1px solid var(--jp-border-color1, var(--vscode-panel-border, #333));overflow:hidden}.pyviewarr-container:focus-within{border-color:var(--jp-brand-color1, var(--vscode-focusBorder, #2196f3));box-shadow:inset 0 0 0 1px var(--jp-brand-color1, var(--vscode-focusBorder, #2196f3))}.pyviewarr-container canvas:focus{outline:none}
|