spatialscape 0.3.0__py3-none-any.whl

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 (58) hide show
  1. spatialscape/__init__.py +13 -0
  2. spatialscape/_app/assets/array-utils-flat-BL8Cekj1.js +13 -0
  3. spatialscape/_app/assets/blosc-DEOJs6D5.js +18 -0
  4. spatialscape/_app/assets/blosc-Df7TknHH.js +18 -0
  5. spatialscape/_app/assets/chunk-INHXZS53-DAOLzMPX.js +2 -0
  6. spatialscape/_app/assets/chunk-INHXZS53-DfBSTIxr.js +2 -0
  7. spatialscape/_app/assets/deflate-B2AbjNo_.js +2 -0
  8. spatialscape/_app/assets/expression-E359vKZW.js +368 -0
  9. spatialscape/_app/assets/get-attribute-from-layouts-Dd2gixpo.js +2 -0
  10. spatialscape/_app/assets/index-BKPnXJSt.css +1 -0
  11. spatialscape/_app/assets/index-Gxpj1e1n.js +8454 -0
  12. spatialscape/_app/assets/jpeg-b0MwBR_9.js +2 -0
  13. spatialscape/_app/assets/lerc-ujbbHc0C.js +2 -0
  14. spatialscape/_app/assets/lz4-BFJkNc3o.js +16 -0
  15. spatialscape/_app/assets/lz4-DNE3IovK.js +16 -0
  16. spatialscape/_app/assets/lzw-DfRkoFB0.js +2 -0
  17. spatialscape/_app/assets/packbits-BDCGuk_Z.js +2 -0
  18. spatialscape/_app/assets/pako.esm-Ch1S5qKo.js +2 -0
  19. spatialscape/_app/assets/raw-vNZ-QFXZ.js +2 -0
  20. spatialscape/_app/assets/webgl-D2Fkccwd.js +345 -0
  21. spatialscape/_app/assets/webimage-DDU0IC05.js +2 -0
  22. spatialscape/_app/assets/worker-CUy87eWB.js +8 -0
  23. spatialscape/_app/assets/zstd-CGD_7wnU.js +16 -0
  24. spatialscape/_app/assets/zstd-qXhZ_1DN.js +16 -0
  25. spatialscape/_app/favicon.svg +161 -0
  26. spatialscape/_app/index.html +23 -0
  27. spatialscape/build.py +630 -0
  28. spatialscape/cli.py +399 -0
  29. spatialscape/config.py +570 -0
  30. spatialscape/convert.py +321 -0
  31. spatialscape/discover.py +98 -0
  32. spatialscape/expression.py +300 -0
  33. spatialscape/fingerprint.py +152 -0
  34. spatialscape/genes.py +251 -0
  35. spatialscape/geometry.py +132 -0
  36. spatialscape/images.py +240 -0
  37. spatialscape/init.py +578 -0
  38. spatialscape/manifest.py +162 -0
  39. spatialscape/obs.py +48 -0
  40. spatialscape/outlines.py +145 -0
  41. spatialscape/polygons.py +85 -0
  42. spatialscape/rds.py +609 -0
  43. spatialscape/rdsread.py +556 -0
  44. spatialscape/readers.py +195 -0
  45. spatialscape/registry.py +83 -0
  46. spatialscape/serve.py +262 -0
  47. spatialscape/session.py +474 -0
  48. spatialscape/site.py +274 -0
  49. spatialscape/synthetic.py +142 -0
  50. spatialscape/thumbnail.py +95 -0
  51. spatialscape/tracks.py +551 -0
  52. spatialscape/validate.py +193 -0
  53. spatialscape/vocab.py +188 -0
  54. spatialscape-0.3.0.dist-info/METADATA +68 -0
  55. spatialscape-0.3.0.dist-info/RECORD +58 -0
  56. spatialscape-0.3.0.dist-info/WHEEL +4 -0
  57. spatialscape-0.3.0.dist-info/entry_points.txt +3 -0
  58. spatialscape-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,193 @@
1
+ """Validate a bundle on disk or over HTTP (schema, shapes, codes, range requests)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import urllib.request
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+
10
+ from .manifest import Manifest
11
+
12
+
13
+ def _problem(problems: list[str], msg: str) -> None:
14
+ problems.append(msg)
15
+
16
+
17
+ def validate_local(root: Path) -> list[str]:
18
+ import zarr
19
+
20
+ p: list[str] = []
21
+ root = Path(root)
22
+ mpath = root / "manifest.json"
23
+ if not mpath.exists():
24
+ return [f"missing {mpath}"]
25
+ try:
26
+ m = Manifest.model_validate_json(mpath.read_text())
27
+ except Exception as e:
28
+ return [f"manifest.json invalid: {e}"]
29
+ genes_union = json.loads((root / "genes.json").read_text()) if (root / "genes.json").exists() else None
30
+ if genes_union is None:
31
+ _problem(p, "missing genes.json")
32
+ if m.defaultGene and genes_union is not None and m.defaultGene not in set(genes_union):
33
+ _problem(p, f"defaultGene {m.defaultGene!r} not in genes.json")
34
+ fpath = root / "features.json"
35
+ if fpath.exists():
36
+ try:
37
+ feats = json.loads(fpath.read_text())
38
+ assert isinstance(feats.get("genes"), list) and isinstance(feats.get("groups"), list)
39
+ for g in feats["groups"]:
40
+ assert all({"id", "label"} <= set(f) for f in g["features"])
41
+ except Exception as e:
42
+ _problem(p, f"features.json malformed: {e}")
43
+ field_ids = {f.id for f in m.fields}
44
+ for f in m.fields:
45
+ if f.type == "categorical" and f.vocabulary not in m.vocabularies:
46
+ _problem(p, f"field {f.id}: vocabulary {f.vocabulary!r} missing")
47
+ for v_id, v in m.vocabularies.items():
48
+ if len(v.categories) != len(v.colors):
49
+ _problem(p, f"vocabulary {v_id}: categories/colors length mismatch")
50
+ for sid in m.layout.order:
51
+ if sid not in {s.id for s in m.samples}:
52
+ _problem(p, f"layout.order references unknown sample {sid!r}")
53
+ for s in m.samples:
54
+ d = root / "samples" / s.id
55
+ if not d.is_dir():
56
+ _problem(p, f"{s.id}: missing sample directory")
57
+ continue
58
+ try:
59
+ g = zarr.open_group(str(d / "expr.zarr"), mode="r")
60
+ u8 = g["u8"]
61
+ if tuple(u8.shape) != (s.nGenes, s.nObs):
62
+ _problem(p, f"{s.id}: expr u8 shape {u8.shape} != ({s.nGenes}, {s.nObs})")
63
+ if u8.chunks[0] != 1:
64
+ _problem(p, f"{s.id}: expr u8 chunk shape {u8.chunks} is not gene-major (1, n_obs)")
65
+ if g["gmax"].shape[0] != s.nGenes:
66
+ _problem(p, f"{s.id}: gmax length mismatch")
67
+ if "gmin" in g and g["gmin"].shape[0] != s.nGenes:
68
+ _problem(p, f"{s.id}: gmin length mismatch")
69
+ gmax = np.asarray(g["gmax"][:])
70
+ if gmax.size and (not np.all(np.isfinite(gmax)) or (gmax <= 0).any()):
71
+ _problem(p, f"{s.id}: gmax has non-finite or non-positive values")
72
+ if s.nGenes:
73
+ _ = u8[0, :] # exercise one chunk read (and the shard index)
74
+ except Exception as e:
75
+ _problem(p, f"{s.id}: expr.zarr unreadable: {e}")
76
+ genes = json.loads((d / "genes.json").read_text()) if (d / "genes.json").exists() else None
77
+ if genes is None or len(genes) != s.nGenes:
78
+ _problem(p, f"{s.id}: genes.json missing or length != nGenes")
79
+ try:
80
+ og = zarr.open_group(str(d / "obs.zarr"), mode="r")
81
+ if tuple(og["xy"].shape) != (s.nObs, 2):
82
+ _problem(p, f"{s.id}: obs xy shape {og['xy'].shape}")
83
+ if tuple(og["order"].shape) != (s.nObs,):
84
+ _problem(p, f"{s.id}: obs order shape")
85
+ elif s.nObs:
86
+ order = np.asarray(og["order"][:])
87
+ if order.min() != 0 or order.max() != s.nObs - 1 or np.unique(order).size != s.nObs:
88
+ _problem(p, f"{s.id}: obs order is not a permutation of the rows")
89
+ for fid in s.fields:
90
+ if fid not in field_ids:
91
+ _problem(p, f"{s.id}: field {fid!r} not declared in manifest.fields")
92
+ continue
93
+ f = m.field_by_id(fid)
94
+ if f.type == "categorical":
95
+ if fid not in og["cat"]:
96
+ _problem(p, f"{s.id}: obs.zarr/cat/{fid} missing")
97
+ continue
98
+ codes = og["cat"][fid][:]
99
+ ncat = len(m.vocabularies[f.vocabulary].categories)
100
+ if codes.shape != (s.nObs,):
101
+ _problem(p, f"{s.id}: obs.zarr/cat/{fid} has {codes.shape[0]} rows, expected {s.nObs}")
102
+ if codes.size and int(codes.max()) >= ncat:
103
+ _problem(p, f"{s.id}: field {fid} has code {int(codes.max())} >= {ncat} categories")
104
+ else:
105
+ if fid not in og["num"]:
106
+ _problem(p, f"{s.id}: obs.zarr/num/{fid} missing")
107
+ elif tuple(og["num"][fid].shape) != (s.nObs,):
108
+ _problem(p, f"{s.id}: obs.zarr/num/{fid} has {og['num'][fid].shape[0]} rows, expected {s.nObs}")
109
+ except Exception as e:
110
+ _problem(p, f"{s.id}: obs.zarr unreadable: {e}")
111
+ nblocks = -(-s.nObs // s.idBlock)
112
+ for b in range(nblocks):
113
+ fb = d / "ids" / f"{b}.json"
114
+ if not fb.exists():
115
+ _problem(p, f"{s.id}: ids/{b}.json missing")
116
+ break
117
+ want = min(s.idBlock, s.nObs - b * s.idBlock)
118
+ try:
119
+ got = len(json.loads(fb.read_text()))
120
+ except ValueError:
121
+ got = -1
122
+ if got != want:
123
+ _problem(p, f"{s.id}: ids/{b}.json has {got} ids, expected {want}")
124
+ break
125
+ for fid in s.outlines or []:
126
+ if not (d / "outlines" / f"{fid}.json").exists():
127
+ _problem(p, f"{s.id}: outlines/{fid}.json listed but missing")
128
+ if s.polygons:
129
+ off = d / "polygons.offsets.u32"
130
+ dl = d / "polygons.i16"
131
+ if not off.exists() or not dl.exists():
132
+ _problem(p, f"{s.id}: polygon files missing")
133
+ elif off.stat().st_size == 4 * (s.nObs + 1) and dl.stat().st_size == 4 * s.polygons.vertices:
134
+ offs = np.fromfile(off, dtype=np.uint32)
135
+ if offs[0] != 0 or offs[-1] != s.polygons.vertices or (np.diff(offs.astype(np.int64)) < 0).any():
136
+ _problem(p, f"{s.id}: polygon offsets are not a non-decreasing 0..vertices run")
137
+ else:
138
+ _problem(p, f"{s.id}: polygon file sizes do not match nObs/vertices")
139
+ for im in s.images:
140
+ try:
141
+ ig = zarr.open_group(str(d / im.path), mode="r")
142
+ lv0 = ig[ig.attrs["multiscales"][0]["datasets"][0]["path"]]
143
+ if tuple(lv0.shape[1:]) != tuple(im.size):
144
+ _problem(p, f"{s.id}: image {im.id} size {lv0.shape[1:]} != {im.size}")
145
+ except Exception as e:
146
+ _problem(p, f"{s.id}: image {im.id} unreadable: {e}")
147
+ if not np.isfinite(s.bbox).all():
148
+ _problem(p, f"{s.id}: bbox not finite")
149
+ return p
150
+
151
+
152
+ ORIGIN = "https://viewer.example.org" # any browser origin; servers that gate CORS on it answer as they would for the viewer
153
+
154
+
155
+ def validate_remote(url: str) -> list[str]:
156
+ p: list[str] = []
157
+ url = url.rstrip("/")
158
+ try:
159
+ req = urllib.request.Request(url + "/manifest.json", headers={"Origin": ORIGIN})
160
+ with urllib.request.urlopen(req, timeout=20) as r:
161
+ m = Manifest.model_validate_json(r.read())
162
+ if not r.headers.get("Access-Control-Allow-Origin"):
163
+ _problem(p, "manifest.json: no Access-Control-Allow-Origin header; the browser will refuse to load the bundle (configure CORS on the host)")
164
+ except Exception as e:
165
+ return [f"cannot fetch manifest: {e}"]
166
+ if not m.samples:
167
+ return p + ["manifest has no samples"]
168
+ s = m.samples[0]
169
+ sharded = s.expr.sharded
170
+ chunk = f"{url}/samples/{s.id}/expr.zarr/u8/c/0/0"
171
+ headers = {"Origin": ORIGIN}
172
+ if sharded:
173
+ headers["Range"] = "bytes=0-15"
174
+ req = urllib.request.Request(chunk, headers=headers)
175
+ try:
176
+ with urllib.request.urlopen(req, timeout=20) as r:
177
+ if sharded:
178
+ if r.status != 206:
179
+ _problem(p, f"range request returned {r.status}, expected 206: this bundle is sharded, so the host must support HTTP Range (or rebuild with --no-shard)")
180
+ exp = r.headers.get("Access-Control-Expose-Headers", "")
181
+ if "content-range" not in exp.lower() and "*" not in exp:
182
+ _problem(p, "Access-Control-Expose-Headers should include Content-Range for sharded bundles")
183
+ elif r.status != 200:
184
+ _problem(p, f"gene chunk request returned {r.status}")
185
+ except Exception as e:
186
+ _problem(p, f"gene chunk request failed for {chunk}: {e}")
187
+ return p
188
+
189
+
190
+ def validate(target: str) -> list[str]:
191
+ if target.startswith(("http://", "https://")):
192
+ return validate_remote(target)
193
+ return validate_local(Path(target))
spatialscape/vocab.py ADDED
@@ -0,0 +1,188 @@
1
+ """Dataset-level categorical vocabularies: stable codes + colors across samples and platforms."""
2
+ from __future__ import annotations
3
+
4
+ import colorsys
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from .config import FieldSpec
13
+ from .manifest import Vocabulary
14
+
15
+ NA_LABEL = "NA"
16
+ NA_COLOR = "#c8c8c8"
17
+
18
+ # Tableau 20 + a few; stable, print-friendly
19
+ QUALITATIVE = [
20
+ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f",
21
+ "#bcbd22", "#17becf", "#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5", "#c49c94",
22
+ "#f7b6d2", "#dbdb8d", "#9edae5", "#393b79", "#637939", "#8c6d31", "#843c39", "#7b4173",
23
+ ]
24
+
25
+
26
+ def _golden_color(i: int) -> str:
27
+ h = (i * 0.618033988749895) % 1.0
28
+ s = 0.55 + 0.35 * ((i * 7) % 3) / 2
29
+ v = 0.75 + 0.2 * ((i * 11) % 2)
30
+ r, g, b = colorsys.hsv_to_rgb(h, s, v)
31
+ return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
32
+
33
+
34
+ def load_palette(path: Path | None) -> dict[str, dict[str, str]]:
35
+ if path is None:
36
+ return {}
37
+ with open(path) as fh:
38
+ raw: dict[str, Any] = json.load(fh)
39
+ out: dict[str, dict[str, str]] = {}
40
+ flat: dict[str, str] = {}
41
+ for k, v in raw.items():
42
+ if k.startswith("_"):
43
+ continue
44
+ if isinstance(v, dict):
45
+ out[k] = {str(a): str(b) for a, b in v.items()}
46
+ elif isinstance(v, str):
47
+ flat[k] = v
48
+ if flat:
49
+ out["_flat"] = flat
50
+ return out
51
+
52
+
53
+ MAX_CATEGORIES = 65_535 # codes are uint16
54
+
55
+
56
+ def _label(c) -> str:
57
+ """A category label as text: integral numbers without a trailing .0, so an int column in one sample and a
58
+ float column (NaN forced it) in another meet in the same category."""
59
+ if isinstance(c, (float, np.floating)):
60
+ return str(int(c)) if np.isfinite(c) and float(c).is_integer() else str(c)
61
+ return str(c)
62
+
63
+
64
+ def _label_strings(s: pd.Series) -> pd.Series:
65
+ if pd.api.types.is_numeric_dtype(s.dtype) and not pd.api.types.is_bool_dtype(s.dtype):
66
+ v = pd.to_numeric(s, errors="coerce")
67
+ return v.map(lambda x: None if pd.isna(x) else _label(float(x))).astype("string")
68
+ return s.astype("string")
69
+
70
+
71
+ class VocabRegistry:
72
+ """Append-only registry: codes never change once assigned, so a rebuilt sample stays consistent with the others."""
73
+
74
+ def __init__(
75
+ self,
76
+ fields: list[FieldSpec],
77
+ palette: dict[str, dict[str, str]] | None = None,
78
+ existing: dict[str, Vocabulary] | None = None,
79
+ ):
80
+ self.fields = {f.id: f for f in fields}
81
+ self.palette = palette or {}
82
+ self.vocabs: dict[str, dict[str, Any]] = {}
83
+ for vid, v in (existing or {}).items():
84
+ self.vocabs[vid] = {
85
+ "categories": list(v.categories),
86
+ "colors": list(v.colors),
87
+ "aliases": dict(v.aliases),
88
+ }
89
+ for f in fields:
90
+ if f.type != "categorical":
91
+ continue
92
+ vid = f.vocabulary or f.id
93
+ v = self.vocabs.setdefault(vid, {"categories": [], "colors": [], "aliases": {}})
94
+ v["aliases"].update(f.aliases)
95
+ v.setdefault("_palette_key", f.palette_key or vid)
96
+ for c in f.categories or []:
97
+ self._add(vid, c)
98
+
99
+ def declare(self, f: FieldSpec) -> None:
100
+ """Register a field discovered at build time (idempotent)."""
101
+ if f.id in self.fields:
102
+ return
103
+ self.fields[f.id] = f
104
+ if f.type == "categorical":
105
+ vid = f.vocabulary or f.id
106
+ v = self.vocabs.setdefault(vid, {"categories": [], "colors": [], "aliases": {}})
107
+ v["aliases"].update(f.aliases)
108
+ v.setdefault("_palette_key", f.palette_key or vid)
109
+
110
+ def adopt_manifest_fields(self, manifest, only: set[str] | None = None) -> None:
111
+ """Keep fields that an existing bundle already declares (e.g. auto-discovered ones); `only` restricts the ids."""
112
+ for mf in manifest.fields:
113
+ if mf.id in self.fields or (only is not None and mf.id not in only):
114
+ continue
115
+ if mf.type == "categorical":
116
+ self.declare(FieldSpec(id=mf.id, name=mf.name, type="categorical", vocabulary=mf.vocabulary, description=mf.description))
117
+ else:
118
+ self.declare(FieldSpec(id=mf.id, name=mf.name, type="continuous", range=mf.range, colormap=mf.colormap, description=mf.description))
119
+
120
+ def recolor_from_palette(self) -> int:
121
+ """Re-derive the color of every existing label that the palette names, keeping codes. Returns how many changed."""
122
+ changed = 0
123
+ for vid, v in self.vocabs.items():
124
+ key = v.get("_palette_key", vid)
125
+ keys = [k.strip() for k in str(key).split(",")] + [vid, "_flat"]
126
+ for i, label in enumerate(v["categories"]):
127
+ if label == NA_LABEL:
128
+ continue
129
+ for pk in keys:
130
+ pal = self.palette.get(pk)
131
+ if pal and label in pal:
132
+ if v["colors"][i] != pal[label]:
133
+ v["colors"][i] = pal[label]
134
+ changed += 1
135
+ break
136
+ return changed
137
+
138
+ def vocab_id(self, field_id: str) -> str:
139
+ f = self.fields[field_id]
140
+ return f.vocabulary or f.id
141
+
142
+ def _color_for(self, vid: str, label: str, index: int) -> str:
143
+ if label == NA_LABEL:
144
+ return NA_COLOR
145
+ key = self.vocabs[vid].get("_palette_key", vid)
146
+ keys = [k.strip() for k in str(key).split(",")] + [vid, "_flat"]
147
+ for pk in keys:
148
+ pal = self.palette.get(pk)
149
+ if pal and label in pal:
150
+ return pal[label]
151
+ if index < len(QUALITATIVE):
152
+ return QUALITATIVE[index]
153
+ return _golden_color(index)
154
+
155
+ def _add(self, vid: str, label: str) -> int:
156
+ v = self.vocabs[vid]
157
+ cats = v["categories"]
158
+ if label in cats:
159
+ return cats.index(label)
160
+ if len(cats) >= MAX_CATEGORIES:
161
+ raise ValueError(f"vocabulary {vid!r}: more than {MAX_CATEGORIES:,} categories across samples")
162
+ cats.append(label)
163
+ v["colors"].append(self._color_for(vid, label, len(cats) - 1))
164
+ return len(cats) - 1
165
+
166
+ def encode(self, field_id: str, values: pd.Series) -> np.ndarray:
167
+ vid = self.vocab_id(field_id)
168
+ aliases = self.vocabs[vid]["aliases"]
169
+ s = values
170
+ if not isinstance(s.dtype, pd.CategoricalDtype):
171
+ s = _label_strings(s).astype("category")
172
+ labels = [aliases.get(_label(c), _label(c)) for c in s.cat.categories]
173
+ if len(labels) > MAX_CATEGORIES:
174
+ raise ValueError(f"field {field_id!r}: {len(labels):,} categories (at most {MAX_CATEGORIES:,}); map a coarser column, or leave this one out of `fields:`")
175
+ # preserve source category order for first-seen appends
176
+ code_map = np.empty(len(labels) + 1, dtype=np.uint16)
177
+ for i, lab in enumerate(labels):
178
+ code_map[i] = self._add(vid, lab)
179
+ code_map[-1] = self._add(vid, NA_LABEL) if (s.cat.codes < 0).any() else 0
180
+ src = s.cat.codes.to_numpy().astype(np.int64)
181
+ src[src < 0] = len(labels)
182
+ return code_map[src]
183
+
184
+ def to_manifest(self) -> dict[str, Vocabulary]:
185
+ return {
186
+ vid: Vocabulary(categories=v["categories"], colors=v["colors"], aliases=v["aliases"])
187
+ for vid, v in self.vocabs.items()
188
+ }
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.5
2
+ Name: spatialscape
3
+ Version: 0.3.0
4
+ Summary: Build static spatialscape viewer bundles from AnnData / SpatialData
5
+ Project-URL: Homepage, https://mictott.github.io/spatialscape/docs/
6
+ Project-URL: Documentation, https://mictott.github.io/spatialscape/docs/
7
+ Project-URL: Repository, https://github.com/mictott/spatialscape
8
+ Project-URL: Issues, https://github.com/mictott/spatialscape/issues
9
+ Project-URL: Changelog, https://github.com/mictott/spatialscape/blob/main/CHANGELOG.md
10
+ Author-email: Michael Totty <mictott@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: deck.gl,single-cell,spatial-transcriptomics,visium,visualization,xenium,zarr
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
23
+ Classifier: Topic :: Scientific/Engineering :: Visualization
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: anndata>=0.12
26
+ Requires-Dist: imageio>=2.34
27
+ Requires-Dist: numcodecs>=0.15
28
+ Requires-Dist: numpy>=1.26
29
+ Requires-Dist: pandas>=2.0
30
+ Requires-Dist: pillow>=10
31
+ Requires-Dist: pyarrow>=15
32
+ Requires-Dist: pydantic>=2.6
33
+ Requires-Dist: pyyaml>=6
34
+ Requires-Dist: scikit-image>=0.22
35
+ Requires-Dist: scipy>=1.11
36
+ Requires-Dist: tifffile>=2024.1
37
+ Requires-Dist: typer>=0.12
38
+ Requires-Dist: zarr>=3.0
39
+ Provides-Extra: dev
40
+ Requires-Dist: build>=1.2; extra == 'dev'
41
+ Requires-Dist: h5py>=3.10; extra == 'dev'
42
+ Requires-Dist: pytest>=8; extra == 'dev'
43
+ Requires-Dist: ruff>=0.5; extra == 'dev'
44
+ Provides-Extra: spatialdata
45
+ Requires-Dist: spatialdata>=0.4; extra == 'spatialdata'
46
+ Provides-Extra: tracks
47
+ Requires-Dist: pybigwig>=0.3.18; extra == 'tracks'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # spatialscape (Python)
51
+
52
+ The command-line tool behind **spatialscape**, a static, reactive browser for spatial transcriptomics and
53
+ snRNA-seq. It turns a saved R object (`SpatialExperiment` / `SingleCellExperiment` as `.rds` or `.rda`, parsed in
54
+ Python) or AnnData / SpatialData files into viewer bundles (sharded Zarr v3 expression, observation arrays in
55
+ microns, OME-Zarr images, domain outlines, cell polygons), and ships the viewer itself so you can open them locally
56
+ or publish them as a website.
57
+
58
+ ```bash
59
+ pip install spatialscape
60
+ spatialscape init analysis/spe_dlpfc.rds --id dlpfc # writes dlpfc.yaml, a menu of what the data contains
61
+ # edit dlpfc.yaml: keep the annotations you want, name them, pick colors and the opening gene
62
+ spatialscape build dlpfc.yaml # -> bundles/dlpfc; rerun after any edit
63
+ spatialscape serve bundles --open # viewer + data at http://127.0.0.1:8787/
64
+ spatialscape site build bundles -o site # folder to upload to any static host
65
+ ```
66
+
67
+ `sscape` is a short alias for the same command. Documentation: https://mictott.github.io/spatialscape/docs/
68
+ Source and issues: https://github.com/MicTott/spatialscape
@@ -0,0 +1,58 @@
1
+ spatialscape/__init__.py,sha256=ZRSD3VYrKj5EZt0WP4kUKs7zjTXiCVyoPinNKnH7VpI,450
2
+ spatialscape/build.py,sha256=XU3z8n5oikH_iJBpB3kMx_TTIhZcTs3fKPLCDKZ-zP4,29398
3
+ spatialscape/cli.py,sha256=u-GW1A2cXuSiTpfGT-ki9LXMKbRFoyEDN_TyAAVW4EQ,21543
4
+ spatialscape/config.py,sha256=wUEys5KfIe11irlG_fYX0Gla1hUzQ-5Xed3-WWS4gTI,26648
5
+ spatialscape/convert.py,sha256=4ulCYNTDWQoo4TgZMko7aTRcURbwWi0kxhxpdxqzlfM,14389
6
+ spatialscape/discover.py,sha256=xgeTMMhdSUhocojxWQQB6cFf1BkOTb4pN0CC8edi47k,4909
7
+ spatialscape/expression.py,sha256=OpQjI-FX-mY6APn2JXm0xt3bqftQP1fmRPJMPg26JIw,14226
8
+ spatialscape/fingerprint.py,sha256=-MhR2NAYnLnulnVFR8gmuIzirlouXQlMrv4jIXW-VWw,6153
9
+ spatialscape/genes.py,sha256=LI0wBCi-k-Vf3j9ame5yR1cbyVy9OTw8SwCt3YRyBxI,11762
10
+ spatialscape/geometry.py,sha256=0C2HAHDzeDeOdndGCsw4--vZ5qvMcX6yI3GcclrKpXU,5109
11
+ spatialscape/images.py,sha256=n3oXkPL-9-iwioagmZMxir5VMcCfMipoyh8bD0HaFEs,9641
12
+ spatialscape/init.py,sha256=Rq8bgl_UIsspjwrMR3lAJcnuglWI-TYj1_lRijz9jAc,29110
13
+ spatialscape/manifest.py,sha256=QraZYehFtUK3fOxodm1Th-qrp0cvnCU17jt7x4bp4HU,5195
14
+ spatialscape/obs.py,sha256=ckB8lzVe5biO1AC5kz0eNu_o8h8SoNmXKrBrY3jcx64,1664
15
+ spatialscape/outlines.py,sha256=LqOc8pLo-2rbE86iEjJxqwwSG2x-8ltX7ZokRsdPuMY,5379
16
+ spatialscape/polygons.py,sha256=doniQ0g22M2P3dEbAlnMiPge8IpOQjRUpcP7SoPcXcc,3617
17
+ spatialscape/rds.py,sha256=ZpMQY0m_5PWeKvouSgbKU7BhTM4X6S321UMi9bKjqzo,27629
18
+ spatialscape/rdsread.py,sha256=CUjPh38jvh8hcIA020hFW2U0H_fbX_KK3LMrUBKA5vw,24195
19
+ spatialscape/readers.py,sha256=Ub9Myvr3IBoEu-TVrYtAgiuD6XpPvsr6dCpHy8wTDoQ,8243
20
+ spatialscape/registry.py,sha256=Z2p0T1Gysb0jF9Kt_8mvBUA4GKrqeaowMaWoPgMsS0U,3291
21
+ spatialscape/serve.py,sha256=zIX6PQkl3qgF_0JPAox_QNj4gR7u556io6nJFrvdeCg,11731
22
+ spatialscape/session.py,sha256=q-nY_CbQQV8R88UDJz315hjE1Fgy6cLCytx9sA5Q-yQ,22557
23
+ spatialscape/site.py,sha256=VPHUU6zKULj3p4Yf4K2HcbXiK2nZJ76IqvbXCfY5dxk,11434
24
+ spatialscape/synthetic.py,sha256=g0SkfUFUi0ht_EEKX1ON40JA5BtABYoIofHwlusc_rs,5836
25
+ spatialscape/thumbnail.py,sha256=ZwIyQH1wAUMzpfp4LKHVyUIojgb6vbAxFCcjDysza8o,3770
26
+ spatialscape/tracks.py,sha256=3qwrSL30f38b-Pc40ciHoHp6o8yBNjAuwPbVKqUExPE,26627
27
+ spatialscape/validate.py,sha256=qd8NxnesTYUzmhLbbN2zvlb2Np-SK62-7yLZWJny_90,9236
28
+ spatialscape/vocab.py,sha256=WX0p_loYTBv9WJef2VibZqrFgi6TbujH8fOzDDqtvv8,7538
29
+ spatialscape/_app/favicon.svg,sha256=EA9COdL3E7nL4g-GX8hfROaq9CcgNClWMEfiumqL3mY,8733
30
+ spatialscape/_app/index.html,sha256=-xoYf4F1VSFJyzAkmPtpuspnHbut3iW3CAvVDUsmTkE,1303
31
+ spatialscape/_app/assets/array-utils-flat-BL8Cekj1.js,sha256=K1qf-eYyZ5eKD0dW1hULZe5SK8zsn0tr_HbvUueItX8,29590
32
+ spatialscape/_app/assets/blosc-DEOJs6D5.js,sha256=WK_5LTSrxQci60GRd06pq9ARX6hEfs0kkCuIDEGGuMo,603099
33
+ spatialscape/_app/assets/blosc-Df7TknHH.js,sha256=znsurYLuyNhY5ZtYiVoVm06F2emLZVt5Xyk4sGbGEw0,603115
34
+ spatialscape/_app/assets/chunk-INHXZS53-DAOLzMPX.js,sha256=eK-f7WqxPABozcGIuKFx0ybXjcFVY9sRdkIbQc76_r8,422
35
+ spatialscape/_app/assets/chunk-INHXZS53-DfBSTIxr.js,sha256=8B-3e6PgX5yK2HSSihnQRAFMoL6nzOhw_aVR7oeSSro,454
36
+ spatialscape/_app/assets/deflate-B2AbjNo_.js,sha256=reCDT2cAlS16SnlI9vxyytZFMqroFHaRbVP5C8mTmKU,218
37
+ spatialscape/_app/assets/expression-E359vKZW.js,sha256=22F9bTwAJW8Rxi7a4pkAMRYjXsRndxgC8JmBTOhE-BI,96648
38
+ spatialscape/_app/assets/get-attribute-from-layouts-Dd2gixpo.js,sha256=b-bTC36ISROsZjQACh7goYIY_QcZBvk9CeUr_0rjfaI,19267
39
+ spatialscape/_app/assets/index-BKPnXJSt.css,sha256=3V6PZhFpUAlsG7n40JKAN_fPFkSov_CpF14Ut8TJi6E,19502
40
+ spatialscape/_app/assets/index-Gxpj1e1n.js,sha256=r974r8rfdlbs0B2_rQ5lm_YE23RwXMj8_fdide7LzQo,1482098
41
+ spatialscape/_app/assets/jpeg-b0MwBR_9.js,sha256=aXO7QO-HTsQPEimOQVmmm3IG6BoGNfA0oSLGJrMDQ10,8750
42
+ spatialscape/_app/assets/lerc-ujbbHc0C.js,sha256=oLHcKH64wvX5l1-72OSh9AQYUSzccqQOyKfV5oyD_8I,67574
43
+ spatialscape/_app/assets/lz4-BFJkNc3o.js,sha256=hkio-tdkKMBLlm1GqpBFSD3US9LOYqz1FouLcfG7ZQg,36277
44
+ spatialscape/_app/assets/lz4-DNE3IovK.js,sha256=zpFKo_9yyGNUoZPyqY4haGu8WkgxhFEVQM4d4McFCXE,36277
45
+ spatialscape/_app/assets/lzw-DfRkoFB0.js,sha256=asUiIAhvpZZ-arOIEpkjl27PgKZt-NQ22PL45T6zUp4,1401
46
+ spatialscape/_app/assets/packbits-BDCGuk_Z.js,sha256=-Y8AefaY1u8ENm9dsSrO22ze4Qkz1peZwcd4WT11d-s,387
47
+ spatialscape/_app/assets/pako.esm-Ch1S5qKo.js,sha256=FJH3XTGtsUOclbHjl5sXgzinn8_IbqzZWpgUExKFefc,46654
48
+ spatialscape/_app/assets/raw-vNZ-QFXZ.js,sha256=dR8oyPe673x6k89ROPUhLQ4NtovvBHIlLjDdUbxNY2o,150
49
+ spatialscape/_app/assets/webgl-D2Fkccwd.js,sha256=pBvvVDVvtb53QS4DTkYC3x1pRyqOSj014oIW0LsCY00,18883
50
+ spatialscape/_app/assets/webimage-DDU0IC05.js,sha256=WzTH1ij9pijApCv3webKJENFq5FL9sT9WSOPlWnem3Q,713
51
+ spatialscape/_app/assets/worker-CUy87eWB.js,sha256=AbSNcRT9CAnl2EIMfSPSgyYIHjV55_HS6VTfd42s0sc,121308
52
+ spatialscape/_app/assets/zstd-CGD_7wnU.js,sha256=jwzWBB4xi-zOFPt3KBVe8XNndOAb0FlLCJPYU2sNlZM,747108
53
+ spatialscape/_app/assets/zstd-qXhZ_1DN.js,sha256=1IC12lrledtmXSo_FoVC0K8IST7pU1DepwGGlyuNlJc,747108
54
+ spatialscape-0.3.0.dist-info/METADATA,sha256=ihFKQJuZH0NdLFsZ2Kf2Ej0Xgcep-YBFfnWT3jV26ww,3184
55
+ spatialscape-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
56
+ spatialscape-0.3.0.dist-info/entry_points.txt,sha256=7p3iXfRSX7AJh6h8gYOSOcyHUBwOCW22VtwaL5LirtY,86
57
+ spatialscape-0.3.0.dist-info/licenses/LICENSE,sha256=kQ6fUkaCdrMJA__sulW822y8lCn20JzRUFrCoUxSxg8,1070
58
+ spatialscape-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ spatialscape = spatialscape.cli:main
3
+ sscape = spatialscape.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Totty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.