@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.
Files changed (93) hide show
  1. package/README.md +215 -11
  2. package/esm2022/index.mjs +20 -1
  3. package/esm2022/lib/contracts/display-types.mjs +20 -2
  4. package/esm2022/lib/contracts/image.contract.mjs +1 -1
  5. package/esm2022/lib/contracts/plot-type.mjs +36 -1
  6. package/esm2022/lib/contracts/ports/spatial-data.port.mjs +3 -0
  7. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +45 -0
  8. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  9. package/esm2022/lib/implementations/napari-js/napari-axes-labels.mjs +10 -19
  10. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +71 -7
  11. package/esm2022/lib/implementations/napari-js/napari-scale-bar.mjs +1 -1
  12. package/esm2022/lib/implementations/napari-js/napari-spatial-tooltip.mjs +96 -0
  13. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +1934 -55
  14. package/esm2022/lib/implementations/napari-js/napari-zoom.mjs +18 -0
  15. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +5 -3
  16. package/esm2022/lib/implementations/osd/osd-region-overlay.mjs +3 -4
  17. package/esm2022/lib/implementations/osd/osd-zoom.mjs +20 -0
  18. package/esm2022/lib/implementations/plotly/omics-trace-builders.mjs +555 -0
  19. package/esm2022/lib/implementations/plotly/plotly.service.mjs +13 -7
  20. package/esm2022/lib/implementations/spatial-data-http/spatial-data-http.service.mjs +304 -0
  21. package/esm2022/lib/implementations/spatial-data-http/spatial-wire.mjs +179 -0
  22. package/esm2022/lib/routing-visualizer.service.mjs +223 -12
  23. package/esm2022/lib/spatial/embedding-compute.mjs +142 -0
  24. package/esm2022/lib/spatial/gene-search.mjs +77 -0
  25. package/esm2022/lib/spatial/spatial-density.mjs +224 -0
  26. package/esm2022/lib/spatial/spatial-encoding.mjs +232 -0
  27. package/esm2022/lib/spatial/spatial-expression.mjs +257 -0
  28. package/esm2022/lib/spatial/spatial-framing.mjs +60 -0
  29. package/esm2022/lib/spatial/spatial-heatmap.mjs +185 -0
  30. package/esm2022/lib/spatial/spatial-hover.mjs +79 -0
  31. package/esm2022/lib/spatial/spatial-sections.mjs +96 -0
  32. package/esm2022/lib/spatial/spatial-selection.mjs +264 -0
  33. package/esm2022/lib/spatial/spatial-volume-image.mjs +163 -0
  34. package/esm2022/lib/spatial/tsne-worker.mjs +10 -0
  35. package/esm2022/lib/spatial/world-grid.mjs +77 -0
  36. package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +1278 -0
  37. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +621 -0
  38. package/esm2022/lib/store/spatial-selection.service.mjs +47 -0
  39. package/esm2022/lib/store/visualizer-store.service.mjs +23 -1
  40. package/esm2022/lib/toolbar/segmentation/cellpose-segmenter.service.mjs +2 -2
  41. package/esm2022/lib/toolbar/segmentation/sam-model-registry.mjs +9 -9
  42. package/esm2022/lib/toolbar/toolbar.component.mjs +30 -7
  43. package/esm2022/lib/visualization.module.mjs +23 -5
  44. package/esm2022/lib/visualizer.component.mjs +283 -22
  45. 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
  46. 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
  47. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs +12 -0
  48. package/fesm2022/jax-data-science-sci-image-visualizer-tsne-worker-B_nXDGf0.mjs.map +1 -0
  49. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +15282 -7829
  50. package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
  51. package/fesm2022/tsne.worker.js +19091 -0
  52. package/index.d.ts +15 -0
  53. package/lib/contracts/display-types.d.ts +131 -0
  54. package/lib/contracts/image.contract.d.ts +7 -0
  55. package/lib/contracts/plot-type.d.ts +42 -2
  56. package/lib/contracts/ports/spatial-data.port.d.ts +90 -0
  57. package/lib/contracts/spatial-dataset.contract.d.ts +281 -0
  58. package/lib/contracts/visualizer.contract.d.ts +99 -1
  59. package/lib/implementations/napari-js/napari-axes-labels.d.ts +9 -7
  60. package/lib/implementations/napari-js/napari-region-overlay.d.ts +38 -4
  61. package/lib/implementations/napari-js/napari-scale-bar.d.ts +7 -3
  62. package/lib/implementations/napari-js/napari-spatial-tooltip.d.ts +32 -0
  63. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +497 -16
  64. package/lib/implementations/napari-js/napari-zoom.d.ts +15 -0
  65. package/lib/implementations/osd/osd-zoom.d.ts +19 -0
  66. package/lib/implementations/plotly/omics-trace-builders.d.ts +193 -0
  67. package/lib/implementations/plotly/plotly.service.d.ts +9 -0
  68. package/lib/implementations/spatial-data-http/spatial-data-http.service.d.ts +123 -0
  69. package/lib/implementations/spatial-data-http/spatial-wire.d.ts +115 -0
  70. package/lib/routing-visualizer.service.d.ts +45 -6
  71. package/lib/spatial/embedding-compute.d.ts +73 -0
  72. package/lib/spatial/gene-search.d.ts +48 -0
  73. package/lib/spatial/spatial-density.d.ts +73 -0
  74. package/lib/spatial/spatial-encoding.d.ts +155 -0
  75. package/lib/spatial/spatial-expression.d.ts +144 -0
  76. package/lib/spatial/spatial-framing.d.ts +37 -0
  77. package/lib/spatial/spatial-heatmap.d.ts +111 -0
  78. package/lib/spatial/spatial-hover.d.ts +49 -0
  79. package/lib/spatial/spatial-sections.d.ts +59 -0
  80. package/lib/spatial/spatial-selection.d.ts +113 -0
  81. package/lib/spatial/spatial-volume-image.d.ts +67 -0
  82. package/lib/spatial/tsne-worker.d.ts +7 -0
  83. package/lib/spatial/world-grid.d.ts +58 -0
  84. package/lib/spatial-controls/spatial-charts/spatial-charts.component.d.ts +439 -0
  85. package/lib/spatial-controls/spatial-controls.component.d.ts +257 -0
  86. package/lib/store/spatial-selection.service.d.ts +27 -0
  87. package/lib/store/visualizer-store.service.d.ts +13 -0
  88. package/lib/toolbar/toolbar.component.d.ts +19 -3
  89. package/lib/visualization.module.d.ts +28 -24
  90. package/lib/visualizer.component.d.ts +73 -2
  91. package/package.json +7 -3
  92. package/src/lib/assets/spatial-omics-3d.svg +31 -0
  93. package/src/lib/assets/spatial-omics.svg +27 -0
@@ -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;