pyviewarr 0.3.2__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyviewarr
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
- - No unreleased changes yet.
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
- - No unreleased changes yet.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyviewarr"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  dependencies = ["anywidget>=0.9,<0.10", "numpy>=2.0,<3"]
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- kwargs["viewer_config"] = ViewerConfig(**dict(viewer_config)).to_js_dict()
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(