@jax-data-science/sci-image-visualizer 0.3.3 → 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 (91) hide show
  1. package/README.md +208 -4
  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/toolbar.component.mjs +30 -7
  41. package/esm2022/lib/visualization.module.mjs +23 -5
  42. package/esm2022/lib/visualizer.component.mjs +283 -22
  43. 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
  44. 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
  45. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
  46. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
  47. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15769 -8316
  48. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  49. package/fesm2022/tsne.worker.js +19091 -0
  50. package/index.d.ts +15 -0
  51. package/lib/contracts/display-types.d.ts +131 -0
  52. package/lib/contracts/image.contract.d.ts +7 -0
  53. package/lib/contracts/plot-type.d.ts +42 -2
  54. package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
  55. package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
  56. package/lib/contracts/visualizer.contract.d.ts +99 -1
  57. package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
  58. package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
  59. package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
  60. package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
  61. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
  62. package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
  63. package/lib/implementations/osd/osd-zoom.d.ts +19 -0
  64. package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
  65. package/lib/implementations/plotly/plotly.service.d.ts +9 -0
  66. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
  67. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
  68. package/lib/routing-visualizer.service.d.ts +45 -6
  69. package/lib/spatial/embedding-compute.d.ts +73 -0
  70. package/lib/spatial/gene-search.d.ts +48 -0
  71. package/lib/spatial/spatial-density.d.ts +73 -0
  72. package/lib/spatial/spatial-encoding.d.ts +155 -0
  73. package/lib/spatial/spatial-expression.d.ts +144 -0
  74. package/lib/spatial/spatial-framing.d.ts +37 -0
  75. package/lib/spatial/spatial-heatmap.d.ts +111 -0
  76. package/lib/spatial/spatial-hover.d.ts +49 -0
  77. package/lib/spatial/spatial-sections.d.ts +59 -0
  78. package/lib/spatial/spatial-selection.d.ts +113 -0
  79. package/lib/spatial/spatial-volume-image.d.ts +67 -0
  80. package/lib/spatial/tsne-worker.d.ts +7 -0
  81. package/lib/spatial/world-grid.d.ts +58 -0
  82. package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
  83. package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
  84. package/lib/store/spatial-selection.service.d.ts +27 -0
  85. package/lib/store/visualizer-store.service.d.ts +13 -0
  86. package/lib/toolbar/toolbar.component.d.ts +19 -3
  87. package/lib/visualization.module.d.ts +28 -24
  88. package/lib/visualizer.component.d.ts +73 -2
  89. package/package.json +6 -2
  90. package/src/lib/assets/spatial-omics-3d.svg +31 -0
  91. package/src/lib/assets/spatial-omics.svg +27 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Plotly traces for the 1-D distribution charts over spatial-omics values:
3
+ * histogram, violin and box.
4
+ *
5
+ * Pure, like `plotly-trace-builders.ts` — and deliberately SEPARATE from it,
6
+ * because that module's `TraceBuildInput` is image-matrix shaped (`frames`,
7
+ * `width`, `ratios`, `trueImageSize`) and cannot express "one value per
8
+ * observation". Forcing the two together would make both worse.
9
+ *
10
+ * Violin and box need no bundling work: `plotly.js-dist-min` is the full
11
+ * distribution and already carries both trace types.
12
+ */
13
+ export type OmicsChartKind = 'histogram' | 'violin' | 'box' | 'counts' | 'heatmap' | 'embedding';
14
+ /** Per-observation grouping for a violin/box, or an overlaid histogram. */
15
+ export interface OmicsGrouping {
16
+ codes: Uint16Array;
17
+ categories: string[];
18
+ /** `#rrggbb` per category, index-aligned — the same colours the map uses. */
19
+ colors: string[];
20
+ }
21
+ export interface OmicsTraceInput {
22
+ /** The values being charted (an annotation column or a gene vector). */
23
+ values: Float32Array;
24
+ /** Axis label for the value — the column or gene name. */
25
+ name: string;
26
+ group?: OmicsGrouping | null;
27
+ /** `1` marks a selected observation. When present, the charts narrow to it. */
28
+ selection?: Uint8Array | null;
29
+ /** Plot `log1p(value)` instead of the raw value. */
30
+ log?: boolean;
31
+ /** Colour for the ungrouped trace. */
32
+ color?: string;
33
+ }
34
+ /**
35
+ * Traces for one chart kind.
36
+ *
37
+ * - **histogram** — the full distribution, plus an overlaid *Selected* trace
38
+ * when a selection exists, so the two are directly comparable. That
39
+ * comparison is the whole point of linking the chart to the map.
40
+ * - **violin / box** — one per category when grouped, otherwise a single trace.
41
+ * These narrow TO the selection rather than overlaying it: a violin per
42
+ * category per selection state is unreadable.
43
+ */
44
+ export declare function buildOmicsTraces(kind: OmicsChartKind, input: OmicsTraceInput): unknown[];
45
+ /** Layout for a chart kind — axis titles reflect the log toggle. */
46
+ export declare function omicsLayout(kind: OmicsChartKind, input: OmicsTraceInput): unknown;
47
+ /** Whether a chart kind needs a categorical grouping to be worth showing. */
48
+ export declare function benefitsFromGrouping(kind: OmicsChartKind): boolean;
49
+ export interface OmicsCountInput {
50
+ /** The categorical being counted, with the map's own colours. */
51
+ group: OmicsGrouping;
52
+ /** `1` marks a selected observation; when present the bars show the selection
53
+ * against the total. */
54
+ selection?: Uint8Array | null;
55
+ /** Bars before the tail is aggregated. */
56
+ max?: number;
57
+ }
58
+ /** Per-category counts, biggest first, with the tail folded into one bar. */
59
+ export declare function countByCategory(input: OmicsCountInput): {
60
+ labels: string[];
61
+ colors: string[];
62
+ totals: number[];
63
+ selected: number[];
64
+ };
65
+ /**
66
+ * Bars of per-category cell counts — the distribution a CATEGORICAL colour
67
+ * source has.
68
+ *
69
+ * A histogram of a category code would be meaningless (the codes are labels, not
70
+ * magnitudes), but "how many cells per class" is a real question, and the same one
71
+ * the legend implies without answering. Horizontal, because taxonomy labels are
72
+ * long, and sorted, because rank is what the chart is read for.
73
+ */
74
+ export declare function buildCountTraces(input: OmicsCountInput): unknown[];
75
+ export declare function countsLayout(input: OmicsCountInput & {
76
+ name: string;
77
+ }): unknown;
78
+ /**
79
+ * Genes × groups mean expression, as a Plotly heatmap.
80
+ *
81
+ * The matrix itself is computed by the pure `heatmapMatrix` in
82
+ * `spatial/spatial-heatmap.ts`; this only shapes it for Plotly. Kept in the
83
+ * matrix's own row-major order and flipped once here, because Plotly draws
84
+ * `z[0]` at the BOTTOM of the y axis while the caller lists genes top-down.
85
+ *
86
+ * The colour scale is its own, NOT the map's `continuousColormap`. This is a
87
+ * different quantity — a z-scored mean per group, not a per-cell value — so
88
+ * sharing one scale would invite reading a heatmap cell as if it were a marker
89
+ * colour. Diverging and centred on zero, because after z-scoring the sign is
90
+ * the reading: above or below this gene's average across the groups.
91
+ */
92
+ /**
93
+ * An embedding scatter — a UMAP, t-SNE or PCA plane over the same observations.
94
+ *
95
+ * Answers a different question from the map: the map says WHERE a population sits in the tissue,
96
+ * the embedding says which populations there are and how close they are in expression. Read side
97
+ * by side, and selecting in one highlighting the other, is the whole point of having both.
98
+ */
99
+ export interface OmicsEmbeddingInput {
100
+ /** Embedding coordinates, index-aligned with the observations. */
101
+ x: Float32Array;
102
+ y: Float32Array;
103
+ /**
104
+ * Third dimension, when the embedding has one.
105
+ *
106
+ * Its presence is what switches the plot to a rotatable 3D scatter. A 3D embedding is
107
+ * always COMPUTED — every embedding published with a spatial dataset is 2D, because a
108
+ * UMAP exists to be looked at — so `derived` is expected alongside it.
109
+ */
110
+ z?: Float32Array;
111
+ /** Axis label stem, e.g. "UMAP" — the axes become "UMAP 1" / "UMAP 2". */
112
+ label: string;
113
+ /** Colour by a categorical column: per-observation codes plus the category names/colours. */
114
+ categories?: {
115
+ codes: Uint16Array;
116
+ names: readonly string[];
117
+ colors: readonly string[];
118
+ };
119
+ /** Marker diameter in px. */
120
+ size?: number;
121
+ /**
122
+ * Selection mask over the observations. When present, unselected points are dimmed rather than
123
+ * dropped: the shape of the whole embedding is the context that makes a selection legible, and
124
+ * removing it would leave a handful of dots floating in an empty plane.
125
+ */
126
+ selection?: Uint8Array;
127
+ /** True when the coordinates were computed here rather than published with the dataset. */
128
+ derived?: boolean;
129
+ /**
130
+ * Fraction of variance per axis, for a LINEAR embedding.
131
+ *
132
+ * Present for a PCA and absent for a UMAP, and that difference is the point of showing
133
+ * a PCA at all: its axes are ordered and each explains a measurable share of the
134
+ * variance, so "PC1 (23%)" says something a reader can act on. A UMAP's coordinates are
135
+ * an arbitrary output of an optimisation — unordered, unitless, and reproducible only
136
+ * up to a rotation — so there is nothing to label them with.
137
+ */
138
+ varianceRatio?: number[];
139
+ /**
140
+ * The view to keep across a redraw.
141
+ *
142
+ * `Plotly.react` resets the 3D scene camera and any zoomed 2D axis range unless the
143
+ * layout carries them, so recolouring or selecting would throw away an orientation the
144
+ * user had set up. Rotating a cloud to see a structure and losing it on the next click
145
+ * makes the plot unusable for the thing it is for.
146
+ *
147
+ * `camera` is a 3D scene camera; `ranges` are 2D axis ranges, and are only passed when
148
+ * the user has actually zoomed — freezing an autoranged axis would stop the plot
149
+ * re-fitting when the data changes.
150
+ */
151
+ view?: {
152
+ camera?: unknown;
153
+ ranges?: {
154
+ x: unknown;
155
+ y: unknown;
156
+ };
157
+ };
158
+ }
159
+ /**
160
+ * Past this many categories, one trace each stops being worth it.
161
+ *
162
+ * A trace per category buys a legend whose entries toggle — click one to isolate a population,
163
+ * which is how these plots are actually read. But Plotly holds per-trace state, and a legend of
164
+ * hundreds of entries is unreadable anyway (the ABC atlas has 338 subclasses), so beyond the cap
165
+ * everything goes into ONE trace with a per-point colour: no legend, but it still draws.
166
+ */
167
+ export declare const EMBEDDING_MAX_LEGEND_CATEGORIES = 40;
168
+ /**
169
+ * Categories past which the legend is not DRAWN, though the traces stay split.
170
+ *
171
+ * A legend is only worth its space if it fits. seqFISH's 22 cell types stack into one tall
172
+ * column in a docked panel, overlapping the axis title and running off the bottom — and
173
+ * the colours already match the map, whose own panel lists them, while hover names the
174
+ * category under the cursor. So past this the plot takes the whole panel and the legend
175
+ * is dropped rather than shown badly.
176
+ */
177
+ export declare const EMBEDDING_MAX_LEGEND_SHOWN = 10;
178
+ export declare function buildEmbeddingTraces(input: OmicsEmbeddingInput): unknown[];
179
+ export declare function embeddingLayout(input: OmicsEmbeddingInput): unknown;
180
+ export interface OmicsHeatmapInput {
181
+ rows: string[];
182
+ cols: string[];
183
+ /** `rows.length × cols.length`, row-major; `NaN` where nothing was measured. */
184
+ values: Float32Array;
185
+ /** Cells behind each column, for the hover. */
186
+ counts?: Uint32Array;
187
+ /** True when `values` are z-scores, which sets the scale and the labels. */
188
+ zScored?: boolean;
189
+ /** Column axis title — the grouping column's name, or "cell". */
190
+ groupLabel?: string;
191
+ }
192
+ export declare function buildHeatmapTraces(input: OmicsHeatmapInput): unknown[];
193
+ export declare function heatmapLayout(input: OmicsHeatmapInput): unknown;
@@ -62,6 +62,15 @@ export declare class PlotlyService implements IVisualizer {
62
62
  private fileName;
63
63
  private dragMode;
64
64
  private urls;
65
+ /**
66
+ * The image currently plotted.
67
+ *
68
+ * The `!` is a definite-assignment ASSERTION, not a guarantee: nothing sets this until
69
+ * an image loads, and a host can clear its image while these paths still run — a
70
+ * spatial-omics dataset that brings no tissue section does exactly that. So reads here
71
+ * are optional and assignments guarded; an unguarded one threw
72
+ * "can't access property isGrayscale" and aborted the load.
73
+ */
65
74
  imageInfo: IImageInfo;
66
75
  private plotUtilities;
67
76
  private plotType;
@@ -0,0 +1,123 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { Observable } from 'rxjs';
3
+ import { SpatialDataPort } from '../../contracts/ports/spatial-data.port';
4
+ import { SpatialColumn, SpatialDataset, SpatialEmbedding, SpatialPolygons } from '../../contracts/spatial-dataset.contract';
5
+ import { SpatialDatasetSummary, SpatialManifest } from './spatial-wire';
6
+ import * as i0 from "@angular/core";
7
+ /**
8
+ * Reference {@link SpatialDataPort} adapter for the wire format the bundled
9
+ * example server speaks (see `spatial-wire.ts` and
10
+ * `examples/tile-server/lib/spatial.mjs`).
11
+ *
12
+ * OPTIONAL AND UNBOUND BY DEFAULT — this service is `@Injectable()` without
13
+ * `providedIn`, so nothing gets it unless a host explicitly provides it:
14
+ *
15
+ * ```ts
16
+ * providers: [
17
+ * SpatialDataHttpService,
18
+ * { provide: SPATIAL_DATA_PORT, useExisting: SpatialDataHttpService },
19
+ * ]
20
+ * ```
21
+ *
22
+ * That mirrors `CellposeSegmenterService`: a concrete implementation the
23
+ * library ships for convenience, not a default the port inversion is quietly
24
+ * giving up on. A host with its own backend implements `SpatialDataPort`
25
+ * directly and never touches this class.
26
+ *
27
+ * Requests go through Angular's `HttpClient` (not `fetch`) so the host's
28
+ * interceptors — auth headers above all — apply, matching `tile-client.ts`.
29
+ */
30
+ /**
31
+ * A dataset selection that a newer selection (or a `clear`) overtook.
32
+ *
33
+ * Thrown rather than resolved so a caller cannot mistake it for "this dataset is
34
+ * now loaded": nothing was published, on purpose.
35
+ */
36
+ export declare class SupersededError extends Error {
37
+ constructor(id: string);
38
+ }
39
+ export declare class SpatialDataHttpService implements SpatialDataPort {
40
+ private http;
41
+ /** Server root, normalised to end with exactly one `/`. */
42
+ private baseUrl;
43
+ /** Per-request ceiling. Vectors are small; a hung request should not wedge
44
+ * the picker. */
45
+ private timeoutMs;
46
+ private readonly dataset$;
47
+ private manifest;
48
+ /** Bumped by every `selectDataset` and every `clear`, so a selection that
49
+ * finishes after a newer intent can tell and drop what it fetched. */
50
+ private selectToken;
51
+ /**
52
+ * Loaded vectors, keyed `column:<name>` / `feature:<name>`. Bounded LRU:
53
+ * colouring by cluster → by a gene → back by cluster must not refetch, but an
54
+ * afternoon of browsing genes must not grow without limit either (one vector
55
+ * is `4·N` bytes — 2 MB at 500k cells).
56
+ */
57
+ private readonly cache;
58
+ private static readonly CACHE_LIMIT;
59
+ /** In-flight requests, so double-clicking a gene issues one fetch. */
60
+ private readonly inFlight;
61
+ private volumePromise;
62
+ private polygonsPromise;
63
+ constructor(http: HttpClient);
64
+ /** Point the adapter at a server. Clears any loaded dataset. */
65
+ configure(options: {
66
+ baseUrl: string;
67
+ timeoutMs?: number;
68
+ }): void;
69
+ /** Datasets this server offers, for a host-side picker. */
70
+ listDatasets(): Promise<SpatialDatasetSummary[]>;
71
+ /**
72
+ * One dataset's manifest, WITHOUT selecting it. Lets a host inspect what a
73
+ * server offers — column and feature metadata, and which image a dataset
74
+ * registers onto — while building a picker, rather than having to load each
75
+ * dataset to find out.
76
+ */
77
+ readManifest(id: string): Promise<SpatialManifest>;
78
+ /**
79
+ * Load a dataset and publish it on {@link getDataset$}. Fetches the manifest,
80
+ * then the vectors that are always needed (coordinates, and ids/radius when
81
+ * the manifest says they exist) — nothing else.
82
+ */
83
+ selectDataset(id: string): Promise<SpatialDataset>;
84
+ /** Drop the loaded dataset and every cached vector. */
85
+ clear(): void;
86
+ getDataset$(): Observable<SpatialDataset | null>;
87
+ getColumn(name: string): Promise<SpatialColumn>;
88
+ getEmbedding(name: string): Promise<SpatialEmbedding>;
89
+ getFeatureVector(name: string): Promise<Float32Array>;
90
+ searchFeatures(query: string, limit?: number): Promise<string[]>;
91
+ getPolygons(): Promise<SpatialPolygons>;
92
+ /**
93
+ * The reference volume's voxels, single-flighted like the polygons.
94
+ *
95
+ * Validated against the manifest's declared dimensions before it is handed
96
+ * back: a short or long buffer read as a 3D texture does not fail, it shears
97
+ * the anatomy into diagonal streaks, and that is far harder to recognise as a
98
+ * transport problem than an error is.
99
+ */
100
+ getVolume(): Promise<Uint8Array>;
101
+ private requireManifest;
102
+ /**
103
+ * Cache + single-flight around a vector fetch.
104
+ *
105
+ * Keys are LOGICAL — `feature:GAPDH`, not `<dataset>/feature:GAPDH` — because `clear()`
106
+ * empties the maps on every switch, so two datasets never hold entries at once. What
107
+ * that does not cover is a request still in the air when the switch happens: `clear()`
108
+ * cannot cancel an HTTP call, so a vector fetched for dataset A can resolve after B has
109
+ * been selected and write itself into B's cache under a key B would read. Both datasets
110
+ * having a gene of that name is the common case, not the unlucky one, and the result is
111
+ * a plot of A's expression over B's cells — which looks like data, not like a fault.
112
+ *
113
+ * So the generation is checked after the await, and the in-flight slot is cleared only
114
+ * when the promise still sitting in it is this one: a late arrival must not evict the
115
+ * entry a newer request for the same key is already sharing.
116
+ */
117
+ private fetchCached;
118
+ private evict;
119
+ private getJson;
120
+ private getBinary;
121
+ static ɵfac: i0.ɵɵFactoryDeclaration<SpatialDataHttpService, never>;
122
+ static ɵprov: i0.ɵɵInjectableDeclaration<SpatialDataHttpService>;
123
+ }
@@ -0,0 +1,115 @@
1
+ import { SpatialColumn, SpatialColumnMeta, SpatialDataset, SpatialEmbedding, SpatialEmbeddingMeta, SpatialFeatureMeta, SpatialImageRef, SpatialObservations, SpatialPolygons, SpatialVolumeMeta } from '../../contracts/spatial-dataset.contract';
2
+ /**
3
+ * The wire format the bundled example server speaks, and its decoders.
4
+ *
5
+ * Pure by design — no Angular, no RxJS, no HTTP — so the format is testable on
6
+ * its own and reusable by any transport (`SpatialDataHttpService` is one).
7
+ * Mirrors the split in `plotly-trace-builders.ts`: the shape lives in a pure
8
+ * module, the service just moves bytes.
9
+ *
10
+ * WHY BINARY
11
+ * ----------
12
+ * Vectors are one `Float32Array`/`Uint16Array` per request, delivered as raw
13
+ * little-endian bytes. JSON would cost ~8–12× the size and a parse that
14
+ * allocates one JS number per value; a typed-array view over the response
15
+ * buffer is zero-copy and uploads to the GPU directly.
16
+ *
17
+ * ENDIANNESS
18
+ * ----------
19
+ * Little-endian, asserted at decode. Every platform a browser runs on today is
20
+ * little-endian, so a `DataView` byte-by-byte read would cost real time for a
21
+ * case that does not occur — but failing loudly beats rendering transposed
22
+ * garbage if it ever does.
23
+ *
24
+ * ENDPOINTS (base = the server root)
25
+ * ----------------------------------
26
+ * ```
27
+ * GET {base}/spatial/datasets -> { datasets: [{ id, name, count }] }
28
+ * GET {base}/spatial/{id}/manifest -> SpatialManifest
29
+ * GET {base}/spatial/{id}/coords -> f32[N] x, f32[N] y, f32[N] z?
30
+ * GET {base}/spatial/{id}/radius -> f32[N] (per-observation radius only)
31
+ * GET {base}/spatial/{id}/ids -> { ids: string[] } (hasIds only)
32
+ * GET {base}/spatial/{id}/column/{name} -> u16[N] codes | f32[N] values
33
+ * GET {base}/spatial/{id}/feature/{name} -> f32[N]
34
+ * GET {base}/spatial/{id}/features?q=&limit= -> { names: string[] }
35
+ * GET {base}/spatial/{id}/polygons -> u32 count, u32[count+1] offsets, f32[2*rings] coords
36
+ * ```
37
+ */
38
+ /** Bumped when the layout changes incompatibly; the client refuses anything else. */
39
+ export declare const SPATIAL_WIRE_VERSION = 1;
40
+ /** Per-observation marker radius: one shared value, or a served f32 vector. */
41
+ export type SpatialRadiusSpec = {
42
+ mode: 'uniform';
43
+ value: number;
44
+ } | {
45
+ mode: 'per-observation';
46
+ };
47
+ /** `GET /spatial/{id}/manifest` — everything cheap enough to send up front. */
48
+ export interface SpatialManifest {
49
+ version: number;
50
+ id: string;
51
+ name: string;
52
+ /** N — observation count; every served vector has exactly this length. */
53
+ count: number;
54
+ hasZ?: boolean;
55
+ hasIds?: boolean;
56
+ radius?: SpatialRadiusSpec;
57
+ columns: SpatialColumnMeta[];
58
+ features?: SpatialFeatureMeta;
59
+ polygons?: {
60
+ count: number;
61
+ };
62
+ imageRef?: SpatialImageRef;
63
+ volume?: SpatialVolumeMeta;
64
+ embeddings?: SpatialEmbeddingMeta[];
65
+ micronsPerUnit?: number;
66
+ }
67
+ /** `GET /spatial/datasets` */
68
+ export interface SpatialDatasetSummary {
69
+ id: string;
70
+ name: string;
71
+ count: number;
72
+ }
73
+ /** True on a little-endian host. */
74
+ export declare function isLittleEndian(): boolean;
75
+ /** Reject a manifest this client cannot read, with a message naming the fix. */
76
+ export declare function assertManifestVersion(manifest: SpatialManifest): void;
77
+ /**
78
+ * `GET /coords` → x/y(/z). One request rather than two or three: the vectors
79
+ * are always fetched together, and a single response keeps them consistent if
80
+ * the dataset changes underneath.
81
+ */
82
+ export declare function decodeCoords(buf: ArrayBuffer, count: number, hasZ?: boolean): Pick<SpatialObservations, 'x' | 'y' | 'z'>;
83
+ /** `GET /radius` → per-observation radii in image pixels. */
84
+ export declare function decodeRadius(buf: ArrayBuffer, count: number): Float32Array;
85
+ /** `GET /feature/{name}` → one gene's expression vector. */
86
+ /**
87
+ * `GET /spatial/{id}/embedding/{name}` — one plane per dimension, same layout as coords.
88
+ *
89
+ * Struct of arrays, not interleaved: every dimension is contiguous, so an axis can be handed to
90
+ * the renderer as a view with no copy and no stride.
91
+ */
92
+ export declare function decodeEmbedding(buf: ArrayBuffer, meta: SpatialEmbeddingMeta, count: number): SpatialEmbedding;
93
+ export declare function decodeFeatureVector(buf: ArrayBuffer, count: number): Float32Array;
94
+ /**
95
+ * `GET /column/{name}` → a loaded column, typed by its descriptor: `u16` codes
96
+ * for a categorical, `f32` values for a continuous. Codes outside the category
97
+ * list are normalised to {@link NO_CATEGORY} so a renderer can trust the
98
+ * invariant instead of bounds-checking every point.
99
+ */
100
+ export declare function decodeColumn(buf: ArrayBuffer, meta: SpatialColumnMeta, count: number): SpatialColumn;
101
+ /**
102
+ * `GET /polygons` → boundary rings. Layout is
103
+ * `[u32 count][u32 offsets × (count+1)][f32 coords × 2·offsets[count]]`; every
104
+ * field is 4-byte wide so each typed-array view stays aligned.
105
+ */
106
+ export declare function decodePolygons(buf: ArrayBuffer): SpatialPolygons;
107
+ /**
108
+ * Fold a manifest plus its fetched coordinate/id/radius vectors into the
109
+ * library-facing {@link SpatialDataset}. Column and feature *values* are not
110
+ * part of this — they stay lazy behind the port.
111
+ */
112
+ export declare function datasetFromManifest(manifest: SpatialManifest, coords: Pick<SpatialObservations, 'x' | 'y' | 'z'>, extras?: {
113
+ ids?: string[];
114
+ radius?: Float32Array;
115
+ }): SpatialDataset;
@@ -6,7 +6,10 @@ import { ClassPreset, PresetSet } from './models/class-preset';
6
6
  import { PlotlyService } from './implementations/plotly/plotly.service';
7
7
  import { OpenSeadragonVisualizerService } from './implementations/osd/openseadragon-visualizer.service';
8
8
  import { PlotType, PlotTypeDescriptor } from './contracts/plot-type';
9
- import { IVisualizer, PixelData, IntensityProfile, IIsosurfaceControls, IIntensityControls, ISurface3dControls } from './contracts/visualizer.contract';
9
+ import { IVisualizer, PixelData, IntensityProfile, IIsosurfaceControls, IIntensityControls, ISurface3dControls, ISpatialControls } from './contracts/visualizer.contract';
10
+ import { SpatialDataPort } from './contracts/ports/spatial-data.port';
11
+ import { RegionStore } from './store/region-store.service';
12
+ import { SpatialSelectionStore } from './store/spatial-selection.service';
10
13
  import { ViewerCapabilities } from './contracts/capabilities.contract';
11
14
  import { IRegionOverlay } from './contracts/region-overlay.contract';
12
15
  import { IRegionEditorApi } from './contracts/region-editor-api.contract';
@@ -21,6 +24,9 @@ export declare class RoutingVisualizerService implements IVisualizer, IRegionEdi
21
24
  private napari;
22
25
  private store;
23
26
  private config;
27
+ private regionStore;
28
+ private selectionStore;
29
+ private spatialData;
24
30
  private currentPlotType;
25
31
  private lastRendered;
26
32
  /** OSD failed for the *current* render cycle (e.g. a fresh file still caching
@@ -32,12 +38,18 @@ export declare class RoutingVisualizerService implements IVisualizer, IRegionEdi
32
38
  /** napari-js failed for the current render cycle → fall back to OSD (then Plotly).
33
39
  * Reset by reset() at the start of every cycle, like {@link osdFellBack}. */
34
40
  private napariFellBack;
35
- constructor(plotly: PlotlyService, osd: OpenSeadragonVisualizerService, napari: NapariVisualizerService, store: VisualizerStore, config: VizConfig);
41
+ constructor(plotly: PlotlyService, osd: OpenSeadragonVisualizerService, napari: NapariVisualizerService, store: VisualizerStore, config: VizConfig, regionStore: RegionStore, selectionStore: SpatialSelectionStore, spatialData?: SpatialDataPort | null);
36
42
  private isImageType;
37
- /** The 2D napari types (image + region-centroid scatter) — a 2D fallback chain (OSD, then Plotly). */
43
+ /** The 2D napari types (image, region-centroid scatter, spatial omics) — a 2D fallback chain
44
+ * (OSD, then Plotly).
45
+ *
46
+ * NOTE for SPATIAL_OMICS: only napari-js draws the observation markers. If it fails (no WebGPU,
47
+ * device loss) the fallback renders the tissue image ALONE — the spatial layer is absent, not
48
+ * degraded. That is deliberate for now: showing the slide beats showing nothing, and a Plotly
49
+ * `scattergl` spatial mode is the planned real fallback (see the plot-mode design doc). */
38
50
  private isNapariImageType;
39
- /** The 3D napari types (volume/isosurface, either resolution) — no 2D fallback exists (OSD is
40
- * image-only), so they fall straight to Plotly. */
51
+ /** The 3D napari types (volume/isosurface/surface/scatter, plus the 3D spatial cloud) — no 2D
52
+ * fallback exists (OSD is image-only), so they fall straight to Plotly. */
41
53
  private isNapari3dType;
42
54
  /**
43
55
  * Backend to attempt for the current plot type this render. The fallback chains below are
@@ -207,6 +219,33 @@ export declare class RoutingVisualizerService implements IVisualizer, IRegionEdi
207
219
  /** Intensity (line-ROI) controls — always Plotly's, since the line profiles
208
220
  * render their inset on Plotly regardless of which backend draws the image. */
209
221
  getIntensityControls(): IIntensityControls | null;
222
+ /**
223
+ * Spatial-omics controls, or null when no `SPATIAL_DATA_PORT` is bound.
224
+ *
225
+ * Implemented HERE rather than on a backend because the state is
226
+ * backend-neutral: the view state lives in the shared `VisualizerStore` (like
227
+ * the colormap), so the controls keep working across a plot-type switch and a
228
+ * host can drive them before any backend has mounted.
229
+ */
230
+ getSpatialControls(): ISpatialControls | null;
231
+ /**
232
+ * Whether `dataset` can answer this colour source.
233
+ *
234
+ * A column is checkable against the declared list. A gene is checkable only when
235
+ * the dataset inlines its feature names — a dataset too wide to inline them
236
+ * (typeahead-only) keeps the source, and a name that turns out not to exist
237
+ * surfaces as the chart's "could not be charted" rather than as a silent reset.
238
+ */
239
+ private canColorBy;
240
+ /** Memoised so consumers can hold the object across calls. */
241
+ private spatialControls;
242
+ /** Latest dataset, mirrored so `selectFromRegions()` can answer synchronously
243
+ * — it runs from a button click and must not await a round-trip. */
244
+ private currentSpatialDataset;
245
+ /** Displayed slice. Only a volume-backed spatial dataset reads it, to keep a
246
+ * region drawn over one section from selecting the whole depth behind it. */
247
+ private currentZIndex;
248
+ private spatialDatasetSub;
210
249
  /** Load pixel frames for intensity sampling when OpenSeadragon owns the image
211
250
  * (it doesn't feed Plotly's frame cache). No-op needed when Plotly renders. */
212
251
  ensureIntensitySampling(imageInfo: IImageInfo, zIndex: number): Promise<void>;
@@ -223,6 +262,6 @@ export declare class RoutingVisualizerService implements IVisualizer, IRegionEdi
223
262
  * ROI at display resolution (sampling always lives in Plotly). */
224
263
  refreshIntensitySamplingForRoi(x: number, y: number, width: number, height: number, zIndex: number): void;
225
264
  unsubscribe(): void;
226
- static ɵfac: i0.ɵɵFactoryDeclaration<RoutingVisualizerService, never>;
265
+ static ɵfac: i0.ɵɵFactoryDeclaration<RoutingVisualizerService, [null, null, null, null, null, null, null, { optional: true; }]>;
227
266
  static ɵprov: i0.ɵɵInjectableDeclaration<RoutingVisualizerService>;
228
267
  }
@@ -0,0 +1,73 @@
1
+ import { SpatialEmbedding, SpatialEmbeddingMeta } from '../contracts/spatial-dataset.contract';
2
+ /**
3
+ * Run a t-SNE in a Web Worker and report progress.
4
+ *
5
+ * The panel offers this for a dataset that publishes no t-SNE — a dropped-in `.h5ad`
6
+ * typically carries one UMAP and nothing else. The server reduces to PCA (it needs the
7
+ * whole expression matrix, which the browser should not download); this takes those
8
+ * scores and embeds them here, where the GPU is.
9
+ *
10
+ * A thin wrapper on purpose: no Angular, no store. It owns one worker and one run, so a
11
+ * component can hold it, show a progress bar, and cancel — and so the whole thing can be
12
+ * tested by substituting a worker.
13
+ */
14
+ export interface ComputeProgress {
15
+ /** 0..1, or null before the first report — a run starts with kNN and P, not iterations. */
16
+ fraction: number | null;
17
+ /** 'webgpu', 'wasm' or 'cpu' once known: it changes what a long wait means. */
18
+ backend: string | null;
19
+ message: string | null;
20
+ }
21
+ export interface ComputeRequest {
22
+ /** PCA scores, row-major `nObs x nDims`. */
23
+ scores: Float32Array;
24
+ nObs: number;
25
+ nDims: number;
26
+ dims: 2 | 3;
27
+ perplexity?: number;
28
+ iterations?: number;
29
+ seed?: number;
30
+ }
31
+ /** Factory, so a test can supply a worker without a bundler. */
32
+ export type WorkerFactory = () => Worker;
33
+ export declare class EmbeddingComputeRun {
34
+ private readonly factory?;
35
+ private worker;
36
+ private settled;
37
+ /**
38
+ * Settles a run that is still waiting, set for as long as one is.
39
+ *
40
+ * `terminate()` is called from outside — a dataset switch, a component being destroyed —
41
+ * and killing the worker on its own leaves the promise `run()` returned pending FOR
42
+ * EVER, because the settlement it was waiting for arrives as a worker message. The
43
+ * awaiting closure, and everything it captured, is retained with it. So termination
44
+ * settles the run itself.
45
+ */
46
+ private abandon;
47
+ /**
48
+ * `factory` is optional: without one the real worker module is imported on demand.
49
+ *
50
+ * Dynamically, and that is not laziness for its own sake — the module holds an
51
+ * `import.meta.url`, which ts-jest's CommonJS compile rejects outright, so a static
52
+ * import here would break every spec that touches this file.
53
+ */
54
+ constructor(factory?: WorkerFactory | undefined);
55
+ /**
56
+ * Start a run. Resolves with the coordinates, or null if cancelled.
57
+ *
58
+ * Rejects only on a real failure. Cancellation is a NULL rather than a rejection: a user
59
+ * pressing Cancel is not an error, and making callers tell the two apart in a catch
60
+ * block is how a cancelled run ends up reported as a crash.
61
+ */
62
+ run(request: ComputeRequest, meta: SpatialEmbeddingMeta, onProgress: (progress: ComputeProgress) => void): Promise<SpatialEmbedding | null>;
63
+ /** Ask the run to stop. It resolves with null shortly after. */
64
+ cancel(): void;
65
+ /**
66
+ * Stop immediately, abandoning any result. Safe to call twice.
67
+ *
68
+ * Unlike {@link cancel}, this does not wait for the worker to acknowledge — the run is
69
+ * settled here, with null, because nothing is left to deliver the answer.
70
+ */
71
+ terminate(): void;
72
+ get running(): boolean;
73
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Narrowing a gene list to the handful a picker should actually render.
3
+ *
4
+ * A targeted panel has a few hundred genes and can simply be listed. A
5
+ * whole-transcriptome assay has ~18k-31k, and handing that many options to a dropdown
6
+ * costs a visible freeze on every keystroke — the control rebuilds and re-filters the
7
+ * whole array, and the browser lays out what the virtual scroller has not yet clipped.
8
+ * Measured on the Visium H&E bundle: 18,078 names.
9
+ *
10
+ * The names themselves are cheap to hold — strings already parsed from the manifest — so
11
+ * this does not fetch anything. What it caps is how many OPTION OBJECTS are ever
12
+ * materialised for the view. The rest of the corpus stays a plain array, searched per
13
+ * keystroke, which is the work a dropdown was doing anyway; the saving is in not asking
14
+ * a component to render, diff and re-filter thousands of rows it will never show.
15
+ *
16
+ * Ranking is PREFIX FIRST, then substring. Typing "Mb" should surface `Mbp` above
17
+ * `Adam1b`, which a plain `includes` would not do. It deliberately mirrors the example
18
+ * server's `/spatial/:id/features?q=` (see `lib/spatial.mjs`) so a dataset that inlines
19
+ * its names and one that does not order their suggestions the same way — the picker must
20
+ * not appear to rank differently depending on how the data happens to be served.
21
+ *
22
+ * Pure — no Angular, no port, no DOM — like `spatial-heatmap.ts`, and tested the same way.
23
+ */
24
+ /**
25
+ * How many options a picker will materialise at once.
26
+ *
27
+ * Above the virtual-scroll threshold, so a capped list is still scrolled rather than laid
28
+ * out in full, and far below the point where rebuilding the array is perceptible.
29
+ */
30
+ export declare const GENE_OPTIONS_MAX = 500;
31
+ /**
32
+ * The best `limit` matches for `query`, prefix matches first.
33
+ *
34
+ * An empty query returns the head of the list rather than nothing: the dropdown opens
35
+ * before anything is typed, and an empty menu reads as "no genes" rather than "start
36
+ * typing". Matching is case-insensitive, because gene symbols are capitalised by
37
+ * convention and nobody types `Slc17a7` exactly.
38
+ */
39
+ export declare function searchGeneNames(names: readonly string[], query: string, limit?: number): string[];
40
+ /**
41
+ * The options a picker should show: the best matches, plus anything already chosen.
42
+ *
43
+ * A multi-select whose option list is capped must still carry its SELECTED values, or the
44
+ * control cannot resolve them — the chips lose their labels and, worse, a value absent
45
+ * from the options can be dropped from the model on the next change. Selections come
46
+ * first so they stay visible while a search narrows everything else.
47
+ */
48
+ export declare function geneOptionsFor(names: readonly string[], query: string, selected?: readonly string[], limit?: number): string[];