@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.
- package/README.md +208 -4
- package/esm2022/index.mjs +20 -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.mjs +36 -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 +5 -3
- 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/routing-visualizer.service.mjs +223 -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 +30 -7
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +283 -22
- 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 +15769 -8316
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +15 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type.d.ts +42 -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 +99 -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/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/routing-visualizer.service.d.ts +45 -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 +19 -3
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +73 -2
- 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;
|
|
@@ -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
|
|
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,
|
|
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,
|
|
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[];
|