@jax-data-science/sci-image-visualizer 0.3.2 → 0.4.0

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 (93) hide show
  1. package/README.md +215 -11
  2. package/esm2022/index.mjs +20 -1
  3. package/esm2022/lib/contracts/display-types.mjs +20 -2
  4. package/esm2022/lib/contracts/image.contract.mjs +1 -1
  5. package/esm2022/lib/contracts/plot-type.mjs +36 -1
  6. package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
  7. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
  8. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  9. package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
  10. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
  11. package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
  12. package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
  13. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
  14. package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
  15. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +5 -3
  16. package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
  17. package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
  18. package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
  19. package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
  20. package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
  21. package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
  22. package/esm2022/lib/routing-visualizer.service.mjs +223 -12
  23. package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
  24. package/esm2022/lib/spatial/gene-search.mjs +77 -0
  25. package/esm2022/lib/spatial/spatial-density.mjs +224 -0
  26. package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
  27. package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
  28. package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
  29. package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
  30. package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
  31. package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
  32. package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
  33. package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
  34. package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
  35. package/esm2022/lib/spatial/world-grid.mjs +77 -0
  36. package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
  37. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
  38. package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
  39. package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
  40. package/esm2022/lib/toolbar/segmentation/cellpose-segmenter.service.mjs +2 -2
  41. package/esm2022/lib/toolbar/segmentation/sam-model-registry.mjs +9 -9
  42. package/esm2022/lib/toolbar/toolbar.component.mjs +30 -7
  43. package/esm2022/lib/visualization.module.mjs +23 -5
  44. package/esm2022/lib/visualizer.component.mjs +283 -22
  45. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs} +6 -4
  46. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs.map} +1 -1
  47. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
  48. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
  49. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15282 -7829
  50. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  51. package/fesm2022/tsne.worker.js +19091 -0
  52. package/index.d.ts +15 -0
  53. package/lib/contracts/display-types.d.ts +131 -0
  54. package/lib/contracts/image.contract.d.ts +7 -0
  55. package/lib/contracts/plot-type.d.ts +42 -2
  56. package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
  57. package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
  58. package/lib/contracts/visualizer.contract.d.ts +99 -1
  59. package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
  60. package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
  61. package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
  62. package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
  63. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
  64. package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
  65. package/lib/implementations/osd/osd-zoom.d.ts +19 -0
  66. package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
  67. package/lib/implementations/plotly/plotly.service.d.ts +9 -0
  68. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
  69. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
  70. package/lib/routing-visualizer.service.d.ts +45 -6
  71. package/lib/spatial/embedding-compute.d.ts +73 -0
  72. package/lib/spatial/gene-search.d.ts +48 -0
  73. package/lib/spatial/spatial-density.d.ts +73 -0
  74. package/lib/spatial/spatial-encoding.d.ts +155 -0
  75. package/lib/spatial/spatial-expression.d.ts +144 -0
  76. package/lib/spatial/spatial-framing.d.ts +37 -0
  77. package/lib/spatial/spatial-heatmap.d.ts +111 -0
  78. package/lib/spatial/spatial-hover.d.ts +49 -0
  79. package/lib/spatial/spatial-sections.d.ts +59 -0
  80. package/lib/spatial/spatial-selection.d.ts +113 -0
  81. package/lib/spatial/spatial-volume-image.d.ts +67 -0
  82. package/lib/spatial/tsne-worker.d.ts +7 -0
  83. package/lib/spatial/world-grid.d.ts +58 -0
  84. package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
  85. package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
  86. package/lib/store/spatial-selection.service.d.ts +27 -0
  87. package/lib/store/visualizer-store.service.d.ts +13 -0
  88. package/lib/toolbar/toolbar.component.d.ts +19 -3
  89. package/lib/visualization.module.d.ts +28 -24
  90. package/lib/visualizer.component.d.ts +73 -2
  91. package/package.json +7 -3
  92. package/src/lib/assets/spatial-omics-3d.svg +31 -0
  93. package/src/lib/assets/spatial-omics.svg +27 -0
package/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export * from './lib/contracts/ports/image-state.port';
11
11
  export * from './lib/contracts/ports/tile-access.port';
12
12
  export * from './lib/contracts/ports/region-io.port';
13
13
  export * from './lib/contracts/ports/preferences.port';
14
+ export * from './lib/contracts/ports/spatial-data.port';
15
+ export * from './lib/contracts/spatial-dataset.contract';
14
16
  export * from './lib/contracts/channel-histogram-api.contract';
15
17
  export * from './lib/contracts/viz-config';
16
18
  export * from './lib/contracts/image.contract';
@@ -23,6 +25,14 @@ export * from './lib/contracts/display-types';
23
25
  export * from './lib/contracts/sam.contract';
24
26
  export * from './lib/contracts/cell-segmenter.contract';
25
27
  export { CellposeSegmenterService } from './lib/toolbar/segmentation/cellpose-segmenter.service';
28
+ export { SpatialDataHttpService, SupersededError } from './lib/implementations/spatial-data-http/spatial-data-http.service';
29
+ export { SPATIAL_WIRE_VERSION, isLittleEndian, assertManifestVersion, datasetFromManifest, decodeCoords, decodeRadius, decodeColumn, decodeFeatureVector, decodePolygons, } from './lib/implementations/spatial-data-http/spatial-wire';
30
+ export type { SpatialManifest, SpatialDatasetSummary, SpatialRadiusSpec, } from './lib/implementations/spatial-data-http/spatial-wire';
31
+ export { encodeCategorical, encodeContinuous, resolveCategoryColors, contrastWindow, markerDiameters, toRgbaTuples, lutFor, DEFAULT_CATEGORICAL_PALETTE, DEFAULT_MUTED_OPACITY, MISSING_COLOR, } from './lib/spatial/spatial-encoding';
32
+ export type { RGBA, CategoricalEncodingOptions, ContinuousEncodingOptions, } from './lib/spatial/spatial-encoding';
33
+ export { selectInRegions, selectByCategory, mutedFromSelection, pointInRing, emptySelection, countMask, maskToIndices, } from './lib/spatial/spatial-selection';
34
+ export type { SpatialSelectionMask } from './lib/spatial/spatial-selection';
35
+ export { SpatialSelectionStore } from './lib/store/spatial-selection.service';
26
36
  export * from './lib/contracts/instance-segmenter.contract';
27
37
  export * from './lib/contracts/semantic-segmenter.contract';
28
38
  export { ProcessingImage } from './lib/processing/processing-image';
@@ -40,3 +50,8 @@ export { VisualizerComponent } from './lib/visualizer.component';
40
50
  export { RegionEditorComponent } from './lib/region-editor/region-editor.component';
41
51
  export { HexColorPickerComponent } from './lib/hex-color-picker/hex-color-picker.component';
42
52
  export { ChannelHistogramComponent } from './lib/channel-histogram/channel-histogram.component';
53
+ export { SpatialControlsComponent, } from './lib/spatial-controls/spatial-controls.component';
54
+ export type { SpatialLegendEntry } from './lib/spatial-controls/spatial-controls.component';
55
+ export { SpatialChartsComponent } from './lib/spatial-controls/spatial-charts/spatial-charts.component';
56
+ export { buildOmicsTraces, omicsLayout, benefitsFromGrouping, } from './lib/implementations/plotly/omics-trace-builders';
57
+ export type { OmicsChartKind, OmicsGrouping, OmicsTraceInput, } from './lib/implementations/plotly/omics-trace-builders';
@@ -39,3 +39,134 @@ export interface IBrushOptions {
39
39
  /** Brush diameter in matrix (image) pixels. */
40
40
  size?: number;
41
41
  }
42
+ /** What drives point colour in the spatial-omics mode: an annotation column
43
+ * from the dataset, or one feature (gene) vector fetched on demand. */
44
+ export interface SpatialColorBy {
45
+ kind: 'column' | 'feature';
46
+ name: string;
47
+ }
48
+ /** Display state for the spatial-omics plot mode. Lives in `VisualizerStore`
49
+ * alongside the colormap/channel state so the renderer reacts to edits the
50
+ * same way it does for an image. */
51
+ export interface SpatialViewState {
52
+ /** Colour source; `null` renders every observation in one neutral colour
53
+ * (useful on its own — it shows where the tissue actually is). */
54
+ colorBy: SpatialColorBy | null;
55
+ /** Multiplier on the dataset's physical marker size. 1 = true size, which for
56
+ * a 55 µm Visium spot is correct but can be invisible when zoomed out. */
57
+ pointScale: number;
58
+ /** Alpha for points that are not muted. */
59
+ opacity: number;
60
+ /** Log-scale continuous values before mapping them onto the colormap. Seeded
61
+ * from the column's `logScaleHint` — count data needs it. */
62
+ logScale: boolean;
63
+ /** Percentile clip for the contrast window as `[lo, hi]` fractions, so a
64
+ * handful of saturated observations don't flatten the rest. */
65
+ percentileClip: [number, number];
66
+ /**
67
+ * Colormap for CONTINUOUS spatial colouring — a gene, a numeric column — as a
68
+ * {@link ColormapValue}, the same shape the display colormap carries: a scale
69
+ * NAME for the built-ins, or an inline `[stop, colour]` array for the rest.
70
+ * Null follows the display colormap.
71
+ *
72
+ * Its own setting because the display colormap belongs to the tissue image and
73
+ * is usually a grey ramp, which is the wrong thing to read a measurement
74
+ * through: grey over grey cannot be told apart from the anatomy. Following it
75
+ * (with a Viridis fallback for exactly that case) is a reasonable default and a
76
+ * poor ceiling, since which gradient reads best is a judgement about the data.
77
+ *
78
+ * One setting for the markers, the 2D gene map and the 3D gene map together, so
79
+ * the field under the cells and the cells over it cannot disagree about what a
80
+ * colour means — and the panel's own colour bar is built from it too.
81
+ */
82
+ continuousColormap: ColormapValue | null;
83
+ /**
84
+ * What the 3D scene draws. The three things in it — the reference volume, the
85
+ * observation cloud and the cluster density volumes — occupy the same space, so
86
+ * any two of them hide each other to some degree. These are independent because
87
+ * the useful views are the combinations: the volumes alone to read an estimated
88
+ * distribution, one section's cells over them to check that estimate against the
89
+ * measurement, the anatomy alone to find a landmark.
90
+ *
91
+ * Visibility only — the layers stay built, so toggling one back on is immediate
92
+ * and does not re-run a rasterisation. {@link densityVolume} is the exception and
93
+ * genuinely gates construction, because building six volumes is not free.
94
+ */
95
+ showVolume: boolean;
96
+ /**
97
+ * The reference volume's opacity.
98
+ *
99
+ * Its own, not {@link opacity} (which belongs to the markers): the volume is a
100
+ * BACKDROP, and reading the cloud or a density field through it means turning
101
+ * the anatomy down without touching the data drawn over it. Ranges lower than
102
+ * the markers' slider for the same reason — a faint anatomical hint is a useful
103
+ * setting for a backdrop and a useless one for the measurement.
104
+ */
105
+ volumeOpacity: number;
106
+ /** Draw the observation cloud (3D). */
107
+ showPoints: boolean;
108
+ /**
109
+ * Restrict the cloud to ONE imaged section, by index into the dataset's sampled
110
+ * sections; null draws every section.
111
+ *
112
+ * The whole cloud is a stack of discs that hides whatever is drawn between them.
113
+ * One section at a time is the view that answers "does the estimated field
114
+ * actually follow the cells I measured" — and it is only offered for data that
115
+ * really is sectioned (see `sampledSections`).
116
+ */
117
+ pointSection: number | null;
118
+ /**
119
+ * Draw each cluster as a translucent DENSITY volume alongside the 3D cloud.
120
+ *
121
+ * Serial sections hundreds of microns apart make a cloud hard to read as an
122
+ * anatomical distribution — the eye cannot integrate discs into a shape. A
123
+ * density field can be estimated between the imaged planes (a continuous
124
+ * estimate, unlike individual cells, which have no correspondence to
125
+ * interpolate along) and renders as a cloud that reads as an estimate.
126
+ */
127
+ densityVolume: boolean;
128
+ /** Multiplier on the default kernel bandwidth. Above 1 is smoother and less
129
+ * committal; below 1 exposes the individual sections. */
130
+ densitySmoothing: number;
131
+ /**
132
+ * Draw the active GENE as a continuous field under the observations (2D).
133
+ *
134
+ * Colouring the markers by a gene says which cells express it; it cannot say
135
+ * where, because the eye will not integrate thousands of dots into a territory.
136
+ * The field is an estimate — a kernel-weighted mean per cell, transparent where
137
+ * no cell was measured — and it is drawn beneath the cells, not instead of them,
138
+ * so the measurement stays on screen next to the interpolation.
139
+ *
140
+ * Ignored unless the colour source is a gene: there is nothing else to map.
141
+ */
142
+ geneMap: boolean;
143
+ /**
144
+ * Restrict the 3D gene map to ONE imaged section, by index into the dataset's
145
+ * sampled sections; null draws every section's sheet.
146
+ *
147
+ * Separate from {@link pointSection} so the combinations stay open: all the
148
+ * sheets with one section's cells over them is a useful view, and so is one
149
+ * sheet inside the whole cloud. Ignored while {@link geneMapVolume} is set —
150
+ * interpolating a single section along z would smear one slide through the
151
+ * specimen's whole depth and call it an estimate.
152
+ */
153
+ geneMapSection: number | null;
154
+ /**
155
+ * Smooth the per-section gene maps along z into a continuous VOLUME (3D).
156
+ *
157
+ * The sheets are what was measured — one field per imaged slide, with the gaps
158
+ * between them empty. This turns them into an estimate defined between the
159
+ * slides, the same move the cluster density volumes make and with the same
160
+ * caveat: a field may be interpolated between sections, individual cells may
161
+ * not, and the result is drawn as a translucent cloud so it cannot be mistaken
162
+ * for measurement.
163
+ */
164
+ geneMapVolume: boolean;
165
+ /** Multiplier on the gene map's kernel bandwidth. */
166
+ geneMapSmoothing: number;
167
+ /** The gene map's own opacity. Separate from {@link opacity}, which belongs to
168
+ * the markers: turning the cells down to read the field underneath must not
169
+ * turn the field down with them. */
170
+ geneMapOpacity: number;
171
+ }
172
+ export declare const DEFAULT_SPATIAL_VIEW: SpatialViewState;
@@ -31,6 +31,13 @@ export interface IImageMetadata {
31
31
  /** Physical pixel size in µm; null/undefined when the format reports none. */
32
32
  mppX?: number | null;
33
33
  mppY?: number | null;
34
+ /** Physical spacing BETWEEN slices in µm — the z counterpart of `mppX`/`mppY`,
35
+ * for a stack that knows it (a resampled volume, a confocal stack). It is what
36
+ * lets the 3D volume view render true anisotropy: with all three present the
37
+ * world box is the image's real physical extent, so 40 x 40 x 200 µm voxels
38
+ * read as a brain and not as a cube-aspect brick. Absent, the view falls back
39
+ * to its resolution-invariant reference box, which is shape-only. */
40
+ mppZ?: number | null;
34
41
  /** Per-channel name/color/LUT for multichannel (non-RGB) images, when known. */
35
42
  channelInfo?: IChannelInfo[] | null;
36
43
  }
@@ -31,7 +31,16 @@ export declare enum PlotType {
31
31
  /** WebGPU 2D scatter of region centroids (napari-js analog of Plotly SCATTER). */
32
32
  NAPARI_SCATTER = "napari-scatter",
33
33
  /** WebGPU 3D scatter of the downsampled voxel cloud (napari-js analog of Plotly SCATTER3D). */
34
- NAPARI_SCATTER3D = "napari-scatter3d"
34
+ NAPARI_SCATTER3D = "napari-scatter3d",
35
+ /** Spatial-omics observations (Visium spots, segmented cells) drawn over the tissue image and
36
+ * coloured by an annotation column or a gene. Needs a `SpatialDataset` on `SPATIAL_DATA_PORT`,
37
+ * so it is gated by `requiresSpatialData` and never appears for an image-only host. */
38
+ SPATIAL_OMICS = "spatial-omics",
39
+ /** Spatial-omics observations as a 3D point cloud, for assays whose observations carry a z —
40
+ * serial sections registered into a common frame (e.g. the Allen CCF), or a genuinely
41
+ * volumetric assay. Gated by `requiresSpatial3d`, a strictly narrower gate than
42
+ * {@link SPATIAL_OMICS}: a single-plane dataset has no z to render. */
43
+ SPATIAL_OMICS_3D = "spatial-omics-3d"
35
44
  }
36
45
  /** How many spatial dimensions a plot type renders in. */
37
46
  export type PlotDimensions = '2d' | '3d';
@@ -39,8 +48,11 @@ export type PlotDimensions = '2d' | '3d';
39
48
  * Where a plot type pulls its data from:
40
49
  * - `image` — the loaded pixel matrices / z-stack (HEATMAP, SURFACE, …)
41
50
  * - `regions` — the drawn/derived regions (SCATTER of region centroids, …)
51
+ * - `spatial` — a `SpatialDataset` supplied through `SPATIAL_DATA_PORT`: N
52
+ * observations (spots/cells) with categorical + continuous columns and a
53
+ * lazily-fetched feature matrix, drawn over the tissue image they came from.
42
54
  */
43
- export type PlotDataSource = 'image' | 'regions';
55
+ export type PlotDataSource = 'image' | 'regions' | 'spatial';
44
56
  /**
45
57
  * Backend-neutral metadata about a plot type. Drives UI affordances (which
46
58
  * types are 3D, which need an image stack, how to label them in a dropdown)
@@ -75,6 +87,17 @@ export interface PlotTypeDescriptor {
75
87
  * surface, intensity profile, isosurface) — only meaningful for grayscale.
76
88
  * Image/Heatmap render RGB fine; Scatter uses regions. */
77
89
  requiresGrayscale?: boolean;
90
+ /** True when the type renders a `SpatialDataset` (`source: 'spatial'`) and is
91
+ * therefore meaningless without one. The selector hides it until a dataset is
92
+ * published on `SPATIAL_DATA_PORT` — the same shape of gate as
93
+ * {@link requiresStack} for a z-stack, so a host with no spatial data never
94
+ * sees the mode. */
95
+ requiresSpatialData?: boolean;
96
+ /** True when the type needs observations with a z coordinate, not merely a
97
+ * `SpatialDataset`. Narrower than {@link requiresSpatialData}: most spatial
98
+ * assays are a single plane, so this hides the 3D mode unless the published
99
+ * dataset actually carries `observations.z`. */
100
+ requiresSpatial3d?: boolean;
78
101
  }
79
102
  /**
80
103
  * Single source of truth for plot-type metadata. LINE is intentionally absent —
@@ -95,6 +118,23 @@ export declare function isNapariSurface(type: PlotType): boolean;
95
118
  export declare function isNapariScatter(type: PlotType): boolean;
96
119
  /** napari-js 3D scatter (voxel point cloud). */
97
120
  export declare function isNapariScatter3d(type: PlotType): boolean;
121
+ /** Spatial-omics observations over the tissue image. */
122
+ export declare function isSpatialOmics(type: PlotType): boolean;
123
+ /** Spatial-omics observations as a 3D point cloud. */
124
+ export declare function isSpatialOmics3d(type: PlotType): boolean;
125
+ /**
126
+ * Whether the renderer, rather than the shell, should handle wheel zoom.
127
+ *
128
+ * The shell's wheel listener applies a FIXED step per event, which is far too sensitive for a
129
+ * renderer that reads the scroll delta: it works out at 30% per event against napari-js's 3%, and
130
+ * ignores the device normalization and easing the renderer does. So every plot type drawn by a
131
+ * renderer that owns the wheel has to opt out here.
132
+ *
133
+ * The image view always did. The spatial-omics modes did NOT, despite being drawn by the same
134
+ * napari-js viewer — the shell gated on an `isHeatmap` flag that defaults to true and only goes
135
+ * false for the 3D Plotly scenes, so it silently took the wheel for them too.
136
+ */
137
+ export declare function rendererOwnsWheel(type: PlotType): boolean;
98
138
  /** Any napari-js 3D plot type (volume, isosurface, surface, or 3D scatter). Resolution is a runtime
99
139
  * decimate factor (Full / ½ / ¼ / ⅛) — see the service's `resolutionScale`. */
100
140
  export declare function isNapari3d(type: PlotType): boolean;
@@ -0,0 +1,90 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { SpatialColumn, SpatialDataset, SpatialEmbedding, SpatialPolygons } from '../spatial-dataset.contract';
4
+ /**
5
+ * Spatial-omics data access, inverted as a port so the visualization library
6
+ * never learns how the data is stored or served — exactly like
7
+ * `TILE_ACCESS_PORT` for tiles and `REGION_IO_PORT` for annotations. The host
8
+ * supplies an adapter; the library consumes typed arrays.
9
+ *
10
+ * WHY A PORT AND NOT A READER
11
+ * ---------------------------
12
+ * The interchange format for this data (SpatialData Zarr v3 + AnnData
13
+ * conventions + GeoParquet shapes) needs three parsers, and its expression
14
+ * matrix is stored observation-major — so reading a single gene's column means
15
+ * touching every row. A server can pre-index that once; a browser cannot. So
16
+ * the library takes vectors it can render and leaves ingest to whoever owns the
17
+ * data. `SpatialDataHttpService` is a ready-made adapter for the wire format
18
+ * the bundled example server speaks; a host with its own backend implements
19
+ * this interface instead.
20
+ *
21
+ * LAZINESS IS THE POINT
22
+ * ---------------------
23
+ * `getDataset$()` yields only what is cheap to hold: coordinates plus column
24
+ * and feature *metadata*. Every accessor below fetches one vector at a time,
25
+ * for the one column or gene currently being displayed.
26
+ */
27
+ export interface SpatialDataPort {
28
+ /**
29
+ * The dataset currently being visualized, or null when none is selected.
30
+ * Emits again when the host switches datasets, so the view can rebuild.
31
+ */
32
+ getDataset$(): Observable<SpatialDataset | null>;
33
+ /**
34
+ * Values for one annotation column, index-aligned with the observations.
35
+ * The column must be one advertised in `SpatialDataset.columns`; reject
36
+ * (don't resolve empty) when it isn't, so a typo surfaces instead of
37
+ * rendering as "no data".
38
+ *
39
+ * Implementations should cache — colouring by cluster, then by a gene, then
40
+ * back by cluster must not refetch.
41
+ */
42
+ getColumn(name: string): Promise<SpatialColumn>;
43
+ /**
44
+ * One feature's (gene's) expression vector, index-aligned with the
45
+ * observations. Rejects for an unknown feature.
46
+ */
47
+ getFeatureVector(name: string): Promise<Float32Array>;
48
+ /**
49
+ * One embedding's coordinates, index-aligned with the observations.
50
+ *
51
+ * The name must be one advertised in `SpatialDataset.embeddings`; rejects otherwise, for the
52
+ * same reason `getColumn` does — a typo should surface rather than render as an empty plot.
53
+ *
54
+ * Optional: a host with no embeddings simply omits it, and the UI offers no embedding views.
55
+ */
56
+ getEmbedding?(name: string): Promise<SpatialEmbedding>;
57
+ /**
58
+ * Typeahead over feature names, for datasets too wide to inline the list
59
+ * (`SpatialFeatureMeta.names` absent). Optional: a host whose panel is small
60
+ * enough to inline every name need not implement it — the UI falls back to
61
+ * filtering `names` locally.
62
+ */
63
+ searchFeatures?(query: string, limit?: number): Promise<string[]>;
64
+ /**
65
+ * Per-observation boundary geometry, when the dataset advertises
66
+ * `polygons`. Optional — a spot-based assay (Visium) has no segmentation.
67
+ */
68
+ getPolygons?(): Promise<SpatialPolygons>;
69
+ /**
70
+ * The reference volume's voxels: a uint8 scalar field, x-fastest, of exactly
71
+ * `width * height * depth` bytes as the dataset's {@link SpatialVolumeMeta}
72
+ * declares.
73
+ *
74
+ * Optional, and only meaningful when the dataset advertises a volume. Separate
75
+ * from `getDataset$()` because it is megabytes: a host that never opens the 3D
76
+ * mode should never pay for it.
77
+ */
78
+ getVolume?(): Promise<Uint8Array>;
79
+ /**
80
+ * Server-side "which observations fall inside this polygon", for datasets
81
+ * too large to hit-test client-side. Optional: without it the library
82
+ * point-in-polygons the resident coordinates itself, which is fine into the
83
+ * 10^5 range. Coordinates are in the same space as the observations.
84
+ */
85
+ queryRoi?(polygon: {
86
+ x: number[];
87
+ y: number[];
88
+ }): Promise<Uint32Array>;
89
+ }
90
+ export declare const SPATIAL_DATA_PORT: InjectionToken<SpatialDataPort>;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Backend-neutral data model for a **spatial-omics dataset**: N observations
3
+ * (Visium spots, segmented cells, nuclei, …) positioned in the pixel space of a
4
+ * tissue image, each carrying categorical and continuous annotations, plus an
5
+ * optional feature (gene) matrix and optional per-observation boundaries.
6
+ *
7
+ * DESIGN
8
+ * ------
9
+ * 1. **Struct-of-arrays, not object-per-cell.** Real datasets run 10^3 (Visium)
10
+ * to 10^6 (Xenium/CosMx) observations. `{x, y}[]` at 500k cells is ~500k
11
+ * objects the GC has to walk; two `Float32Array`s are two allocations and
12
+ * upload to the GPU without a copy.
13
+ *
14
+ * 2. **Metadata is eager, values are lazy.** A dataset advertises *which*
15
+ * columns and features exist ({@link SpatialColumnMeta},
16
+ * {@link SpatialFeatureMeta}); the vectors themselves are fetched on demand
17
+ * through {@link SpatialDataPort}. A Visium table is ~30k genes wide — the
18
+ * dense matrix is ~800 MB — so "load the dataset" can never mean "load the
19
+ * matrix". Only the handful of columns actually being displayed is resident.
20
+ *
21
+ * 3. **Data stays data.** No I/O, no RxJS, no Angular in this file — the same
22
+ * rule `plotly-trace-builders.ts` follows. Fetching lives on the port.
23
+ *
24
+ * The shape mirrors how the field already stores this (SpatialData/AnnData:
25
+ * `obs` annotations, `var` features, `obsm/spatial` coordinates, shapes with a
26
+ * coordinate transform) so an adapter is a projection, not a translation.
27
+ */
28
+ /**
29
+ * Observation positions. Every array is length {@link count} and index-aligned:
30
+ * observation `i` is at `(x[i], y[i])`, and column/feature vectors index the
31
+ * same way. This shared index is the join key for the whole model.
32
+ */
33
+ export interface SpatialObservations {
34
+ /** N — number of observations. */
35
+ readonly count: number;
36
+ /** X in the reference image's pixel space (see {@link SpatialImageRef}). */
37
+ x: Float32Array;
38
+ /** Y in the reference image's pixel space. */
39
+ y: Float32Array;
40
+ /** Z, for serial sections or genuinely 3D assays. Absent for a single plane. */
41
+ z?: Float32Array;
42
+ /** Stable per-observation ids (barcodes, cell ids) for tooltips and export.
43
+ * Optional — omit for large datasets where the strings cost more than they
44
+ * are worth. */
45
+ ids?: string[];
46
+ /** Marker RADIUS in image pixels: one value shared by all observations (a
47
+ * Visium spot is a fixed 55 µm ⇒ `27.5 / mppX` px), or one per observation
48
+ * (segmented cells). Absent ⇒ the renderer picks a display default. */
49
+ radius?: Float32Array | number;
50
+ }
51
+ /** Fields common to every column descriptor. */
52
+ interface SpatialColumnMetaBase {
53
+ /** Column name as the user sees it (`leiden`, `total_counts`). */
54
+ name: string;
55
+ /** Longer human-readable description for tooltips/menus. */
56
+ description?: string;
57
+ }
58
+ /**
59
+ * A discrete annotation — cluster, cell type, region, sample. Rendered as a
60
+ * palette + legend; a legend click selects the category's observations.
61
+ */
62
+ export interface CategoricalColumnMeta extends SpatialColumnMetaBase {
63
+ kind: 'categorical';
64
+ /** Category labels; a value's label is `categories[codes[i]]`. */
65
+ categories: string[];
66
+ /** Authored display colours as `#rrggbb`, index-aligned with
67
+ * {@link categories}. Supply these to keep the viewer's palette identical to
68
+ * the figures the same analysis produced in R/Python; omit to let the viewer
69
+ * derive one. */
70
+ colors?: string[];
71
+ }
72
+ /**
73
+ * A continuous measurement — expression, QC metric, area, density. Rendered
74
+ * through the shared colormap/LUT + contrast window.
75
+ */
76
+ export interface ContinuousColumnMeta extends SpatialColumnMetaBase {
77
+ kind: 'continuous';
78
+ /** Unit for axis/tooltip labels (`counts`, `µm²`). */
79
+ unit?: string;
80
+ /** True when the column reads best log-scaled — count data almost always
81
+ * does, and a linear scale collapses it against a few bright outliers. */
82
+ logScaleHint?: boolean;
83
+ /** Observed extremes, when the producer knows them. Lets the UI seed a
84
+ * contrast window and axis range without first scanning the vector. */
85
+ min?: number;
86
+ max?: number;
87
+ }
88
+ export type SpatialColumnMeta = CategoricalColumnMeta | ContinuousColumnMeta;
89
+ /**
90
+ * Code meaning "this observation has no category" (unassigned / filtered out /
91
+ * not in tissue). Renderers should draw it in the muted background style rather
92
+ * than as a real category, and it must never index {@link
93
+ * CategoricalColumnMeta.categories}.
94
+ */
95
+ export declare const NO_CATEGORY = 65535;
96
+ /** A loaded categorical column: per-observation indices into `meta.categories`,
97
+ * or {@link NO_CATEGORY}. */
98
+ export interface CategoricalColumn {
99
+ meta: CategoricalColumnMeta;
100
+ /** Length = {@link SpatialObservations.count}. */
101
+ codes: Uint16Array;
102
+ }
103
+ /** A loaded continuous column. `NaN` marks a missing value. */
104
+ export interface ContinuousColumn {
105
+ meta: ContinuousColumnMeta;
106
+ /** Length = {@link SpatialObservations.count}. */
107
+ values: Float32Array;
108
+ }
109
+ export type SpatialColumn = CategoricalColumn | ContinuousColumn;
110
+ export declare function isCategoricalColumn(c: SpatialColumn): c is CategoricalColumn;
111
+ export declare function isContinuousColumn(c: SpatialColumn): c is ContinuousColumn;
112
+ /**
113
+ * The feature (gene) matrix, described but never delivered whole. Vectors come
114
+ * one at a time from {@link SpatialDataPort.getFeatureVector}.
115
+ */
116
+ export interface SpatialFeatureMeta {
117
+ /** Number of features available. */
118
+ count: number;
119
+ /**
120
+ * All feature names, when the producer chose to inline them. Present for
121
+ * targeted panels (Xenium/CosMx ship 300–5,000 genes ⇒ a few dozen KB) and
122
+ * absent for whole-transcriptome data (Visium ships ~31k ⇒ ~350 KB), where
123
+ * the UI should use {@link SpatialDataPort.searchFeatures} for typeahead
124
+ * instead of downloading the list. Consumers must handle both.
125
+ */
126
+ names?: string[];
127
+ /** What a feature value means (`log1p normalized`, `raw counts`). */
128
+ unit?: string;
129
+ /** True when feature vectors read best log-scaled (raw counts). */
130
+ logScaleHint?: boolean;
131
+ }
132
+ /**
133
+ * Per-observation boundaries (cell/nucleus segmentation, spot outlines) as flat
134
+ * rings — deliberately NOT GeoJSON objects, which cost one object + one array
135
+ * per cell and would dominate memory at 10^5 polygons.
136
+ *
137
+ * Ring `i` occupies `coords[2*offsets[i] .. 2*offsets[i+1])` as `x0,y0,x1,y1,…`
138
+ * and is implicitly closed. `offsets` has length `count + 1`.
139
+ */
140
+ export interface SpatialPolygons {
141
+ coords: Float32Array;
142
+ offsets: Uint32Array;
143
+ /** Number of rings — `offsets.length - 1`. */
144
+ readonly count: number;
145
+ }
146
+ /** Presence/size of the boundary geometry, without loading it. */
147
+ export interface SpatialPolygonsMeta {
148
+ count: number;
149
+ }
150
+ /**
151
+ * How observation coordinates relate to the tissue image they are drawn over.
152
+ * Applied as `world = coord * scale + translate`, matching the affine that
153
+ * SpatialData records per coordinate system — and the full-resolution world
154
+ * convention the napari-js backend already uses so pre-saved regions align on
155
+ * pyramidal slides.
156
+ */
157
+ export interface SpatialImageRef {
158
+ /** Host-defined id of the image these coordinates live in, so the host can
159
+ * resolve it to an `IImageInfo`. */
160
+ imageId?: string;
161
+ /** Data→world scale. Defaults to `[1, 1]` (coordinates already in
162
+ * full-resolution image pixels). */
163
+ scale?: [number, number];
164
+ /** Data→world translation, in the same units as {@link scale}'s output. */
165
+ translate?: [number, number];
166
+ /** Physical pixel size in µm — drives the scale bar and physical marker
167
+ * sizing (a 55 µm Visium spot). */
168
+ mppX?: number;
169
+ mppY?: number;
170
+ }
171
+ /**
172
+ * A spatial-omics dataset: everything cheap enough to hold resident. Column and
173
+ * feature *values*, and polygon *geometry*, are fetched through
174
+ * {@link SpatialDataPort} as they are displayed.
175
+ */
176
+ /**
177
+ * A precomputed low-dimensional embedding over the SAME observations — UMAP, t-SNE, PCA.
178
+ *
179
+ * Announced in the dataset and fetched on demand, exactly like a column or a gene: the
180
+ * coordinates are another per-observation vector, and a dataset may carry several.
181
+ */
182
+ export interface SpatialEmbeddingMeta {
183
+ /** Key as the source names it, e.g. `X_umap`. */
184
+ name: string;
185
+ /** Short label for menus; falls back to {@link name}. */
186
+ label?: string;
187
+ /** 2 for a plane, 3 for a cloud. */
188
+ dims: 2 | 3;
189
+ /**
190
+ * True when the coordinates were COMPUTED for this bundle rather than published with the
191
+ * dataset. Worth surfacing: a UMAP recomputed with different parameters is a different picture,
192
+ * and a reader comparing against a paper's figure needs to know which they are looking at.
193
+ */
194
+ derived?: boolean;
195
+ /**
196
+ * Fraction of total variance each axis accounts for, index-aligned with the dimensions.
197
+ *
198
+ * Only meaningful for a LINEAR embedding — PCA. Its axes are ordered, and each one explains a
199
+ * measurable share of the variance, which is the whole reason to look at a PCA rather than a
200
+ * UMAP: "PC1 (23%)" says something, where "UMAP 1" says nothing at all. A UMAP's coordinates
201
+ * are an arbitrary output of an optimisation with no variance to report, so this stays absent
202
+ * there rather than being faked.
203
+ */
204
+ varianceRatio?: number[];
205
+ /**
206
+ * How a derived embedding was computed, in a few words — e.g. "PCA(50) then perplexity 30,
207
+ * seed 0".
208
+ *
209
+ * Not decoration. A t-SNE or UMAP is stochastic, and its picture changes materially with
210
+ * perplexity or `n_neighbors`; two runs at different settings are different pictures of the same
211
+ * cells, and neither is more correct. Saying "computed here" without saying HOW leaves a reader
212
+ * unable to reproduce or compare it. Absent for a published embedding, whose parameters belong
213
+ * to whoever published it.
214
+ */
215
+ params?: string;
216
+ }
217
+ /**
218
+ * One embedding's coordinates.
219
+ *
220
+ * Deliberately shaped like {@link SpatialObservations}' `x`/`y`/`z` rather than an (N, D) matrix:
221
+ * the point of an embedding here is to be swapped in as the scatter's coordinate source, and
222
+ * matching that shape means the renderer, the hover hit-test and the selection code need no
223
+ * variant for it.
224
+ */
225
+ export interface SpatialEmbedding {
226
+ meta: SpatialEmbeddingMeta;
227
+ x: Float32Array;
228
+ y: Float32Array;
229
+ z?: Float32Array;
230
+ }
231
+ export interface SpatialDataset {
232
+ /** Stable id — the key the port's lazy accessors are scoped to. */
233
+ id: string;
234
+ /** Human-readable name for menus. */
235
+ name: string;
236
+ observations: SpatialObservations;
237
+ /** Every column available, whether or not its values are loaded. */
238
+ columns: SpatialColumnMeta[];
239
+ features?: SpatialFeatureMeta;
240
+ polygons?: SpatialPolygonsMeta;
241
+ imageRef?: SpatialImageRef;
242
+ /** Presence + geometry of a reference volume the observations sit inside. */
243
+ volume?: SpatialVolumeMeta;
244
+ /** Embeddings available for these observations, whose coordinates are fetched on demand. */
245
+ embeddings?: SpatialEmbeddingMeta[];
246
+ /**
247
+ * Microns per observation coordinate unit — what makes a scale bar possible.
248
+ *
249
+ * The 2D path gets this from `imageRef.mppX`, because there the coordinates are
250
+ * image pixels. A 3D cloud has no image, so it has to say for itself: `1` means
251
+ * the coordinates are already microns.
252
+ *
253
+ * Absent means the unit is UNKNOWN, and a renderer must then draw no scale bar
254
+ * rather than assume one. A bar labelled in microns over pixel-space
255
+ * coordinates is worse than no bar: it looks like a measurement.
256
+ */
257
+ micronsPerUnit?: number;
258
+ }
259
+ /**
260
+ * A 3D scalar reference volume registered to the observation coordinates — the
261
+ * anatomical backdrop for a point cloud (an atlas template, or an image z-stack).
262
+ *
263
+ * Geometry only; the voxels are fetched through
264
+ * {@link SpatialDataPort.getVolume} when something is going to draw them. A cloud
265
+ * without one still renders, just in empty space.
266
+ *
267
+ * The observations' coordinate frame must be the volume's own: voxel `(i, j, k)`
268
+ * covers `[i * voxelSize[0], (i + 1) * voxelSize[0])` on x, and so on. That means
269
+ * the volume's near corner sits at the coordinate origin, so a renderer can place
270
+ * the two together knowing nothing else.
271
+ */
272
+ export interface SpatialVolumeMeta {
273
+ width: number;
274
+ height: number;
275
+ depth: number;
276
+ /** World size of one voxel per axis, in the observations' units. */
277
+ voxelSize: [number, number, number];
278
+ }
279
+ /** Look a column's descriptor up by name. */
280
+ export declare function findColumnMeta(dataset: SpatialDataset, name: string): SpatialColumnMeta | undefined;
281
+ export {};