@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.
- package/README.md +268 -4
- package/esm2022/index.mjs +21 -1
- package/esm2022/lib/contracts/display-types.mjs +20 -2
- package/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +66 -0
- package/esm2022/lib/contracts/plot-type.mjs +41 -1
- package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
- package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
- package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
- package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +73 -8
- package/esm2022/lib/implementations/osd/osd-coordinate-transform.mjs +8 -1
- package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
- package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
- package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
- package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
- package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
- package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
- package/esm2022/lib/plot-mode/plot-mode-controller.mjs +329 -0
- package/esm2022/lib/routing-visualizer.service.mjs +228 -12
- package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
- package/esm2022/lib/spatial/gene-search.mjs +77 -0
- package/esm2022/lib/spatial/spatial-density.mjs +224 -0
- package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
- package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
- package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
- package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
- package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
- package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
- package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
- package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
- package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
- package/esm2022/lib/spatial/world-grid.mjs +77 -0
- package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
- package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
- package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
- package/esm2022/lib/toolbar/toolbar.component.mjs +61 -21
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +527 -44
- 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
- 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
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15971 -7808
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +16 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type-contribution.contract.d.ts +172 -0
- package/lib/contracts/plot-type.d.ts +52 -2
- package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
- package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
- package/lib/contracts/visualizer.contract.d.ts +104 -1
- package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
- package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
- package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
- package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +21 -0
- package/lib/implementations/osd/osd-coordinate-transform.d.ts +6 -0
- package/lib/implementations/osd/osd-zoom.d.ts +19 -0
- package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
- package/lib/implementations/plotly/plotly.service.d.ts +9 -0
- package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
- package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
- package/lib/plot-mode/plot-mode-controller.d.ts +130 -0
- package/lib/routing-visualizer.service.d.ts +49 -6
- package/lib/spatial/embedding-compute.d.ts +73 -0
- package/lib/spatial/gene-search.d.ts +48 -0
- package/lib/spatial/spatial-density.d.ts +73 -0
- package/lib/spatial/spatial-encoding.d.ts +155 -0
- package/lib/spatial/spatial-expression.d.ts +144 -0
- package/lib/spatial/spatial-framing.d.ts +37 -0
- package/lib/spatial/spatial-heatmap.d.ts +111 -0
- package/lib/spatial/spatial-hover.d.ts +49 -0
- package/lib/spatial/spatial-sections.d.ts +59 -0
- package/lib/spatial/spatial-selection.d.ts +113 -0
- package/lib/spatial/spatial-volume-image.d.ts +67 -0
- package/lib/spatial/tsne-worker.d.ts +7 -0
- package/lib/spatial/world-grid.d.ts +58 -0
- package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
- package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
- package/lib/store/spatial-selection.service.d.ts +27 -0
- package/lib/store/visualizer-store.service.d.ts +13 -0
- package/lib/toolbar/toolbar.component.d.ts +36 -8
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +139 -7
- package/package.json +6 -2
- package/src/lib/assets/spatial-omics-3d.svg +31 -0
- 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
|
+
}
|