@jax-data-science/sci-image-visualizer 0.3.3 → 0.5.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 (98) hide show
  1. package/README.md +268 -4
  2. package/esm2022/index.mjs +21 -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-contribution.contract.mjs +66 -0
  6. package/esm2022/lib/contracts/plot-type.mjs +41 -1
  7. package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
  8. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
  9. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  10. package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
  11. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
  12. package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
  13. package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
  14. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
  15. package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
  16. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +73 -8
  17. package/esm2022/lib/implementations/osd/osd-coordinate-transform.mjs +8 -1
  18. package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
  19. package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
  20. package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
  21. package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
  22. package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
  23. package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
  24. package/esm2022/lib/plot-mode/plot-mode-controller.mjs +329 -0
  25. package/esm2022/lib/routing-visualizer.service.mjs +228 -12
  26. package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
  27. package/esm2022/lib/spatial/gene-search.mjs +77 -0
  28. package/esm2022/lib/spatial/spatial-density.mjs +224 -0
  29. package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
  30. package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
  31. package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
  32. package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
  33. package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
  34. package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
  35. package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
  36. package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
  37. package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
  38. package/esm2022/lib/spatial/world-grid.mjs +77 -0
  39. package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
  40. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
  41. package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
  42. package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
  43. package/esm2022/lib/toolbar/toolbar.component.mjs +61 -21
  44. package/esm2022/lib/visualization.module.mjs +23 -5
  45. package/esm2022/lib/visualizer.component.mjs +527 -44
  46. 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
  47. 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
  48. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
  49. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
  50. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15971 -7808
  51. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  52. package/fesm2022/tsne.worker.js +19091 -0
  53. package/index.d.ts +16 -0
  54. package/lib/contracts/display-types.d.ts +131 -0
  55. package/lib/contracts/image.contract.d.ts +7 -0
  56. package/lib/contracts/plot-type-contribution.contract.d.ts +172 -0
  57. package/lib/contracts/plot-type.d.ts +52 -2
  58. package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
  59. package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
  60. package/lib/contracts/visualizer.contract.d.ts +104 -1
  61. package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
  62. package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
  63. package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
  64. package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
  65. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
  66. package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
  67. package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +21 -0
  68. package/lib/implementations/osd/osd-coordinate-transform.d.ts +6 -0
  69. package/lib/implementations/osd/osd-zoom.d.ts +19 -0
  70. package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
  71. package/lib/implementations/plotly/plotly.service.d.ts +9 -0
  72. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
  73. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
  74. package/lib/plot-mode/plot-mode-controller.d.ts +130 -0
  75. package/lib/routing-visualizer.service.d.ts +49 -6
  76. package/lib/spatial/embedding-compute.d.ts +73 -0
  77. package/lib/spatial/gene-search.d.ts +48 -0
  78. package/lib/spatial/spatial-density.d.ts +73 -0
  79. package/lib/spatial/spatial-encoding.d.ts +155 -0
  80. package/lib/spatial/spatial-expression.d.ts +144 -0
  81. package/lib/spatial/spatial-framing.d.ts +37 -0
  82. package/lib/spatial/spatial-heatmap.d.ts +111 -0
  83. package/lib/spatial/spatial-hover.d.ts +49 -0
  84. package/lib/spatial/spatial-sections.d.ts +59 -0
  85. package/lib/spatial/spatial-selection.d.ts +113 -0
  86. package/lib/spatial/spatial-volume-image.d.ts +67 -0
  87. package/lib/spatial/tsne-worker.d.ts +7 -0
  88. package/lib/spatial/world-grid.d.ts +58 -0
  89. package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
  90. package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
  91. package/lib/store/spatial-selection.service.d.ts +27 -0
  92. package/lib/store/visualizer-store.service.d.ts +13 -0
  93. package/lib/toolbar/toolbar.component.d.ts +36 -8
  94. package/lib/visualization.module.d.ts +28 -24
  95. package/lib/visualizer.component.d.ts +139 -7
  96. package/package.json +6 -2
  97. package/src/lib/assets/spatial-omics-3d.svg +31 -0
  98. 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;
@@ -0,0 +1,130 @@
1
+ import { PlotType, PlotTypeId } from '../contracts/plot-type';
2
+ import { ContributedPlotTypeDescriptor, PlotModeContext, PlotModeSession, PlotTypeContribution } from '../contracts/plot-type-contribution.contract';
3
+ /**
4
+ * Lifecycle of the contributed plot modes ({@link PlotTypeContribution}) for one
5
+ * visualizer instance. Package-internal: the visualizer component owns one and
6
+ * drives it from its render pipeline.
7
+ *
8
+ * Owns three things the component should not have to reason about:
9
+ * - which contributions are usable (validated and de-duplicated once);
10
+ * - the single live session, so `deactivate()` runs exactly once per session
11
+ * however many exits race each other (type change, image change, destroy,
12
+ * a superseded async activation);
13
+ * - isolation: nothing a contribution throws or rejects escapes this class.
14
+ * A failure is logged and reported through `onFailed`, and the component
15
+ * falls back to the mode's base type.
16
+ */
17
+ /** The live activation of a contributed mode, as the component renders it. */
18
+ export interface ActivePlotMode {
19
+ readonly contribution: PlotTypeContribution;
20
+ readonly ctx: PlotModeContext;
21
+ readonly session: PlotModeSession;
22
+ /** Host element handed to a `mount` panel, or null for a component / no panel. */
23
+ readonly panelHost: HTMLElement | null;
24
+ }
25
+ export interface PlotModeControllerHooks {
26
+ /** A session is live (and a `mount` panel, if any, has been mounted). */
27
+ onActivated(active: ActivePlotMode): void;
28
+ /** The live session is about to end. Called BEFORE any teardown, so a
29
+ * component panel can be destroyed while its session is still valid. */
30
+ onDeactivating(active: ActivePlotMode): void;
31
+ /** Activation failed (threw, rejected, never got a ready viewport, or its
32
+ * panel failed to mount). The component falls back to the base type. */
33
+ onFailed(contribution: PlotTypeContribution, error: unknown): void;
34
+ }
35
+ /** Minimal logger, injectable for tests. */
36
+ export interface PlotModeLogger {
37
+ warn(...args: unknown[]): void;
38
+ error(...args: unknown[]): void;
39
+ }
40
+ /** Base types a contributed mode can ride on in this version. */
41
+ export declare const SUPPORTED_CONTRIBUTION_BASE_TYPES: readonly PlotType[];
42
+ /**
43
+ * Validate what arrived on `PLOT_TYPE_CONTRIBUTIONS`. A malformed or clashing
44
+ * contribution is dropped with a warning rather than breaking the selector:
45
+ * - a nested array (`useValue: [a, b], multi: true`) is flattened one level;
46
+ * - `type` must be a non-empty string that is not a built-in `PlotType` value;
47
+ * - the first contribution with a given `type` wins;
48
+ * - `baseType` must be one of {@link SUPPORTED_CONTRIBUTION_BASE_TYPES};
49
+ * - `activate` must be a function.
50
+ */
51
+ export declare function normalizeContributions(raw: readonly unknown[] | null | undefined, log?: PlotModeLogger): PlotTypeContribution[];
52
+ export declare class PlotModeController {
53
+ private readonly hooks;
54
+ private readonly log;
55
+ readonly contributions: readonly PlotTypeContribution[];
56
+ private readonly byId;
57
+ private active;
58
+ private teardown;
59
+ /** Bumped by every activate/deactivate, so a pending async activation can
60
+ * tell it has been superseded. */
61
+ private generation;
62
+ private readyFrame;
63
+ /** Contributions whose last cleanup (panel teardown / `deactivate()`) threw or
64
+ * rejected. Their next automatic re-activation (base re-render, image switch)
65
+ * falls back to the base type instead; an explicit selection clears the mark. */
66
+ private readonly failedCleanup;
67
+ private readyCb;
68
+ constructor(raw: readonly unknown[] | null | undefined, hooks: PlotModeControllerHooks, log?: PlotModeLogger);
69
+ /** Descriptors of the usable contributions, in registration order. */
70
+ descriptors(): ContributedPlotTypeDescriptor[];
71
+ /** The contribution for a selector id, if it is a contributed mode. */
72
+ find(id: PlotTypeId | null | undefined): PlotTypeContribution | undefined;
73
+ /**
74
+ * The built-in type that renders `id`: a built-in type is itself, a
75
+ * contributed id is its `baseType`, and an id that is neither (a stale
76
+ * contributed id whose provider is gone) resolves to `PlotType.IMAGE`, the
77
+ * default view.
78
+ */
79
+ baseTypeOf(id: PlotTypeId | null | undefined): PlotType;
80
+ /** The live session, if any. */
81
+ get current(): ActivePlotMode | null;
82
+ /** Whether an activation is waiting on the contribution or the viewport. */
83
+ get pending(): boolean;
84
+ private pendingId;
85
+ /**
86
+ * Start a session for `contribution`. Ends any live one first. Resolves when
87
+ * the attempt has finished (successfully or not); it never rejects.
88
+ */
89
+ activate(contribution: PlotTypeContribution, ctx: PlotModeContext): Promise<void>;
90
+ /**
91
+ * End the live session, if any: the component panel is dropped (via
92
+ * `onDeactivating`), a `mount` panel's teardown runs, then
93
+ * `session.deactivate()`, then the mount host is removed. Each step is
94
+ * isolated. A pending activation is cancelled; if it resolves later its
95
+ * session is deactivated on arrival. Idempotent.
96
+ */
97
+ deactivate(): void;
98
+ /**
99
+ * Forget recorded cleanup failures. The host calls this when the USER picks a
100
+ * plot type: an explicit choice gets a fresh attempt, only automatic
101
+ * re-activations fall back.
102
+ */
103
+ clearCleanupFailures(): void;
104
+ private start;
105
+ /**
106
+ * The host could not make the live mode usable (its panel threw while being
107
+ * created or first rendered): end the session once, then fall back to its base.
108
+ * No-op when nothing is live.
109
+ */
110
+ failActive(err: unknown): void;
111
+ private fail;
112
+ private guard;
113
+ /** Like `guard`, for a contribution callback typed `void` that may still return a
114
+ * promise: a rejection is logged instead of escaping as an unhandled rejection. */
115
+ private guardLogged;
116
+ /** Run a live session's cleanup; a throw OR a rejection goes to `cleanupFailed`. */
117
+ private cleanup;
118
+ /**
119
+ * A contribution's cleanup failed. If that same contribution is live or starting
120
+ * again (a base re-render / image switch re-activated it — or, for an async
121
+ * rejection, already has), end it and fall back to its base type. Otherwise the
122
+ * user has moved on: remember it, so the next automatic re-activation falls back.
123
+ */
124
+ private cleanupFailed;
125
+ /** Call `cb(true)` once the viewport reports ready (now, or on a later
126
+ * animation frame), or `cb(false)` after {@link MAX_READY_FRAMES}. */
127
+ private whenReady;
128
+ /** Stop waiting for the viewport; the waiting activation resolves as superseded. */
129
+ private cancelReadyWait;
130
+ }