@jax-data-science/sci-image-visualizer 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -11
- package/esm2022/index.mjs +20 -1
- package/esm2022/lib/contracts/display-types.mjs +20 -2
- package/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type.mjs +36 -1
- package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
- package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
- package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
- package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
- package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +5 -3
- package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
- package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
- package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
- package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
- package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
- package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
- package/esm2022/lib/routing-visualizer.service.mjs +223 -12
- package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
- package/esm2022/lib/spatial/gene-search.mjs +77 -0
- package/esm2022/lib/spatial/spatial-density.mjs +224 -0
- package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
- package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
- package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
- package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
- package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
- package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
- package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
- package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
- package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
- package/esm2022/lib/spatial/world-grid.mjs +77 -0
- package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
- package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
- package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
- package/esm2022/lib/toolbar/segmentation/cellpose-segmenter.service.mjs +2 -2
- package/esm2022/lib/toolbar/segmentation/sam-model-registry.mjs +9 -9
- package/esm2022/lib/toolbar/toolbar.component.mjs +30 -7
- package/esm2022/lib/visualization.module.mjs +23 -5
- package/esm2022/lib/visualizer.component.mjs +283 -22
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs} +6 -4
- package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-ci1q0xnJ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-BwP-A11m.mjs.map} +1 -1
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
- package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15282 -7829
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/fesm2022/tsne.worker.js +19091 -0
- package/index.d.ts +15 -0
- package/lib/contracts/display-types.d.ts +131 -0
- package/lib/contracts/image.contract.d.ts +7 -0
- package/lib/contracts/plot-type.d.ts +42 -2
- package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
- package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
- package/lib/contracts/visualizer.contract.d.ts +99 -1
- package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
- package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
- package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
- package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
- package/lib/implementations/osd/osd-zoom.d.ts +19 -0
- package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
- package/lib/implementations/plotly/plotly.service.d.ts +9 -0
- package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
- package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
- package/lib/routing-visualizer.service.d.ts +45 -6
- package/lib/spatial/embedding-compute.d.ts +73 -0
- package/lib/spatial/gene-search.d.ts +48 -0
- package/lib/spatial/spatial-density.d.ts +73 -0
- package/lib/spatial/spatial-encoding.d.ts +155 -0
- package/lib/spatial/spatial-expression.d.ts +144 -0
- package/lib/spatial/spatial-framing.d.ts +37 -0
- package/lib/spatial/spatial-heatmap.d.ts +111 -0
- package/lib/spatial/spatial-hover.d.ts +49 -0
- package/lib/spatial/spatial-sections.d.ts +59 -0
- package/lib/spatial/spatial-selection.d.ts +113 -0
- package/lib/spatial/spatial-volume-image.d.ts +67 -0
- package/lib/spatial/tsne-worker.d.ts +7 -0
- package/lib/spatial/world-grid.d.ts +58 -0
- package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
- package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
- package/lib/store/spatial-selection.service.d.ts +27 -0
- package/lib/store/visualizer-store.service.d.ts +13 -0
- package/lib/toolbar/toolbar.component.d.ts +19 -3
- package/lib/visualization.module.d.ts +28 -24
- package/lib/visualizer.component.d.ts +73 -2
- package/package.json +7 -3
- package/src/lib/assets/spatial-omics-3d.svg +31 -0
- package/src/lib/assets/spatial-omics.svg +27 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SpatialDataset, SpatialObservations } from '../contracts/spatial-dataset.contract';
|
|
2
|
+
/**
|
|
3
|
+
* Turning a cell cloud into a density field — the honest way to render a cluster
|
|
4
|
+
* in 3D when the sections it was measured on are hundreds of microns apart.
|
|
5
|
+
*
|
|
6
|
+
* Individual cells CANNOT be interpolated between sections: consecutive sections
|
|
7
|
+
* sample entirely different cells, so there is no correspondence to interpolate
|
|
8
|
+
* along, and inventing positions would fabricate observations indistinguishable
|
|
9
|
+
* from measured ones. A density field is a different object: a continuous
|
|
10
|
+
* estimate, legitimately defined between the planes that were imaged, and it
|
|
11
|
+
* renders as a translucent cloud rather than as dots, so it reads as an estimate.
|
|
12
|
+
*
|
|
13
|
+
* Two things make the estimate honest rather than decorative, and both are
|
|
14
|
+
* implemented here:
|
|
15
|
+
* - the kernel is **anisotropic**, with a z bandwidth at least the section
|
|
16
|
+
* spacing. An isotropic kernel leaves one disc per section — a sampling
|
|
17
|
+
* artefact that looks like biology;
|
|
18
|
+
* - the field is **coverage-normalised** along z (Nadaraya–Watson): unsampled
|
|
19
|
+
* planes would otherwise read as genuinely empty tissue rather than as tissue
|
|
20
|
+
* nobody imaged.
|
|
21
|
+
*/
|
|
22
|
+
/** The voxel lattice a density field is estimated on. */
|
|
23
|
+
export interface DensityGrid {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
depth: number;
|
|
27
|
+
/** World size of one voxel per axis, in the observations' units. */
|
|
28
|
+
voxelSize: [number, number, number];
|
|
29
|
+
}
|
|
30
|
+
export interface DensityOptions {
|
|
31
|
+
/** Kernel σ per axis, in the observations' units. */
|
|
32
|
+
sigma: [number, number, number];
|
|
33
|
+
/** Observations to include. Every observation when absent. */
|
|
34
|
+
indices?: Uint32Array;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The lattice to estimate on.
|
|
38
|
+
*
|
|
39
|
+
* A dataset with a registered volume estimates on that volume's own box, so the
|
|
40
|
+
* field lands exactly on the anatomy it will be drawn inside — `stride` coarsens
|
|
41
|
+
* it (density is smooth by construction, and an eighth of the voxels is an eighth
|
|
42
|
+
* of the work) while keeping the physical extent identical, so the two still
|
|
43
|
+
* align. Without a volume the box comes from the observations' own bounds.
|
|
44
|
+
*/
|
|
45
|
+
export declare function densityGrid(dataset: SpatialDataset, stride?: number, targetLongAxis?: number, zStride?: number): DensityGrid | null;
|
|
46
|
+
/**
|
|
47
|
+
* In-place separable Gaussian along one axis of a w x h x d field.
|
|
48
|
+
*
|
|
49
|
+
* Exported because the gene-map volume estimates a different quantity on the same
|
|
50
|
+
* lattice with the same kernel (see `spatial-expression.ts`): two copies of a
|
|
51
|
+
* separable blur would be two chances to smooth the estimate and its normaliser
|
|
52
|
+
* differently, which is the one thing a Nadaraya-Watson field cannot survive.
|
|
53
|
+
*/
|
|
54
|
+
export declare function blurVolumeAxis(field: Float32Array, w: number, h: number, d: number, axis: 0 | 1 | 2, sigmaVox: number): void;
|
|
55
|
+
/**
|
|
56
|
+
* Rasterise observations into a uint8 density field on `grid`.
|
|
57
|
+
*
|
|
58
|
+
* Returned as uint8 scaled to its own maximum: a `VolumeLayer` renders 0..255, and
|
|
59
|
+
* the absolute cell count per voxel is not the readable quantity anyway — where
|
|
60
|
+
* the cluster is dense RELATIVE to itself is. Null when nothing lands on the grid,
|
|
61
|
+
* so the caller draws no layer rather than an empty box.
|
|
62
|
+
*/
|
|
63
|
+
export declare function rasterizeDensity(obs: SpatialObservations, grid: DensityGrid, opts: DensityOptions): Uint8Array | null;
|
|
64
|
+
/**
|
|
65
|
+
* Default kernel for a grid: in-plane tight, along z at least the section
|
|
66
|
+
* spacing.
|
|
67
|
+
*
|
|
68
|
+
* The z term is the load-bearing one — 1.5 voxels of a grid whose depth voxel IS
|
|
69
|
+
* the section spacing puts σ just above one section gap, which is the smallest
|
|
70
|
+
* bandwidth that can bridge one without leaving a disc per plane. `smoothing`
|
|
71
|
+
* scales all three for the user control.
|
|
72
|
+
*/
|
|
73
|
+
export declare function defaultSigma(grid: DensityGrid, smoothing?: number): [number, number, number];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Rgb } from '../contracts/colormap-lut';
|
|
2
|
+
import { CategoricalColumnMeta, SpatialObservations } from '../contracts/spatial-dataset.contract';
|
|
3
|
+
import { ColormapValue } from '../contracts/display-types';
|
|
4
|
+
/**
|
|
5
|
+
* Turning spatial-omics columns into per-point visual attributes.
|
|
6
|
+
*
|
|
7
|
+
* Pure — no Angular, no RxJS, no renderer — like `plotly-trace-builders.ts`, so
|
|
8
|
+
* the encodings are testable on their own and shared by any backend that grows
|
|
9
|
+
* a spatial mode.
|
|
10
|
+
*
|
|
11
|
+
* The rules here are the ones the CosMx analysis guidance prescribes, made
|
|
12
|
+
* explicit rather than left to each call site:
|
|
13
|
+
* - count-like columns read on a **log** scale (linear collapses them against a
|
|
14
|
+
* few bright outliers);
|
|
15
|
+
* - contrast windows come from **percentiles**, not min/max, so a single
|
|
16
|
+
* saturated spot doesn't flatten everything else;
|
|
17
|
+
* - context is kept by **muting** rather than hiding — background points stay
|
|
18
|
+
* visible at low alpha so the tissue shape survives.
|
|
19
|
+
*
|
|
20
|
+
* COLOUR REPRESENTATION
|
|
21
|
+
* ---------------------
|
|
22
|
+
* Internally everything is a flat `Float32Array` of `[r,g,b,a, …]` in 0..1 —
|
|
23
|
+
* one allocation for N points, GPU-upload shaped. napari-js 0.12.0's
|
|
24
|
+
* `PointsLayer.faceColor` takes `RGBA[]` (an array of 4-tuples) instead, so
|
|
25
|
+
* {@link toRgbaTuples} adapts at the boundary; when the layer accepts a typed
|
|
26
|
+
* array the adapter goes away and the flat buffer is passed straight through.
|
|
27
|
+
*/
|
|
28
|
+
/** napari-js per-point colour tuple: r, g, b, a in 0..1. */
|
|
29
|
+
export type RGBA = [number, number, number, number];
|
|
30
|
+
/** Neutral grey for observations with no value (NaN, or {@link NO_CATEGORY}). */
|
|
31
|
+
export declare const MISSING_COLOR: Rgb;
|
|
32
|
+
/** Default alpha for muted (background / unselected) points — CosMx-style context. */
|
|
33
|
+
export declare const DEFAULT_MUTED_OPACITY = 0.15;
|
|
34
|
+
/**
|
|
35
|
+
* Largest number of CATEGORIES whose colours survive the 3D points layer's LUT
|
|
36
|
+
* intact.
|
|
37
|
+
*
|
|
38
|
+
* That layer has no per-point RGBA — it maps a per-point SCALAR through a
|
|
39
|
+
* 256-entry colormap LUT — so a categorical palette is encoded as one contiguous
|
|
40
|
+
* block of LUT entries per value. Measured against napari-js, every K in 2..96
|
|
41
|
+
* blocks round-trips exactly and K=97 is the first that does not: 256 texels
|
|
42
|
+
* cannot keep more values apart than that.
|
|
43
|
+
*
|
|
44
|
+
* One of those 96 blocks is spent on {@link NO_CATEGORY}, which every column can
|
|
45
|
+
* contain, so what fits is **95 categories** — the number published here. It was
|
|
46
|
+
* 96 while the encoder rejected at 96, which meant a 96-category column drew flat
|
|
47
|
+
* with the panel showing no warning: the one failure this ceiling exists to
|
|
48
|
+
* prevent, arrived at from the other direction.
|
|
49
|
+
*
|
|
50
|
+
* Above it the cloud draws FLAT rather than draw a lie, because subtly wrong
|
|
51
|
+
* category colours under a confident legend is the failure nobody catches.
|
|
52
|
+
*
|
|
53
|
+
* A limit of that one layer, and of nothing else: the 2D markers carry per-point
|
|
54
|
+
* RGBA, and a density volume is a scalar field per cluster. Shared here so the
|
|
55
|
+
* renderer that enforces it and the panel that explains it cannot drift apart.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SPATIAL_3D_MAX_CATEGORIES = 95;
|
|
58
|
+
/** LUT blocks the 3D layer can keep apart — {@link SPATIAL_3D_MAX_CATEGORIES}
|
|
59
|
+
* plus the one reserved for a missing value. */
|
|
60
|
+
export declare const SPATIAL_3D_LUT_BLOCKS: number;
|
|
61
|
+
/**
|
|
62
|
+
* Fallback categorical palette. Colour-blind-safe and stable, so a category
|
|
63
|
+
* without an authored colour still lands somewhere sensible and lands there
|
|
64
|
+
* every run (`fallbackColorFor` hashes the name — same name, same colour).
|
|
65
|
+
* Okabe–Ito, plus two greys for long tails.
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_CATEGORICAL_PALETTE: string[];
|
|
68
|
+
interface MuteOptions {
|
|
69
|
+
/** Alpha for points that are not muted. Default 1. */
|
|
70
|
+
opacity?: number;
|
|
71
|
+
/** Alpha for muted points. Default {@link DEFAULT_MUTED_OPACITY}. */
|
|
72
|
+
mutedOpacity?: number;
|
|
73
|
+
/** `1` at index i mutes observation i. Absent ⇒ nothing is muted. */
|
|
74
|
+
muted?: Uint8Array | null;
|
|
75
|
+
}
|
|
76
|
+
export interface CategoricalEncodingOptions extends MuteOptions {
|
|
77
|
+
/** Per-category `#rrggbb`, index-aligned with the column's categories. */
|
|
78
|
+
colors: string[];
|
|
79
|
+
/** Colour for {@link NO_CATEGORY}. Default {@link MISSING_COLOR}. */
|
|
80
|
+
missingColor?: Rgb;
|
|
81
|
+
}
|
|
82
|
+
export interface ContinuousEncodingOptions extends MuteOptions {
|
|
83
|
+
/** 256-entry RGB table from {@link buildColormapLut}. */
|
|
84
|
+
lut: Rgb[];
|
|
85
|
+
/** Contrast window in VALUE units (pre-log). */
|
|
86
|
+
min: number;
|
|
87
|
+
max: number;
|
|
88
|
+
/** Apply `log1p` to the value and the window before normalising. */
|
|
89
|
+
log?: boolean;
|
|
90
|
+
/** Colour for `NaN`. Default {@link MISSING_COLOR}. */
|
|
91
|
+
missingColor?: Rgb;
|
|
92
|
+
}
|
|
93
|
+
/** Parse `#rgb` / `#rrggbb` to 0–255 RGB; unparseable input falls back to grey.
|
|
94
|
+
* Exported so the 3D LUT path parses palettes exactly as the 2D path does. */
|
|
95
|
+
export declare function parseHex(hex: string): Rgb;
|
|
96
|
+
/**
|
|
97
|
+
* Per-category colours: the column's authored `colors` where present (so the
|
|
98
|
+
* viewer matches the figures the same analysis produced elsewhere), otherwise
|
|
99
|
+
* the palette by position, wrapping through a name-hash past its end so a
|
|
100
|
+
* 40-cluster column still gets stable, distinct-ish colours.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveCategoryColors(meta: CategoricalColumnMeta, palette?: string[]): string[];
|
|
103
|
+
/** Categorical column → flat per-point RGBA (length `4 * codes.length`). */
|
|
104
|
+
export declare function encodeCategorical(codes: Uint16Array, opts: CategoricalEncodingOptions): Float32Array;
|
|
105
|
+
/** Continuous column / gene vector → flat per-point RGBA. */
|
|
106
|
+
export declare function encodeContinuous(values: Float32Array, opts: ContinuousEncodingOptions): Float32Array;
|
|
107
|
+
/**
|
|
108
|
+
* Contrast window from percentiles of the finite values — the CosMx
|
|
109
|
+
* "cap outliers at percentiles to improve contrast" rule. `lo`/`hi` are
|
|
110
|
+
* fractions (0.01 / 0.99 clips the extreme 1% at each end).
|
|
111
|
+
*
|
|
112
|
+
* Returns `[0, 1]` for an empty or all-missing vector so callers always get a
|
|
113
|
+
* usable window.
|
|
114
|
+
*/
|
|
115
|
+
export declare function contrastWindow(values: Float32Array, lo?: number, hi?: number): [number, number];
|
|
116
|
+
/**
|
|
117
|
+
* Marker sizes for the points layer. napari-js `PointsLayer.size` is a
|
|
118
|
+
* **diameter** in data units while `SpatialObservations.radius` is a radius —
|
|
119
|
+
* the doubling lives here so no call site has to remember it.
|
|
120
|
+
*
|
|
121
|
+
* `fallbackRadius` covers a dataset that declares no radius (segmented cells
|
|
122
|
+
* often don't); it is in image pixels, like everything else in this space.
|
|
123
|
+
*/
|
|
124
|
+
export declare function markerDiameters(observations: SpatialObservations, fallbackRadius?: number): number | Float32Array;
|
|
125
|
+
/**
|
|
126
|
+
* Adapt a flat RGBA buffer to the `RGBA[]` shape napari-js 0.12.0's
|
|
127
|
+
* `PointsLayer.faceColor` accepts. One 4-element array per point — the reason
|
|
128
|
+
* this is a named boundary rather than inlined: it is the one place the model
|
|
129
|
+
* stops being typed-array shaped, and it is what a future napari-js taking a
|
|
130
|
+
* `Float32Array` would delete.
|
|
131
|
+
*/
|
|
132
|
+
export declare function toRgbaTuples(flat: Float32Array): RGBA[];
|
|
133
|
+
/** Convenience: a 256-entry LUT for a colormap value, falling back to Viridis. */
|
|
134
|
+
export declare function lutFor(colormapValue: unknown, reverse?: boolean): Rgb[];
|
|
135
|
+
/** True when every entry of a LUT is a neutral grey. Tested on the VALUES rather
|
|
136
|
+
* than the colormap's name, so a reversed or renamed grey ramp still counts. */
|
|
137
|
+
export declare function isGrayscaleLut(lut: readonly Rgb[]): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* LUT for a spatial CONTINUOUS encoding — a gene, a numeric column.
|
|
140
|
+
*
|
|
141
|
+
* `override` is the caller's explicit choice and wins outright, grey included: a
|
|
142
|
+
* grey ramp picked FOR the data is a decision, not the image's colormap leaking
|
|
143
|
+
* in.
|
|
144
|
+
*
|
|
145
|
+
* Without one, the display colormap — unless that colormap is grayscale. The
|
|
146
|
+
* tissue underneath is grayscale, so a grey overlay of a measurement is
|
|
147
|
+
* indistinguishable from the anatomy it is drawn over, and the reader cannot tell
|
|
148
|
+
* which channel a bright pixel belongs to. Viridis is then the fallback, which is
|
|
149
|
+
* what the CosMx guidance prescribes for continuous values anyway.
|
|
150
|
+
*
|
|
151
|
+
* Shared by the markers, both gene maps and the panel's colour bar, so none of
|
|
152
|
+
* them can disagree about what a colour means, whichever way the choice falls.
|
|
153
|
+
*/
|
|
154
|
+
export declare function spatialContinuousLut(colormapValue: unknown, reverse?: boolean, override?: ColormapValue | null): Rgb[];
|
|
155
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { SpatialImageRef, SpatialObservations } from '../contracts/spatial-dataset.contract';
|
|
2
|
+
import { DensityGrid } from './spatial-density';
|
|
3
|
+
/**
|
|
4
|
+
* Turning per-cell expression into a **gene map** — a continuous field over the
|
|
5
|
+
* tissue, drawn under the cells rather than instead of them.
|
|
6
|
+
*
|
|
7
|
+
* A scatter coloured by a gene answers "which cells express it"; it does not answer
|
|
8
|
+
* "where is it expressed", because the eye cannot integrate thousands of small dots
|
|
9
|
+
* into a territory. A field can be estimated between the cells and read as a
|
|
10
|
+
* region, which is what the CosMx guidance and Spatial-Live's heat layer both do.
|
|
11
|
+
*
|
|
12
|
+
* The quantity is the kernel-weighted **MEAN per cell**, not a sum:
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* mean(p) = Σᵢ w(p, xᵢ)·eᵢ / Σᵢ w(p, xᵢ)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* A sum would conflate "many cells here" with "high expression here" — a dense
|
|
19
|
+
* region would glow whatever its cells were doing, which is the single easiest way
|
|
20
|
+
* to read a gene map wrongly. The denominator is also what makes emptiness
|
|
21
|
+
* expressible: where no cell was measured the mean is *undefined*, not zero, so the
|
|
22
|
+
* caller gets a `support` channel and can draw nothing there instead of drawing the
|
|
23
|
+
* colormap's low end over unmeasured tissue.
|
|
24
|
+
*
|
|
25
|
+
* Pure — no Angular, no GPU, no colour — like `spatial-density.ts`, and tested the
|
|
26
|
+
* same way.
|
|
27
|
+
*/
|
|
28
|
+
export interface ExpressionFieldOptions {
|
|
29
|
+
/** Data→image affine, so the field lands in the displayed image's pixels. */
|
|
30
|
+
ref?: SpatialImageRef;
|
|
31
|
+
/** Raster size in field pixels. */
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
/** Image pixels per field pixel — the field is coarser than the slide it covers. */
|
|
35
|
+
step: number;
|
|
36
|
+
/** Kernel σ in FIELD pixels. */
|
|
37
|
+
sigma: number;
|
|
38
|
+
/** One expression value per observation. */
|
|
39
|
+
values: Float32Array;
|
|
40
|
+
/** Observations to include; every one when absent (a plane, a selection). */
|
|
41
|
+
indices?: Uint32Array;
|
|
42
|
+
}
|
|
43
|
+
export interface ExpressionField {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
/** Kernel-weighted mean expression, valid only where `support > 0`. */
|
|
47
|
+
mean: Float32Array;
|
|
48
|
+
/** Kernel-weighted cell count per field pixel — 0 where nothing was measured. */
|
|
49
|
+
support: Float32Array;
|
|
50
|
+
/** Median positive support: the scale at which a pixel is "properly sampled",
|
|
51
|
+
* and a data-driven saturation point for an opacity ramp. */
|
|
52
|
+
supportScale: number;
|
|
53
|
+
/** Finite range of `mean` over the supported pixels, for a contrast window. */
|
|
54
|
+
range: [number, number];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Estimate a gene's expression field over the image the observations sit in.
|
|
58
|
+
*
|
|
59
|
+
* Null when nothing lands on the raster — the caller then draws no layer, rather
|
|
60
|
+
* than an empty one the reader has to interpret.
|
|
61
|
+
*/
|
|
62
|
+
export declare function expressionField(obs: SpatialObservations, opts: ExpressionFieldOptions): ExpressionField | null;
|
|
63
|
+
/**
|
|
64
|
+
* Colour a field into RGBA8 for a `channels: 4` image source.
|
|
65
|
+
*
|
|
66
|
+
* Alpha comes from the SUPPORT, ramped to the field's own median support rather
|
|
67
|
+
* than a constant: a pixel backed by one distant cell is faint, a properly sampled
|
|
68
|
+
* one is solid, and unmeasured tissue is fully transparent — so the map never
|
|
69
|
+
* asserts a measurement where there was none. `opacity` scales the whole layer on
|
|
70
|
+
* top of that.
|
|
71
|
+
*/
|
|
72
|
+
export declare function colorExpressionField(field: ExpressionField, lut: readonly (readonly [number, number, number])[], window: [number, number], opts?: {
|
|
73
|
+
log?: boolean;
|
|
74
|
+
opacity?: number;
|
|
75
|
+
}): Uint8Array;
|
|
76
|
+
/**
|
|
77
|
+
* The same estimator in 3D: a gene's expression over a serially sectioned
|
|
78
|
+
* specimen, on a {@link DensityGrid} lattice.
|
|
79
|
+
*
|
|
80
|
+
* Two modes, and the difference between them is the difference between a
|
|
81
|
+
* measurement and an estimate:
|
|
82
|
+
*
|
|
83
|
+
* - **sheets** (`interpolate` unset) blurs in-plane only, so exactly the z planes
|
|
84
|
+
* that hold cells are filled and the gaps between sections stay empty. This is
|
|
85
|
+
* the measured sections, stacked — the 2D gene map of each slide, at its own z.
|
|
86
|
+
* - **volume** (`interpolate` set) also smooths along z, so the planes between
|
|
87
|
+
* the sections carry an interpolated value. Smoothing the numerator and the
|
|
88
|
+
* denominator together is what makes that honest: it is Nadaraya-Watson, so an
|
|
89
|
+
* unimaged plane inherits its neighbours' MEAN rather than their sum thinned
|
|
90
|
+
* over a gap nobody sampled. Outside the sampled range the field is zeroed —
|
|
91
|
+
* "smoothed between the imaged sections" must not put expression in front of
|
|
92
|
+
* the first section or behind the last.
|
|
93
|
+
*
|
|
94
|
+
* Individual cells are still never interpolated: consecutive sections sample
|
|
95
|
+
* different cells, so there is nothing to interpolate along. What is interpolated
|
|
96
|
+
* is a field, which is legitimately defined between the planes.
|
|
97
|
+
*/
|
|
98
|
+
export interface ExpressionVolumeOptions {
|
|
99
|
+
/** Kernel σ per axis, in the observations' own units. The z term is used only
|
|
100
|
+
* when {@link interpolate} is set. */
|
|
101
|
+
sigma: [number, number, number];
|
|
102
|
+
/** One expression value per observation. */
|
|
103
|
+
values: Float32Array;
|
|
104
|
+
/** Observations to include; every one when absent — one section, or a selection. */
|
|
105
|
+
indices?: Uint32Array;
|
|
106
|
+
/** Smooth along z into a continuous estimate rather than leaving the measured
|
|
107
|
+
* sheets isolated at their own planes. */
|
|
108
|
+
interpolate?: boolean;
|
|
109
|
+
}
|
|
110
|
+
export interface ExpressionVolumeField {
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
depth: number;
|
|
114
|
+
/** Kernel-weighted mean expression, valid only where `support > 0`. */
|
|
115
|
+
mean: Float32Array;
|
|
116
|
+
/** Kernel-weighted cell count per voxel — 0 where nothing was measured. */
|
|
117
|
+
support: Float32Array;
|
|
118
|
+
/** Finite range of `mean` over the supported voxels, for a contrast window. */
|
|
119
|
+
range: [number, number];
|
|
120
|
+
}
|
|
121
|
+
export declare function expressionVolume(obs: SpatialObservations, grid: DensityGrid, opts: ExpressionVolumeOptions): ExpressionVolumeField | null;
|
|
122
|
+
/**
|
|
123
|
+
* Encode a field into the uint8 scalar a `VolumeLayer` renders, windowed the same
|
|
124
|
+
* way the 2D map and the markers are so the three cannot disagree.
|
|
125
|
+
*
|
|
126
|
+
* `floor` lifts every MEASURED voxel to at least this fraction of full scale, so
|
|
127
|
+
* that "the gene is not here" can be told from "nobody sectioned here" — the
|
|
128
|
+
* distinction the 2D map keeps with its separate support channel, which a scalar
|
|
129
|
+
* volume cannot express because its raymarch takes alpha from the same number it
|
|
130
|
+
* takes colour from.
|
|
131
|
+
*
|
|
132
|
+
* It defaults to OFF, because in a volume that trade does not pay. A ray crosses
|
|
133
|
+
* tens of measured planes and the DVR composites every one of them, so a floor
|
|
134
|
+
* large enough to see on a single plane saturates to an opaque shell over the
|
|
135
|
+
* whole specimen and buries the signal it was meant to contextualise, while one
|
|
136
|
+
* small enough to stay faint is already invisible per plane. The measured envelope
|
|
137
|
+
* is better read from the anatomy layer and the observations, which are drawn in
|
|
138
|
+
* the same scene — and the surrounding density volumes take the same no-floor
|
|
139
|
+
* approach, so the scene stays internally consistent.
|
|
140
|
+
*/
|
|
141
|
+
export declare function encodeExpressionVolume(field: ExpressionVolumeField, window: [number, number], opts?: {
|
|
142
|
+
log?: boolean;
|
|
143
|
+
floor?: number;
|
|
144
|
+
}): Uint8Array;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framing a 2D scatter on its own coordinates.
|
|
3
|
+
*
|
|
4
|
+
* The 2D spatial camera is normally fitted to the IMAGE, because the observations are recorded in
|
|
5
|
+
* that image's pixel space — frame the image and they come with it. A dataset with no reference
|
|
6
|
+
* image has coordinates in whatever units its source used: seqFISH's span about 5 x 7. Left with
|
|
7
|
+
* the previous image's framing, such a dataset lands as a handful of pixels far off-centre, which
|
|
8
|
+
* looks exactly like nothing having loaded rather than like a framing problem.
|
|
9
|
+
*
|
|
10
|
+
* Pure, like `spatial-density.ts` and the rest of this directory — the renderer applies the result.
|
|
11
|
+
*/
|
|
12
|
+
/** Fraction of the viewport the coordinates are fitted into, so the outermost markers are not
|
|
13
|
+
* flush against the edge. Matches napari-js's own fit margin. */
|
|
14
|
+
export declare const SPATIAL_FIT_MARGIN = 0.95;
|
|
15
|
+
export interface SpatialFraming {
|
|
16
|
+
/** World point to put at the centre of the viewport. */
|
|
17
|
+
center: [number, number];
|
|
18
|
+
/** World units per pixel, inverted — the camera's `zoom`. */
|
|
19
|
+
zoom: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Camera centre and zoom that fit `positions` into a `viewportW` x `viewportH` box.
|
|
23
|
+
*
|
|
24
|
+
* `positions` is interleaved `[x0, y0, x1, y1, …]`, the layout the points layer takes.
|
|
25
|
+
*
|
|
26
|
+
* Null when there is nothing to fit — no points, a viewport with no area, or coordinates that are
|
|
27
|
+
* entirely non-finite. Null means LEAVE THE CAMERA ALONE: a caller that substituted a default
|
|
28
|
+
* would move the view for a dataset it cannot frame, which is worse than not moving it.
|
|
29
|
+
*
|
|
30
|
+
* A degenerate extent — one point, or every point on a line — yields a centre but no zoom, since
|
|
31
|
+
* there is no span to divide by. The caller keeps its current zoom and just recentres, rather than
|
|
32
|
+
* dividing by zero into an infinite one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function framePositions(positions: Float32Array, viewportW: number, viewportH: number, margin?: number): SpatialFraming | {
|
|
35
|
+
center: [number, number];
|
|
36
|
+
zoom: null;
|
|
37
|
+
} | null;
|
|
@@ -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;
|