napari-spatialdata 0.7.1__tar.gz → 0.7.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.
Files changed (99) hide show
  1. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/workflows/benchmarks.yml +2 -2
  2. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/workflows/test.yml +1 -1
  3. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.pre-commit-config.yaml +3 -3
  4. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.readthedocs.yaml +1 -1
  5. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/PKG-INFO +6 -6
  6. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/README.md +2 -2
  7. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/asv.conf.json +1 -1
  8. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/setup.cfg +3 -3
  9. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_model.py +2 -3
  10. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_scatterwidgets.py +24 -28
  11. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_sdata_widgets.py +57 -4
  12. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_version.py +3 -3
  13. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_view.py +5 -13
  14. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_viewer.py +57 -14
  15. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/utils/_utils.py +55 -10
  16. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/PKG-INFO +6 -6
  17. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/SOURCES.txt +2 -0
  18. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/requires.txt +1 -1
  19. napari_spatialdata-0.7.3/src/napari_spatialdata.egg-info/scm_file_list.json +92 -0
  20. napari_spatialdata-0.7.3/src/napari_spatialdata.egg-info/scm_version.json +8 -0
  21. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/conftest.py +57 -3
  22. napari_spatialdata-0.7.3/tests/test_3d_visualization.py +350 -0
  23. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_scatterwidgets.py +2 -2
  24. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_widgets.py +1 -1
  25. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tox.ini +3 -3
  26. napari_spatialdata-0.7.1/tests/test_3d_visualization.py +0 -161
  27. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/BENCHMARK_FAIL_TEMPLATE.md +0 -0
  28. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/dependabot.yml +0 -0
  29. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/release.yml +0 -0
  30. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/workflows/benchmarks_report.yml +0 -0
  31. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.github/workflows/release.yml +0 -0
  32. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.gitignore +0 -0
  33. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.mypy.ini +0 -0
  34. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/.napari/DESCRIPTION.md +0 -0
  35. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/LICENSE +0 -0
  36. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/MANIFEST.in +0 -0
  37. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/Readme.md +0 -0
  38. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/__init__.py +0 -0
  39. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/benchmark.in +0 -0
  40. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/benchmark.txt +0 -0
  41. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/benchmark_model.py +0 -0
  42. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/benchmarks/benchmark_qt_widget.py +0 -0
  43. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/Makefile +0 -0
  44. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/_static/.gitkeep +0 -0
  45. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/_templates/.gitkeep +0 -0
  46. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/_templates/autosummary/class.rst +0 -0
  47. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/api.md +0 -0
  48. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/changelog.md +0 -0
  49. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/cli.md +0 -0
  50. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/conf.py +0 -0
  51. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/contributing.md +0 -0
  52. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/extensions/typed_returns.py +0 -0
  53. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/index.md +0 -0
  54. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/limitations.md +0 -0
  55. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/make.bat +0 -0
  56. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/notebooks/mibitof_analysis.ipynb +0 -0
  57. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/notebooks/nanostring_analysis.ipynb +0 -0
  58. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/notebooks/scatterwidget.ipynb +0 -0
  59. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/notebooks/scatterwidget_annotation.ipynb +0 -0
  60. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/notebooks/spatialdata.ipynb +0 -0
  61. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/references.bib +0 -0
  62. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/docs/references.md +0 -0
  63. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/examples/spatialdata_empty.py +0 -0
  64. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/examples/spatialdata_visium.py +0 -0
  65. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/pyproject.toml +0 -0
  66. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/__init__.py +0 -0
  67. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/__main__.py +0 -0
  68. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_annotationwidgets.py +0 -0
  69. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_interactive.py +0 -0
  70. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_reader.py +0 -0
  71. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/_widgets.py +0 -0
  72. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/constants/__init__.py +0 -0
  73. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/constants/_constants.py +0 -0
  74. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/constants/_pkg_constants.py +0 -0
  75. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/constants/_utils.py +0 -0
  76. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/constants/config.py +0 -0
  77. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/napari.yaml +0 -0
  78. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/resources/exclamation.png +0 -0
  79. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/resources/icons8-home-48.png +0 -0
  80. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/resources/icons8-pencil-drawing-50.png +0 -0
  81. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/resources/icons8-polygon-80.png +0 -0
  82. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/resources/icons8-rectangle-48.png +0 -0
  83. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/utils/__init__.py +0 -0
  84. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/utils/_categoricals_utils.py +0 -0
  85. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/utils/_test_utils.py +0 -0
  86. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata/utils/_viewer_utils.py +0 -0
  87. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/dependency_links.txt +0 -0
  88. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/entry_points.txt +0 -0
  89. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/src/napari_spatialdata.egg-info/top_level.txt +0 -0
  90. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/__init__.py +0 -0
  91. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/plots/groundtruth/Images_can_add_element_image.png +0 -0
  92. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/plots/groundtruth/Images_can_add_element_label.png +0 -0
  93. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/plots/groundtruth/Images_can_add_element_multiple.png +0 -0
  94. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_cli.py +0 -0
  95. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_interactive.py +0 -0
  96. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_spatialdata.py +0 -0
  97. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_utils.py +0 -0
  98. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_view.py +0 -0
  99. {napari_spatialdata-0.7.1 → napari_spatialdata-0.7.3}/tests/test_viewer.py +0 -0
@@ -66,7 +66,7 @@ jobs:
66
66
  - uses: actions/setup-python@v6
67
67
  name: Install Python
68
68
  with:
69
- python-version: "3.11"
69
+ python-version: "3.12"
70
70
  cache-dependency-path: pyproject.toml
71
71
 
72
72
  - uses: tlambert03/setup-qt-libs@v1
@@ -140,7 +140,7 @@ jobs:
140
140
  env:
141
141
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
142
142
  PLATFORM: ${{ matrix.runs-on }}
143
- PYTHON: "3.11"
143
+ PYTHON: "3.12"
144
144
  BACKEND: ${{ matrix.benchmark-name }}
145
145
  RUN_ID: ${{ github.run_id }}
146
146
  TITLE: "[test-bot] Benchmark tests failing"
@@ -24,7 +24,7 @@ jobs:
24
24
  fail-fast: false
25
25
  matrix:
26
26
  platform: [ubuntu-latest, macos-latest] # [ubuntu-latest, windows-latest, macos-latest]
27
- python-version: ["3.11", "3.13"]
27
+ python-version: ["3.12", "3.13", "3.14"]
28
28
 
29
29
  steps:
30
30
  - uses: actions/checkout@v6
@@ -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]
@@ -3,7 +3,7 @@ version: 2
3
3
  build:
4
4
  os: ubuntu-22.04
5
5
  tools:
6
- python: "3.11"
6
+ python: "3.12"
7
7
  sphinx:
8
8
  configuration: docs/conf.py
9
9
  fail_on_warning: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-spatialdata
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -16,12 +16,12 @@ Classifier: Framework :: napari
16
16
  Classifier: Topic :: Software Development :: Testing
17
17
  Classifier: Programming Language :: Python
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
21
20
  Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Operating System :: OS Independent
23
23
  Classifier: License :: OSI Approved :: BSD License
24
- Requires-Python: >=3.11
24
+ Requires-Python: >=3.12
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Requires-Dist: anndata>=0.12.0
@@ -78,10 +78,10 @@ Requires-Dist: pydantic>2; extra == "readthedocs"
78
78
  Provides-Extra: pre
79
79
  Requires-Dist: spatialdata>=0.7.3a1; extra == "pre"
80
80
  Provides-Extra: all
81
- Requires-Dist: napari[pyqt5]; extra == "all"
81
+ Requires-Dist: napari[pyqt6]; extra == "all"
82
82
  Dynamic: license-file
83
83
 
84
- ![SpatialData banner](https://github.com/scverse/spatialdata/blob/main/docs/_static/img/spatialdata_horizontal.png?raw=true)
84
+ ![SpatialData banner](https://github.com/scverse/spatialdata/blob/main/docs/_static/img/spatialdata_horizontal.svg?raw=true)
85
85
 
86
86
  # napari-spatialdata: interactive exploration and annotation of spatial omics data
87
87
 
@@ -108,7 +108,7 @@ You can install `napari-spatialdata` via [pip]:
108
108
 
109
109
  pip install napari-spatialdata[all]
110
110
 
111
- The `all` command will install the qt bindings `PyQt5`.
111
+ The `all` extra also installs the Qt bindings `PyQt6` (via `napari[pyqt6]`).
112
112
 
113
113
  Napari now also includes multiple triangulation backends. These improve the speed by which a napari 'Shapes' layer gets
114
114
  loaded (this does not improve the speed of editing large numbers of shapes yet!). See also the napari
@@ -1,4 +1,4 @@
1
- ![SpatialData banner](https://github.com/scverse/spatialdata/blob/main/docs/_static/img/spatialdata_horizontal.png?raw=true)
1
+ ![SpatialData banner](https://github.com/scverse/spatialdata/blob/main/docs/_static/img/spatialdata_horizontal.svg?raw=true)
2
2
 
3
3
  # napari-spatialdata: interactive exploration and annotation of spatial omics data
4
4
 
@@ -25,7 +25,7 @@ You can install `napari-spatialdata` via [pip]:
25
25
 
26
26
  pip install napari-spatialdata[all]
27
27
 
28
- The `all` command will install the qt bindings `PyQt5`.
28
+ The `all` extra also installs the Qt bindings `PyQt6` (via `napari[pyqt6]`).
29
29
 
30
30
  Napari now also includes multiple triangulation backends. These improve the speed by which a napari 'Shapes' layer gets
31
31
  loaded (this does not improve the speed of editing large numbers of shapes yet!). See also the napari
@@ -35,7 +35,7 @@
35
35
  "show_commit_url": "http://github.com/napari/napari/commit/",
36
36
 
37
37
  // The Pythons you'd like to test against.
38
- "pythons": ["3.11"],
38
+ "pythons": ["3.12"],
39
39
 
40
40
  // The directory (relative to the current directory) to cache the Python
41
41
  // environments in.
@@ -19,16 +19,16 @@ classifiers =
19
19
  Topic :: Software Development :: Testing
20
20
  Programming Language :: Python
21
21
  Programming Language :: Python :: 3
22
- Programming Language :: Python :: 3.11
23
22
  Programming Language :: Python :: 3.12
24
23
  Programming Language :: Python :: 3.13
24
+ Programming Language :: Python :: 3.14
25
25
  Operating System :: OS Independent
26
26
  License :: OSI Approved :: BSD License
27
27
 
28
28
  [options]
29
29
  packages = find:
30
30
  include_package_data = True
31
- python_requires = >=3.11
31
+ python_requires = >=3.12
32
32
  setup_requires = setuptools_scm
33
33
  install_requires =
34
34
  anndata>=0.12.0
@@ -89,7 +89,7 @@ readthedocs =
89
89
  pre =
90
90
  spatialdata>=0.7.3a1
91
91
  all =
92
- napari[pyqt5]
92
+ napari[pyqt6]
93
93
 
94
94
  [options.packages.find]
95
95
  where = src
@@ -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
 
@@ -138,7 +136,8 @@ class DataModel:
138
136
  except KeyError:
139
137
  raise KeyError(f"Key `{name}` not found in `adata.var_names`.") from None
140
138
 
141
- column = self.adata._get_X(layer=self.adata_layer)[ix]
139
+ X = self.adata.X if self.adata_layer is None else self.adata.layers[self.adata_layer]
140
+ column = X[ix]
142
141
  index = self.adata.obs[[self.instance_key]].set_index(self.instance_key).index
143
142
  return column, self._format_key(name, adata_layer=True), index
144
143
 
@@ -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
 
@@ -931,11 +923,11 @@ class PlotWidget(GraphicsLayoutWidget):
931
923
  logger.debug("Left press detected")
932
924
 
933
925
  # Get the position of the click in the scene
934
- scene_pos = event.pos()
926
+ scene_pos = self.mapToScene(event.pos())
935
927
 
936
928
  # Check if the click is within the scatter plot view box
937
929
  if not self.scatter_plot.vb.sceneBoundingRect().contains(scene_pos):
938
- logger.debug("Click outside scatter plot view box, scene position: {scene_pos}")
930
+ logger.debug(f"Click outside scatter plot view box, scene position: {scene_pos}")
939
931
  return
940
932
 
941
933
  pen = pg.mkPen(color="gray", width=2)
@@ -946,7 +938,7 @@ class PlotWidget(GraphicsLayoutWidget):
946
938
 
947
939
  if self.drawing:
948
940
 
949
- plot_pos = self.scatter_plot.vb.mapSceneToView(event.pos())
941
+ plot_pos = self.scatter_plot.vb.mapSceneToView(scene_pos)
950
942
 
951
943
  self.current_roi = pg.PolyLineROI(
952
944
  [], closed=True, removable=True, pen=pen, handlePen=handlePen, hoverPen=hoverPen
@@ -954,7 +946,7 @@ class PlotWidget(GraphicsLayoutWidget):
954
946
  self.scatter_plot.addItem(self.current_roi)
955
947
 
956
948
  self.current_points = [(plot_pos.x(), plot_pos.y())]
957
- self.last_pos = (event.pos().x(), event.pos().y())
949
+ self.last_pos = (scene_pos.x(), scene_pos.y())
958
950
 
959
951
  event.accept()
960
952
 
@@ -962,7 +954,7 @@ class PlotWidget(GraphicsLayoutWidget):
962
954
 
963
955
  logger.info("Rectangle")
964
956
 
965
- plot_pos = self.scatter_plot.vb.mapSceneToView(event.pos())
957
+ plot_pos = self.scatter_plot.vb.mapSceneToView(scene_pos)
966
958
  self.current_roi = pg.RectROI(
967
959
  pos=(plot_pos.x(), plot_pos.y()),
968
960
  size=(0, 0),
@@ -987,16 +979,17 @@ class PlotWidget(GraphicsLayoutWidget):
987
979
  if self.drawing and self.current_roi is not None:
988
980
 
989
981
  if self.last_pos is not None:
990
- dist = np.sqrt((event.pos().x() - self.last_pos[0]) ** 2 + (event.pos().y() - self.last_pos[1]) ** 2)
982
+ scene_pos = self.mapToScene(event.pos())
983
+ dist = np.sqrt((scene_pos.x() - self.last_pos[0]) ** 2 + (scene_pos.y() - self.last_pos[1]) ** 2)
991
984
 
992
985
  if dist > self.vertex_add_threshold:
993
986
 
994
- plot_pos = self.scatter_plot.vb.mapSceneToView(event.pos())
987
+ plot_pos = self.scatter_plot.vb.mapSceneToView(scene_pos)
995
988
  self.current_points.append((plot_pos.x(), plot_pos.y()))
996
989
 
997
990
  self.current_roi.setPoints(self.current_points)
998
991
 
999
- self.last_pos = (event.pos().x(), event.pos().y())
992
+ self.last_pos = (scene_pos.x(), scene_pos.y())
1000
993
 
1001
994
  logger.debug(f"Updated points for PolyLineROI: {self.current_points}")
1002
995
 
@@ -1004,7 +997,7 @@ class PlotWidget(GraphicsLayoutWidget):
1004
997
 
1005
998
  elif self.rectangle and self.current_roi is not None and self.initial_pos is not None:
1006
999
 
1007
- plot_pos = self.scatter_plot.vb.mapSceneToView(event.pos())
1000
+ plot_pos = self.scatter_plot.vb.mapSceneToView(self.mapToScene(event.pos()))
1008
1001
  width = plot_pos.x() - self.initial_pos.x() # type: ignore[attr-defined]
1009
1002
  height = plot_pos.y() - self.initial_pos.y() # type: ignore[attr-defined]
1010
1003
  self.current_roi.setSize([width, height])
@@ -1018,7 +1011,7 @@ class PlotWidget(GraphicsLayoutWidget):
1018
1011
  super().mouseMoveEvent(event)
1019
1012
 
1020
1013
  # Get cursor position
1021
- scene_pos = event.pos()
1014
+ scene_pos = self.mapToScene(event.pos())
1022
1015
  plot_pos = self.scatter_plot.vb.mapSceneToView(scene_pos)
1023
1016
 
1024
1017
  # Update the cursor position label
@@ -1029,7 +1022,10 @@ class PlotWidget(GraphicsLayoutWidget):
1029
1022
 
1030
1023
  def mouseReleaseEvent(self, event: Any) -> None:
1031
1024
 
1032
- logger.debug(f"Mouse release event detected at {self.scatter_plot.vb.mapSceneToView(event.pos())}")
1025
+ # also called by scene().sigMouseClicked with a pyqtgraph MouseClickEvent, whose pos() mapToScene can't take
1026
+ if isinstance(event, QtGui.QMouseEvent):
1027
+ scene_pos = self.mapToScene(event.pos())
1028
+ logger.debug(f"Mouse release event detected at {self.scatter_plot.vb.mapSceneToView(scene_pos)}")
1033
1029
 
1034
1030
  if self.drawing and event.button() == Qt.LeftButton:
1035
1031
 
@@ -1057,7 +1053,7 @@ class PlotWidget(GraphicsLayoutWidget):
1057
1053
  def mouseDoubleClickEvent(self, event: Any) -> None:
1058
1054
  if event.button() == Qt.LeftButton:
1059
1055
 
1060
- plot_pos = self.scatter_plot.vb.mapSceneToView(event.pos())
1056
+ plot_pos = self.scatter_plot.vb.mapSceneToView(self.mapToScene(event.pos()))
1061
1057
  point = Point(plot_pos.x(), plot_pos.y())
1062
1058
 
1063
1059
  self.remove_roi(point=point)
@@ -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.3'
22
+ __version_tuple__ = version_tuple = (0, 7, 3)
23
23
 
24
- __commit_id__ = commit_id = 'g3a8b87fb6'
24
+ __commit_id__ = commit_id = 'g2cb4e2ca7'
@@ -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