napari-spatialdata 0.7.1__tar.gz → 0.7.2__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.
Files changed (99) hide show
  1. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.pre-commit-config.yaml +3 -3
  2. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/PKG-INFO +1 -1
  3. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_model.py +0 -2
  4. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_scatterwidgets.py +8 -16
  5. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_sdata_widgets.py +57 -4
  6. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_version.py +3 -3
  7. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_view.py +5 -13
  8. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_viewer.py +57 -14
  9. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/utils/_utils.py +55 -10
  10. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/PKG-INFO +1 -1
  11. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/SOURCES.txt +2 -0
  12. napari_spatialdata-0.7.2/src/napari_spatialdata.egg-info/scm_file_list.json +92 -0
  13. napari_spatialdata-0.7.2/src/napari_spatialdata.egg-info/scm_version.json +8 -0
  14. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/conftest.py +57 -1
  15. napari_spatialdata-0.7.2/tests/test_3d_visualization.py +350 -0
  16. napari_spatialdata-0.7.1/tests/test_3d_visualization.py +0 -161
  17. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/BENCHMARK_FAIL_TEMPLATE.md +0 -0
  18. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/dependabot.yml +0 -0
  19. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/release.yml +0 -0
  20. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/workflows/benchmarks.yml +0 -0
  21. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/workflows/benchmarks_report.yml +0 -0
  22. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/workflows/release.yml +0 -0
  23. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.github/workflows/test.yml +0 -0
  24. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.gitignore +0 -0
  25. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.mypy.ini +0 -0
  26. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.napari/DESCRIPTION.md +0 -0
  27. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/.readthedocs.yaml +0 -0
  28. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/LICENSE +0 -0
  29. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/MANIFEST.in +0 -0
  30. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/README.md +0 -0
  31. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/asv.conf.json +0 -0
  32. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/Readme.md +0 -0
  33. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/__init__.py +0 -0
  34. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/benchmark.in +0 -0
  35. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/benchmark.txt +0 -0
  36. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/benchmark_model.py +0 -0
  37. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/benchmarks/benchmark_qt_widget.py +0 -0
  38. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/Makefile +0 -0
  39. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/_static/.gitkeep +0 -0
  40. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/_templates/.gitkeep +0 -0
  41. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/_templates/autosummary/class.rst +0 -0
  42. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/api.md +0 -0
  43. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/changelog.md +0 -0
  44. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/cli.md +0 -0
  45. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/conf.py +0 -0
  46. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/contributing.md +0 -0
  47. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/extensions/typed_returns.py +0 -0
  48. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/index.md +0 -0
  49. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/limitations.md +0 -0
  50. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/make.bat +0 -0
  51. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/notebooks/mibitof_analysis.ipynb +0 -0
  52. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/notebooks/nanostring_analysis.ipynb +0 -0
  53. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/notebooks/scatterwidget.ipynb +0 -0
  54. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/notebooks/scatterwidget_annotation.ipynb +0 -0
  55. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/notebooks/spatialdata.ipynb +0 -0
  56. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/references.bib +0 -0
  57. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/docs/references.md +0 -0
  58. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/examples/spatialdata_empty.py +0 -0
  59. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/examples/spatialdata_visium.py +0 -0
  60. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/pyproject.toml +0 -0
  61. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/setup.cfg +0 -0
  62. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/__init__.py +0 -0
  63. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/__main__.py +0 -0
  64. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_annotationwidgets.py +0 -0
  65. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_interactive.py +0 -0
  66. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_reader.py +0 -0
  67. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/_widgets.py +0 -0
  68. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/constants/__init__.py +0 -0
  69. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/constants/_constants.py +0 -0
  70. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/constants/_pkg_constants.py +0 -0
  71. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/constants/_utils.py +0 -0
  72. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/constants/config.py +0 -0
  73. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/napari.yaml +0 -0
  74. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/resources/exclamation.png +0 -0
  75. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/resources/icons8-home-48.png +0 -0
  76. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/resources/icons8-pencil-drawing-50.png +0 -0
  77. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/resources/icons8-polygon-80.png +0 -0
  78. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/resources/icons8-rectangle-48.png +0 -0
  79. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/utils/__init__.py +0 -0
  80. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/utils/_categoricals_utils.py +0 -0
  81. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/utils/_test_utils.py +0 -0
  82. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata/utils/_viewer_utils.py +0 -0
  83. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/dependency_links.txt +0 -0
  84. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/entry_points.txt +0 -0
  85. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/requires.txt +0 -0
  86. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/src/napari_spatialdata.egg-info/top_level.txt +0 -0
  87. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/__init__.py +0 -0
  88. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/plots/groundtruth/Images_can_add_element_image.png +0 -0
  89. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/plots/groundtruth/Images_can_add_element_label.png +0 -0
  90. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/plots/groundtruth/Images_can_add_element_multiple.png +0 -0
  91. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_cli.py +0 -0
  92. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_interactive.py +0 -0
  93. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_scatterwidgets.py +0 -0
  94. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_spatialdata.py +0 -0
  95. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_utils.py +0 -0
  96. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_view.py +0 -0
  97. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_viewer.py +0 -0
  98. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tests/test_widgets.py +0 -0
  99. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.2}/tox.ini +0 -0
@@ -7,13 +7,13 @@ default_stages:
7
7
  minimum_pre_commit_version: 2.9.3
8
8
  repos:
9
9
  - repo: https://github.com/pre-commit/mirrors-mypy
10
- rev: v1.19.1
10
+ rev: v1.20.2
11
11
  hooks:
12
12
  - id: mypy
13
13
  additional_dependencies: [numpy>=1.23]
14
14
  exclude: docs
15
15
  - repo: https://github.com/psf/black-pre-commit-mirror
16
- rev: 25.12.0
16
+ rev: 26.3.1
17
17
  hooks:
18
18
  - id: black
19
19
  additional_dependencies: [toml]
@@ -49,7 +49,7 @@ repos:
49
49
  hooks:
50
50
  - id: blacken-docs
51
51
  - repo: https://github.com/astral-sh/ruff-pre-commit
52
- rev: v0.14.10
52
+ rev: v0.15.12
53
53
  hooks:
54
54
  - id: ruff
55
55
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-spatialdata
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Interactive visualization of spatial omics data with napari
5
5
  Home-page: https://github.com/scverse/napari-spatialdata.git
6
6
  Author: giovanni palla
@@ -28,8 +28,6 @@ class DataModel:
28
28
  _layer: Layer = field(init=False, default=None, repr=True)
29
29
  _adata: AnnData | None = field(init=False, default=None, repr=True)
30
30
  _adata_layer: str | None = field(init=False, default=None, repr=False)
31
- _region_key: str | None = field(default=None, repr=True)
32
- _instance_key: str | None = field(default=None, repr=True)
33
31
  _color_by: str = field(default="", repr=True, init=False)
34
32
  _system_name: str | None = field(default=None, repr=True)
35
33
 
@@ -251,14 +251,12 @@ class DiscreteColorWidget(GraphicsWidget):
251
251
  color_button = ColorButton(color=color)
252
252
  color_button.setMinimumSize(60, 30)
253
253
  color_button.setMaximumSize(60, 30)
254
- color_button.setStyleSheet(
255
- """
254
+ color_button.setStyleSheet("""
256
255
  QPushButton {
257
256
  background-color: transparent;
258
257
  border: none;
259
258
  }
260
- """
261
- )
259
+ """)
262
260
 
263
261
  self.color_buttons[obj_category] = color_button
264
262
 
@@ -384,8 +382,7 @@ class PlotWidget(GraphicsLayoutWidget):
384
382
  self.drawing_mode_button = QPushButton(self)
385
383
  self.drawing_mode_button.setIcon(QIcon(str(Path(__file__).parent / "resources/icons8-polygon-80.png")))
386
384
  self.drawing_mode_button.setIconSize(QSize(24, 24))
387
- self.drawing_mode_button.setStyleSheet(
388
- f"""
385
+ self.drawing_mode_button.setStyleSheet(f"""
389
386
  QPushButton {{
390
387
  background-color: transparent;
391
388
  border: none;
@@ -393,8 +390,7 @@ class PlotWidget(GraphicsLayoutWidget):
393
390
  QPushButton:checked {{
394
391
  border: 1px solid rgb{self.color};
395
392
  }}
396
- """
397
- )
393
+ """)
398
394
  self.drawing_mode_button.setCheckable(True)
399
395
  self.drawing_mode_button.clicked.connect(self.toggle_drawing_mode)
400
396
  self.drawing_mode_button.setToolTip("Add freehand ROIs.")
@@ -405,8 +401,7 @@ class PlotWidget(GraphicsLayoutWidget):
405
401
  self.rectangle_mode_button = QPushButton(self)
406
402
  self.rectangle_mode_button.setIcon(QIcon(str(Path(__file__).parent / "resources/icons8-rectangle-48.png")))
407
403
  self.rectangle_mode_button.setIconSize(QSize(24, 24))
408
- self.rectangle_mode_button.setStyleSheet(
409
- f"""
404
+ self.rectangle_mode_button.setStyleSheet(f"""
410
405
  QPushButton {{
411
406
  background-color: transparent;
412
407
  border: none;
@@ -414,8 +409,7 @@ class PlotWidget(GraphicsLayoutWidget):
414
409
  QPushButton:checked {{
415
410
  border: 1px solid rgb{self.color};
416
411
  }}
417
- """
418
- )
412
+ """)
419
413
  self.rectangle_mode_button.setCheckable(True)
420
414
  self.rectangle_mode_button.clicked.connect(self.toggle_rectangle_mode)
421
415
  self.rectangle_mode_button.setToolTip("Add rectangular ROIs.")
@@ -873,14 +867,12 @@ class PlotWidget(GraphicsLayoutWidget):
873
867
  # view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
874
868
  # sets stle of the vertical slider
875
869
  view.setStyleSheet(get_current_stylesheet())
876
- view.setStyleSheet(
877
- """
870
+ view.setStyleSheet("""
878
871
  QGraphicsView {
879
872
  border: none;
880
873
  background: rgb(0, 0, 0);
881
874
  }
882
- """
883
- )
875
+ """)
884
876
 
885
877
  view.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
886
878
 
@@ -5,7 +5,7 @@ from collections.abc import Iterable
5
5
  from importlib.metadata import version
6
6
  from operator import itemgetter
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, cast
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  import numpy as np
11
11
  import shapely
@@ -15,11 +15,20 @@ from napari.utils.notifications import show_info
15
15
  from packaging.version import parse as parse_version
16
16
  from qtpy.QtCore import QThread, Signal
17
17
  from qtpy.QtGui import QIcon
18
- from qtpy.QtWidgets import QLabel, QListWidget, QListWidgetItem, QProgressBar, QVBoxLayout, QWidget
18
+ from qtpy.QtWidgets import (
19
+ QCheckBox,
20
+ QLabel,
21
+ QListWidget,
22
+ QListWidgetItem,
23
+ QProgressBar,
24
+ QVBoxLayout,
25
+ QWidget,
26
+ )
19
27
  from spatialdata import SpatialData
20
28
  from spatialdata.models._utils import DEFAULT_COORDINATE_SYSTEM
21
29
 
22
30
  from napari_spatialdata._viewer import SpatialDataViewer
31
+ from napari_spatialdata.constants import config
23
32
  from napari_spatialdata.constants.config import N_CIRCLES_WARNING_THRESHOLD, N_SHAPES_WARNING_THRESHOLD
24
33
  from napari_spatialdata.utils._utils import _get_sdata_key, get_duplicate_element_names, get_elements_meta_mapping
25
34
 
@@ -174,11 +183,39 @@ class SdataWidget(QWidget):
174
183
  self.slider.setRange(0, 0)
175
184
  self.slider.setVisible(False)
176
185
 
186
+ self.discard_z_points = QCheckBox("Discard z for 3D points")
187
+ self.discard_z_points.setChecked(config.PROJECT_3D_POINTS_TO_2D)
188
+ self.discard_z_points.setToolTip(
189
+ "When checked, the z coordinate of new points layers is discarded so they are loaded in 2D. "
190
+ "Only applies to new layers; layers already displayed are not affected."
191
+ )
192
+ self.discard_z_points.toggled.connect(self._on_discard_z_points_toggled)
193
+
194
+ self.discard_z_shapes = QCheckBox("Discard z for 2.5D shapes")
195
+ self.discard_z_shapes.setChecked(config.PROJECT_2_5D_SHAPES_TO_2D)
196
+ self.discard_z_shapes.setToolTip(
197
+ "When checked, the z coordinate of new shapes layers is discarded so they are loaded in 2D. "
198
+ "Only applies to new layers; layers already displayed are not affected."
199
+ )
200
+ self.discard_z_shapes.toggled.connect(self._on_discard_z_shapes_toggled)
201
+
202
+ # The 3D toggles only matter when at least one element across the loaded
203
+ # SpatialData objects has a z axis. Otherwise we hide them to save screen
204
+ # real estate for users working with 2D-only data.
205
+ self._has_z_data = self._sdatas_have_z_axis(self._sdata)
206
+ self._three_d_settings_label = QLabel("3D Settings:")
207
+ self._three_d_settings_label.setVisible(self._has_z_data)
208
+ self.discard_z_points.setVisible(self._has_z_data)
209
+ self.discard_z_shapes.setVisible(self._has_z_data)
210
+
177
211
  self.layout().addWidget(self.slider)
178
212
  self.layout().addWidget(QLabel("Coordinate System:"))
179
213
  self.layout().addWidget(self.coordinate_system_widget)
180
214
  self.layout().addWidget(QLabel("Elements:"))
181
215
  self.layout().addWidget(self.elements_widget)
216
+ self.layout().addWidget(self._three_d_settings_label)
217
+ self.layout().addWidget(self.discard_z_points)
218
+ self.layout().addWidget(self.discard_z_shapes)
182
219
  self.elements_widget.itemDoubleClicked.connect(self._on_click_item)
183
220
  self.coordinate_system_widget.currentItemChanged.connect(
184
221
  lambda item: self.elements_widget._onItemChange(item.text())
@@ -219,8 +256,6 @@ class SdataWidget(QWidget):
219
256
  }:
220
257
  return
221
258
 
222
- type_ = cast(str, type_)
223
-
224
259
  self.worker_thread.load_data(type_, text, sdata, selected_cs, multi)
225
260
  if not PROBLEMATIC_NUMPY_MACOS:
226
261
  self.slider.setVisible(True)
@@ -258,6 +293,24 @@ class SdataWidget(QWidget):
258
293
  layer.metadata["_active_in_cs"].add(coordinate_system)
259
294
  layer.metadata["_current_cs"] = coordinate_system
260
295
 
296
+ def _on_discard_z_points_toggled(self, checked: bool) -> None:
297
+ config.PROJECT_3D_POINTS_TO_2D = checked
298
+
299
+ def _on_discard_z_shapes_toggled(self, checked: bool) -> None:
300
+ config.PROJECT_2_5D_SHAPES_TO_2D = checked
301
+
302
+ @staticmethod
303
+ def _sdatas_have_z_axis(sdatas: EventedList) -> bool:
304
+ """Return ``True`` if any element across the given ``SpatialData`` objects has a z axis.
305
+
306
+ Used to decide whether to expose the 3D / 2.5D projection toggles in the widget.
307
+ """
308
+ for sdata in sdatas:
309
+ for _, _, element in sdata._gen_elements():
310
+ if SpatialDataViewer._has_z_axis(element):
311
+ return True
312
+ return False
313
+
261
314
  def _get_shapes(self, sdata: SpatialData, key: str, selected_cs: str, multi: bool) -> Shapes | Points:
262
315
  original_name = key[: key.rfind("_")] if multi else key
263
316
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.1'
22
- __version_tuple__ = version_tuple = (0, 7, 1)
21
+ __version__ = version = '0.7.2'
22
+ __version_tuple__ = version_tuple = (0, 7, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g3a8b87fb6'
24
+ __commit_id__ = commit_id = 'ga1150de77'
@@ -65,9 +65,7 @@ class QtAdataScatterWidget(QWidget):
65
65
  self._viewer = napari_viewer
66
66
 
67
67
  self._model = (
68
- model
69
- if model is not None
70
- else self._viewer.window._dock_widgets["SpatialData"].widget().viewer_model._model
68
+ model if model is not None else self._viewer.window.dock_widgets["SpatialData"].viewer_model._model
71
69
  )
72
70
 
73
71
  self._select_layer()
@@ -241,9 +239,8 @@ class QtAdataScatterWidget(QWidget):
241
239
 
242
240
  # check that the view widget is present
243
241
  # and trigger its update
244
- if "View (napari-spatialdata)" in dict(self._viewer.window._dock_widgets.items()):
245
- view_widget = dict(self._viewer.window._dock_widgets.items())["View (napari-spatialdata)"]
246
- qtAdataViewWidget = [x for x in view_widget.children() if isinstance(x, QtAdataViewWidget)][0]
242
+ if "View (napari-spatialdata)" in self._viewer.window.dock_widgets:
243
+ qtAdataViewWidget = self._viewer.window.dock_widgets["View (napari-spatialdata)"]
247
244
  qtAdataViewWidget._on_layer_update()
248
245
 
249
246
  # new annotation - update options
@@ -306,9 +303,7 @@ class QtAdataScatterWidget(QWidget):
306
303
  selected_table = self.table_name_widget.currentText()
307
304
 
308
305
  # trigger saving of the table
309
- # TODO: have to find another way to pass this as this is deprecated from 0.5.0 onwards
310
- # it's used in the save_sdata method
311
- self._viewer_model = self._viewer.window._dock_widgets["SpatialData"].widget().viewer_model
306
+ self._viewer_model = self._viewer.window.dock_widgets["SpatialData"].viewer_model
312
307
  self._viewer_model._write_element_to_disk(
313
308
  selected_layer.metadata["sdata"],
314
309
  selected_table,
@@ -611,8 +606,6 @@ class QtAdataViewWidget(QWidget):
611
606
  if self.model.adata.shape == (0, 0):
612
607
  return
613
608
 
614
- self.model._region_key = layer.metadata["region_key"] if isinstance(layer, Labels) else None
615
- self.model._instance_key = layer.metadata["instance_key"] if isinstance(layer, Labels) else None
616
609
  self.model.system_name = layer.metadata.get("name", None)
617
610
 
618
611
  if hasattr(
@@ -695,8 +688,7 @@ class QtAdataAnnotationWidget(QWidget):
695
688
  def __init__(self, napari_viewer: Viewer):
696
689
  super().__init__()
697
690
  self._viewer = napari_viewer
698
- # TODO: have to find another way to pass this as this is deprecated from 0.5.0 onwards
699
- self._viewer_model = self._viewer.window._dock_widgets["SpatialData"].widget().viewer_model
691
+ self._viewer_model = self._viewer.window.dock_widgets["SpatialData"].viewer_model
700
692
 
701
693
  self.setLayout(QGridLayout())
702
694
  self._current_color = "#FFFFFF"
@@ -189,9 +189,6 @@ class SpatialDataViewer(QObject):
189
189
  raise ValueError("Cannot export a points element with no points")
190
190
  transformed_data = np.array([layer_to_save.data_to_world(xy) for xy in layer_to_save.data])
191
191
  swap_data = np.fliplr(transformed_data)
192
- # ignore z axis if present
193
- if swap_data.shape[1] == 3:
194
- swap_data = swap_data[:, :2]
195
192
  parsed = PointsModel.parse(swap_data, transformations=transformation)
196
193
 
197
194
  # saving to disk of points temporarily disabled until the interface update that will unify the view widget,
@@ -261,14 +258,21 @@ class SpatialDataViewer(QObject):
261
258
  for shape in layer_to_save._data_view.shapes
262
259
  ]
263
260
 
264
- def _fix_coords(coords: ArrayLike) -> ArrayLike:
265
- remove_z = coords.shape[1] == 3
266
- first_index = 1 if remove_z else 0
267
- coords = coords[:, first_index::]
268
- return np.fliplr(coords)
261
+ has_z = coords[0].shape[1] == 3
269
262
 
270
- polygons: list[Polygon] = [Polygon(_fix_coords(p)) for p in coords]
271
- gdf = GeoDataFrame({"geometry": polygons})
263
+ def _fix_coords(coords: ArrayLike) -> tuple[ArrayLike, float | None]:
264
+ if coords.shape[1] == 3:
265
+ z_val = float(coords[0, 0])
266
+ yx = coords[:, 1:]
267
+ return np.fliplr(yx), z_val
268
+ return np.fliplr(coords), None
269
+
270
+ fixed = [_fix_coords(p) for p in coords]
271
+ polygons: list[Polygon] = [Polygon(xy) for xy, _ in fixed]
272
+ gdf_dict: dict[str, Any] = {"geometry": polygons}
273
+ if has_z:
274
+ gdf_dict["z"] = [z_val for _, z_val in fixed]
275
+ gdf = GeoDataFrame(gdf_dict)
272
276
 
273
277
  force_2d(gdf)
274
278
  parsed = ShapesModel.parse(gdf, transformations=transformation)
@@ -514,11 +518,15 @@ class SpatialDataViewer(QObject):
514
518
  original_name = original_name[: original_name.rfind("_")]
515
519
 
516
520
  df = sdata.shapes[original_name]
517
- affine = _get_transform(sdata.shapes[original_name], selected_cs)
521
+ axes = get_axes_names(df)
522
+ include_z = "z" in axes and not config.PROJECT_2_5D_SHAPES_TO_2D
523
+ affine = _get_transform(sdata.shapes[original_name], selected_cs, include_z=include_z)
518
524
 
519
- # 2.5D circles not supported yet
520
525
  xy = np.array([df.geometry.x, df.geometry.y]).T
521
526
  yx = np.fliplr(xy)
527
+ if include_z:
528
+ z_vals = df["z"].to_numpy()
529
+ yx = np.column_stack([z_vals, yx])
522
530
  radii = df.radius.to_numpy()
523
531
 
524
532
  adata, table_name, table_names = self._get_table_data(sdata, original_name)
@@ -561,7 +569,7 @@ class SpatialDataViewer(QObject):
561
569
  else:
562
570
  kwargs |= {"border_color": "white"}
563
571
  # useful code to have readily available to debug the correct radius of circles when represented as points
564
- ellipses = _get_ellipses_from_circles(yx=yx, radii=radii)
572
+ ellipses = _get_ellipses_from_circles(coords=yx, radii=radii)
565
573
  layer = Shapes(
566
574
  ellipses,
567
575
  shape_type="ellipse",
@@ -804,8 +812,43 @@ class SpatialDataViewer(QObject):
804
812
  sdata = metadata["sdata"]
805
813
  element_name = metadata["name"]
806
814
  element_data = sdata[element_name]
807
- affine = _get_transform(element_data, coordinate_system)
815
+ include_z = self._should_include_z(element_data)
816
+ affine = _get_transform(element_data, coordinate_system, include_z=include_z)
808
817
  if affine is not None:
809
818
  layer.affine = affine
810
819
  if layer._type_string == "points":
811
820
  self._adjust_radii_of_points_layer(layer, affine)
821
+
822
+ @staticmethod
823
+ def _has_z_axis(element: Any) -> bool:
824
+ """Return ``True`` if ``element`` exposes a ``z`` axis.
825
+
826
+ For raster elements (images / labels) the ``z`` axis is reported by
827
+ :func:`spatialdata.models.get_axes_names`. For vector elements (points
828
+ as :class:`~dask.dataframe.DataFrame`, shapes as
829
+ :class:`~geopandas.GeoDataFrame`) the same helper is used.
830
+ """
831
+ from xarray import DataArray, DataTree
832
+
833
+ if not isinstance(element, DataArray | DataTree | DaskDataFrame | GeoDataFrame):
834
+ return False
835
+ return "z" in get_axes_names(element)
836
+
837
+ @staticmethod
838
+ def _should_include_z(element: DaskDataFrame | GeoDataFrame) -> bool:
839
+ """Determine whether to include the z axis for a given spatial element.
840
+
841
+ For raster data (images, labels) z is always included when present.
842
+ For vector data (points, shapes) z inclusion depends on the user-facing
843
+ projection config flags.
844
+ """
845
+ from xarray import DataArray, DataTree
846
+
847
+ if isinstance(element, DataArray | DataTree):
848
+ return True
849
+ axes = get_axes_names(element)
850
+ if "z" not in axes:
851
+ return False
852
+ if isinstance(element, DaskDataFrame):
853
+ return not config.PROJECT_3D_POINTS_TO_2D
854
+ return not config.PROJECT_2_5D_SHAPES_TO_2D
@@ -184,6 +184,32 @@ def _transform_coordinates(data: list[Any], f: Callable[..., Any]) -> list[Any]:
184
184
  def _get_transform(
185
185
  element: SpatialElement, coordinate_system_name: str | None = None, include_z: bool | None = None
186
186
  ) -> None | ArrayLike:
187
+ """Return the affine matrix for ``element`` in the given coordinate system.
188
+
189
+ The z axis is included in the returned affine when **both**:
190
+
191
+ * ``include_z`` is truthy, **and**
192
+ * the element (and therefore its underlying transformation) has a ``z`` axis,
193
+ as reported by :func:`spatialdata.models.get_axes_names`.
194
+
195
+ If ``include_z`` is requested but the element / transformation does not expose a
196
+ ``z`` axis, the flag is silently ignored and a 2D ``(y, x)`` affine is returned.
197
+
198
+ Parameters
199
+ ----------
200
+ element
201
+ The :class:`spatialdata.models.SpatialElement` for which to compute the affine.
202
+ coordinate_system_name
203
+ Coordinate system to use. If ``None``, the first available is selected.
204
+ include_z
205
+ Whether to include the z axis in the affine. The z is only included when the
206
+ element / transformation also has a z axis; otherwise this flag is ignored.
207
+
208
+ Returns
209
+ -------
210
+ The affine matrix as an ``ArrayLike`` (``(3, 3)`` for 2D and ``(4, 4)`` for 2.5D/3D),
211
+ or ``None`` if no transformation is defined for the requested coordinate system.
212
+ """
187
213
  if not isinstance(element, DataArray | DataTree | DaskDataFrame | GeoDataFrame):
188
214
  raise RuntimeError("Cannot get transform for {type(element)}")
189
215
 
@@ -459,13 +485,17 @@ def generate_random_color_hex() -> str:
459
485
  return f"#{randint(0, 255):02x}{randint(0, 255):02x}{randint(0, 255):02x}ff"
460
486
 
461
487
 
462
- def _get_ellipses_from_circles(yx: ArrayLike, radii: ArrayLike) -> ArrayLike:
488
+ def _get_ellipses_from_circles(coords: ArrayLike, radii: ArrayLike) -> ArrayLike:
463
489
  """Convert circles to ellipses.
464
490
 
491
+ Supports both 2D and 2.5D centroids. For 2.5D input the radius is applied only to
492
+ y and x while z is kept constant across the four corner vertices.
493
+
465
494
  Parameters
466
495
  ----------
467
- yx
468
- Centroids of the circles.
496
+ coords
497
+ Centroids of the circles with shape ``(N, 2)`` in ``(y, x)`` order or ``(N, 3)``
498
+ in ``(z, y, x)`` order.
469
499
  radii
470
500
  Radii of the circles.
471
501
 
@@ -474,14 +504,29 @@ def _get_ellipses_from_circles(yx: ArrayLike, radii: ArrayLike) -> ArrayLike:
474
504
  ArrayLike
475
505
  Ellipses.
476
506
  """
477
- ndim = yx.shape[1]
478
- assert ndim == 2
479
- r = np.stack([radii] * ndim, axis=1)
480
- lower_left = yx - r
481
- upper_right = yx + r
507
+ ndim = coords.shape[1]
508
+ if ndim not in (2, 3):
509
+ raise ValueError(f"Expected centroids with 2 or 3 columns (yx or zyx), got shape {coords.shape}.")
510
+
511
+ if ndim == 3:
512
+ z = coords[:, :1]
513
+ yx_2d = coords[:, 1:]
514
+ else:
515
+ yx_2d = coords
516
+
517
+ r = np.stack([radii, radii], axis=1)
518
+ lower_left = yx_2d - r
519
+ upper_right = yx_2d + r
482
520
  r[:, 0] = -r[:, 0]
483
- lower_right = yx - r
484
- upper_left = yx + r
521
+ lower_right = yx_2d - r
522
+ upper_left = yx_2d + r
523
+
524
+ if ndim == 3:
525
+ lower_left = np.column_stack([z, lower_left])
526
+ lower_right = np.column_stack([z, lower_right])
527
+ upper_right = np.column_stack([z, upper_right])
528
+ upper_left = np.column_stack([z, upper_left])
529
+
485
530
  ellipses = np.stack([lower_left, lower_right, upper_right, upper_left], axis=1)
486
531
  assert isinstance(ellipses, np.ndarray)
487
532
  return ellipses
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-spatialdata
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Interactive visualization of spatial omics data with napari
5
5
  Home-page: https://github.com/scverse/napari-spatialdata.git
6
6
  Author: giovanni palla
@@ -63,6 +63,8 @@ src/napari_spatialdata.egg-info/SOURCES.txt
63
63
  src/napari_spatialdata.egg-info/dependency_links.txt
64
64
  src/napari_spatialdata.egg-info/entry_points.txt
65
65
  src/napari_spatialdata.egg-info/requires.txt
66
+ src/napari_spatialdata.egg-info/scm_file_list.json
67
+ src/napari_spatialdata.egg-info/scm_version.json
66
68
  src/napari_spatialdata.egg-info/top_level.txt
67
69
  src/napari_spatialdata/constants/__init__.py
68
70
  src/napari_spatialdata/constants/_constants.py
@@ -0,0 +1,92 @@
1
+ {
2
+ "files": [
3
+ ".pre-commit-config.yaml",
4
+ "README.md",
5
+ "setup.cfg",
6
+ "tox.ini",
7
+ "LICENSE",
8
+ "pyproject.toml",
9
+ "asv.conf.json",
10
+ ".mypy.ini",
11
+ "MANIFEST.in",
12
+ ".gitignore",
13
+ ".readthedocs.yaml",
14
+ ".napari/DESCRIPTION.md",
15
+ "docs/Makefile",
16
+ "docs/api.md",
17
+ "docs/references.md",
18
+ "docs/contributing.md",
19
+ "docs/limitations.md",
20
+ "docs/make.bat",
21
+ "docs/changelog.md",
22
+ "docs/cli.md",
23
+ "docs/references.bib",
24
+ "docs/conf.py",
25
+ "docs/index.md",
26
+ "docs/extensions/typed_returns.py",
27
+ "docs/_static/.gitkeep",
28
+ "docs/_templates/.gitkeep",
29
+ "docs/_templates/autosummary/class.rst",
30
+ "docs/notebooks/scatterwidget.ipynb",
31
+ "docs/notebooks/scatterwidget_annotation.ipynb",
32
+ "docs/notebooks/mibitof_analysis.ipynb",
33
+ "docs/notebooks/nanostring_analysis.ipynb",
34
+ "docs/notebooks/spatialdata.ipynb",
35
+ "src/napari_spatialdata/__init__.py",
36
+ "src/napari_spatialdata/_reader.py",
37
+ "src/napari_spatialdata/_interactive.py",
38
+ "src/napari_spatialdata/_scatterwidgets.py",
39
+ "src/napari_spatialdata/_view.py",
40
+ "src/napari_spatialdata/_annotationwidgets.py",
41
+ "src/napari_spatialdata/_widgets.py",
42
+ "src/napari_spatialdata/_viewer.py",
43
+ "src/napari_spatialdata/_sdata_widgets.py",
44
+ "src/napari_spatialdata/_model.py",
45
+ "src/napari_spatialdata/__main__.py",
46
+ "src/napari_spatialdata/napari.yaml",
47
+ "src/napari_spatialdata/resources/icons8-rectangle-48.png",
48
+ "src/napari_spatialdata/resources/icons8-home-48.png",
49
+ "src/napari_spatialdata/resources/icons8-polygon-80.png",
50
+ "src/napari_spatialdata/resources/exclamation.png",
51
+ "src/napari_spatialdata/resources/icons8-pencil-drawing-50.png",
52
+ "src/napari_spatialdata/constants/_pkg_constants.py",
53
+ "src/napari_spatialdata/constants/__init__.py",
54
+ "src/napari_spatialdata/constants/config.py",
55
+ "src/napari_spatialdata/constants/_constants.py",
56
+ "src/napari_spatialdata/constants/_utils.py",
57
+ "src/napari_spatialdata/utils/__init__.py",
58
+ "src/napari_spatialdata/utils/_test_utils.py",
59
+ "src/napari_spatialdata/utils/_utils.py",
60
+ "src/napari_spatialdata/utils/_viewer_utils.py",
61
+ "src/napari_spatialdata/utils/_categoricals_utils.py",
62
+ "tests/__init__.py",
63
+ "tests/test_utils.py",
64
+ "tests/test_spatialdata.py",
65
+ "tests/test_3d_visualization.py",
66
+ "tests/test_widgets.py",
67
+ "tests/test_interactive.py",
68
+ "tests/test_viewer.py",
69
+ "tests/test_view.py",
70
+ "tests/conftest.py",
71
+ "tests/test_cli.py",
72
+ "tests/test_scatterwidgets.py",
73
+ "tests/plots/groundtruth/Images_can_add_element_label.png",
74
+ "tests/plots/groundtruth/Images_can_add_element_multiple.png",
75
+ "tests/plots/groundtruth/Images_can_add_element_image.png",
76
+ "benchmarks/Readme.md",
77
+ "benchmarks/__init__.py",
78
+ "benchmarks/benchmark_qt_widget.py",
79
+ "benchmarks/benchmark_model.py",
80
+ "benchmarks/benchmark.in",
81
+ "benchmarks/benchmark.txt",
82
+ "examples/spatialdata_empty.py",
83
+ "examples/spatialdata_visium.py",
84
+ ".github/release.yml",
85
+ ".github/BENCHMARK_FAIL_TEMPLATE.md",
86
+ ".github/dependabot.yml",
87
+ ".github/workflows/test.yml",
88
+ ".github/workflows/release.yml",
89
+ ".github/workflows/benchmarks_report.yml",
90
+ ".github/workflows/benchmarks.yml"
91
+ ]
92
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.7.2",
3
+ "distance": 0,
4
+ "node": "ga1150de77d9c42467ab0f27b4aa94a048765707e",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-05-15"
8
+ }