@jax-data-science/sci-image-visualizer 0.5.0 → 0.7.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 (42) hide show
  1. package/README.md +49 -0
  2. package/esm2022/lib/contracts/display-types.mjs +28 -1
  3. package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +1 -1
  4. package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -1
  5. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +3 -1
  6. package/esm2022/lib/contracts/toolbar-tool.contract.mjs +46 -4
  7. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  8. package/esm2022/lib/implementations/napari-js/napari-navigator.mjs +195 -0
  9. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +10 -2
  10. package/esm2022/lib/implementations/napari-js/napari-spatial-tiles.mjs +1098 -0
  11. package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +6 -3
  12. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +186 -10
  13. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +2 -2
  14. package/esm2022/lib/implementations/plotly/plotly.service.mjs +2 -3
  15. package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +120 -2
  16. package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +76 -1
  17. package/esm2022/lib/routing-visualizer.service.mjs +10 -1
  18. package/esm2022/lib/spatial/spatial-tiles.mjs +506 -0
  19. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +659 -19
  20. package/esm2022/lib/toolbar/brush/brush-tool.service.mjs +44 -3
  21. package/esm2022/lib/toolbar/toolbar.component.mjs +15 -3
  22. package/esm2022/lib/visualizer.component.mjs +221 -7
  23. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +3238 -84
  24. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  25. package/lib/contracts/display-types.d.ts +104 -1
  26. package/lib/contracts/plot-type-contribution.contract.d.ts +27 -0
  27. package/lib/contracts/ports/spatial-data.port.d.ts +59 -1
  28. package/lib/contracts/spatial-dataset.contract.d.ts +167 -0
  29. package/lib/contracts/toolbar-tool.contract.d.ts +84 -5
  30. package/lib/contracts/visualizer.contract.d.ts +22 -1
  31. package/lib/implementations/napari-js/napari-navigator.d.ts +72 -0
  32. package/lib/implementations/napari-js/napari-region-overlay.d.ts +3 -0
  33. package/lib/implementations/napari-js/napari-spatial-tiles.d.ts +240 -0
  34. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +42 -2
  35. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +26 -2
  36. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +34 -1
  37. package/lib/spatial/spatial-tiles.d.ts +222 -0
  38. package/lib/spatial-controls/spatial-controls.component.d.ts +234 -3
  39. package/lib/toolbar/brush/brush-tool.service.d.ts +21 -0
  40. package/lib/toolbar/toolbar.component.d.ts +11 -3
  41. package/lib/visualizer.component.d.ts +54 -2
  42. package/package.json +1 -1
@@ -38,6 +38,16 @@ export interface IWandOptions {
38
38
  export interface IBrushOptions {
39
39
  /** Brush diameter in matrix (image) pixels. */
40
40
  size?: number;
41
+ /**
42
+ * Paint as one class: new regions take this label, and a stroke only extends
43
+ * or merges regions with the same label. Unset = the plain brush (label
44
+ * `'Region'`, extends any region). `setBrushMode(true, …)` sets the class in
45
+ * full; `setBrushOptions` changes it only when `label` or `color` is passed,
46
+ * so a size-only update keeps it.
47
+ */
48
+ label?: string;
49
+ /** Colour for the painted regions instead of the shape colour. Kept against preset re-apply. */
50
+ color?: string;
41
51
  }
42
52
  /** What drives point colour in the spatial-omics mode: an annotation column
43
53
  * from the dataset, or one feature (gene) vector fetched on demand. */
@@ -103,7 +113,8 @@ export interface SpatialViewState {
103
113
  * setting for a backdrop and a useless one for the measurement.
104
114
  */
105
115
  volumeOpacity: number;
106
- /** Draw the observation cloud (3D). */
116
+ /** Draw the observation markers — the 3D cloud, or the 2D circles. In 2D they also
117
+ * stand in for cells too small on screen to outline. */
107
118
  showPoints: boolean;
108
119
  /**
109
120
  * Restrict the cloud to ONE imaged section, by index into the dataset's sampled
@@ -168,5 +179,97 @@ export interface SpatialViewState {
168
179
  * the markers: turning the cells down to read the field underneath must not
169
180
  * turn the field down with them. */
170
181
  geneMapOpacity: number;
182
+ /**
183
+ * Draw cell boundaries (for a dataset with `polygonTiles` or `polygons`).
184
+ *
185
+ * Outlines are level-of-detail: coarse rings when zoomed out, every vertex at the
186
+ * finest zoom, and none at all once a cell is a few pixels wide — there the
187
+ * observation markers already draw it as a dot.
188
+ *
189
+ * `null` (the default) means automatic: on for any dataset that has outlines, since a
190
+ * segmented cell is better shown as its shape than as a circle. A boolean is the
191
+ * user's explicit choice.
192
+ */
193
+ showCells: boolean | null;
194
+ /** Which boundary set (`cell`, `nucleus`, or `both` — cells as set by
195
+ * {@link cellDraw}, nuclei outlined over them); null takes the dataset's default. */
196
+ cellSet: string | null;
197
+ /**
198
+ * What colours the cells — Xenium Explorer's "Cell Color": the active group
199
+ * ({@link cellTypeColumn}), one gene's expression ({@link cellColorGene}), the cell's
200
+ * transcript density, one flat colour, or how the cell was segmented.
201
+ */
202
+ cellColorMode: 'group' | 'gene' | 'transcriptDensity' | 'single' | 'segmentation';
203
+ cellColorGene: string | null;
204
+ /** `#rrggbb` for {@link cellColorMode} `single`. */
205
+ cellSingleColor: string;
206
+ /**
207
+ * Categories of {@link cellTypeColumn} switched off in the groups list: their cells are
208
+ * not drawn, nor the transcripts inside them. Cleared when the group column changes.
209
+ */
210
+ hiddenGroups: string[];
211
+ /** Draw the tissue image (the Images section's switch). */
212
+ showImage: boolean;
213
+ /** Draw the annotation regions (the Annotations section's switch). Selecting from them
214
+ * still works while they are hidden. */
215
+ showAnnotations: boolean;
216
+ /** Fill the cells with their colour, outline them, or both. */
217
+ cellDraw: 'fill' | 'outline' | 'both';
218
+ /** Opacity of the cell fill (outlines draw opaque). */
219
+ cellOpacity: number;
220
+ /**
221
+ * The categorical column that says what TYPE each cell is: it colours the cells
222
+ * whenever nothing else is chosen, and it colours every transcript by the cell it
223
+ * falls in. Null picks the dataset's first categorical column. Swapping between a
224
+ * pipeline's clustering and a curated annotation is swapping this.
225
+ */
226
+ cellTypeColumn: string | null;
227
+ /** Genes whose transcripts are drawn. */
228
+ transcriptGenes: string[];
229
+ /** Selected genes switched off with their eye toggle — kept in the list, not drawn. */
230
+ transcriptHiddenGenes: string[];
231
+ /** Per-gene marker colour (`#rrggbb`); genes not listed take the palette by position. */
232
+ transcriptGeneColors: Record<string, string>;
233
+ /** Named gene groups (e.g. marker genes per cell type) for the selected-genes tree. */
234
+ transcriptGeneGroups: {
235
+ name: string;
236
+ genes: string[];
237
+ }[];
238
+ /**
239
+ * Draw EVERY gene's transcripts instead of {@link transcriptGenes}: grouped into bins
240
+ * sized by how many transcripts they hold, finer as the camera zooms in, down to one
241
+ * marker per transcript once those fit {@link transcriptBudget}.
242
+ */
243
+ transcriptAllGenes: boolean;
244
+ /**
245
+ * Most transcript markers drawn at once. The level of detail is chosen to stay under it,
246
+ * which is what keeps pan and zoom responsive however dense the data is.
247
+ */
248
+ transcriptBudget: number;
249
+ /**
250
+ * How transcripts are drawn: `circles` sized by how many transcripts each stands for,
251
+ * `glyphs` (one icon shape per gene), or a `density` raster. Circles and glyphs are
252
+ * level-of-detail — aggregated when zoomed out, one per transcript at the finest zoom.
253
+ */
254
+ transcriptMode: 'off' | 'circles' | 'glyphs' | 'density';
255
+ /** Colour transcripts by the type of the cell they fall in, or by gene. */
256
+ transcriptColorBy: 'cellType' | 'gene';
257
+ /** Glyph per gene for `glyphs` mode; genes not listed take one by position. */
258
+ transcriptGlyphs: Record<string, TranscriptGlyphName>;
259
+ /** Multiplier on transcript marker size. */
260
+ transcriptScale: number;
261
+ transcriptOpacity: number;
262
+ /** `high` keeps only confidently decoded transcripts. */
263
+ transcriptQuality: 'high' | 'all';
264
+ /** Opacity of the transcript density raster. */
265
+ densityOpacity: number;
266
+ /** Density bin edge in observation units (µm): 10, 20, 40 or 80. */
267
+ densityBin: number;
268
+ /** Colour window in transcripts per unit² (µm²); null derives it from the data. */
269
+ densityRange: [number, number] | null;
270
+ /** Colormap for the density bins; null uses Inferno. */
271
+ densityColormap: ColormapValue | null;
171
272
  }
273
+ /** Transcript glyph names — see `TRANSCRIPT_GLYPHS` in `spatial/spatial-tiles.ts`. */
274
+ export type TranscriptGlyphName = 'circle' | 'star' | 'triangle' | 'square' | 'diamond' | 'cross' | 'hexagon' | 'triangle-down' | 'pentagon' | 'x';
172
275
  export declare const DEFAULT_SPATIAL_VIEW: SpatialViewState;
@@ -103,6 +103,31 @@ export interface PlotModeViewport {
103
103
  * A new subscriber first receives the current rect when the viewport is ready. */
104
104
  settled$: Observable<PlotModeRect>;
105
105
  }
106
+ /** The class a {@link PlotModeTools.armBrush} stroke paints. */
107
+ export interface PlotModeBrushClass {
108
+ /** Label for the painted regions; the stroke only extends or merges regions with it. */
109
+ label?: string;
110
+ /** Colour for the painted regions (instead of the toolbar's shape colour). */
111
+ color?: string;
112
+ }
113
+ /**
114
+ * The visualizer's own toolbar tools, armed on a mode's behalf. Going through
115
+ * this rather than {@link IVisualizer} keeps the toolbar in step: the armed
116
+ * tool's button is highlighted and its controls (e.g. the brush size) show.
117
+ */
118
+ export interface PlotModeTools {
119
+ /**
120
+ * Arm the toolbar brush, painting `brushClass`. Calling it again while the
121
+ * brush is armed switches class; the next stroke starts a region of it.
122
+ * The user picking any tool from the toolbar (including the brush) replaces
123
+ * this with the plain tool.
124
+ */
125
+ armBrush(brushClass?: PlotModeBrushClass): void;
126
+ /** Disarm whatever tool is armed, as if the user toggled it off. */
127
+ disarm(): void;
128
+ /** The armed toolbar tool id ('brush', 'pan', … or null). Replays the current value. */
129
+ activeTool$: Observable<string | null>;
130
+ }
106
131
  /** Everything a contributed mode gets to work with while it is active. */
107
132
  export interface PlotModeContext {
108
133
  /** Full public visualizer API: regions, region overlay (tool modes), undo, etc. */
@@ -110,6 +135,8 @@ export interface PlotModeContext {
110
135
  viewport: PlotModeViewport;
111
136
  /** Current image info from IMAGE_STATE_PORT (null until an image is loaded). */
112
137
  imageInfo$: Observable<IImageInfo | null>;
138
+ /** Toolbar tools (since 0.6.0). Optional so a context built for 0.5.0 still type-checks. */
139
+ tools?: PlotModeTools;
113
140
  }
114
141
  /** A live activation of a contributed mode. */
115
142
  export interface PlotModeSession {
@@ -1,6 +1,21 @@
1
1
  import { InjectionToken } from '@angular/core';
2
2
  import { Observable } from 'rxjs';
3
- import { SpatialColumn, SpatialDataset, SpatialEmbedding, SpatialPolygons } from '../spatial-dataset.contract';
3
+ import { SpatialColumn, SpatialDataset, SpatialDensityRaster, SpatialEmbedding, SpatialPolygonTile, CategoricalColumnMeta, SpatialPolygons, SpatialTranscriptCounts, SpatialTranscriptSummary, SpatialTranscriptTile } from '../spatial-dataset.contract';
4
+ /** Options for {@link SpatialDataPort.getTranscriptTile}. */
5
+ export interface TranscriptTileQuery {
6
+ /**
7
+ * Genes to include, by name; the tile's `gene` codes index this list. `[ALL_GENES]`
8
+ * asks for every gene, unaggregated, at level 0 only and clipped to {@link box} — the
9
+ * `gene` codes are then the dataset's own gene indices.
10
+ */
11
+ genes: string[];
12
+ /** `high` (default) keeps only confidently decoded transcripts. */
13
+ quality?: 'high' | 'all';
14
+ /** Clip to `[x0, y0, x1, y1]` in observation units. */
15
+ box?: [number, number, number, number];
16
+ }
17
+ /** {@link TranscriptTileQuery.genes} value meaning "every gene". */
18
+ export declare const ALL_GENES = "*";
4
19
  /**
5
20
  * Spatial-omics data access, inverted as a port so the visualization library
6
21
  * never learns how the data is stored or served — exactly like
@@ -66,6 +81,49 @@ export interface SpatialDataPort {
66
81
  * `polygons`. Optional — a spot-based assay (Visium) has no segmentation.
67
82
  */
68
83
  getPolygons?(): Promise<SpatialPolygons>;
84
+ /**
85
+ * One tile of boundaries from `polygonTiles`. An empty tile resolves with `count` 0 —
86
+ * a tile outside the tissue is an answer, not an error. Optional: only datasets that
87
+ * advertise `polygonTiles` need it.
88
+ */
89
+ getPolygonTile?(set: string, level: number, gx: number, gy: number): Promise<SpatialPolygonTile>;
90
+ /**
91
+ * One tile of transcripts from `transcriptTiles`, restricted to `query.genes`.
92
+ * Optional: only datasets that advertise `transcriptTiles` need it.
93
+ */
94
+ getTranscriptTile?(level: number, gx: number, gy: number, query: TranscriptTileQuery): Promise<SpatialTranscriptTile>;
95
+ /**
96
+ * One tile of the all-gene grouping pyramid (`transcriptBins`): an entry per bin, with
97
+ * `weight` its transcript count and `observation` its dominant cell.
98
+ */
99
+ getTranscriptBins?(level: number, tx: number, ty: number): Promise<SpatialTranscriptTile>;
100
+ /**
101
+ * What is inside `box` (observation units): transcript count, distinct genes and cells,
102
+ * the most frequent of each — optionally restricted to `genes` — plus display ids for
103
+ * `cells`. Omit `box` for a cell-id lookup only. Optional; drives the hover details of
104
+ * transcript markers.
105
+ */
106
+ getTranscriptSummary?(query: {
107
+ box?: [number, number, number, number];
108
+ genes?: string[];
109
+ cells?: number[];
110
+ }): Promise<SpatialTranscriptSummary>;
111
+ /**
112
+ * The summed transcript density of `genes` (`[ALL_GENES]` for every gene) on the
113
+ * dataset's `density` raster, re-binned to `binSize` observation units when given.
114
+ * Optional: only datasets that advertise `density` need it.
115
+ */
116
+ getDensity?(genes: string[], binSize?: number): Promise<SpatialDensityRaster>;
117
+ /**
118
+ * Import a cell grouping — a CSV/TSV of `cell_id` and `group` — under `label`. The new
119
+ * categorical column is added to the dataset (which re-emits) and resolved. Optional.
120
+ */
121
+ importGroups?(label: string, table: string): Promise<{
122
+ column: CategoricalColumnMeta;
123
+ matched: number;
124
+ }>;
125
+ /** Transcript totals per gene, for estimating how many markers a selection draws. */
126
+ getTranscriptCounts?(genes: string[]): Promise<SpatialTranscriptCounts>;
69
127
  /**
70
128
  * The reference volume's voxels: a uint8 scalar field, x-fastest, of exactly
71
129
  * `width * height * depth` bytes as the dataset's {@link SpatialVolumeMeta}
@@ -63,6 +63,20 @@ export interface CategoricalColumnMeta extends SpatialColumnMetaBase {
63
63
  kind: 'categorical';
64
64
  /** Category labels; a value's label is `categories[codes[i]]`. */
65
65
  categories: string[];
66
+ /**
67
+ * Heading the column is listed under in a group picker — e.g. "Xenium Onboard Analysis
68
+ * groups" for a pipeline's clusterings, "Imported groups" for ones added by a user.
69
+ */
70
+ section?: string;
71
+ /**
72
+ * Columns that are variants of one choice — k-means at k = 2…10 — share a family: a
73
+ * picker lists the family once and offers the variants beside it.
74
+ */
75
+ family?: {
76
+ id: string;
77
+ label: string;
78
+ variant: string;
79
+ };
66
80
  /** Authored display colours as `#rrggbb`, index-aligned with
67
81
  * {@link categories}. Supply these to keep the viewer's palette identical to
68
82
  * the figures the same analysis produced in R/Python; omit to let the viewer
@@ -228,6 +242,149 @@ export interface SpatialEmbedding {
228
242
  y: Float32Array;
229
243
  z?: Float32Array;
230
244
  }
245
+ /**
246
+ * Level-of-detail geometry, served one grid tile at a time.
247
+ *
248
+ * Whole-dataset vectors stop working somewhere past 10^5 observations: 717k Xenium cell
249
+ * outlines are ~140 MB, and a whole-transcriptome run has ~10^9 transcripts. What scales
250
+ * is what map tiles do — a square grid in the observations' coordinate space, one grid
251
+ * per level, each level's tiles twice the size of the one below and carrying coarser
252
+ * geometry. The renderer asks only for the tiles on screen, at the level the zoom needs.
253
+ *
254
+ * Tile `(gx, gy)` at level `l` covers `[gx·s, (gx+1)·s) × [gy·s, (gy+1)·s)` with
255
+ * `s = levels[l].tileSize`, in the same units as {@link SpatialObservations.x}.
256
+ */
257
+ export interface SpatialTileLevel {
258
+ /** Edge length of one tile, in observation coordinate units. */
259
+ tileSize: number;
260
+ }
261
+ /** Axis-aligned extent `[minX, minY, maxX, maxY]` in observation coordinates. */
262
+ export type SpatialBounds = [number, number, number, number];
263
+ /** Tiled cell/nucleus boundaries. Level 0 is the finest (every vertex). */
264
+ export interface SpatialPolygonTilesMeta {
265
+ bounds: SpatialBounds;
266
+ /** Boundary sets on offer — typically `cell` and `nucleus`. */
267
+ sets: {
268
+ name: string;
269
+ label: string;
270
+ }[];
271
+ /** The set to draw when the user has not chosen. */
272
+ defaultSet?: string;
273
+ levels: SpatialTileLevel[];
274
+ }
275
+ /** Tiled transcripts. Level 0 is one entry per transcript; coarser levels aggregate. */
276
+ export interface SpatialTranscriptTilesMeta {
277
+ bounds: SpatialBounds;
278
+ /** Total transcripts, for display. */
279
+ count?: number;
280
+ /** Real genes (control probes excluded). Names come from the feature search. */
281
+ geneCount: number;
282
+ hasZ: boolean;
283
+ levels: (SpatialTileLevel & {
284
+ /** True when an entry stands for several transcripts (see {@link SpatialTranscriptTile.weight}). */
285
+ aggregated: boolean;
286
+ })[];
287
+ /**
288
+ * Levels up to and including this one assign each entry to the cell it lies in
289
+ * exactly; above it the assignment names the cell nearest an aggregate's centroid.
290
+ */
291
+ exactCellLevel?: number;
292
+ }
293
+ /**
294
+ * The all-gene transcript grouping pyramid: square bins, four times coarser per level,
295
+ * each holding a count, a centroid and the cell that contributed most of its transcripts.
296
+ * Served per tile, in the {@link SpatialTranscriptTile} layout (one entry per bin).
297
+ *
298
+ * Tile `(tx, ty)` of level `l` covers `origin + [tx·s, (tx+1)·s) × [ty·s, (ty+1)·s)` with
299
+ * `s = levels[l].tileSize` — note the origin, which the per-gene tiles do not have.
300
+ */
301
+ export interface SpatialTranscriptBinsMeta {
302
+ bounds: SpatialBounds;
303
+ origin: [number, number];
304
+ /** Transcripts the pyramid was built from. */
305
+ count: number;
306
+ levels: {
307
+ binSize: number;
308
+ tileSize: number;
309
+ }[];
310
+ }
311
+ /**
312
+ * A grouping pyramid the server is still building (it builds one itself when a dataset
313
+ * arrives without it). "All genes" becomes available once it is done.
314
+ */
315
+ export interface SpatialTranscriptBinsStatus {
316
+ state: 'queued' | 'building' | 'failed';
317
+ /** Source tiles processed so far, of `total`. */
318
+ done?: number;
319
+ total?: number;
320
+ message?: string;
321
+ }
322
+ /** What is in an area — the hover details of a transcript marker or grouped bin. */
323
+ export interface SpatialTranscriptSummary {
324
+ /** Absent for a pure cell-id lookup. */
325
+ transcripts?: number;
326
+ /** Distinct genes among them. */
327
+ genes?: number;
328
+ topGenes?: {
329
+ name: string;
330
+ count: number;
331
+ }[];
332
+ /** Distinct cells they fall in, and how many fall in none. */
333
+ cells?: number;
334
+ unassigned?: number;
335
+ topCells?: {
336
+ index: number;
337
+ id?: string;
338
+ count: number;
339
+ }[];
340
+ /** Display ids of the cells asked for, by observation index. */
341
+ cellIds?: Record<number, string>;
342
+ }
343
+ /** A per-gene transcript-count raster covering the section. */
344
+ export interface SpatialDensityMeta {
345
+ /** Size of one raster cell, in observation units, `[x, y]`. */
346
+ gridSize: [number, number];
347
+ /** Observation-space position of raster cell (0, 0)'s near corner. */
348
+ origin: [number, number];
349
+ rows: number;
350
+ cols: number;
351
+ }
352
+ /** "No observation" in {@link SpatialTranscriptTile.observation}. */
353
+ export declare const NO_OBSERVATION = 4294967295;
354
+ /** One tile of boundaries: rings as in {@link SpatialPolygons}, plus who owns each ring. */
355
+ export interface SpatialPolygonTile extends SpatialPolygons {
356
+ /** Observation index of ring `i` — the join key to columns and features. */
357
+ observation: Uint32Array;
358
+ }
359
+ /** One tile of transcripts (or aggregates of them). Every array has length {@link count}. */
360
+ export interface SpatialTranscriptTile {
361
+ readonly count: number;
362
+ aggregated: boolean;
363
+ x: Float32Array;
364
+ y: Float32Array;
365
+ z: Float32Array;
366
+ /** Transcripts each entry stands for — 1 at an unaggregated level. */
367
+ weight: Uint32Array;
368
+ /** Observation (cell) the entry falls in, or {@link NO_OBSERVATION}. */
369
+ observation: Uint32Array;
370
+ /** Index into the `genes` list the tile was requested with. */
371
+ gene: Uint16Array;
372
+ }
373
+ /** Transcript totals for an estimate of how many markers a selection would draw. */
374
+ export interface SpatialTranscriptCounts {
375
+ /** Total transcripts per requested gene, over the whole dataset. */
376
+ counts: Record<string, number>;
377
+ /** Total over every gene. */
378
+ total: number;
379
+ /** Extent the totals are spread over (observation units). */
380
+ bounds: SpatialBounds;
381
+ }
382
+ /** A summed density raster for a list of genes; `values` is row-major `rows × cols`. */
383
+ export interface SpatialDensityRaster {
384
+ meta: SpatialDensityMeta;
385
+ genes: string[];
386
+ values: Float32Array;
387
+ }
231
388
  export interface SpatialDataset {
232
389
  /** Stable id — the key the port's lazy accessors are scoped to. */
233
390
  id: string;
@@ -243,6 +400,16 @@ export interface SpatialDataset {
243
400
  volume?: SpatialVolumeMeta;
244
401
  /** Embeddings available for these observations, whose coordinates are fetched on demand. */
245
402
  embeddings?: SpatialEmbeddingMeta[];
403
+ /** Level-of-detail boundaries, fetched per tile — see {@link SpatialPolygonTilesMeta}. */
404
+ polygonTiles?: SpatialPolygonTilesMeta;
405
+ /** Level-of-detail transcripts, fetched per tile — see {@link SpatialTranscriptTilesMeta}. */
406
+ transcriptTiles?: SpatialTranscriptTilesMeta;
407
+ /** A served per-gene transcript density raster. */
408
+ density?: SpatialDensityMeta;
409
+ /** The all-gene transcript grouping pyramid, when it has been built. */
410
+ transcriptBins?: SpatialTranscriptBinsMeta;
411
+ /** Present while the server is still building {@link transcriptBins}. */
412
+ transcriptBinsStatus?: SpatialTranscriptBinsStatus;
246
413
  /**
247
414
  * Microns per observation coordinate unit — what makes a scale bar possible.
248
415
  *
@@ -35,6 +35,17 @@
35
35
  * than an escape hatch that returns a component — the moment one tool draws
36
36
  * itself, the toolbar stops being able to lay tools out consistently.
37
37
  *
38
+ * DIALOG TOOLS
39
+ * Some tools are not "set parameters, run once" but an interactive session: the
40
+ * user paints, the tool retrains and redraws, over and over (DIANNE). A schema
41
+ * cannot describe that, so {@link ToolbarDialogToolContribution} is the one
42
+ * variant that renders its own body. The toolbar still owns the button, its
43
+ * placement and the dialog chrome; the tool only fills the dialog, with plain
44
+ * DOM (`mount`), so it needs neither Angular nor this library's PrimeNG version.
45
+ * Its button sits with the host's own actions at the start of the toolbar, and
46
+ * shows only in the Image view, the view its context ({@link ToolDialogContext})
47
+ * draws over.
48
+ *
38
49
  * WHY AN EMPTY MODEL LIST HIDES THE TOOL
39
50
  * {@link ToolbarToolContribution.models} returning `[]` removes the tool from
40
51
  * the toolbar entirely. This preserves the behaviour the built-in tools already
@@ -45,6 +56,7 @@
45
56
  import { InjectionToken } from '@angular/core';
46
57
  import type { Observable } from 'rxjs';
47
58
  import type { IVisualizer } from './visualizer.contract';
59
+ import type { PlotModeContext, PlotModeSession, PlotModeTools } from './plot-type-contribution.contract';
48
60
  /** A number field. Rendered as a stepper, matching the built-in dialogs. */
49
61
  export interface NumberParamSpec {
50
62
  id: string;
@@ -177,14 +189,81 @@ export interface ToolbarToolContribution {
177
189
  run(viz: IVisualizer, params: Record<string, unknown>): Promise<number>;
178
190
  }
179
191
  /**
180
- * Multi-provider token for contributed tools.
192
+ * What a dialog tool gets while its dialog is open: the same surface a
193
+ * contributed plot mode gets (the public visualizer, the Image view's viewport
194
+ * and the current image), with the toolbar tools always present.
195
+ */
196
+ export type ToolDialogContext = PlotModeContext & {
197
+ tools: PlotModeTools;
198
+ };
199
+ /** A live dialog-tool session. `deactivate()` runs exactly once, after the body's teardown. */
200
+ export type ToolDialogSession = PlotModeSession;
201
+ /**
202
+ * A toolbar tool that opens a dialog instead of running once (see DIALOG TOOLS
203
+ * above). Clicking its button opens the dialog and starts a session; clicking it
204
+ * again, or closing the dialog, ends it.
205
+ *
206
+ * The session is bound to the Image view on screen. When that view is re-rendered
207
+ * (another image, another slice) the session ends and, with the dialog still
208
+ * open, a fresh one starts once the new view is ready. Leaving the Image view
209
+ * closes the dialog. As with plot modes, nothing the tool throws or rejects
210
+ * escapes: a failed start closes the dialog with a warning.
211
+ */
212
+ export interface ToolbarDialogToolContribution {
213
+ kind: 'dialog';
214
+ /** Stable identity. Must not clash with another toolbar tool. */
215
+ id: string;
216
+ /** Short name; the dialog title unless `dialog.title` is set. */
217
+ label: string;
218
+ icon: ToolIcon;
219
+ /** Tooltip on the button. */
220
+ tooltip: string;
221
+ /** Order among the dialog tools. Lower sorts first; ties keep registration order. */
222
+ order?: number;
223
+ dialog?: {
224
+ title?: string;
225
+ /** CSS width of the dialog. Default `'22rem'`. */
226
+ width?: string;
227
+ };
228
+ /** Entry for the help dialog's tool list, rendered as HTML (trusted content). */
229
+ help?: {
230
+ body: string;
231
+ };
232
+ /** Start a session, once the Image view's viewport is ready. */
233
+ activate(ctx: ToolDialogContext): ToolDialogSession | Promise<ToolDialogSession>;
234
+ /**
235
+ * Render the dialog body into `host`; return its teardown (called before
236
+ * `deactivate()`). Called after `activate` resolves, once the dialog has rendered and
237
+ * `host` is in the document, so the body can measure itself.
238
+ */
239
+ mount(host: HTMLElement, ctx: ToolDialogContext, session: ToolDialogSession): () => void;
240
+ }
241
+ /** Anything provided on {@link TOOLBAR_TOOLS}. A run tool has no `kind`. */
242
+ export type ToolbarContribution = ToolbarToolContribution | ToolbarDialogToolContribution;
243
+ /** Whether a contribution is a {@link ToolbarDialogToolContribution}. */
244
+ export declare function isDialogToolContribution(c: unknown): c is ToolbarDialogToolContribution;
245
+ /**
246
+ * Multi-provider token for contributed tools: run tools
247
+ * ({@link ToolbarToolContribution}) and, since 0.6.0, dialog tools
248
+ * ({@link ToolbarDialogToolContribution}).
181
249
  *
182
250
  * Deliberately has no `factory`: unregistered means no tools, which is what
183
251
  * makes this library shippable without the closed model packages. Inject it
184
252
  * `{ optional: true }` and treat null as empty.
185
253
  */
186
- export declare const TOOLBAR_TOOLS: InjectionToken<readonly ToolbarToolContribution[]>;
254
+ export declare const TOOLBAR_TOOLS: InjectionToken<readonly ToolbarContribution[]>;
187
255
  /** Sort contributed tools into display order. */
188
- export declare function sortToolContributions(tools: readonly ToolbarToolContribution[]): ToolbarToolContribution[];
189
- /** The tools a toolbar should actually show: those with at least one model. */
190
- export declare function visibleToolContributions(tools: readonly ToolbarToolContribution[] | null | undefined): ToolbarToolContribution[];
256
+ export declare function sortToolContributions<T extends {
257
+ order?: number;
258
+ }>(tools: readonly T[]): T[];
259
+ /** The run tools a toolbar should actually show: those with at least one model.
260
+ * Dialog tools are left out (see {@link dialogToolContributions}). */
261
+ export declare function visibleToolContributions(tools: readonly ToolbarContribution[] | null | undefined): ToolbarToolContribution[];
262
+ /**
263
+ * The dialog tools, in display order. A malformed one (no string `id`, no
264
+ * `activate` or `mount` function) or a repeated `id` is dropped with a warning
265
+ * rather than breaking the toolbar.
266
+ */
267
+ export declare function dialogToolContributions(tools: readonly ToolbarContribution[] | null | undefined, log?: {
268
+ warn(...args: unknown[]): void;
269
+ }): ToolbarDialogToolContribution[];
@@ -8,7 +8,7 @@ import { ViewerCapabilities } from './capabilities.contract';
8
8
  import { IRegionOverlay } from './region-overlay.contract';
9
9
  import { IHistogram } from './channel-histogram-api.contract';
10
10
  import { ColormapNode, IWandOptions, IBrushOptions, SpatialViewState, SpatialColorBy } from './display-types';
11
- import { SpatialDataset, SpatialEmbedding } from './spatial-dataset.contract';
11
+ import { CategoricalColumnMeta, SpatialDataset, SpatialEmbedding, SpatialTranscriptCounts } from './spatial-dataset.contract';
12
12
  import { SpatialSelectionMask } from '../spatial/spatial-selection';
13
13
  import type { PlotModeViewport } from './plot-type-contribution.contract';
14
14
  /**
@@ -334,6 +334,27 @@ export interface ISpatialControls {
334
334
  getEmbedding?(name: string): Promise<SpatialEmbedding>;
335
335
  /** Names of the categorical columns available to group by. */
336
336
  categoricalColumns(): string[];
337
+ /**
338
+ * Import a cell grouping (CSV/TSV of `cell_id`, `group`) as a new categorical column.
339
+ * Optional: only data sources that can join cell ids implement it.
340
+ */
341
+ importGroups?(label: string, table: string): Promise<{
342
+ column: CategoricalColumnMeta;
343
+ matched: number;
344
+ }>;
345
+ /** Transcript totals per gene — for the points estimate. Optional. */
346
+ transcriptCounts?(genes: string[]): Promise<SpatialTranscriptCounts>;
347
+ /** Estimated transcripts in view for the current selection, against the marker budget. */
348
+ getTranscriptEstimate$?(): Observable<{
349
+ points: number;
350
+ max: number;
351
+ } | null>;
352
+ /** The density map's colour window in use (auto-derived or set) and its densest bin. */
353
+ getDensityStats$?(): Observable<{
354
+ lo: number;
355
+ hi: number;
356
+ max: number;
357
+ } | null>;
337
358
  /**
338
359
  * The z positions of the sections the dataset was imaged at, ascending — or
339
360
  * null when its z is continuous rather than sectioned (so there are no sections
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Overview navigator (minimap) for the napari-js 2D views, mirroring the OSD backend's
3
+ * built-in navigator: a thumbnail of the whole image in the bottom-right corner with the
4
+ * current viewport drawn on it. Clicking re-centres the main view on that point at the
5
+ * same zoom; dragging pans continuously — exactly OSD's behaviour.
6
+ *
7
+ * napari keeps layers at full-resolution pixel scale, so the camera's world units ARE
8
+ * image pixels and the thumbnail maps onto `[0, width) × [0, height)` directly.
9
+ */
10
+ /** The slice of the napari 2D camera the navigator reads and drives. */
11
+ export interface NavigatorCamera {
12
+ center: readonly [number, number];
13
+ readonly zoom: number;
14
+ readonly changed: {
15
+ connect(listener: () => void): () => void;
16
+ };
17
+ }
18
+ /** Where the thumbnail sits inside the navigator box, in CSS px. */
19
+ export interface NavigatorLayout {
20
+ /** Box size. */
21
+ width: number;
22
+ height: number;
23
+ /** CSS px per world unit (image pixel). */
24
+ scale: number;
25
+ }
26
+ /** Fraction of the host's width the navigator takes — OSD's `navigatorSizeRatio`. */
27
+ export declare const NAVIGATOR_SIZE_RATIO = 0.16;
28
+ /** Box size and scale for a `worldW × worldH` image in a host `hostW` wide. */
29
+ export declare function navigatorLayout(hostW: number, worldW: number, worldH: number, ratio?: number): NavigatorLayout | null;
30
+ /** The part of the world the camera shows, as `[x0, y0, x1, y1]`. */
31
+ export declare function viewportWorldRect(center: readonly [number, number], zoom: number, hostW: number, hostH: number): [number, number, number, number];
32
+ /** A point in the navigator box → world coordinates, clamped to the image. */
33
+ export declare function navigatorToWorld(px: number, py: number, layout: NavigatorLayout, worldW: number, worldH: number): [number, number];
34
+ export declare class NapariNavigator {
35
+ private readonly host;
36
+ private readonly camera;
37
+ private worldW;
38
+ private worldH;
39
+ /** Called when the pointer enters the navigator — e.g. to hide a hover tooltip that
40
+ * was describing the canvas underneath. */
41
+ private readonly onEnter?;
42
+ private readonly box;
43
+ private readonly canvas;
44
+ private readonly region;
45
+ private readonly disconnectCamera;
46
+ private readonly resizeObserver?;
47
+ /** Aborted by destroy() to remove every DOM listener the box holds. */
48
+ private readonly listeners;
49
+ private layout;
50
+ private image;
51
+ private dragging;
52
+ private visible;
53
+ constructor(host: HTMLElement, camera: NavigatorCamera, worldW: number, worldH: number,
54
+ /** Called when the pointer enters the navigator — e.g. to hide a hover tooltip that
55
+ * was describing the canvas underneath. */
56
+ onEnter?: (() => void) | undefined);
57
+ /** The thumbnail to draw: any image of the whole world (a coarse pyramid level). */
58
+ setImage(image: CanvasImageSource | null): void;
59
+ /** The image's world size changed (another image was loaded). */
60
+ setWorld(width: number, height: number): void;
61
+ setVisible(visible: boolean): void;
62
+ destroy(): void;
63
+ private relayout;
64
+ private drawImage;
65
+ /** Draw the viewport rectangle, clipped to the box (as OSD does when zoomed out). */
66
+ private updateRegion;
67
+ /** Re-centre the main view on the navigator point under the pointer, keeping the zoom. */
68
+ private panTo;
69
+ private readonly onDown;
70
+ private readonly onMove;
71
+ private readonly onUp;
72
+ }
@@ -48,6 +48,7 @@ export declare class NapariRegionOverlay implements IRegionOverlay {
48
48
  private marqueeEl;
49
49
  private draftPath;
50
50
  private drawing;
51
+ private regionsVisible;
51
52
  /** In-progress manipulation (select/move modes): dragging a body, a polygon vertex, or a
52
53
  * rectangle corner. `anchor` is the fixed opposite corner for a rectangle resize. */
53
54
  private edit;
@@ -124,6 +125,8 @@ export declare class NapariRegionOverlay implements IRegionOverlay {
124
125
  private commitRectangle;
125
126
  private commitPolygon;
126
127
  private hitTest;
128
+ /** Show or hide the drawn regions. A region being drawn stays visible either way. */
129
+ setRegionsVisible(visible: boolean): void;
127
130
  redraw(): void;
128
131
  /** Draw a region's classification label at its top-left, when labels are enabled (matches OSD). */
129
132
  private drawLabel;