napari-spatialdata 0.3.1__tar.gz → 0.4.1__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 (79) hide show
  1. napari-spatialdata-0.4.1/.github/workflows/release.yml +30 -0
  2. napari-spatialdata-0.3.1/.github/workflows/test_and_deploy.yml → napari-spatialdata-0.4.1/.github/workflows/test.yml +0 -26
  3. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/.pre-commit-config.yaml +4 -4
  4. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/.readthedocs.yaml +1 -0
  5. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/CHANGELOG.md +15 -1
  6. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/PKG-INFO +9 -3
  7. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/README.md +4 -0
  8. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/conf.py +1 -1
  9. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/pyproject.toml +6 -5
  10. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/setup.cfg +4 -2
  11. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/__main__.py +2 -0
  12. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_constants/_constants.py +1 -0
  13. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_constants/_pkg_constants.py +1 -0
  14. napari-spatialdata-0.4.1/src/napari_spatialdata/_constants/config.py +4 -0
  15. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_interactive.py +16 -0
  16. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_model.py +90 -102
  17. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_scatterwidgets.py +8 -8
  18. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_sdata_widgets.py +42 -5
  19. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_version.py +2 -2
  20. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_view.py +163 -37
  21. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_viewer.py +233 -54
  22. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_widgets.py +88 -70
  23. napari-spatialdata-0.4.1/src/napari_spatialdata/resources/exclamation.png +0 -0
  24. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/utils/_categoricals_utils.py +18 -4
  25. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/utils/_test_utils.py +24 -10
  26. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/utils/_utils.py +101 -13
  27. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/utils/_viewer_utils.py +2 -2
  28. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/PKG-INFO +9 -3
  29. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/SOURCES.txt +4 -1
  30. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/requires.txt +5 -2
  31. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/conftest.py +7 -2
  32. napari-spatialdata-0.4.1/tests/plots/groundtruth/Images_can_add_element_image.png +0 -0
  33. napari-spatialdata-0.4.1/tests/plots/groundtruth/Images_can_add_element_multiple.png +0 -0
  34. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_scatterwidgets.py +10 -8
  35. napari-spatialdata-0.4.1/tests/test_spatialdata.py +346 -0
  36. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_utils.py +6 -6
  37. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_widgets.py +53 -86
  38. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tox.ini +1 -0
  39. napari-spatialdata-0.3.1/tests/plots/groundtruth/Images_can_add_element_image.png +0 -0
  40. napari-spatialdata-0.3.1/tests/plots/groundtruth/Images_can_add_element_multiple.png +0 -0
  41. napari-spatialdata-0.3.1/tests/test_spatialdata.py +0 -229
  42. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/.gitignore +0 -0
  43. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/.mypy.ini +0 -0
  44. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/.napari/DESCRIPTION.md +0 -0
  45. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/LICENSE +0 -0
  46. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/MANIFEST.in +0 -0
  47. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/Makefile +0 -0
  48. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/_static/.gitkeep +0 -0
  49. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/_templates/.gitkeep +0 -0
  50. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/_templates/autosummary/class.rst +0 -0
  51. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/api.md +0 -0
  52. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/cli.md +0 -0
  53. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/contributing.md +0 -0
  54. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/extensions/typed_returns.py +0 -0
  55. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/index.md +0 -0
  56. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/make.bat +0 -0
  57. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/notebooks/mibitof_analysis.ipynb +0 -0
  58. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/notebooks/nanostring_analysis.ipynb +0 -0
  59. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/notebooks/scatterwidget.ipynb +0 -0
  60. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/notebooks/spatialdata.ipynb +0 -0
  61. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/references.bib +0 -0
  62. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/references.md +0 -0
  63. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/docs/template_usage.md +0 -0
  64. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/examples/spatialdata_empty.py +0 -0
  65. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/examples/spatialdata_visium.py +0 -0
  66. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/__init__.py +0 -0
  67. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_constants/__init__.py +0 -0
  68. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_constants/_utils.py +0 -0
  69. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/_reader.py +0 -0
  70. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/napari.yaml +0 -0
  71. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata/utils/__init__.py +0 -0
  72. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/dependency_links.txt +0 -0
  73. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/entry_points.txt +0 -0
  74. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/src/napari_spatialdata.egg-info/top_level.txt +0 -0
  75. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/__init__.py +0 -0
  76. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/plots/groundtruth/Images_can_add_element_label.png +0 -0
  77. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_cli.py +0 -0
  78. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_interactive.py +0 -0
  79. {napari-spatialdata-0.3.1 → napari-spatialdata-0.4.1}/tests/test_viewer.py +0 -0
@@ -0,0 +1,30 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ release:
9
+ # requires that you have put your twine API key in your
10
+ # github secrets (see readme for details)
11
+ runs-on: ubuntu-latest
12
+ if: contains(github.ref, 'tags')
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v2
17
+ with:
18
+ python-version: "3.x"
19
+ - name: Install dependencies
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ pip install -U setuptools setuptools_scm wheel twine build
23
+ - name: Build and publish
24
+ env:
25
+ TWINE_USERNAME: __token__
26
+ TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
27
+ run: |
28
+ git tag
29
+ python -m build .
30
+ twine upload dist/*
@@ -62,29 +62,3 @@ jobs:
62
62
 
63
63
  - name: Coverage
64
64
  uses: codecov/codecov-action@v3
65
-
66
- deploy:
67
- # this will run when you have tagged a commit, starting with "v*"
68
- # and requires that you have put your twine API key in your
69
- # github secrets (see readme for details)
70
- needs: [test]
71
- runs-on: ubuntu-latest
72
- if: contains(github.ref, 'tags')
73
- steps:
74
- - uses: actions/checkout@v2
75
- - name: Set up Python
76
- uses: actions/setup-python@v2
77
- with:
78
- python-version: "3.x"
79
- - name: Install dependencies
80
- run: |
81
- python -m pip install --upgrade pip
82
- pip install -U setuptools setuptools_scm wheel twine build
83
- - name: Build and publish
84
- env:
85
- TWINE_USERNAME: __token__
86
- TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
87
- run: |
88
- git tag
89
- python -m build .
90
- twine upload dist/*
@@ -7,18 +7,18 @@ 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.6.1
10
+ rev: v1.9.0
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
16
- rev: 23.10.1
16
+ rev: 24.3.0
17
17
  hooks:
18
18
  - id: black
19
19
  additional_dependencies: [toml]
20
20
  - repo: https://github.com/pre-commit/mirrors-prettier
21
- rev: v3.0.3
21
+ rev: v4.0.0-alpha.8
22
22
  hooks:
23
23
  - id: prettier
24
24
  - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -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.1.4
52
+ rev: v0.3.4
53
53
  hooks:
54
54
  - id: ruff
55
55
  args: [--fix, --exit-non-zero-on-fix]
@@ -15,3 +15,4 @@ python:
15
15
  - "doc"
16
16
  - "all"
17
17
  - "readthedocs"
18
+ - "pre"
@@ -8,7 +8,21 @@ and this project adheres to [Semantic Versioning][].
8
8
  [keep a changelog]: https://keepachangelog.com/en/1.0.0/
9
9
  [semantic versioning]: https://semver.org/spec/v2.0.0.html
10
10
 
11
- ## [0.3.2] - TBD
11
+ ## [0.4.0] - 2023-03-24
12
+
13
+ ### Added
14
+
15
+ - Multi table support #199 @melonora
16
+ - Color shapes and points by values in dataframe in addition to tables #216
17
+
18
+ ### Fixed
19
+
20
+ - Avoid exception when registering shortcuts #201 @aeisenbarth
21
+ - Fix wrong point size when affine specified #193
22
+ - Wrong element visiblity when changing coordinate system #207
23
+ - Cleaned up data model #180
24
+ - Fixes correct matching of geometies and annoations via join #208
25
+ - Better docstrings #219
12
26
 
13
27
  ## [0.3.1] - 2023-11-11
14
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: napari-spatialdata
3
- Version: 0.3.1
3
+ Version: 0.4.1
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
@@ -30,7 +30,7 @@ Requires-Dist: dask
30
30
  Requires-Dist: geopandas
31
31
  Requires-Dist: loguru
32
32
  Requires-Dist: matplotlib
33
- Requires-Dist: napari
33
+ Requires-Dist: napari>=0.4.19
34
34
  Requires-Dist: napari-matplotlib
35
35
  Requires-Dist: numba
36
36
  Requires-Dist: numpy
@@ -38,6 +38,7 @@ Requires-Dist: packaging
38
38
  Requires-Dist: pandas
39
39
  Requires-Dist: pillow
40
40
  Requires-Dist: qtpy
41
+ Requires-Dist: scanpy
41
42
  Requires-Dist: scipy
42
43
  Requires-Dist: shapely
43
44
  Requires-Dist: scikit-learn
@@ -66,9 +67,10 @@ Requires-Dist: sphinx-copybutton; extra == "doc"
66
67
  Requires-Dist: sphinx-qt-documentation; extra == "doc"
67
68
  Requires-Dist: myst-nb; extra == "doc"
68
69
  Requires-Dist: squidpy; extra == "doc"
69
- Requires-Dist: scanpy; extra == "doc"
70
70
  Provides-Extra: readthedocs
71
71
  Requires-Dist: pydantic<2; extra == "readthedocs"
72
+ Provides-Extra: pre
73
+ Requires-Dist: spatialdata>=0.1.0-pre0; extra == "pre"
72
74
  Provides-Extra: all
73
75
  Requires-Dist: PyQt5; extra == "all"
74
76
 
@@ -149,6 +151,10 @@ Distributed under the terms of the [BSD-3] license,
149
151
 
150
152
  If you encounter any problems, please file an [issue] along with a detailed description.
151
153
 
154
+ ## Citation
155
+
156
+ Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and universal data framework for spatial omics. Nat Methods (2024). https://doi.org/10.1038/s41592-024-02212-x
157
+
152
158
  [napari]: https://github.com/napari/napari
153
159
  [cookiecutter]: https://github.com/audreyr/cookiecutter
154
160
  [@napari]: https://github.com/napari
@@ -75,6 +75,10 @@ Distributed under the terms of the [BSD-3] license,
75
75
 
76
76
  If you encounter any problems, please file an [issue] along with a detailed description.
77
77
 
78
+ ## Citation
79
+
80
+ Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and universal data framework for spatial omics. Nat Methods (2024). https://doi.org/10.1038/s41592-024-02212-x
81
+
78
82
  [napari]: https://github.com/napari/napari
79
83
  [cookiecutter]: https://github.com/audreyr/cookiecutter
80
84
  [@napari]: https://github.com/napari
@@ -135,7 +135,7 @@ nitpick_ignore = [
135
135
  # ("py:obj", "napari_spatialdata.QtAdataViewWidget.insertAction"),
136
136
  # ("py:obj", "napari_spatialdata.QtAdataViewWidget.scroll"),
137
137
  # ("py:obj", "napari_spatialdata.QtAdataViewWidget.setTabOrder"),
138
- # ("py:class", "napari_spatialdata._model.ImageModel"),
138
+ # ("py:class", "napari_spatialdata._model.DataModel"),
139
139
  # ("py:obj", "napari_spatialdata.QtAdataScatterWidget.scroll"),
140
140
  # ("py:obj", "napari_spatialdata.QtAdataScatterWidget.insertAction"),
141
141
  # If building the documentation fails because of a missing link that is outside your control,
@@ -37,6 +37,9 @@ exclude = [
37
37
  "dist",
38
38
  "setup.py",
39
39
  ]
40
+ line-length = 120
41
+ target-version = "py38"
42
+ [tool.ruff.lint]
40
43
  ignore = [
41
44
  # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient
42
45
  "E731",
@@ -56,7 +59,6 @@ ignore = [
56
59
  # Missing docstring in magic method
57
60
  "D105",
58
61
  ]
59
- line-length = 120
60
62
  select = [
61
63
  "D", # flake8-docstrings
62
64
  "I", # isort
@@ -76,8 +78,9 @@ select = [
76
78
  "RET", # flake8-raise
77
79
  ]
78
80
  unfixable = ["B", "UP", "C4", "BLE", "T20", "RET"]
79
- target-version = "py38"
80
- [tool.ruff.per-file-ignores]
81
+ [tool.ruff.lint.pydocstyle]
82
+ convention = "numpy"
83
+ [tool.ruff.lint.per-file-ignores]
81
84
  "tests/*" = ["D", "PT", "B024"]
82
85
  "*/__init__.py" = ["F401", "D104", "D107"]
83
86
  "docs/*" = ["D","B","E","A"]
@@ -86,5 +89,3 @@ target-version = "py38"
86
89
  "src/napari_spatialdata/_constants/_utils.py" = ["D101","D102", "B024"]
87
90
  "src/napari_spatialdata/_widgets.py" = ["D"]
88
91
  "src/napari_spatialdata/_scatterwidgets.py" = ["D"]
89
- [tool.ruff.pydocstyle]
90
- convention = "numpy"
@@ -37,7 +37,7 @@ install_requires =
37
37
  geopandas
38
38
  loguru
39
39
  matplotlib
40
- napari
40
+ napari>=0.4.19
41
41
  napari-matplotlib
42
42
  numba
43
43
  numpy
@@ -45,6 +45,7 @@ install_requires =
45
45
  pandas
46
46
  pillow
47
47
  qtpy
48
+ scanpy
48
49
  scipy
49
50
  shapely
50
51
  scikit-learn
@@ -77,9 +78,10 @@ doc =
77
78
  sphinx-qt-documentation
78
79
  myst-nb
79
80
  squidpy
80
- scanpy
81
81
  readthedocs =
82
82
  pydantic<2
83
+ pre =
84
+ spatialdata>=0.1.0-pre0
83
85
  all =
84
86
  PyQt5
85
87
 
@@ -32,6 +32,8 @@ def view(path: tuple[str], headless: bool) -> None:
32
32
  ----------
33
33
  path
34
34
  Path to one or more SpatialData datasets.
35
+ headless
36
+ Whether to run the napari application or to run in headless mode.
35
37
  """
36
38
  assert type(path) == tuple
37
39
 
@@ -1,4 +1,5 @@
1
1
  """Constants that user deals with."""
2
+
2
3
  from enum import unique
3
4
 
4
5
  from napari_spatialdata._constants._utils import ModeEnum
@@ -1,4 +1,5 @@
1
1
  """Internal constants not exposed to the user."""
2
+
2
3
  from typing import Any, Callable, Optional
3
4
 
4
5
  from anndata import AnnData
@@ -0,0 +1,4 @@
1
+ POLYGON_THRESHOLD = 100
2
+ POINT_THRESHOLD = 100000
3
+ N_CIRCLES_WARNING_THRESHOLD = 250000
4
+ N_SHAPES_WARNING_THRESHOLD = 10000
@@ -34,6 +34,19 @@ class Interactive:
34
34
  """
35
35
 
36
36
  def add_element(self, element: str, element_coordinate_system: str, view_element_system: bool = False) -> None:
37
+ """
38
+ Add an element of a spatial data object being visualized with interactive to the viewer.
39
+
40
+ Parameters
41
+ ----------
42
+ element
43
+ The name of the element in the spatial data object to add as napari layer.
44
+ element_coordinate_system
45
+ The coordinate system in which the layer should be visualized.
46
+ view_element_system
47
+ Whether to switch to element_coordinate_system or to switch back to current active coordinate system
48
+ after adding an element as layer.
49
+ """
37
50
  duplicate_element_names, _ = get_duplicate_element_names(self._sdata)
38
51
  elements, name_to_add = get_elements_meta_mapping(
39
52
  self._sdata, element_coordinate_system, duplicate_element_names, element
@@ -58,6 +71,7 @@ class Interactive:
58
71
  raise ValueError(f"Element {element} not found in coordinate system {element_coordinate_system}.")
59
72
 
60
73
  def switch_coordinate_system(self, coordinate_system: str) -> None:
74
+ """Switch to a coordinate system present in the spatialdata object(s)."""
61
75
  widget_item = get_itemindex_by_text(self._sdata_widget.coordinate_system_widget, coordinate_system)
62
76
  if widget_item:
63
77
  self._sdata_widget.coordinate_system_widget.setCurrentItem(widget_item)
@@ -80,7 +94,9 @@ class Interactive:
80
94
  self.run()
81
95
 
82
96
  def run(self) -> None:
97
+ """Run the napari application."""
83
98
  napari.run()
84
99
 
85
100
  def screenshot(self) -> NDArrayA | Any:
101
+ """Take a screenshot of the viewer in its current state."""
86
102
  return self._viewer.screenshot(canvas_only=False)
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
2
+ from typing import Any, Optional, Sequence, Tuple, Union
3
3
 
4
4
  import numpy as np
5
5
  import pandas as pd
@@ -8,65 +8,63 @@ from napari.layers import Layer
8
8
  from napari.utils.events import EmitterGroup, Event
9
9
 
10
10
  from napari_spatialdata._constants._constants import Symbol
11
- from napari_spatialdata._constants._pkg_constants import Key
12
11
  from napari_spatialdata.utils._utils import NDArrayA, _ensure_dense_vector
13
12
 
14
- __all__ = ["ImageModel"]
13
+ __all__ = ["DataModel"]
15
14
 
16
15
 
17
16
  @dataclass
18
- class ImageModel:
17
+ class DataModel:
19
18
  """Model which holds the data for interactive visualization."""
20
19
 
21
20
  events: EmitterGroup = field(init=False, default=None, repr=True)
21
+ _table_names: Sequence[Optional[str]] = field(default_factory=list, init=False)
22
+ _active_table_name: Optional[str] = field(default=None, init=False, repr=True)
22
23
  _layer: Layer = field(init=False, default=None, repr=True)
23
24
  _adata: Optional[AnnData] = field(init=False, default=None, repr=True)
24
- _spatial_key: str = field(default=Key.obsm.spatial, repr=False)
25
25
  _adata_layer: Optional[str] = field(init=False, default=None, repr=False)
26
- _label_key: Optional[str] = field(default=None, repr=True)
27
- _coordinates: Optional[NDArrayA] = field(init=False, default=None, repr=True)
28
- _points_coordinates: Optional[NDArrayA] = field(init=False, default=None, repr=True)
29
- _points_var: Optional[pd.Series] = field(init=False, default=None, repr=True)
30
- _scale: Optional[float] = field(init=False, default=None)
26
+ _region_key: Optional[str] = field(default=None, repr=True)
27
+ _instance_key: Optional[str] = field(default=None, repr=True)
28
+ _color_by: str = field(default="", repr=True, init=False)
31
29
  _system_name: Optional[str] = field(default=None, repr=True)
32
30
 
33
- _spot_diameter: Union[NDArrayA, float] = field(init=False, default=1)
34
- _point_diameter: Union[NDArrayA, float] = field(init=False, default=1)
35
31
  _scale_key: Optional[str] = field(init=False, default="tissue_hires_scalef") # TODO(giovp): use constants for these
36
32
 
37
33
  _palette: Optional[str] = field(init=False, default=None, repr=False)
38
34
  _cmap: str = field(init=False, default="viridis", repr=False)
39
35
  _symbol: str = field(init=False, default=Symbol.DISC, repr=False)
40
36
 
41
- VALID_ATTRIBUTES = ("obs", "var", "obsm", "points")
37
+ VALID_ATTRIBUTES = ("obs", "var", "obsm", "columns_df")
42
38
 
43
39
  def __post_init__(self) -> None:
44
40
  self.events = EmitterGroup(
45
41
  source=self,
46
42
  layer=Event,
47
43
  adata=Event,
44
+ color_by=Event,
48
45
  )
49
46
 
50
- def get_items(self, attr: str) -> Tuple[str, ...]:
47
+ def get_items(self, attr: str) -> Optional[Tuple[str, ...]]:
51
48
  """
52
49
  Return valid keys for an attribute.
53
50
 
54
51
  Parameters
55
52
  ----------
56
53
  attr
57
- Attribute of :mod:`anndata.AnnData` to access.
54
+ Attribute of :mod:`anndata.AnnData` to access or if columns_df indicates
55
+ the column is part of the SpatialElement dataframe and this will be retrieved.
58
56
 
59
57
  Returns
60
58
  -------
61
- The available items.
59
+ The available items coerced to a tuple of strings.
62
60
  """
63
61
  if attr in ("obs", "obsm"):
64
62
  return tuple(map(str, getattr(self.adata, attr).keys()))
65
- if attr == "points":
66
- if self.points_var is not None:
67
- return tuple(map(str, self.points_var.unique()))
68
- return tuple(["No points found."]) # noqa: C409
69
- return tuple(map(str, getattr(self.adata, attr).index))
63
+ if attr == "columns_df" and self.layer is not None and (df_cols := self.layer.metadata.get("_columns_df")):
64
+ return tuple(map(str, df_cols.columns))
65
+ if attr == "var":
66
+ return tuple(map(str, getattr(self.adata, attr).index))
67
+ return None
70
68
 
71
69
  @_ensure_dense_vector
72
70
  def get_obs(
@@ -86,12 +84,35 @@ class ImageModel:
86
84
  """
87
85
  if name not in self.adata.obs.columns:
88
86
  raise KeyError(f"Key `{name}` not found in `adata.obs`.")
89
- return self.adata.obs[name], self._format_key(name)
87
+ if name != self.instance_key:
88
+ adata_obs = self.adata.obs[[self.instance_key, name]]
89
+ adata_obs.set_index(self.instance_key, inplace=True)
90
+ else:
91
+ adata_obs = self.adata.obs
92
+ return adata_obs[name], self._format_key(name)
93
+
94
+ @_ensure_dense_vector
95
+ def get_columns_df(self, name: Union[str, int], **_: Any) -> Tuple[Optional[NDArrayA], str]:
96
+ """
97
+ Return a column of the dataframe of the SpatialElement.
98
+
99
+ Parameters
100
+ ----------
101
+ name
102
+ Name of the column in the dataframe to retrieve.
103
+
104
+ Returns
105
+ -------
106
+ The dataframe column of interest and the formatted name of the column.
107
+ """
108
+ if self.layer is None:
109
+ raise ValueError("Layer must be present")
110
+ return self.layer.metadata["_columns_df"][name], self._format_key(name)
90
111
 
91
112
  @_ensure_dense_vector
92
113
  def get_var(self, name: Union[str, int], **_: Any) -> Tuple[Optional[NDArrayA], str]: # TODO(giovp): fix docstring
93
114
  """
94
- Return a gene.
115
+ Return a column in anndata.var_names.
95
116
 
96
117
  Parameters
97
118
  ----------
@@ -151,25 +172,6 @@ class ImageModel:
151
172
 
152
173
  return (res if res.ndim == 1 else res[:, index]), pretty_name
153
174
 
154
- def get_points(self, name: str, **_: Any) -> Tuple[Optional[NDArrayA], str]: # TODO(giovp): fix docstring
155
- """
156
- Return a gene in spatial coordinates.
157
-
158
- Parameters
159
- ----------
160
- name
161
- Gene name.
162
-
163
- Returns
164
- -------
165
- The values and the formatted ``name``.
166
- """
167
- if name not in self.points_var.unique():
168
- raise KeyError(f"Key `{name}` not found in `adata.uns['points']['gene']`.")
169
- coords = self.points_coordinates
170
- coords = coords[self.points_var == name]
171
- return np.insert(coords[:, ::-1][:, :2] * self.scale, 0, values=0, axis=1), self._format_key(name)
172
-
173
175
  def _format_key(
174
176
  self, key: Union[str, int], index: Optional[Union[int, str]] = None, adata_layer: bool = False
175
177
  ) -> str:
@@ -180,8 +182,28 @@ class ImageModel:
180
182
 
181
183
  return str(key) + (f":{self.layer}" if self.layer is not None else ":X")
182
184
 
185
+ @property
186
+ def color_by(self) -> str:
187
+ """The name by which the layer is currently colored."""
188
+ return self._color_by
189
+
190
+ @color_by.setter
191
+ def color_by(self, color_column: str) -> None:
192
+ self._color_by = color_column
193
+ self.events.color_by()
194
+
195
+ @property
196
+ def table_names(self) -> Sequence[Optional[str]]:
197
+ """The table names annotating the current active napari layer, if any."""
198
+ return self._table_names
199
+
200
+ @table_names.setter
201
+ def table_names(self, table_names: Sequence[Optional[str]]) -> None:
202
+ self._table_names = table_names
203
+
183
204
  @property
184
205
  def layer(self) -> Optional[Layer]: # noqa: D102
206
+ """The current active napari layer."""
185
207
  return self._layer
186
208
 
187
209
  @layer.setter
@@ -189,8 +211,18 @@ class ImageModel:
189
211
  self._layer = layer
190
212
  self.events.layer()
191
213
 
214
+ @property
215
+ def active_table_name(self) -> Optional[str]:
216
+ """The table name currently active in the widget."""
217
+ return self._active_table_name
218
+
219
+ @active_table_name.setter
220
+ def active_table_name(self, active_table_name: Optional[str]) -> None:
221
+ self._active_table_name = active_table_name
222
+
192
223
  @property
193
224
  def adata(self) -> AnnData: # noqa: D102
225
+ """The anndata object corresponding to the current active table name."""
194
226
  return self._adata
195
227
 
196
228
  @adata.setter
@@ -198,16 +230,9 @@ class ImageModel:
198
230
  self._adata = adata
199
231
  self.events.adata()
200
232
 
201
- @property
202
- def spatial_key(self) -> str: # noqa: D102
203
- return self._spatial_key
204
-
205
- @spatial_key.setter
206
- def spatial_key(self, key: str) -> None:
207
- self._spatial_key = key
208
-
209
233
  @property
210
234
  def adata_layer(self) -> Optional[str]: # noqa: D102
235
+ """The current anndata layer."""
211
236
  return self._adata_layer
212
237
 
213
238
  @adata_layer.setter
@@ -215,65 +240,26 @@ class ImageModel:
215
240
  self._adata_layer = adata_layer
216
241
 
217
242
  @property
218
- def coordinates(self) -> NDArrayA: # noqa: D102
219
- return self._coordinates # type: ignore[return-value]
220
-
221
- @coordinates.setter
222
- def coordinates(self, coordinates: NDArrayA) -> None:
223
- self._coordinates = coordinates
224
-
225
- @property
226
- def points_coordinates(self) -> NDArrayA: # noqa: D102
227
- if TYPE_CHECKING:
228
- assert self._points_coordinates is not None
229
- return self._points_coordinates
230
-
231
- @points_coordinates.setter
232
- def points_coordinates(self, points_coordinates: NDArrayA) -> None:
233
- self._points_coordinates = points_coordinates
234
-
235
- @property
236
- def points_var(self) -> pd.Series: # noqa: D102
237
- return self._points_var
238
-
239
- @points_var.setter
240
- def points_var(self, points_var: pd.Series) -> None:
241
- self._points_var = points_var
242
-
243
- @property
244
- def scale(self) -> Optional[float]: # noqa: D102
245
- return self._scale
246
-
247
- @scale.setter
248
- def scale(self, scale: float) -> None:
249
- self._scale = scale
250
-
251
- @property
252
- def spot_diameter(self) -> Union[NDArrayA, float]: # noqa: D102
253
- return self._spot_diameter
254
-
255
- @spot_diameter.setter
256
- def spot_diameter(self, spot_diameter: Union[NDArrayA, float]) -> None:
257
- self._spot_diameter = spot_diameter
258
-
259
- @property
260
- def point_diameter(self) -> Union[NDArrayA, float]: # noqa: D102
261
- return self._point_diameter
243
+ def region_key(self) -> Optional[str]: # noqa: D102
244
+ """The region key of the currently active table in the widget."""
245
+ return self._region_key
262
246
 
263
- @point_diameter.setter
264
- def point_diameter(self, point_diameter: Union[NDArrayA, float]) -> None:
265
- self._point_diameter = point_diameter
247
+ @region_key.setter
248
+ def region_key(self, region_key: str) -> None:
249
+ self._region_key = region_key
266
250
 
267
251
  @property
268
- def labels_key(self) -> Optional[str]: # noqa: D102
269
- return self._labels_key
252
+ def instance_key(self) -> Optional[str]: # noqa: D102
253
+ """The instance key of the currently active table in the widget."""
254
+ return self._instance_key
270
255
 
271
- @labels_key.setter
272
- def labels_key(self, region_key: str) -> None:
273
- self._labels_key = region_key
256
+ @instance_key.setter
257
+ def instance_key(self, instance_key: str) -> None:
258
+ self._instance_key = instance_key
274
259
 
275
260
  @property
276
261
  def palette(self) -> Optional[str]: # noqa: D102
262
+ """The palette from which to draw the colors."""
277
263
  return self._palette
278
264
 
279
265
  @palette.setter
@@ -282,6 +268,7 @@ class ImageModel:
282
268
 
283
269
  @property
284
270
  def cmap(self) -> str: # noqa: D102
271
+ """The continuous color map to draw colors from."""
285
272
  return self._cmap
286
273
 
287
274
  @cmap.setter
@@ -306,6 +293,7 @@ class ImageModel:
306
293
 
307
294
  @property
308
295
  def system_name(self) -> Optional[str]: # noqa: D102
296
+ """The layer name."""
309
297
  return self._system_name
310
298
 
311
299
  @system_name.setter