pyviewarr 0.3.2__tar.gz → 0.3.4__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.2 → pyviewarr-0.3.4}/PKG-INFO +55 -2
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/README.md +54 -1
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/pyproject.toml +15 -3
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/src/pyviewarr/__init__.py +61 -2
- pyviewarr-0.3.4/src/pyviewarr/static/widget.js +48 -0
- pyviewarr-0.3.2/src/pyviewarr/static/widget.js +0 -48
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/.gitignore +0 -0
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/LICENSE +0 -0
- {pyviewarr-0.3.2 → pyviewarr-0.3.4}/src/pyviewarr/static/widget.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyviewarr
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
License-File: LICENSE
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: anywidget<0.10,>=0.9
|
|
@@ -107,6 +107,9 @@ Use the left and right arrow buttons to rotate in 15º increments, or enter a nu
|
|
|
107
107
|
|
|
108
108
|
By default, rotation is about the image center, but you can set a pivot point by Cmd/Ctrl-Shift-clicking the desired point.
|
|
109
109
|
|
|
110
|
+
Shift-clicking on the image calls an optional Python callback with continuous data-space coordinates (`x`, `y` including fractional pixel position).
|
|
111
|
+
Overlay help text is optional and only shown when you provide `overlay_message`.
|
|
112
|
+
|
|
110
113
|
### Matplotlib integration
|
|
111
114
|
|
|
112
115
|
Once you've rotated and panned and stretched and zoomed you may want to save a plot, but the viewer doesn't handle plotting.
|
|
@@ -140,6 +143,36 @@ pyviewarr.show(x, vmin=-10, vmax=10)
|
|
|
140
143
|
|
|
141
144
|
See [example](./preconfigured_state_example.ipynb).
|
|
142
145
|
|
|
146
|
+
#### Marker coordinates API
|
|
147
|
+
|
|
148
|
+
Use `widget.markers` to supply marker positions as continuous image coordinates.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
widget = pyviewarr.viewarr(x)
|
|
152
|
+
widget.markers = [(10.5, 20.25), (100.0, 42.0)]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Markers are rendered as fixed-size plus signs in screen space, and follow pan/zoom/rotation transforms.
|
|
156
|
+
|
|
157
|
+
#### Shift-click callback API
|
|
158
|
+
|
|
159
|
+
Use `ViewerConfig.on_shift_click` to receive continuous data-space click coordinates:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
def on_shift_click(x: float, y: float) -> None:
|
|
163
|
+
print(f"Shift-click at x={x:.3f}, y={y:.3f}")
|
|
164
|
+
|
|
165
|
+
cfg = pyviewarr.ViewerConfig(
|
|
166
|
+
on_shift_click=on_shift_click,
|
|
167
|
+
overlay_message="Shift-click to report coordinates",
|
|
168
|
+
)
|
|
169
|
+
widget = pyviewarr.viewarr(x, viewer_config=cfg)
|
|
170
|
+
widget
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
For a demo that combines callbacks with marker updates, see
|
|
174
|
+
[shift-click callback demo](./notebooks/shift_click_callback_demo.ipynb).
|
|
175
|
+
|
|
143
176
|
#### Setting data
|
|
144
177
|
|
|
145
178
|
If you retain a reference to the widget, you can set its contents from a later cell (if you want).
|
|
@@ -252,7 +285,27 @@ GitHub releases are automatically pushed to PyPI by the workflow in [`.github/wo
|
|
|
252
285
|
|
|
253
286
|
### Unreleased (since `v0.3.2`)
|
|
254
287
|
|
|
255
|
-
|
|
288
|
+
#### pyviewarr changes
|
|
289
|
+
|
|
290
|
+
- Added `ViewerConfig.on_shift_click` Python callback to receive shift-click coordinates in data space (`x`, `y`) with fractional precision.
|
|
291
|
+
- Refactored overlay text to generic `ViewerConfig.overlay_message` (not callback-specific).
|
|
292
|
+
- Removed default overlay text; overlay is now hidden unless `overlay_message` is provided.
|
|
293
|
+
- Added tests covering shift-click callback wiring and `ViewerConfig` JS serialization exclusions for Python-only fields.
|
|
294
|
+
- Added a demonstration notebook: [`notebooks/shift_click_callback_demo.ipynb`](./notebooks/shift_click_callback_demo.ipynb).
|
|
295
|
+
- Updated notebook callback logging example to use `ipywidgets.Output` for reliable async callback output display.
|
|
296
|
+
- Updated usage docs with shift-click callback example and notebook link.
|
|
297
|
+
- Updated bundled `viewarr` submodule to include shift-click callback and overlay layout improvements.
|
|
298
|
+
- Updated bundled `viewarr` submodule to include diverging-mode colorbar limit textbox fixes.
|
|
299
|
+
|
|
300
|
+
#### Included `viewarr` changes
|
|
301
|
+
|
|
302
|
+
- Added shift-click callback events via `onClick(...)` with continuous (fractional) data-space coordinates.
|
|
303
|
+
- Added generic overlay message APIs: `getOverlayMessage(...)` and `setOverlayMessage(...)`.
|
|
304
|
+
- Added `overlayMessage` support to `setViewerState(...)`.
|
|
305
|
+
- Renamed overlay API from shift-click-specific names to generic names (`getOverlayMessage`/`setOverlayMessage`, `overlayMessage` in state config).
|
|
306
|
+
- Fixed diverging/symmetric colorbar limit editing/reporting behavior and `vmin` textbox refresh when switching back to non-symmetric modes.
|
|
307
|
+
- Removed default overlay text; overlay only renders when message text is supplied.
|
|
308
|
+
- Updated overlay placement to compute a live safe area between bottom overlays, avoiding overlap with hover coordinates and zoom controls while keeping the message centered in that gap.
|
|
256
309
|
|
|
257
310
|
### `v0.3.2`
|
|
258
311
|
|
|
@@ -93,6 +93,9 @@ Use the left and right arrow buttons to rotate in 15º increments, or enter a nu
|
|
|
93
93
|
|
|
94
94
|
By default, rotation is about the image center, but you can set a pivot point by Cmd/Ctrl-Shift-clicking the desired point.
|
|
95
95
|
|
|
96
|
+
Shift-clicking on the image calls an optional Python callback with continuous data-space coordinates (`x`, `y` including fractional pixel position).
|
|
97
|
+
Overlay help text is optional and only shown when you provide `overlay_message`.
|
|
98
|
+
|
|
96
99
|
### Matplotlib integration
|
|
97
100
|
|
|
98
101
|
Once you've rotated and panned and stretched and zoomed you may want to save a plot, but the viewer doesn't handle plotting.
|
|
@@ -126,6 +129,36 @@ pyviewarr.show(x, vmin=-10, vmax=10)
|
|
|
126
129
|
|
|
127
130
|
See [example](./preconfigured_state_example.ipynb).
|
|
128
131
|
|
|
132
|
+
#### Marker coordinates API
|
|
133
|
+
|
|
134
|
+
Use `widget.markers` to supply marker positions as continuous image coordinates.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
widget = pyviewarr.viewarr(x)
|
|
138
|
+
widget.markers = [(10.5, 20.25), (100.0, 42.0)]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Markers are rendered as fixed-size plus signs in screen space, and follow pan/zoom/rotation transforms.
|
|
142
|
+
|
|
143
|
+
#### Shift-click callback API
|
|
144
|
+
|
|
145
|
+
Use `ViewerConfig.on_shift_click` to receive continuous data-space click coordinates:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
def on_shift_click(x: float, y: float) -> None:
|
|
149
|
+
print(f"Shift-click at x={x:.3f}, y={y:.3f}")
|
|
150
|
+
|
|
151
|
+
cfg = pyviewarr.ViewerConfig(
|
|
152
|
+
on_shift_click=on_shift_click,
|
|
153
|
+
overlay_message="Shift-click to report coordinates",
|
|
154
|
+
)
|
|
155
|
+
widget = pyviewarr.viewarr(x, viewer_config=cfg)
|
|
156
|
+
widget
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For a demo that combines callbacks with marker updates, see
|
|
160
|
+
[shift-click callback demo](./notebooks/shift_click_callback_demo.ipynb).
|
|
161
|
+
|
|
129
162
|
#### Setting data
|
|
130
163
|
|
|
131
164
|
If you retain a reference to the widget, you can set its contents from a later cell (if you want).
|
|
@@ -238,7 +271,27 @@ GitHub releases are automatically pushed to PyPI by the workflow in [`.github/wo
|
|
|
238
271
|
|
|
239
272
|
### Unreleased (since `v0.3.2`)
|
|
240
273
|
|
|
241
|
-
|
|
274
|
+
#### pyviewarr changes
|
|
275
|
+
|
|
276
|
+
- Added `ViewerConfig.on_shift_click` Python callback to receive shift-click coordinates in data space (`x`, `y`) with fractional precision.
|
|
277
|
+
- Refactored overlay text to generic `ViewerConfig.overlay_message` (not callback-specific).
|
|
278
|
+
- Removed default overlay text; overlay is now hidden unless `overlay_message` is provided.
|
|
279
|
+
- Added tests covering shift-click callback wiring and `ViewerConfig` JS serialization exclusions for Python-only fields.
|
|
280
|
+
- Added a demonstration notebook: [`notebooks/shift_click_callback_demo.ipynb`](./notebooks/shift_click_callback_demo.ipynb).
|
|
281
|
+
- Updated notebook callback logging example to use `ipywidgets.Output` for reliable async callback output display.
|
|
282
|
+
- Updated usage docs with shift-click callback example and notebook link.
|
|
283
|
+
- Updated bundled `viewarr` submodule to include shift-click callback and overlay layout improvements.
|
|
284
|
+
- Updated bundled `viewarr` submodule to include diverging-mode colorbar limit textbox fixes.
|
|
285
|
+
|
|
286
|
+
#### Included `viewarr` changes
|
|
287
|
+
|
|
288
|
+
- Added shift-click callback events via `onClick(...)` with continuous (fractional) data-space coordinates.
|
|
289
|
+
- Added generic overlay message APIs: `getOverlayMessage(...)` and `setOverlayMessage(...)`.
|
|
290
|
+
- Added `overlayMessage` support to `setViewerState(...)`.
|
|
291
|
+
- Renamed overlay API from shift-click-specific names to generic names (`getOverlayMessage`/`setOverlayMessage`, `overlayMessage` in state config).
|
|
292
|
+
- Fixed diverging/symmetric colorbar limit editing/reporting behavior and `vmin` textbox refresh when switching back to non-symmetric modes.
|
|
293
|
+
- Removed default overlay text; overlay only renders when message text is supplied.
|
|
294
|
+
- Updated overlay placement to compute a live safe area between bottom overlays, avoiding overlap with hover coordinates and zoom controls while keeping the message centered in that gap.
|
|
242
295
|
|
|
243
296
|
### `v0.3.2`
|
|
244
297
|
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyviewarr"
|
|
7
|
-
version = "0.3.2"
|
|
8
7
|
dependencies = ["anywidget>=0.9,<0.10", "numpy>=2.0,<3"]
|
|
9
8
|
readme = "README.md"
|
|
10
9
|
requires-python = ">=3.10"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
|
|
12
|
+
# Single source of truth for the version: the monorepo git tag (e.g. v0.3.4),
|
|
13
|
+
# shared with jupyterlab-fitsview so both packages always release the same
|
|
14
|
+
# version. root = ".." points setuptools_scm at the monorepo root (.git).
|
|
15
|
+
[tool.hatch.version]
|
|
16
|
+
source = "vcs"
|
|
17
|
+
raw-options = { root = "..", local_scheme = "no-local-version" }
|
|
11
18
|
|
|
12
19
|
# For projects not using `uv`, you can install these development dependencies with:
|
|
13
20
|
# `pip install -e ".[dev]"`
|
|
@@ -33,7 +40,12 @@ dependencies = ["hatch-jupyter-builder>=0.5.0"]
|
|
|
33
40
|
|
|
34
41
|
[tool.hatch.build.hooks.jupyter-builder.build-kwargs]
|
|
35
42
|
npm = "npm"
|
|
36
|
-
|
|
43
|
+
# Monorepo single-source: bundle the widget from the already-built shared
|
|
44
|
+
# viewarr/pkg (`build`), rather than recompiling the WASM with wasm-pack
|
|
45
|
+
# (`build:all`). The editable-install path runs this hook even when
|
|
46
|
+
# skip-if-exists matches, and wasm-pack is intentionally not installed
|
|
47
|
+
# outside the build-viewarr CI job. Build viewarr first (see root README).
|
|
48
|
+
build_cmd = "build"
|
|
37
49
|
|
|
38
50
|
[tool.pytest.ini_options]
|
|
39
51
|
testpaths = ["tests"]
|
|
@@ -2,7 +2,7 @@ import importlib.metadata
|
|
|
2
2
|
import pathlib
|
|
3
3
|
from IPython.display import display
|
|
4
4
|
from dataclasses import asdict, dataclass
|
|
5
|
-
from typing import Any, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Union
|
|
5
|
+
from typing import Any, Callable, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Union
|
|
6
6
|
|
|
7
7
|
import anywidget
|
|
8
8
|
import numpy as np
|
|
@@ -47,6 +47,9 @@ _CMAP_CANONICAL_NAMES = {
|
|
|
47
47
|
"rdylbu": "RdYlBu",
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
ShiftClickCallback = Callable[[float, float], None]
|
|
51
|
+
MarkerPoint = Tuple[float, float]
|
|
52
|
+
|
|
50
53
|
|
|
51
54
|
def _numpy_dtype_to_viewarr(dtype: np.dtype) -> str:
|
|
52
55
|
"""Convert numpy dtype to viewarr type string."""
|
|
@@ -262,10 +265,19 @@ class ViewerConfig:
|
|
|
262
265
|
rotation: Optional[float] = None
|
|
263
266
|
pivot: Optional[Tuple[float, float]] = None
|
|
264
267
|
show_pivot_marker: Optional[bool] = None
|
|
268
|
+
markers: Optional[list[MarkerPoint]] = None
|
|
269
|
+
on_shift_click: Optional[ShiftClickCallback] = None
|
|
270
|
+
overlay_message: Optional[str] = None
|
|
265
271
|
|
|
266
272
|
def to_js_dict(self) -> Dict[str, Any]:
|
|
267
273
|
"""Convert config to the JS object shape expected by the frontend."""
|
|
268
274
|
raw = asdict(self)
|
|
275
|
+
raw.pop("on_shift_click", None)
|
|
276
|
+
raw.pop("overlay_message", None)
|
|
277
|
+
if raw.get("markers") is not None:
|
|
278
|
+
raw["markers"] = [
|
|
279
|
+
(float(point[0]), float(point[1])) for point in raw["markers"]
|
|
280
|
+
]
|
|
269
281
|
if raw.get("cmap") is not None:
|
|
270
282
|
cmap = str(raw["cmap"]).strip()
|
|
271
283
|
raw["cmap"] = _CMAP_CANONICAL_NAMES.get(cmap.lower(), cmap)
|
|
@@ -324,6 +336,10 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
324
336
|
|
|
325
337
|
# Optional initial viewer state object (mapped to JS state keys)
|
|
326
338
|
viewer_config = traitlets.Dict(default_value={}).tag(sync=True)
|
|
339
|
+
# Optional overlay message shown at bottom-center of the viewer
|
|
340
|
+
overlay_message = traitlets.Unicode("").tag(sync=True)
|
|
341
|
+
# Latest shift-click event from frontend: {"x": float, "y": float, "token": int}
|
|
342
|
+
_shift_click_event = traitlets.Dict(default_value={}).tag(sync=True)
|
|
327
343
|
|
|
328
344
|
# =========================================================================
|
|
329
345
|
# Viewer state properties (bidirectional sync with frontend)
|
|
@@ -373,6 +389,10 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
373
389
|
|
|
374
390
|
# Whether to show the pivot marker
|
|
375
391
|
show_pivot_marker = traitlets.Bool(False).tag(sync=True)
|
|
392
|
+
# Marker list as continuous image coordinates: [(x, y), ...]
|
|
393
|
+
markers = traitlets.List(
|
|
394
|
+
traitlets.Tuple(traitlets.Float(), traitlets.Float()), default_value=[]
|
|
395
|
+
).tag(sync=True)
|
|
376
396
|
|
|
377
397
|
# Internal flag to prevent feedback loops during sync
|
|
378
398
|
_sync_from_viewer = traitlets.Bool(False).tag(sync=True)
|
|
@@ -382,14 +402,50 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
382
402
|
viewer_config: Optional[Union[ViewerConfig, Dict[str, Any]]] = None,
|
|
383
403
|
**kwargs,
|
|
384
404
|
):
|
|
405
|
+
shift_click_callback = None
|
|
385
406
|
if viewer_config is not None:
|
|
386
407
|
if isinstance(viewer_config, ViewerConfig):
|
|
408
|
+
shift_click_callback = viewer_config.on_shift_click
|
|
409
|
+
if viewer_config.overlay_message is not None:
|
|
410
|
+
kwargs["overlay_message"] = viewer_config.overlay_message
|
|
411
|
+
if viewer_config.markers is not None:
|
|
412
|
+
kwargs["markers"] = [
|
|
413
|
+
(float(x), float(y)) for (x, y) in viewer_config.markers
|
|
414
|
+
]
|
|
387
415
|
kwargs["viewer_config"] = viewer_config.to_js_dict()
|
|
388
416
|
else:
|
|
389
|
-
|
|
417
|
+
config_dict = dict(viewer_config)
|
|
418
|
+
shift_click_callback = config_dict.pop("on_shift_click", None)
|
|
419
|
+
overlay_message = config_dict.pop("overlay_message", None)
|
|
420
|
+
markers = config_dict.get("markers")
|
|
421
|
+
if overlay_message is not None:
|
|
422
|
+
kwargs["overlay_message"] = overlay_message
|
|
423
|
+
if markers is not None:
|
|
424
|
+
kwargs["markers"] = [(float(x), float(y)) for (x, y) in markers]
|
|
425
|
+
kwargs["viewer_config"] = ViewerConfig(**config_dict).to_js_dict()
|
|
390
426
|
super().__init__(**kwargs)
|
|
427
|
+
self._on_shift_click = shift_click_callback
|
|
391
428
|
self._array = None
|
|
392
429
|
self.observe(self._on_slice_indices_changed, names=["current_slice_indices"])
|
|
430
|
+
self.observe(self._on_shift_click_event, names=["_shift_click_event"])
|
|
431
|
+
|
|
432
|
+
def set_shift_click_callback(
|
|
433
|
+
self, callback: Optional[ShiftClickCallback]
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Set or clear the Python callback invoked on shift-click events."""
|
|
436
|
+
self._on_shift_click = callback
|
|
437
|
+
|
|
438
|
+
def _on_shift_click_event(self, change):
|
|
439
|
+
callback = self._on_shift_click
|
|
440
|
+
if callback is None:
|
|
441
|
+
return
|
|
442
|
+
event = change.get("new", {})
|
|
443
|
+
if not isinstance(event, dict):
|
|
444
|
+
return
|
|
445
|
+
x = event.get("x")
|
|
446
|
+
y = event.get("y")
|
|
447
|
+
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
|
|
448
|
+
callback(float(x), float(y))
|
|
393
449
|
|
|
394
450
|
def _on_slice_indices_changed(self, change):
|
|
395
451
|
"""Update the displayed slice when slice indices change."""
|
|
@@ -497,6 +553,9 @@ class ViewArrWidget(anywidget.AnyWidget):
|
|
|
497
553
|
rotation=self.rotation,
|
|
498
554
|
pivot=self.pivot,
|
|
499
555
|
show_pivot_marker=self.show_pivot_marker,
|
|
556
|
+
markers=self.markers,
|
|
557
|
+
on_shift_click=self._on_shift_click,
|
|
558
|
+
overlay_message=self.overlay_message,
|
|
500
559
|
)
|
|
501
560
|
|
|
502
561
|
def plot_to_matplotlib(
|