@jax-data-science/sci-image-visualizer 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -4
- package/esm2022/index.mjs +21 -1
- package/esm2022/lib/contracts/display-types.mjs +20 -2
- package/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +66 -0
- package/esm2022/lib/contracts/plot-type.mjs +41 -1
- package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
- package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
- package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
- package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +73 -8
- package/esm2022/lib/implementations/osd/osd-coordinate-transform.mjs +8 -1
- package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
- package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
- package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
- package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
- package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
- package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
- package/esm2022/lib/plot-mode/plot-mode-controller.mjs +329 -0
- package/esm2022/lib/routing-visualizer.service.mjs +228 -12
- package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
- package/esm2022/lib/spatial/gene-search.mjs +77 -0
- package/esm2022/lib/spatial/spatial-density.mjs +224 -0
- package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
- package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
- package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
- package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
- package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
- package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
- package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
- package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
- package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
- package/esm2022/lib/spatial/world-grid.mjs +77 -0
- package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
- package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
- package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
- package/esm2022/lib/toolbar/toolbar.component.mjs +61 -21
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +527 -44
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs} +6 -4
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs.map} +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15971 -7808
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +16 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type-contribution.contract.d.ts +172 -0
- package/lib/contracts/plot-type.d.ts +52 -2
- package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
- package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
- package/lib/contracts/visualizer.contract.d.ts +104 -1
- package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
- package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
- package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
- package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +21 -0
- package/lib/implementations/osd/osd-coordinate-transform.d.ts +6 -0
- package/lib/implementations/osd/osd-zoom.d.ts +19 -0
- package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
- package/lib/implementations/plotly/plotly.service.d.ts +9 -0
- package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
- package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
- package/lib/plot-mode/plot-mode-controller.d.ts +130 -0
- package/lib/routing-visualizer.service.d.ts +49 -6
- package/lib/spatial/embedding-compute.d.ts +73 -0
- package/lib/spatial/gene-search.d.ts +48 -0
- package/lib/spatial/spatial-density.d.ts +73 -0
- package/lib/spatial/spatial-encoding.d.ts +155 -0
- package/lib/spatial/spatial-expression.d.ts +144 -0
- package/lib/spatial/spatial-framing.d.ts +37 -0
- package/lib/spatial/spatial-heatmap.d.ts +111 -0
- package/lib/spatial/spatial-hover.d.ts +49 -0
- package/lib/spatial/spatial-sections.d.ts +59 -0
- package/lib/spatial/spatial-selection.d.ts +113 -0
- package/lib/spatial/spatial-volume-image.d.ts +67 -0
- package/lib/spatial/tsne-worker.d.ts +7 -0
- package/lib/spatial/world-grid.d.ts +58 -0
- package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
- package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
- package/lib/store/spatial-selection.service.d.ts +27 -0
- package/lib/store/visualizer-store.service.d.ts +13 -0
- package/lib/toolbar/toolbar.component.d.ts +36 -8
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +139 -7
- package/package.json +6 -2
- package/src/lib/assets/spatial-omics-3d.svg +31 -0
- package/src/lib/assets/spatial-omics.svg +27 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mean expression per group, for the gene × class heatmap.
|
|
3
|
+
*
|
|
4
|
+
* A scatter coloured by one gene answers "where is this gene". It cannot answer
|
|
5
|
+
* "which classes express which genes", which is the question a marker panel is
|
|
6
|
+
* actually read for — and the standard object for that is the mean-expression
|
|
7
|
+
* matrix per cluster (scanpy's `matrixplot`/`dotplot`), genes against classes.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT cells × genes at the full dataset: 374k columns is not a
|
|
10
|
+
* heatmap. Cells become columns only inside a selection, where a few thousand
|
|
11
|
+
* of them render and the question changes to "what is in this region".
|
|
12
|
+
*
|
|
13
|
+
* Two things this has to get right to be readable:
|
|
14
|
+
*
|
|
15
|
+
* - A cell with no measurement for a gene (`NaN`) is SKIPPED, not counted as
|
|
16
|
+
* zero. Counting it would drag a group's mean towards absence for a reason
|
|
17
|
+
* that has nothing to do with the biology.
|
|
18
|
+
* - Z-scoring is per GENE, across groups. Without it a gene expressed an order
|
|
19
|
+
* of magnitude above the others saturates the colour scale and the rest of
|
|
20
|
+
* the panel reads as uniformly blank — the matrix would show which gene is
|
|
21
|
+
* loudest rather than which class expresses what.
|
|
22
|
+
*
|
|
23
|
+
* Pure — no Angular, no Plotly, no renderer — like `spatial-density.ts`, and
|
|
24
|
+
* tested the same way.
|
|
25
|
+
*/
|
|
26
|
+
/** One gene's values, with the name to label its row. */
|
|
27
|
+
export interface HeatmapGene {
|
|
28
|
+
name: string;
|
|
29
|
+
/** One value per observation; `NaN` where the gene was not measured. */
|
|
30
|
+
values: Float32Array;
|
|
31
|
+
}
|
|
32
|
+
/** The columns to aggregate into: a categorical column's categories. */
|
|
33
|
+
export interface HeatmapGroups {
|
|
34
|
+
/** Per-observation category index, or {@link NO_CATEGORY}. */
|
|
35
|
+
codes: Uint16Array;
|
|
36
|
+
categories: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
export interface HeatmapMatrixOptions {
|
|
39
|
+
/** Observations to include; every one when absent (an ROI selection). */
|
|
40
|
+
indices?: Uint32Array;
|
|
41
|
+
/**
|
|
42
|
+
* Z-score each gene across the groups, so one loud gene cannot flatten the
|
|
43
|
+
* panel. On by default — the raw means are only readable when every gene is
|
|
44
|
+
* on a comparable scale.
|
|
45
|
+
*/
|
|
46
|
+
zScore?: boolean;
|
|
47
|
+
/** Drop groups with fewer than this many measured cells. A mean over one or
|
|
48
|
+
* two cells is noise presented with the same weight as a real one. */
|
|
49
|
+
minCells?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Keep at most this many columns, chosen by the STRONGEST signal any picked
|
|
52
|
+
* gene shows in them.
|
|
53
|
+
*
|
|
54
|
+
* `subclass` has 338 categories. Drawn in full that is a texture rather than a
|
|
55
|
+
* panel — every column a couple of pixels wide, every label unreadable — and
|
|
56
|
+
* the question being asked ("which groups express my genes") is answered by
|
|
57
|
+
* the handful of groups where a gene actually stands out. Ranking by
|
|
58
|
+
* max |value| across the rows surfaces exactly those; ranking by cell count
|
|
59
|
+
* would instead surface the biggest groups, which is a different question.
|
|
60
|
+
*
|
|
61
|
+
* Unset keeps every column that cleared `minCells`.
|
|
62
|
+
*/
|
|
63
|
+
maxCols?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface HeatmapMatrix {
|
|
66
|
+
/** Gene names, one per row, in the order given. */
|
|
67
|
+
rows: string[];
|
|
68
|
+
/** Columns dropped by `maxCols`, so the caller can say how many are hidden
|
|
69
|
+
* rather than silently showing a subset as if it were everything. */
|
|
70
|
+
hiddenCols: number;
|
|
71
|
+
/** Group labels, one per column — only the groups that survived `minCells`. */
|
|
72
|
+
cols: string[];
|
|
73
|
+
/**
|
|
74
|
+
* `rows.length × cols.length`, row-major: `values[r * cols.length + c]`.
|
|
75
|
+
* `NaN` where a group had nothing measured for that gene, which the caller
|
|
76
|
+
* draws as a gap rather than as a zero.
|
|
77
|
+
*/
|
|
78
|
+
values: Float32Array;
|
|
79
|
+
/** Measured cells behind each column, index-aligned with {@link cols}. */
|
|
80
|
+
counts: Uint32Array;
|
|
81
|
+
/** Finite range of `values`, for a symmetric or absolute colour scale. */
|
|
82
|
+
range: [number, number];
|
|
83
|
+
}
|
|
84
|
+
/** Default floor for a group to earn a column. */
|
|
85
|
+
export declare const HEATMAP_MIN_CELLS = 3;
|
|
86
|
+
/**
|
|
87
|
+
* Mean expression of each gene in each group.
|
|
88
|
+
*
|
|
89
|
+
* Null when nothing survives — no genes, no groups, or every group filtered
|
|
90
|
+
* out — so the caller draws no chart rather than an empty grid.
|
|
91
|
+
*/
|
|
92
|
+
export declare function heatmapMatrix(genes: readonly HeatmapGene[], groups: HeatmapGroups, opts?: HeatmapMatrixOptions): HeatmapMatrix | null;
|
|
93
|
+
/**
|
|
94
|
+
* Groups standing in for individual cells, for the in-selection view.
|
|
95
|
+
*
|
|
96
|
+
* Inside an ROI, "which classes express what" is often the wrong question and
|
|
97
|
+
* "what is in front of me" is the right one — so each cell becomes its own
|
|
98
|
+
* column. Capped: past a few hundred columns a heatmap is a texture, not a
|
|
99
|
+
* readable panel, and the cap is applied by even thinning so the selection is
|
|
100
|
+
* still represented across its whole extent rather than truncated to its first
|
|
101
|
+
* cells.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: every column here holds exactly ONE cell, so the caller must pass
|
|
104
|
+
* `minCells: 1` to {@link heatmapMatrix}. The default floor of
|
|
105
|
+
* {@link HEATMAP_MIN_CELLS} exists to suppress noisy group means and would
|
|
106
|
+
* filter out every column of this view, returning null.
|
|
107
|
+
*/
|
|
108
|
+
export declare function cellsAsGroups(indices: Uint32Array, observationCount: number, max?: number): {
|
|
109
|
+
groups: HeatmapGroups;
|
|
110
|
+
indices: Uint32Array;
|
|
111
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is under the cursor, and what to say about it.
|
|
3
|
+
*
|
|
4
|
+
* A coloured cloud answers "how is this annotation distributed"; it cannot answer
|
|
5
|
+
* "which class is THAT" — the legend has 34 entries, several of them similar
|
|
6
|
+
* colours, and matching a dot to a swatch by eye is exactly the task a tooltip
|
|
7
|
+
* exists to remove.
|
|
8
|
+
*
|
|
9
|
+
* Pure — no DOM, no camera, no data access. The renderer supplies screen
|
|
10
|
+
* positions (it owns the projection) and the active colour source; everything
|
|
11
|
+
* here is arithmetic and string formatting, so it is tested directly.
|
|
12
|
+
*/
|
|
13
|
+
/** The colour source a tooltip describes, as the renderer already has it. */
|
|
14
|
+
export type HoverSource = {
|
|
15
|
+
kind: 'categorical';
|
|
16
|
+
/** Column name, for the tooltip's first line. */
|
|
17
|
+
name: string;
|
|
18
|
+
categories: readonly string[];
|
|
19
|
+
/** Per-observation category index, or `NO_CATEGORY`. */
|
|
20
|
+
codes: Uint16Array;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'continuous';
|
|
23
|
+
/** Column or gene name. */
|
|
24
|
+
name: string;
|
|
25
|
+
values: Float32Array;
|
|
26
|
+
/** Unit for the value, when the dataset declares one. */
|
|
27
|
+
unit?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Index of the drawn observation nearest `(x, y)` within `maxDist` screen pixels,
|
|
31
|
+
* or -1.
|
|
32
|
+
*
|
|
33
|
+
* `screen` is `[x0, y0, x1, y1, …]` indexed BY OBSERVATION, with NaN for anything
|
|
34
|
+
* not currently drawn — off screen, behind the eye, or on a hidden section. NaN
|
|
35
|
+
* fails every comparison, so those are skipped without a special case.
|
|
36
|
+
*
|
|
37
|
+
* Ties go to the LATER observation, which is the one drawn on top and therefore
|
|
38
|
+
* the one the user believes they are pointing at.
|
|
39
|
+
*/
|
|
40
|
+
export declare function nearestObservation(screen: Float32Array, x: number, y: number, maxDist: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* The tooltip's lines for one observation: what it is, then which column said so.
|
|
43
|
+
*
|
|
44
|
+
* Null when the source cannot describe this observation — a code outside the
|
|
45
|
+
* category list, or a value that was never measured. Saying nothing is better
|
|
46
|
+
* than a tooltip that reads "undefined", and better than inventing a label for a
|
|
47
|
+
* cell whose annotation is genuinely missing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function hoverText(source: HoverSource | null, index: number): string[] | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SpatialObservations } from '../contracts/spatial-dataset.contract';
|
|
2
|
+
/**
|
|
3
|
+
* The planes a **serially sectioned** dataset was actually imaged at.
|
|
4
|
+
*
|
|
5
|
+
* A 3D spatial-omics dataset of this kind is not a cloud that happens to have a z:
|
|
6
|
+
* it is a stack of thin sections, hundreds of microns apart, each one a separate
|
|
7
|
+
* slide. Every observation on a given slide shares that slide's registered z
|
|
8
|
+
* exactly, so the distinct z values ARE the sections — no binning, no tolerance,
|
|
9
|
+
* and no need for a dataset-specific section-label column.
|
|
10
|
+
*
|
|
11
|
+
* That matters for the 3D view, where the cloud reads as a stack of discs and the
|
|
12
|
+
* discs hide the density volumes drawn between them. Being able to show one
|
|
13
|
+
* section's cells against the estimated field is how you check the estimate against
|
|
14
|
+
* the measurement; being able to hide them is how you see the field at all.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a binning of a continuous z (that is
|
|
17
|
+
* `observationsInSlice`'s job, against a volume's voxel planes). If the z is
|
|
18
|
+
* continuous rather than sectioned there are no sections to offer, and
|
|
19
|
+
* {@link sampledSections} says so by returning null rather than inventing some.
|
|
20
|
+
*
|
|
21
|
+
* Pure — no Angular, no GPU — like `spatial-density.ts`, and tested the same way.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Distinct z values beyond which the data is treated as continuous rather than
|
|
25
|
+
* sectioned. Serial sectioning runs to tens or low hundreds of slides (53 for the
|
|
26
|
+
* ABC whole-brain MERFISH atlas); a z that keeps producing new values is a
|
|
27
|
+
* measurement axis, and a "pick one section" control over it would be a lie.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MAX_SAMPLED_SECTIONS = 512;
|
|
30
|
+
/**
|
|
31
|
+
* The dataset's sampled section positions, ascending — or null when it has no z,
|
|
32
|
+
* no observations, or a z too finely divided to be sections at all.
|
|
33
|
+
*
|
|
34
|
+
* Bails out as soon as the distinct count passes `max`, so a continuous z costs one
|
|
35
|
+
* partial pass rather than a set with one entry per observation.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sampledSections(obs: SpatialObservations, max?: number): Float32Array | null;
|
|
38
|
+
/**
|
|
39
|
+
* Indices of the observations lying on one sampled section.
|
|
40
|
+
*
|
|
41
|
+
* Exact equality on purpose: `z` comes from {@link sampledSections}, which read the
|
|
42
|
+
* values out of this same array, so the comparison cannot drift. Passing a z that
|
|
43
|
+
* is not one of the sampled positions yields nothing, which is the honest answer.
|
|
44
|
+
*/
|
|
45
|
+
export declare function observationsInSection(obs: SpatialObservations, z: number): Uint32Array;
|
|
46
|
+
/**
|
|
47
|
+
* {@link sampledSections} for a dataset's observations, computed once.
|
|
48
|
+
*
|
|
49
|
+
* Both the renderer (which filters the cloud's geometry to one section) and the
|
|
50
|
+
* control panel (which offers the sections to pick from) need this list, and the
|
|
51
|
+
* scan reads the z of up to 3.7M observations — so it is memoized on the
|
|
52
|
+
* observations object itself rather than each caller keeping its own copy keyed by
|
|
53
|
+
* a dataset id it has to remember to invalidate. Still referentially transparent:
|
|
54
|
+
* the same observations always give the same sections.
|
|
55
|
+
*
|
|
56
|
+
* Default `max` only; a caller wanting a different cap should call
|
|
57
|
+
* {@link sampledSections} directly and cache it itself.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sectionsOf(obs: SpatialObservations): Float32Array | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Region } from '../models/region';
|
|
2
|
+
import { SpatialImageRef, SpatialObservations } from '../contracts/spatial-dataset.contract';
|
|
3
|
+
/**
|
|
4
|
+
* Which observations fall inside a set of drawn regions.
|
|
5
|
+
*
|
|
6
|
+
* Pure, like `spatial-encoding.ts` — no store, no renderer — so the geometry is
|
|
7
|
+
* testable on its own.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS IS THE WHOLE SELECTION MECHANISM
|
|
10
|
+
* -----------------------------------------
|
|
11
|
+
* Regions already come from every on-canvas tool the library has: rectangle,
|
|
12
|
+
* polygon, freehand, magic wand, brush. Testing observations against them turns
|
|
13
|
+
* all of those into spatial-omics selection tools without adding a single new
|
|
14
|
+
* canvas interaction — which is why there is no bespoke marquee here.
|
|
15
|
+
*
|
|
16
|
+
* COORDINATE FRAMES
|
|
17
|
+
* -----------------
|
|
18
|
+
* Regions are drawn in WORLD (image) coordinates. Observations live in the
|
|
19
|
+
* dataset's own frame and reach the world through `imageRef` — so they are
|
|
20
|
+
* transformed forward before testing, matching exactly what the renderer draws.
|
|
21
|
+
*/
|
|
22
|
+
/** A selection over N observations: `mask[i] === 1` when i is selected. */
|
|
23
|
+
export interface SpatialSelectionMask {
|
|
24
|
+
mask: Uint8Array;
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
/** An empty selection over `count` observations. */
|
|
28
|
+
export declare function emptySelection(count?: number): SpatialSelectionMask;
|
|
29
|
+
/** Number of set entries in a mask. */
|
|
30
|
+
export declare function countMask(mask: Uint8Array): number;
|
|
31
|
+
/** Selected indices, for export or a downstream query. */
|
|
32
|
+
export declare function maskToIndices(mask: Uint8Array): Uint32Array;
|
|
33
|
+
/**
|
|
34
|
+
* Renderer-facing inverse: which observations to MUTE. With nothing selected
|
|
35
|
+
* nothing is muted (the whole tissue reads normally); with a selection, every
|
|
36
|
+
* unselected observation is muted — the CosMx highlight-vs-mute rule.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mutedFromSelection(selection: SpatialSelectionMask): Uint8Array | null;
|
|
39
|
+
/** Even-odd ray cast: is (px, py) inside the closed ring xs/ys? */
|
|
40
|
+
export declare function pointInRing(xs: readonly number[], ys: readonly number[], px: number, py: number): boolean;
|
|
41
|
+
/** Axis-aligned bounds of a ring, for a cheap reject before the ray cast. */
|
|
42
|
+
interface Bounds {
|
|
43
|
+
minX: number;
|
|
44
|
+
minY: number;
|
|
45
|
+
maxX: number;
|
|
46
|
+
maxY: number;
|
|
47
|
+
}
|
|
48
|
+
/** A region flattened to something testable: bounds + a hit test. */
|
|
49
|
+
interface Shape {
|
|
50
|
+
bounds: Bounds;
|
|
51
|
+
hit(px: number, py: number): boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Flatten a region to testable shapes. Returns an empty array for anything that
|
|
55
|
+
* encloses no area — an open polyline, an intensity-profile line, a degenerate
|
|
56
|
+
* rectangle — so those never contribute to a selection.
|
|
57
|
+
*/
|
|
58
|
+
export declare function regionShapes(region: Region): Shape[];
|
|
59
|
+
/**
|
|
60
|
+
* Observations inside ANY of `regions` (union). Coordinates are transformed by
|
|
61
|
+
* `imageRef` first, so the test happens in the same world space the regions were
|
|
62
|
+
* drawn in.
|
|
63
|
+
*
|
|
64
|
+
* `candidates`, when given, restricts the test to those observation indices — how
|
|
65
|
+
* a 2D view of a 3D dataset keeps a drawn region meaning what it looks like it
|
|
66
|
+
* means: the shape was drawn over ONE section, so it selects that section's cells
|
|
67
|
+
* rather than the whole depth of the specimen behind them. The returned mask stays
|
|
68
|
+
* indexed by observation, so every consumer (charts, legend, the 3D cloud) reads it
|
|
69
|
+
* the same way.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* A selection from an explicit list of observation indices.
|
|
73
|
+
*
|
|
74
|
+
* What a lasso in a linked plot produces: the plot knows which points the user drew
|
|
75
|
+
* around, and the map needs that as a mask over ALL observations. Out-of-range and
|
|
76
|
+
* duplicate indices are ignored rather than rejected — a plot may hand back the same
|
|
77
|
+
* point twice, and a stale index from a plot drawn before a dataset change must not
|
|
78
|
+
* throw in the middle of a selection gesture.
|
|
79
|
+
*/
|
|
80
|
+
export declare function selectByIndices(indices: Iterable<number>, observationCount: number): SpatialSelectionMask;
|
|
81
|
+
export declare function selectInRegions(observations: SpatialObservations, imageRef: SpatialImageRef | undefined, regions: readonly Region[], candidates?: Uint32Array): SpatialSelectionMask;
|
|
82
|
+
/**
|
|
83
|
+
* Select observations whose SCREEN projection falls inside the drawn regions.
|
|
84
|
+
*
|
|
85
|
+
* This is how a region selection works in the 3D cloud. There is no data-space
|
|
86
|
+
* affine to map a drawn shape through, because the shape was drawn on a 2D screen
|
|
87
|
+
* against a perspective camera — so the observations come the other way, already
|
|
88
|
+
* projected to canvas pixels by the renderer (which owns the camera), and the
|
|
89
|
+
* regions are tested in that same screen space.
|
|
90
|
+
*
|
|
91
|
+
* The consequence is worth being explicit about: a screen-space lasso selects
|
|
92
|
+
* through the WHOLE DEPTH of the cloud, like a cookie cutter, not a slab at some
|
|
93
|
+
* chosen z. That is inherent to drawing on a flat screen, and it is what napari
|
|
94
|
+
* and every other orbit-camera point picker do. Orbit and draw again to cut from
|
|
95
|
+
* another angle.
|
|
96
|
+
*
|
|
97
|
+
* `screen` is `[x0, y0, x1, y1, …]` in canvas pixels, `count` entries long. An
|
|
98
|
+
* observation the camera puts behind the eye is given NaN by the projector and
|
|
99
|
+
* never selected.
|
|
100
|
+
*/
|
|
101
|
+
export declare function selectInRegionsProjected(screen: Float32Array, count: number, regions: readonly Region[]): SpatialSelectionMask;
|
|
102
|
+
/** Every observation whose categorical code equals `code` — the legend click. */
|
|
103
|
+
/**
|
|
104
|
+
* Whether two selections hold exactly the same observations.
|
|
105
|
+
*
|
|
106
|
+
* Lets a click toggle against the SELECTION rather than against a remembered
|
|
107
|
+
* click: picking a class in the legend and then clicking that same class on the
|
|
108
|
+
* canvas should clear it, and a remembered click cannot know about the legend.
|
|
109
|
+
* Counts are compared first, so two different selections usually cost nothing.
|
|
110
|
+
*/
|
|
111
|
+
export declare function sameSelection(a: SpatialSelectionMask, b: SpatialSelectionMask): boolean;
|
|
112
|
+
export declare function selectByCategory(codes: Uint16Array, code: number): SpatialSelectionMask;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { IImageInfo } from '../contracts/image.contract';
|
|
2
|
+
import { SpatialDataset, SpatialImageRef, SpatialObservations, SpatialVolumeMeta } from '../contracts/spatial-dataset.contract';
|
|
3
|
+
/**
|
|
4
|
+
* A dataset's reference volume, encoded as the z-stack image the 2D path renders,
|
|
5
|
+
* plus the blob URLs backing it.
|
|
6
|
+
*
|
|
7
|
+
* The URLs belong to the CALLER: nothing else can know when the image stops being
|
|
8
|
+
* displayed, and a dataset switch that forgets to revoke them leaks a volume's
|
|
9
|
+
* worth of PNGs per switch.
|
|
10
|
+
*/
|
|
11
|
+
export interface VolumeStackImage {
|
|
12
|
+
info: IImageInfo;
|
|
13
|
+
urls: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Turn a spatial dataset's registered volume into a scrubbable z-stack image.
|
|
17
|
+
*
|
|
18
|
+
* A 3D omics dataset arrives as one volume and NO `imageRef`, so there is no image
|
|
19
|
+
* behind it — which left every image-shaped surface (the Image view, the slice
|
|
20
|
+
* bar, the contrast window, regions) with nothing to work on, and the Image view
|
|
21
|
+
* showing whatever slide happened to be loaded before. Encoding each z plane as a
|
|
22
|
+
* blob PNG makes the volume itself the image, so the existing stack machinery
|
|
23
|
+
* drives it unchanged: the same `urls[z]` + `isStack` shape a host emits for a
|
|
24
|
+
* multi-page TIFF, which is exactly what this volume is.
|
|
25
|
+
*
|
|
26
|
+
* Every plane is encoded up front rather than on demand, because the slice count
|
|
27
|
+
* comes from `urls.length` — the component has to know the depth before the user
|
|
28
|
+
* scrubs. The voxels are already resident (megabytes, fetched in one call), and a
|
|
29
|
+
* plane is a few KB of PNG, so the cost is bounded by the volume the host already
|
|
30
|
+
* chose to fetch.
|
|
31
|
+
*
|
|
32
|
+
* Returns null when the dataset declares no volume, or the byte count contradicts
|
|
33
|
+
* the declared geometry — a short buffer would silently produce slices of shifted
|
|
34
|
+
* anatomy, which is worse than no image at all.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildVolumeStackImage(dataset: SpatialDataset, voxels: Uint8Array): Promise<VolumeStackImage | null>;
|
|
37
|
+
/**
|
|
38
|
+
* The data→image affine a registered volume implies.
|
|
39
|
+
*
|
|
40
|
+
* Once the volume IS the image ({@link buildVolumeStackImage}), one slice is a
|
|
41
|
+
* `width x height` pixel grid whose voxel `(i, j)` covers
|
|
42
|
+
* `[i * voxelSize[0], (i + 1) * voxelSize[0])` of the observations' coordinate
|
|
43
|
+
* space — the contract's rule that the volume's near corner sits at the
|
|
44
|
+
* coordinate origin. So observations reach pixel space by dividing out the voxel
|
|
45
|
+
* size, and nothing has to move.
|
|
46
|
+
*
|
|
47
|
+
* Shaped as a `SpatialImageRef` on purpose: the marker layer and the ROI
|
|
48
|
+
* selection then take the SAME transform they take for a dataset that ships a
|
|
49
|
+
* real `imageRef`, instead of each growing a volume special case.
|
|
50
|
+
*/
|
|
51
|
+
export declare function volumeImageRef(volume: SpatialVolumeMeta, micronsPerUnit?: number): SpatialImageRef;
|
|
52
|
+
/** The voxel plane a coordinate falls in, clamped into the volume. */
|
|
53
|
+
export declare function sliceIndexOf(z: number, volume: SpatialVolumeMeta): number;
|
|
54
|
+
/**
|
|
55
|
+
* Indices of the observations that fall in one voxel plane of the volume.
|
|
56
|
+
*
|
|
57
|
+
* A 2D view of a 3D dataset shows ONE plane, so it must draw one plane's
|
|
58
|
+
* observations: without this the whole depth of the specimen piles onto a single
|
|
59
|
+
* section and reads as a solid smear rather than a section's cells. The plane is
|
|
60
|
+
* a slab `voxelSize[2]` thick, which is exactly the sampling the volume itself
|
|
61
|
+
* has — for serial sections registered into a common frame, one slab is one
|
|
62
|
+
* section.
|
|
63
|
+
*
|
|
64
|
+
* Every observation is in the plane when the dataset carries no z: a genuinely
|
|
65
|
+
* flat dataset over a volume has nothing to filter on.
|
|
66
|
+
*/
|
|
67
|
+
export declare function observationsInSlice(obs: SpatialObservations, volume: SpatialVolumeMeta, slice: number): Uint32Array;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for the t-SNE Web Worker, isolated in its own module so the `import.meta.url`
|
|
3
|
+
* reference — which the bundler needs in order to locate the worker — is never pulled
|
|
4
|
+
* into the ts-jest CommonJS compile of anything that uses it. Unit tests `jest.mock` this
|
|
5
|
+
* module, or pass their own factory. Same arrangement as `mask-worker.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createTsneWorker(): Worker;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How finely a drawn shape's vertices may be placed in world space.
|
|
3
|
+
*
|
|
4
|
+
* Region geometry has always been stored in whole world units, and for an image that is
|
|
5
|
+
* exactly right: world units ARE pixels, a region should align to them, and integers keep
|
|
6
|
+
* the stored geometry exact and tidy.
|
|
7
|
+
*
|
|
8
|
+
* It is wrong for a dataset whose coordinates are not pixels. seqFISH's observations span
|
|
9
|
+
* about 5 x 7 units in total, so snapping vertices to integers leaves roughly SIX BY EIGHT
|
|
10
|
+
* distinct positions in the whole sample — every ROI collapses to the same handful of
|
|
11
|
+
* boxes and drawing a fine shape is not possible at any zoom. Nothing errors; the tool
|
|
12
|
+
* simply cannot express what the user is drawing.
|
|
13
|
+
*
|
|
14
|
+
* So the step is chosen from the world's own extent rather than assumed to be one. An
|
|
15
|
+
* image keeps integers, because its extent is thousands of units and a finer step would
|
|
16
|
+
* buy nothing; a unitless dataset gets a step fine enough to draw with.
|
|
17
|
+
*
|
|
18
|
+
* Pure — no renderer, no store — so both region overlays can share one answer, and the
|
|
19
|
+
* napari and OSD paths cannot drift into disagreeing about what a vertex may be.
|
|
20
|
+
*/
|
|
21
|
+
/** One world unit: the step for a world whose units are image pixels. */
|
|
22
|
+
export declare const PIXEL_WORLD_QUANTUM = 1;
|
|
23
|
+
/**
|
|
24
|
+
* Distinct positions a drawn shape should be able to reach across the world's extent.
|
|
25
|
+
*
|
|
26
|
+
* Set by what a person can actually place with a pointer, not by float precision: a few
|
|
27
|
+
* thousand steps across the sample is finer than any screen it will be drawn on, so the
|
|
28
|
+
* limit stops being the grid and becomes the hand.
|
|
29
|
+
*/
|
|
30
|
+
export declare const WORLD_GRID_STEPS = 4096;
|
|
31
|
+
/**
|
|
32
|
+
* The step size for a world of this extent, for a world whose units are NOT pixels.
|
|
33
|
+
*
|
|
34
|
+
* The caller decides which world it has, because the extent cannot say: a 2,000 unit world
|
|
35
|
+
* is a small slide if the units are pixels and an enormous one if they are microns, and
|
|
36
|
+
* only the caller knows which. An image keeps {@link PIXEL_WORLD_QUANTUM} by never asking.
|
|
37
|
+
*
|
|
38
|
+
* Never returns anything coarser than {@link PIXEL_WORLD_QUANTUM}. A non-pixel world can
|
|
39
|
+
* still be large — the Allen atlas is ~11,000 microns across — and there a finer step than
|
|
40
|
+
* one unit buys nothing.
|
|
41
|
+
*
|
|
42
|
+
* Rounded DOWN to a power of ten, so the step is a number a person reading the stored
|
|
43
|
+
* geometry can recognise — 0.001 rather than 0.0017044 — and so it does not shift when the
|
|
44
|
+
* extent changes slightly between datasets.
|
|
45
|
+
*
|
|
46
|
+
* A non-finite or empty extent falls back to the pixel grid: an unknown world is treated
|
|
47
|
+
* as the conservative, established case rather than given an invented precision.
|
|
48
|
+
*/
|
|
49
|
+
export declare function worldQuantumForExtent(spanX: number, spanY: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Snap one world coordinate to the grid.
|
|
52
|
+
*
|
|
53
|
+
* A step of zero or less means no snapping — the caller keeps full float precision.
|
|
54
|
+
* Division and multiplication by the step leaves a float artefact (0.001 * 3 is not
|
|
55
|
+
* exactly 0.003), so the result is rounded to the step's own number of decimals; without
|
|
56
|
+
* that, "tidy" geometry acquires a trail of digits the grid was supposed to remove.
|
|
57
|
+
*/
|
|
58
|
+
export declare function snapToWorldGrid(value: number, quantum: number): number;
|