@jax-data-science/sci-image-visualizer 0.6.0 → 0.8.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 (34) hide show
  1. package/esm2022/lib/contracts/display-types.mjs +28 -1
  2. package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +1 -1
  3. package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -1
  4. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +3 -1
  5. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  6. package/esm2022/lib/implementations/napari-js/napari-loading-badge.mjs +73 -0
  7. package/esm2022/lib/implementations/napari-js/napari-navigator.mjs +195 -0
  8. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +10 -2
  9. package/esm2022/lib/implementations/napari-js/napari-spatial-tiles.mjs +1487 -0
  10. package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +6 -3
  11. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +219 -8
  12. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +8 -1
  13. package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +131 -2
  14. package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +77 -1
  15. package/esm2022/lib/routing-visualizer.service.mjs +14 -1
  16. package/esm2022/lib/spatial/spatial-tiles.mjs +649 -0
  17. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +848 -19
  18. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +3773 -78
  19. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  20. package/lib/contracts/display-types.d.ts +96 -1
  21. package/lib/contracts/plot-type-contribution.contract.d.ts +9 -0
  22. package/lib/contracts/ports/spatial-data.port.d.ts +70 -1
  23. package/lib/contracts/spatial-dataset.contract.d.ts +197 -0
  24. package/lib/contracts/visualizer.contract.d.ts +28 -1
  25. package/lib/implementations/napari-js/napari-loading-badge.d.ts +20 -0
  26. package/lib/implementations/napari-js/napari-navigator.d.ts +72 -0
  27. package/lib/implementations/napari-js/napari-region-overlay.d.ts +3 -0
  28. package/lib/implementations/napari-js/napari-spatial-tiles.d.ts +315 -0
  29. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +49 -2
  30. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +28 -2
  31. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +35 -1
  32. package/lib/spatial/spatial-tiles.d.ts +259 -0
  33. package/lib/spatial-controls/spatial-controls.component.d.ts +288 -3
  34. package/package.json +1 -1
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Level-of-detail planning for tiled spatial geometry — cell outlines and transcripts.
3
+ *
4
+ * The renderer knows its camera (a world-space centre, a zoom in canvas pixels per world
5
+ * unit, a canvas size); a tiled dataset knows its grids (one per level, in observation
6
+ * units). This module is the bridge: which level the zoom calls for, which tiles are on
7
+ * screen, how big a transcript marker should be. Everything here is pure, so the policy
8
+ * — the part that decides whether the view is fast and legible — is testable without a
9
+ * GPU.
10
+ *
11
+ * Observation units map onto world units through the dataset's `imageRef` affine
12
+ * (`world = data · scale + translate`), so "pixels per observation unit" is
13
+ * `zoom · scale`. For a Xenium bundle the data unit is the micron and the world unit the
14
+ * morphology pixel.
15
+ */
16
+ import type { SpatialBounds, SpatialImageRef, SpatialTileLevel, SpatialTranscriptTile } from '../contracts/spatial-dataset.contract';
17
+ import type { SpatialViewState } from '../contracts/display-types';
18
+ import type { SpatialDataset } from '../contracts/spatial-dataset.contract';
19
+ /** A rectangle in observation coordinates. */
20
+ export interface DataRect {
21
+ x0: number;
22
+ y0: number;
23
+ x1: number;
24
+ y1: number;
25
+ }
26
+ /** Tile address. */
27
+ export interface TileKey {
28
+ level: number;
29
+ gx: number;
30
+ gy: number;
31
+ }
32
+ /** Whether cell outlines are drawn: the user's choice, else on when the dataset has any. */
33
+ export declare function cellsShown(dataset: Pick<SpatialDataset, 'polygonTiles' | 'polygons'> | null, view: Pick<SpatialViewState, 'showCells'>): boolean;
34
+ /** A column holding a hand-curated cell-type annotation, by naming convention. */
35
+ export declare function isCuratedColumn(name: string): boolean;
36
+ /**
37
+ * The categorical column that names each cell's type: the view's choice when it is a
38
+ * column of this dataset, else the first categorical column that is not a curated
39
+ * annotation (a pipeline's clustering is the default; curation is opted into), else
40
+ * any categorical column.
41
+ */
42
+ export declare function cellTypeColumnFor(dataset: SpatialDataset, view: Pick<SpatialViewState, 'cellTypeColumn'>): string | null;
43
+ export declare function tileId(k: TileKey): string;
44
+ /** Canvas pixels per observation unit along x, for a camera zoom and a data→world affine. */
45
+ export declare function pixelsPerDataUnit(zoom: number, ref?: SpatialImageRef | null): number;
46
+ /**
47
+ * The part of observation space the camera shows, padded by `margin` (a fraction of the
48
+ * viewport) so tiles just off screen are already there when a pan brings them in.
49
+ */
50
+ export declare function visibleDataRect(center: readonly [number, number], zoom: number, canvasW: number, canvasH: number, ref?: SpatialImageRef | null, margin?: number): DataRect | null;
51
+ /**
52
+ * Tiles of one level intersecting `rect`, clipped to the dataset's `bounds` (so zooming
53
+ * out over empty canvas does not ask for tiles that cannot exist). Ordered from the
54
+ * rectangle's centre outward, so the middle of the screen fills in first.
55
+ */
56
+ export declare function tilesInRect(rect: DataRect, level: number, levels: readonly SpatialTileLevel[], bounds?: SpatialBounds | null, limit?: number): TileKey[];
57
+ /**
58
+ * Typical cell diameter, in observation units, used to judge how large cells appear.
59
+ * Derived from the per-observation radius when there is one; 12 µm otherwise.
60
+ */
61
+ export declare function typicalCellDiameter(radius: Float32Array | number | undefined, fallback?: number): number;
62
+ /**
63
+ * On-screen cell diameter (px) at or above which each polygon level is used. A level
64
+ * halves the vertices of the one below, so each should take over when the cell is about
65
+ * half as large on screen. Below the last finite threshold outlines are not drawn at
66
+ * all — a cell a few pixels wide is a dot, and the observation markers already draw it
67
+ * as one.
68
+ *
69
+ * Xenium's fourth level (3 vertices per cell) is deliberately never used: seen on
70
+ * screen it is a field of triangles, which reads as an artefact rather than as cells.
71
+ * The dots below 8 px say the same thing more honestly.
72
+ */
73
+ export declare const POLYGON_LEVEL_MIN_CELL_PX: number[];
74
+ /**
75
+ * Which polygon level suits this zoom, or -1 for "too small to outline".
76
+ * `detail` > 1 prefers finer levels (sharper, slower); < 1 coarser.
77
+ */
78
+ export declare function polygonLevelFor(pxPerUnit: number, cellDiameter: number, levelCount: number, detail?: number): number;
79
+ /**
80
+ * Which transcript level suits this zoom: the one whose tiles come out closest to
81
+ * `targetTilePx` on screen. Coarser levels aggregate more, so this keeps the number of
82
+ * markers per screen roughly constant however far out the camera is — the property that
83
+ * keeps the view fast.
84
+ */
85
+ export declare function transcriptLevelFor(pxPerUnit: number, levels: readonly SpatialTileLevel[], targetTilePx?: number): number;
86
+ /**
87
+ * The bin a selection of genes is grouped into at this zoom, in observation units, or null
88
+ * to draw every transcript. Bins follow the all-gene pyramid's ladder (`baseBin` × 2^m) so a
89
+ * gene selection and "all genes" group alike: the finest bin whose markers land at least
90
+ * `spacingPx` apart on screen. Once the finest bin is that wide, transcripts are drawn as
91
+ * themselves.
92
+ */
93
+ export declare function geneBinSize(pxPerUnit: number, baseBin: number, levels?: number, spacingPx?: number): number | null;
94
+ /** The entries of `t` inside `rect` (observation units). */
95
+ export declare function clipTranscripts(t: SpatialTranscriptTile, rect: DataRect): SpatialTranscriptTile;
96
+ /**
97
+ * Group transcripts into `bin`-wide squares of a grid anchored at the origin (so a pan does
98
+ * not move them), one entry per (group, square): `groupOf(gene slot)` says which group a gene
99
+ * is in — its cluster in the gene tree, or itself. Each entry sits at its transcripts'
100
+ * centroid, weighted by how many each stands for; it takes the gene and the cell holding most
101
+ * of them, each summed over all its transcripts in the entry. `group[i]` is entry i's group.
102
+ */
103
+ export declare function groupTranscripts(t: SpatialTranscriptTile, bin: number, groupOf?: (geneSlot: number) => number): {
104
+ tile: SpatialTranscriptTile;
105
+ group: Int32Array;
106
+ };
107
+ /**
108
+ * The colour of each gene-tree cluster of a selection, for colouring transcripts by cluster: a
109
+ * cluster named like a group of the cells' grouping takes that group's colour (its cells and its
110
+ * transcripts agree); the others take palette colours in tree order. Clusters are the tree's
111
+ * groups holding a selected gene, then each ungrouped selected gene on its own. Every gene of a
112
+ * cluster gets the same colour — the tree's swatches and the markers use this one map.
113
+ */
114
+ export declare function clusterColorMap(genes: readonly string[], groups: readonly {
115
+ name: string;
116
+ genes: readonly string[];
117
+ }[], cellColors: ReadonlyMap<string, string>, palette: readonly string[]): Map<string, string>;
118
+ /** The cluster a selected gene is in: its first gene-tree group holding it, else itself. */
119
+ export declare function clusterOfGene(gene: string, groups: readonly {
120
+ name: string;
121
+ genes: readonly string[];
122
+ }[]): string;
123
+ /** Smallest and largest transcript marker, in canvas pixels. */
124
+ export declare const TRANSCRIPT_MIN_PX = 4;
125
+ export declare const TRANSCRIPT_MAX_PX = 32;
126
+ /**
127
+ * Physical diameter a single transcript is drawn at, in µm — clearly larger than the
128
+ * imaged spot (~0.3 µm) so it stays legible, and small enough that neighbouring
129
+ * molecules inside one cell stay apart.
130
+ */
131
+ export declare const TRANSCRIPT_PHYSICAL_UM = 1.2;
132
+ /**
133
+ * Marker diameter in canvas pixels for an entry standing for `weight` transcripts.
134
+ *
135
+ * The convention spatial viewers share (deck.gl's `radiusMinPixels`/`radiusMaxPixels`,
136
+ * Xenium Explorer's transcript size): a PHYSICAL size, clamped to a screen-pixel range.
137
+ * Zoomed out, markers sit at the minimum and never vanish; zoomed in, they grow with the
138
+ * tissue instead of shrinking to specks next to cells hundreds of pixels wide. A fixed
139
+ * screen size does the opposite at high zoom, which is what made transcripts disappear
140
+ * at pixel-level zoom.
141
+ *
142
+ * Aggregates grow with the cube root of their count: area ∝ count would let one dense
143
+ * aggregate swallow its neighbours, the cube root keeps a 1000-transcript cluster clearly
144
+ * larger than a 10-transcript one without covering the tile.
145
+ *
146
+ * `pxPerMicron` is omitted for data with no known physical unit; the marker is then
147
+ * screen-sized only.
148
+ */
149
+ export declare function transcriptMarkerPx(weight: number, scale?: number, pxPerMicron?: number): number;
150
+ /** Marker shapes for the transcript "icon" mode. All star-convex, so a fan fills them. */
151
+ export declare const TRANSCRIPT_GLYPHS: readonly ["circle", "star", "triangle", "square", "diamond", "cross", "hexagon", "triangle-down", "pentagon", "x"];
152
+ export type TranscriptGlyph = typeof TRANSCRIPT_GLYPHS[number];
153
+ /** Unit outline of a glyph, radius ≈ 1, centred on the origin, as `[x0, y0, x1, y1, …]`. */
154
+ export declare function glyphOutline(glyph: TranscriptGlyph): Float32Array;
155
+ /** The glyph a gene gets by default: by its position in the selected list. */
156
+ export declare function defaultGlyphFor(slot: number): TranscriptGlyph;
157
+ /**
158
+ * Expand point markers into glyph rings for a shapes layer: entry `i` becomes a copy of
159
+ * `glyphs[gene[i]]` scaled to `radius[i]` and centred on `(x[i], y[i])`.
160
+ */
161
+ export declare function glyphRings(x: Float32Array, y: Float32Array, radius: Float32Array, glyphOf: (i: number) => Float32Array): {
162
+ coords: Float32Array;
163
+ offsets: Uint32Array;
164
+ };
165
+ /**
166
+ * A shapes layer colours through a colormap LUT, not per-shape RGBA. A categorical
167
+ * palette becomes a piecewise-constant colormap: category `c` of `n` owns the band
168
+ * `[c/n, (c+1)/n)`, and a shape takes the value at its band's centre, so the LUT's
169
+ * linear filtering never blends two categories.
170
+ *
171
+ * Returns the stops (linear 0..1 RGB) and a function mapping a category code to the
172
+ * value to hand the layer. `missing` is the colour of the extra band used for "no
173
+ * category".
174
+ */
175
+ export declare function discreteColormapStops(rgb: readonly (readonly [number, number, number])[], missing: readonly [number, number, number]): {
176
+ stops: {
177
+ t: number;
178
+ color: [number, number, number];
179
+ }[];
180
+ valueOf: (code: number) => number;
181
+ };
182
+ /**
183
+ * Separable Gaussian blur of a row-major raster, `sigma` in raster cells.
184
+ *
185
+ * A per-gene count raster at 10 µm is mostly zeros and ones — drawn as-is it is speckle,
186
+ * not a density. A σ of one to two cells (10–20 µm, about a cell diameter) turns it into
187
+ * a field that reads as territory while keeping structures a few cells wide.
188
+ */
189
+ export declare function smoothRaster(values: Float32Array, rows: number, cols: number, sigma: number): Float32Array;
190
+ /**
191
+ * Colour a density raster as RGBA bytes: log-scaled, windowed at a high percentile of the
192
+ * non-zero values, and faded in from transparent so empty tissue shows what is under it.
193
+ */
194
+ export declare function colorDensity(values: Float32Array, lut: readonly (readonly [number, number, number])[], opacity: number, { log, percentile }?: {
195
+ log?: boolean | undefined;
196
+ percentile?: number | undefined;
197
+ }): Uint8Array;
198
+ /** Smallest on-screen spacing between groups, in px — below it groups read as noise. */
199
+ export declare const GROUP_MIN_SPACING_PX = 14;
200
+ /** Area of `rect` inside `bounds` (the whole rect when there are none). */
201
+ export declare function visibleArea(rect: DataRect, bounds?: SpatialBounds | null): number;
202
+ export type AllGenesPlan = {
203
+ kind: 'individual';
204
+ } | {
205
+ kind: 'bins';
206
+ level: number;
207
+ } | {
208
+ kind: 'none';
209
+ };
210
+ /**
211
+ * How to draw every transcript in view without exceeding `budget` markers.
212
+ *
213
+ * - The transcripts themselves, once the estimated count in view fits the budget.
214
+ * - Otherwise the finest pyramid level whose bins are at least `minSpacingPx` apart on
215
+ * screen and number no more than the budget — groups big enough to read, few enough
216
+ * to draw.
217
+ *
218
+ * The count in view is estimated from the average density, which over- or under-shoots
219
+ * locally; the renderer still caps what it draws, so an estimate is enough to choose.
220
+ */
221
+ export declare function allGenesPlan(o: {
222
+ rect: DataRect;
223
+ bounds: SpatialBounds;
224
+ total: number;
225
+ pxPerUnit: number;
226
+ levels: readonly {
227
+ binSize: number;
228
+ }[];
229
+ budget: number;
230
+ canIndividual: boolean;
231
+ minSpacingPx?: number;
232
+ }): AllGenesPlan;
233
+ /**
234
+ * Diameter in px of a group of `count` transcripts in a bin `binPx` wide, where
235
+ * `refCount` is a typical busy bin in view (its 95th percentile).
236
+ *
237
+ * Area grows with the count — a bin with a quarter of the transcripts gets half the
238
+ * diameter — between a third of the bin (so a sparse bin is still visible) and a little
239
+ * over the full bin (so dense neighbours just touch rather than pile up).
240
+ */
241
+ export declare function groupedMarkerPx(count: number, refCount: number, binPx: number, scale?: number): number;
242
+ /** The `p` quantile (0..1) of a count vector — a sample of it for large vectors. */
243
+ export declare function quantileOf(values: ArrayLike<number>, p: number): number;
244
+ /** {@link tilesInRect} for a grid whose tile (0, 0) starts at `origin`. */
245
+ export declare function tilesInRectFrom(origin: readonly [number, number], rect: DataRect, level: number, levels: readonly SpatialTileLevel[], bounds?: SpatialBounds | null, limit?: number): TileKey[];
246
+ /** A density window from the non-empty bins: 1st to 99th percentile. */
247
+ export declare function densityAutoRange(values: ArrayLike<number>): [number, number];
248
+ /**
249
+ * Colour density bins through `lut` over the window `[lo, hi]` — Xenium Explorer's
250
+ * "scale threshold". Bins at or below `lo` are transparent, so empty tissue shows the
251
+ * image under it; bins at or above `hi` saturate.
252
+ */
253
+ export declare function colorDensityWindow(values: ArrayLike<number>, lut: readonly (readonly [number, number, number])[], opacity: number, lo: number, hi: number): Uint8Array;
254
+ /**
255
+ * Inferno (matplotlib), as an inline `[stop, colour]` scale: the density map's default,
256
+ * as in Xenium Explorer. Inline because the colormap tree's `INFERNO_LUT` key is resolved
257
+ * at runtime by the store, which the renderer does not see.
258
+ */
259
+ export declare const INFERNO_SCALE: [number, string][];
@@ -1,7 +1,7 @@
1
- import { EventEmitter, OnDestroy, OnInit } from '@angular/core';
1
+ import { EventEmitter, NgZone, OnDestroy, OnInit } from '@angular/core';
2
2
  import { IVisualizer, ISpatialControls } from '../contracts/visualizer.contract';
3
3
  import { SpatialDataset } from '../contracts/spatial-dataset.contract';
4
- import { ColormapNode, SpatialViewState } from '../contracts/display-types';
4
+ import { ColormapNode, SpatialViewState, TranscriptGlyphName } from '../contracts/display-types';
5
5
  import { SpatialSelectionMask } from '../spatial/spatial-selection';
6
6
  import * as i0 from "@angular/core";
7
7
  /** One legend row for a categorical colouring. */
@@ -9,6 +9,14 @@ export interface SpatialLegendEntry {
9
9
  label: string;
10
10
  color: string;
11
11
  }
12
+ /**
13
+ * `group,gene` rows (CSV or TSV, header optional) → gene groups, in file order.
14
+ * Exported for tests.
15
+ */
16
+ export declare function parseGeneGroups(text: string): {
17
+ name: string;
18
+ genes: string[];
19
+ }[];
12
20
  /**
13
21
  * Spatial-omics controls: a non-modal, resizable, draggable dialog for choosing
14
22
  * what the observation markers are coloured by, and how they are drawn.
@@ -25,6 +33,7 @@ export interface SpatialLegendEntry {
25
33
  */
26
34
  export declare class SpatialControlsComponent implements OnInit, OnDestroy {
27
35
  private readonly viz;
36
+ private readonly zone;
28
37
  /** The embedded chart, so a dialog resize can re-fit it. */
29
38
  private charts?;
30
39
  /**
@@ -130,7 +139,7 @@ export declare class SpatialControlsComponent implements OnInit, OnDestroy {
130
139
  private geneSearchToken;
131
140
  private keyToken;
132
141
  private readonly subs;
133
- constructor(viz: IVisualizer);
142
+ constructor(viz: IVisualizer, zone: NgZone);
134
143
  ngOnInit(): void;
135
144
  ngOnDestroy(): void;
136
145
  onVisibleChange(value: boolean): void;
@@ -145,6 +154,23 @@ export declare class SpatialControlsComponent implements OnInit, OnDestroy {
145
154
  * hop further away.
146
155
  */
147
156
  onGeneFilter(query: string): Promise<void>;
157
+ /**
158
+ * Opening either gene dropdown — "Colour by gene" or the transcript genes. Both share
159
+ * one list and one search, so each opens on the head of the full list rather than on
160
+ * whatever the other was last filtered to.
161
+ *
162
+ * A whole-transcriptome dataset does not inline its ~30k names; they are fetched here,
163
+ * once, on first open — the list is then resident and every keystroke filters locally,
164
+ * exactly as for a targeted panel. Until it arrives the dropdown falls back to asking
165
+ * the server per keystroke.
166
+ */
167
+ ensureGeneList(): Promise<void>;
168
+ /** Most names fetched for the lazy list — well above any panel, whole-transcriptome included. */
169
+ private static readonly GENE_LIST_MAX;
170
+ private geneListLoading;
171
+ /** Options for `names`, plus the genes already chosen (a multi-select shows a chip only
172
+ * for a value it can find among its options). */
173
+ private withSelected;
148
174
  /** A gene was picked; it supersedes any column selection. */
149
175
  onGene(name: string | null): void;
150
176
  /** Select every observation inside the drawn regions (their union). */
@@ -221,6 +247,265 @@ export declare class SpatialControlsComponent implements OnInit, OnDestroy {
221
247
  /** Mirrors the renderer's cap, for the note only. */
222
248
  private static readonly DENSITY_MAX_CLUSTERS;
223
249
  onClip(value: [number, number]): void;
250
+ readonly cellDrawOptions: {
251
+ label: string;
252
+ value: string;
253
+ }[];
254
+ readonly transcriptColorOptions: {
255
+ label: string;
256
+ value: string;
257
+ }[];
258
+ readonly glyphOptions: {
259
+ label: string;
260
+ value: TranscriptGlyphName;
261
+ points: string;
262
+ }[];
263
+ readonly budgetOptions: {
264
+ label: string;
265
+ value: number;
266
+ }[];
267
+ /** Boundaries on offer: tiled (level-of-detail) or whole-dataset rings. */
268
+ get hasCells(): boolean;
269
+ get hasTranscripts(): boolean;
270
+ /** Whether outlines are on — the explicit choice, or automatically for data that has them. */
271
+ get cellsOn(): boolean;
272
+ get activeCellSet(): string | null;
273
+ /**
274
+ * Option lists for the cell/transcript controls. Built once per dataset rather than in
275
+ * getters: a getter returns a fresh array on every change-detection pass, and PrimeNG
276
+ * re-renders its buttons whenever the array identity changes — which made them
277
+ * impossible to click.
278
+ */
279
+ cellSetOptions: {
280
+ label: string;
281
+ value: string;
282
+ }[];
283
+ transcriptModeOptions: {
284
+ label: string;
285
+ value: SpatialViewState['transcriptMode'];
286
+ }[];
287
+ private buildTileOptions;
288
+ /** Cell colour modes on offer (Xenium Explorer's "Cell Color"). */
289
+ cellColorOptions: {
290
+ label: string;
291
+ value: SpatialViewState['cellColorMode'];
292
+ }[];
293
+ /**
294
+ * The group picker: categorical columns under their section heading, a family of
295
+ * variants (k-means at k = 2…10) listed once. Values are a column name, or
296
+ * `family:<id>` for a family.
297
+ */
298
+ groupOptions: {
299
+ label: string;
300
+ items: {
301
+ label: string;
302
+ value: string;
303
+ }[];
304
+ }[];
305
+ /** Variants of the active family (k = 2…10), when a family is active. */
306
+ groupVariantOptions: {
307
+ label: string;
308
+ value: string;
309
+ }[];
310
+ /** The last variant chosen per family, so switching away and back keeps k. */
311
+ private familyChoice;
312
+ private buildGroupOptions;
313
+ /** The picker's value for the active group column. */
314
+ get activeGroupEntry(): string | null;
315
+ private refreshVariants;
316
+ onGroupEntry(value: string): void;
317
+ onGroupVariant(name: string): void;
318
+ /** The active grouping's categories with colours and cell counts, largest first. */
319
+ groupRows: {
320
+ label: string;
321
+ color: string;
322
+ count: number;
323
+ }[];
324
+ groupTotal: number;
325
+ groupsExpanded: boolean;
326
+ private groupRowsFor;
327
+ groupImportError: string | null;
328
+ groupImporting: boolean;
329
+ private refreshGroups;
330
+ isGroupShown(label: string): boolean;
331
+ get allGroupsShown(): boolean;
332
+ onGroupShown(label: string, on: boolean): void;
333
+ onAllGroupsShown(on: boolean): void;
334
+ get canImportGroups(): boolean;
335
+ /** '+': a CSV/TSV of `cell_id` and group, named after the file. */
336
+ onImportGroupsFile(input: HTMLInputElement): Promise<void>;
337
+ get activeCellTypeColumn(): string | null;
338
+ onShowCells(on: boolean): void;
339
+ onCellSet(set: string): void;
340
+ onCellDraw(draw: SpatialViewState['cellDraw']): void;
341
+ onCellOpacity(value: number | undefined): void;
342
+ onCellTypeColumn(name: string | null): void;
343
+ onCellColorMode(mode: SpatialViewState['cellColorMode']): void;
344
+ onCellColorGene(gene: string | null): void;
345
+ onCellSingleColor(hex: string): void;
346
+ /** Fill opacity as a 0–100 number, for the box beside the slider. */
347
+ onCellOpacityPercent(v: number | null): void;
348
+ onShowImage(on: boolean): void;
349
+ onShowAnnotations(on: boolean): void;
350
+ /** Which collapsible sections are open. Per dialog instance, not persisted. */
351
+ open: Record<'images' | 'cells' | 'transcripts' | 'annotations' | 'observations', boolean>;
352
+ toggleSection(name: keyof SpatialControlsComponent['open']): void;
353
+ /** The mode the section header's switch turns back on. */
354
+ private lastTranscriptMode;
355
+ onTranscriptsOn(on: boolean): void;
356
+ /** Estimated transcripts in view, against the budget — Explorer's points bar. */
357
+ estimate: {
358
+ points: number;
359
+ max: number;
360
+ } | null;
361
+ /** The density window in use and its densest bin, for the threshold control. */
362
+ densityStats: {
363
+ lo: number;
364
+ hi: number;
365
+ max: number;
366
+ } | null;
367
+ get estimatePercent(): number;
368
+ get estimateOverMax(): boolean;
369
+ editingMax: boolean;
370
+ /** The selected genes as Explorer's tree: named groups, then the ungrouped ones. */
371
+ /** Rebuilt when the view changes — never per change-detection pass (see buildTileOptions). */
372
+ geneTree: {
373
+ name: string | null;
374
+ genes: string[];
375
+ }[];
376
+ geneMenu: {
377
+ label: string;
378
+ icon: string;
379
+ command: () => void;
380
+ disabled?: boolean;
381
+ }[];
382
+ private buildGeneTree;
383
+ /** Real genes in the panel — the tree's denominator. */
384
+ get geneTotal(): number;
385
+ collapsedGeneGroups: Set<string>;
386
+ geneTreeOpen: boolean;
387
+ toggleGeneGroup(name: string): void;
388
+ isGeneShown(gene: string): boolean;
389
+ areGenesShown(genes: string[]): boolean;
390
+ /** The eye toggle: hide or show genes without removing them from the selection. */
391
+ onGenesShown(genes: string[], on: boolean): void;
392
+ onGeneColor(gene: string, hex: string): void;
393
+ /** Swatches in the picker: the categorical palette genes are coloured from by default. */
394
+ readonly colorPresets: string[];
395
+ /** The gene the open picker edits. */
396
+ styleGene: string | null;
397
+ openGeneStyle(event: Event, gene: string, panel: {
398
+ toggle(e: Event): void;
399
+ hide(): void;
400
+ }): void;
401
+ /** The icon the open picker shows as chosen. */
402
+ get styleGlyph(): TranscriptGlyphName | null;
403
+ glyphPoints(glyph: TranscriptGlyphName): string;
404
+ /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
405
+ onGeneHex(gene: string, text: string): void;
406
+ /** Back to the gene's default icon and colour (by its position in the list). */
407
+ resetGeneStyle(gene: string): void;
408
+ /** '±': every gene, or back to the chosen list. */
409
+ onToggleAllGenes(): void;
410
+ /** '⋮' menu. */
411
+ private buildGeneMenu;
412
+ /** The hidden file input the menu's import opens. */
413
+ private geneGroupFileRef?;
414
+ private get geneGroupInput();
415
+ trackByLabel: (_i: number, row: {
416
+ label: string;
417
+ }) => string;
418
+ trackByNode: (_i: number, node: {
419
+ name: string | null;
420
+ }) => string;
421
+ trackByLabelString: (_i: number, s: string) => string;
422
+ get cellOpacityPercent(): number;
423
+ get densityOpacityPercent(): number;
424
+ geneGroupError: string | null;
425
+ /** Transcripts of each selected gene in the current view, from the renderer. */
426
+ geneCounts: Record<string, number> | null;
427
+ geneCountOf(gene: string): number | null;
428
+ markersOpen: boolean;
429
+ markerColumn: string | null;
430
+ markerPerGroup: number;
431
+ readonly markerPerGroupOptions: {
432
+ label: string;
433
+ value: number;
434
+ }[];
435
+ markerClusters: string[];
436
+ markerClusterOptions: {
437
+ label: string;
438
+ value: string;
439
+ }[];
440
+ markerLoading: boolean;
441
+ markerError: string | null;
442
+ get canAddMarkers(): boolean;
443
+ /** Every categorical column but the segmentation method, which says nothing about genes. */
444
+ get markerColumnOptions(): {
445
+ label: string;
446
+ value: string;
447
+ }[];
448
+ openMarkers(): void;
449
+ onMarkerColumn(column: string): void;
450
+ /** The clusters of the chosen column, all picked to start with. */
451
+ private refreshMarkerClusters;
452
+ /**
453
+ * Add each picked cluster's top marker genes as a gene group named after it, and select
454
+ * them. A gene that marks several clusters goes to the one it is most specific to, so the
455
+ * tree lists it once.
456
+ */
457
+ addMarkerGenes(): Promise<void>;
458
+ onNewGeneGroup(): void;
459
+ /**
460
+ * Import gene groups: CSV/TSV with a group and a gene column (header optional), e.g.
461
+ * marker genes per cell type. The genes are added to the selection.
462
+ */
463
+ onImportGeneGroups(input: HTMLInputElement): Promise<void>;
464
+ readonly densityBins: number[];
465
+ get densityBinIndex(): number;
466
+ onDensityBinIndex(i: number | undefined): void;
467
+ onDensityOpacityPercent(v: number | null): void;
468
+ /**
469
+ * The threshold window shown: the one set, else the one derived. A stored array, not a
470
+ * getter: a range slider's `ngModel` given a fresh array every change-detection pass
471
+ * schedules another pass, forever — which hung the page the moment the density
472
+ * controls appeared.
473
+ */
474
+ densityWindow: [number, number];
475
+ densitySliderMax: number;
476
+ densityStep: number;
477
+ private refreshDensityWindow;
478
+ onDensityRange(range: [number, number] | number[] | undefined): void;
479
+ onDensityRangeEnd(which: 0 | 1, v: number | null): void;
480
+ onDensityAuto(): void;
481
+ selectedDensityColormapNode: ColormapNode | null;
482
+ onDensityColormap(node: ColormapNode | null): void;
483
+ get densityColorBarCss(): string;
484
+ onTranscriptMode(mode: SpatialViewState['transcriptMode']): void;
485
+ onTranscriptGenes(genes: string[] | null): void;
486
+ /** "All genes" needs the grouping pyramid, or at least tiles to draw individually. */
487
+ get canShowAllGenes(): boolean;
488
+ /** Why "All genes" is not offered yet, while the server builds its pyramid. */
489
+ get allGenesPreparing(): string | null;
490
+ get showingAllGenes(): boolean;
491
+ onTranscriptAllGenes(on: boolean): void;
492
+ onTranscriptBudget(n: number): void;
493
+ onTranscriptColorBy(by: SpatialViewState['transcriptColorBy']): void;
494
+ onTranscriptScale(value: number | undefined): void;
495
+ onTranscriptOpacity(value: number | undefined): void;
496
+ onTranscriptQuality(includeLow: boolean): void;
497
+ onDensityOpacity(value: number | undefined): void;
498
+ glyphOf(gene: string, slot: number): TranscriptGlyphName;
499
+ onGlyph(gene: string, glyph: TranscriptGlyphName): void;
500
+ /** The colour gene `slot` is drawn in when transcripts are coloured by gene. */
501
+ geneColor(slot: number): string;
502
+ /** Colours of the cells' groups, by name — what a cluster of the same name is drawn in. */
503
+ private cellGroupColors;
504
+ private cellGroupColorsFor;
505
+ private refreshCellGroupColors;
506
+ /** In Cluster colouring, a gene's swatch is its cluster's colour, as its markers are. */
507
+ geneSwatchOf(gene: string): string;
508
+ geneColorOf(gene: string): string;
224
509
  reset(): void;
225
510
  /**
226
511
  * Description of the active column, when it has one. Surfaced because a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jax-data-science/sci-image-visualizer",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "engines": {
5
5
  "node": ">=20.9.0"
6
6
  },