@vitessce/spatial-utils 2.0.2 → 2.0.3

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.
@@ -0,0 +1,11 @@
1
+ import { B as BaseDecoder } from "./index.75f0a489.mjs";
2
+ import "@vitessce/vit-s";
3
+ import "react";
4
+ class RawDecoder extends BaseDecoder {
5
+ decodeBlock(buffer) {
6
+ return buffer;
7
+ }
8
+ }
9
+ export {
10
+ RawDecoder as default
11
+ };
@@ -0,0 +1,31 @@
1
+ import { B as BaseDecoder } from "./index.75f0a489.mjs";
2
+ import "@vitessce/vit-s";
3
+ import "react";
4
+ class WebImageDecoder extends BaseDecoder {
5
+ constructor() {
6
+ super();
7
+ if (typeof createImageBitmap === "undefined") {
8
+ throw new Error("Cannot decode WebImage as `createImageBitmap` is not available");
9
+ } else if (typeof document === "undefined" && typeof OffscreenCanvas === "undefined") {
10
+ throw new Error("Cannot decode WebImage as neither `document` nor `OffscreenCanvas` is not available");
11
+ }
12
+ }
13
+ async decode(fileDirectory, buffer) {
14
+ const blob = new Blob([buffer]);
15
+ const imageBitmap = await createImageBitmap(blob);
16
+ let canvas;
17
+ if (typeof document !== "undefined") {
18
+ canvas = document.createElement("canvas");
19
+ canvas.width = imageBitmap.width;
20
+ canvas.height = imageBitmap.height;
21
+ } else {
22
+ canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
23
+ }
24
+ const ctx = canvas.getContext("2d");
25
+ ctx.drawImage(imageBitmap, 0, 0);
26
+ return ctx.getImageData(0, 0, imageBitmap.width, imageBitmap.height).data.buffer;
27
+ }
28
+ }
29
+ export {
30
+ WebImageDecoder as default
31
+ };
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/spatial-utils",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -8,20 +8,20 @@
8
8
  "url": "git+https://github.com/vitessce/vitessce.git"
9
9
  },
10
10
  "license": "MIT",
11
- "main": "dist/index.js",
11
+ "main": "dist/index.mjs",
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "src"
14
15
  ],
15
16
  "dependencies": {
16
17
  "lodash": "^4.17.21",
17
18
  "math.gl": "^3.5.6",
18
19
  "mathjs": "^9.2.0",
19
- "@vitessce/gl": "2.0.2",
20
- "@vitessce/utils": "2.0.2"
20
+ "@vitessce/gl": "2.0.3",
21
+ "@vitessce/utils": "2.0.3"
21
22
  },
22
23
  "scripts": {
23
- "start": "tsc --watch",
24
- "build": "tsc",
24
+ "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.js",
25
25
  "test": "pnpm exec vitest --run -r ../../../ --dir ."
26
26
  }
27
27
  }
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export {
2
+ GLOBAL_LABELS,
3
+ DEFAULT_CELLS_LAYER,
4
+ DEFAULT_MOLECULES_LAYER,
5
+ DEFAULT_NEIGHBORHOODS_LAYER,
6
+ DEFAULT_RASTER_DOMAIN_TYPE,
7
+ DEFAULT_RASTER_LAYER_PROPS,
8
+ DEFAULT_LAYER_TYPE_ORDERING,
9
+ square,
10
+ initializeLayerChannels,
11
+ initializeRasterLayersAndChannels,
12
+ getSourceFromLoader,
13
+ isInterleaved,
14
+ isRgb,
15
+ } from './spatial';
@@ -0,0 +1,129 @@
1
+ import { Matrix4 } from 'math.gl';
2
+ import { viv } from '@vitessce/gl';
3
+
4
+ async function getSingleSelectionStats2D({ loader, selection }) {
5
+ const data = Array.isArray(loader) ? loader[loader.length - 1] : loader;
6
+ const raster = await data.getRaster({ selection });
7
+ const selectionStats = viv.getChannelStats(raster.data);
8
+ const { domain, contrastLimits: slider } = selectionStats;
9
+ return { domain, slider };
10
+ }
11
+
12
+ async function getSingleSelectionStats3D({ loader, selection }) {
13
+ const lowResSource = loader[loader.length - 1];
14
+ const { shape, labels } = lowResSource;
15
+ // eslint-disable-next-line no-bitwise
16
+ const sizeZ = shape[labels.indexOf('z')] >> (loader.length - 1);
17
+ const raster0 = await lowResSource.getRaster({
18
+ selection: { ...selection, z: 0 },
19
+ });
20
+ const rasterMid = await lowResSource.getRaster({
21
+ selection: { ...selection, z: Math.floor(sizeZ / 2) },
22
+ });
23
+ const rasterTop = await lowResSource.getRaster({
24
+ selection: { ...selection, z: Math.max(0, sizeZ - 1) },
25
+ });
26
+ const stats0 = viv.getChannelStats(raster0.data);
27
+ const statsMid = viv.getChannelStats(rasterMid.data);
28
+ const statsTop = viv.getChannelStats(rasterTop.data);
29
+ return {
30
+ domain: [
31
+ Math.min(stats0.domain[0], statsMid.domain[0], statsTop.domain[0]),
32
+ Math.max(stats0.domain[1], statsMid.domain[1], statsTop.domain[1]),
33
+ ],
34
+ slider: [
35
+ Math.min(
36
+ stats0.contrastLimits[0],
37
+ statsMid.contrastLimits[0],
38
+ statsTop.contrastLimits[0],
39
+ ),
40
+ Math.max(
41
+ stats0.contrastLimits[1],
42
+ statsMid.contrastLimits[1],
43
+ statsTop.contrastLimits[1],
44
+ ),
45
+ ],
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Get bounding cube for a given loader i.e [[0, width], [0, height], [0, depth]]
51
+ * @param {Object} loader PixelSource|PixelSource[]
52
+ * @param {[]} selection Selection for stats.
53
+ * @param {boolean} use3d Whether or not to get 3d stats.
54
+ * @returns {Object} { domains, sliders }
55
+ */
56
+ export const getSingleSelectionStats = async ({ loader, selection, use3d }) => {
57
+ const getStats = use3d
58
+ ? getSingleSelectionStats3D
59
+ : getSingleSelectionStats2D;
60
+ return getStats({ loader, selection });
61
+ };
62
+
63
+ export const getMultiSelectionStats = async ({ loader, selections, use3d }) => {
64
+ const stats = await Promise.all(
65
+ selections.map(selection => getSingleSelectionStats({ loader, selection, use3d })),
66
+ );
67
+ const domains = stats.map(stat => stat.domain);
68
+ const sliders = stats.map(stat => stat.slider);
69
+ return { domains, sliders };
70
+ };
71
+
72
+ /**
73
+ * Get physical size scaling Matrix4
74
+ * @param {Object} loader PixelSource
75
+ * @returns {Object} matrix
76
+ */
77
+ export function getPhysicalSizeScalingMatrix(loader) {
78
+ const { x, y, z } = loader?.meta?.physicalSizes ?? {};
79
+ if (x?.size && y?.size && z?.size) {
80
+ const min = Math.min(z.size, x.size, y.size);
81
+ const ratio = [x.size / min, y.size / min, z.size / min];
82
+ return new Matrix4().scale(ratio);
83
+ }
84
+ return new Matrix4().identity();
85
+ }
86
+
87
+ /**
88
+ * Get bounding cube for a given loader
89
+ * @param {Object} loader PixelSource|PixelSource[]
90
+ * @returns {Array} [0, width], [0, height], [0, depth]]
91
+ */
92
+ export function getBoundingCube(loader) {
93
+ const source = Array.isArray(loader) ? loader[0] : loader;
94
+ const { shape, labels } = source;
95
+ const physicalSizeScalingMatrix = getPhysicalSizeScalingMatrix(source);
96
+ const xSlice = [0, physicalSizeScalingMatrix[0] * shape[labels.indexOf('x')]];
97
+ const ySlice = [0, physicalSizeScalingMatrix[5] * shape[labels.indexOf('y')]];
98
+ const zSlice = [
99
+ 0,
100
+ physicalSizeScalingMatrix[10] * shape[labels.indexOf('z')],
101
+ ];
102
+ return [xSlice, ySlice, zSlice];
103
+ }
104
+
105
+ export function abbreviateNumber(value) {
106
+ // Return an abbreviated representation of value, in 5 characters or less.
107
+
108
+ const maxLength = 5;
109
+ let maxNaiveDigits = maxLength;
110
+
111
+ /* eslint-disable no-plusplus */
112
+ if (!Number.isInteger(value)) {
113
+ --maxNaiveDigits;
114
+ } // Wasted on "."
115
+ if (value < 1) {
116
+ --maxNaiveDigits;
117
+ } // Wasted on "0."
118
+ /* eslint-disable no-plusplus */
119
+
120
+ const naive = Intl.NumberFormat('en-US', {
121
+ maximumSignificantDigits: maxNaiveDigits,
122
+ useGrouping: false,
123
+ }).format(value);
124
+ if (naive.length <= maxLength) return naive;
125
+
126
+ // "e+9" consumes 3 characters, so if we even had two significant digits,
127
+ // it would take take us to six characters, including the decimal point.
128
+ return value.toExponential(0);
129
+ }
package/src/spatial.js ADDED
@@ -0,0 +1,318 @@
1
+ import isEqual from 'lodash/isEqual';
2
+ import { Matrix4 } from 'math.gl';
3
+ import { divide, compare, unit } from 'mathjs';
4
+ import { VIEWER_PALETTE } from '@vitessce/utils';
5
+ import { viv } from '@vitessce/gl';
6
+ import { getMultiSelectionStats } from './layer-controller';
7
+
8
+ export function square(x, y, r) {
9
+ return [[x, y + r], [x + r, y], [x, y - r], [x - r, y]];
10
+ }
11
+
12
+ export const GLOBAL_LABELS = ['z', 't'];
13
+
14
+ export const DEFAULT_RASTER_DOMAIN_TYPE = 'Min/Max';
15
+
16
+ export const DEFAULT_RASTER_LAYER_PROPS = {
17
+ visible: true,
18
+ colormap: null,
19
+ opacity: 1,
20
+ domainType: DEFAULT_RASTER_DOMAIN_TYPE,
21
+ transparentColor: [0, 0, 0],
22
+ renderingMode: viv.RENDERING_MODES.ADDITIVE,
23
+ use3d: false,
24
+ };
25
+
26
+ export const DEFAULT_MOLECULES_LAYER = {
27
+ opacity: 1, radius: 20, visible: true,
28
+ };
29
+ export const DEFAULT_CELLS_LAYER = {
30
+ opacity: 1, radius: 50, visible: true, stroked: false,
31
+ };
32
+ export const DEFAULT_NEIGHBORHOODS_LAYER = {
33
+ visible: false,
34
+ };
35
+
36
+ export const DEFAULT_LAYER_TYPE_ORDERING = [
37
+ 'molecules',
38
+ 'cells',
39
+ 'neighborhoods',
40
+ 'raster',
41
+ ];
42
+
43
+ /**
44
+ * Get a representative PixelSource from a loader object returned from
45
+ * the Vitessce imaging loaders
46
+ * @param {object} loader { data: (PixelSource[]|PixelSource), metadata, channels } object
47
+ * @param {number=} level Level of the multiscale loader from which to get a PixelSource
48
+ * @returns {object} PixelSource object
49
+ */
50
+ export function getSourceFromLoader(loader, level) {
51
+ const { data } = loader;
52
+ const source = Array.isArray(data) ? data[(level || data.length - 1)] : data;
53
+ return source;
54
+ }
55
+
56
+ /**
57
+ * Helper method to determine whether pixel data is interleaved and rgb or not.
58
+ * @param {object} loader
59
+ * @param {array|null} channels
60
+ */
61
+ export function isRgb(loader, channels) {
62
+ const source = getSourceFromLoader(loader);
63
+ const { shape, dtype, labels } = source;
64
+ const channelSize = shape[(labels.includes('channel') ? labels.indexOf('channel') : labels.indexOf('c'))];
65
+ if (channelSize === 3 && dtype === 'Uint8') {
66
+ return true;
67
+ }
68
+ if (channels && channels.length === 3
69
+ && isEqual(channels[0].color, [255, 0, 0])
70
+ && isEqual(channels[1].color, [0, 255, 0])
71
+ && isEqual(channels[2].color, [0, 0, 255])
72
+ ) {
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ // From spatial/utils.js
79
+
80
+ function getMetaWithTransformMatrices(imageMeta, imageLoaders) {
81
+ // Do not fill in transformation matrices if any of the layers specify one.
82
+ const sources = imageLoaders.map(loader => getSourceFromLoader(loader));
83
+ if (
84
+ imageMeta.map(meta => meta?.metadata?.transform?.matrix
85
+ || meta?.metadata?.transform?.scale
86
+ || meta?.metadata?.transform?.translate).some(Boolean)
87
+ || sources.every(
88
+ source => !source.meta?.physicalSizes?.x || !source.meta?.physicalSizes?.y,
89
+ )
90
+ ) {
91
+ return imageMeta;
92
+ }
93
+ // Get the minimum physical among all the current images.
94
+ const minPhysicalSize = sources.reduce((acc, source) => {
95
+ const hasZPhyscialSize = source.meta?.physicalSizes?.z?.size;
96
+ const sizes = [
97
+ unit(`${source.meta?.physicalSizes.x.size} ${source.meta?.physicalSizes.x.unit}`.replace('µ', 'u')),
98
+ unit(`${source.meta?.physicalSizes.y.size} ${source.meta?.physicalSizes.y.unit}`.replace('µ', 'u')),
99
+ ];
100
+ if (hasZPhyscialSize) {
101
+ sizes.push(unit(`${source.meta?.physicalSizes.z.size} ${source.meta?.physicalSizes.z.unit}`.replace('µ', 'u')));
102
+ }
103
+ acc[0] = (acc[0] === undefined || compare(sizes[0], acc[0]) === -1) ? sizes[0] : acc[0];
104
+ acc[1] = (acc[1] === undefined || compare(sizes[1], acc[1]) === -1) ? sizes[1] : acc[1];
105
+ acc[2] = (acc[2] === undefined || compare(sizes[2], acc[2]) === -1) ? sizes[2] : acc[2];
106
+ return acc;
107
+ }, []);
108
+ const imageMetaWithTransform = imageMeta.map((meta, j) => {
109
+ const source = sources[j];
110
+ const hasZPhyscialSize = source.meta?.physicalSizes?.z?.size;
111
+ const sizes = [
112
+ unit(`${source.meta?.physicalSizes.x.size} ${source.meta?.physicalSizes.x.unit}`.replace('µ', 'u')),
113
+ unit(`${source.meta?.physicalSizes.y.size} ${source.meta?.physicalSizes.y.unit}`.replace('µ', 'u')),
114
+ ];
115
+ if (hasZPhyscialSize) {
116
+ sizes.push(unit(`${source.meta?.physicalSizes.z.size} ${source.meta?.physicalSizes.z.unit}`.replace('µ', 'u')));
117
+ }
118
+ // Find the ratio of the sizes to get the scaling factor.
119
+ const scale = sizes.map((i, k) => divide(i, minPhysicalSize[k]));
120
+ // Add in z dimension needed for Matrix4 scale API.
121
+ if (!scale[2]) {
122
+ scale[2] = 1;
123
+ }
124
+ // no need to store/use identity scaling
125
+ if (isEqual(scale, [1, 1, 1])) {
126
+ return meta;
127
+ }
128
+ // Make sure to scale the z direction by one.
129
+ const matrix = new Matrix4().scale([...scale]);
130
+ const newMeta = { ...meta };
131
+ newMeta.metadata = {
132
+ ...newMeta.metadata,
133
+ // We don't want to store matrix objects in the view config.
134
+ transform: { matrix: matrix.toArray() },
135
+ };
136
+ return newMeta;
137
+ });
138
+ return imageMetaWithTransform;
139
+ }
140
+
141
+ /**
142
+ * Return the midpoint of the global dimensions.
143
+ * @param {object} source PixelSource object from Viv
144
+ * @returns {object} The selection.
145
+ */
146
+ function getDefaultGlobalSelection(source) {
147
+ const globalIndices = source.labels
148
+ .filter(dim => GLOBAL_LABELS.includes(dim));
149
+ const selection = {};
150
+ globalIndices.forEach((dim) => {
151
+ selection[dim] = Math.floor(
152
+ (source.shape[source.labels.indexOf(dim)] || 0) / 2,
153
+ );
154
+ });
155
+ return selection;
156
+ }
157
+
158
+
159
+ /**
160
+ * Create a default selection using the midpoint of the available global dimensions,
161
+ * and then the first four available selections from the first selectable channel.
162
+ * @param {object} source PixelSource object from Viv
163
+ * @returns {object} The selection.
164
+ */
165
+ function buildDefaultSelection(source) {
166
+ const selection = [];
167
+ const globalSelection = getDefaultGlobalSelection(source);
168
+ // First non-global dimension with some sort of selectable values
169
+ const firstNonGlobalDimension = source.labels.filter(
170
+ dim => !GLOBAL_LABELS.includes(dim)
171
+ && source.shape[source.labels.indexOf(dim)],
172
+ )[0];
173
+ for (let i = 0; i < Math.min(4, source.shape[
174
+ source.labels.indexOf(firstNonGlobalDimension)
175
+ ]); i += 1) {
176
+ selection.push(
177
+ {
178
+ [firstNonGlobalDimension]: i,
179
+ ...globalSelection,
180
+ },
181
+ );
182
+ }
183
+ return selection;
184
+ }
185
+
186
+ /**
187
+ * @param {Array.<number>} shape loader shape
188
+ */
189
+ export function isInterleaved(shape) {
190
+ const lastDimSize = shape[shape.length - 1];
191
+ return lastDimSize === 3 || lastDimSize === 4;
192
+ }
193
+
194
+ /**
195
+ * Initialize the channel selections for an individual layer.
196
+ * @param {object} loader A viv loader instance with channel names appended by Vitessce loaders
197
+ * of the form { data: (PixelSource[]|PixelSource), metadata: Object, channels }
198
+ * @returns {object[]} An array of selected channels with default
199
+ * domain/slider settings.
200
+ */
201
+ export async function initializeLayerChannels(loader, use3d) {
202
+ const result = [];
203
+ const source = getSourceFromLoader(loader);
204
+ // Add channel automatically as the first avaialable value for each dimension.
205
+ let defaultSelection = buildDefaultSelection(source);
206
+ defaultSelection = isInterleaved(source.shape)
207
+ ? [{ ...defaultSelection[0], c: 0 }] : defaultSelection;
208
+ const stats = await getMultiSelectionStats({
209
+ loader: loader.data, selections: defaultSelection, use3d,
210
+ });
211
+
212
+ const domains = isRgb(loader, null)
213
+ ? [[0, 255], [0, 255], [0, 255]]
214
+ : stats.domains;
215
+ const colors = isRgb(loader, null)
216
+ ? [[255, 0, 0], [0, 255, 0], [0, 0, 255]]
217
+ : null;
218
+ const sliders = isRgb(loader, null)
219
+ ? [[0, 255], [0, 255], [0, 255]]
220
+ : stats.sliders;
221
+
222
+ defaultSelection.forEach((selection, i) => {
223
+ const domain = domains[i];
224
+ const slider = sliders[i];
225
+ const channel = {
226
+ selection,
227
+ // eslint-disable-next-line no-nested-ternary
228
+ color: colors ? colors[i]
229
+ : defaultSelection.length !== 1
230
+ ? VIEWER_PALETTE[i] : [255, 255, 255],
231
+ visible: true,
232
+ slider: slider || domain,
233
+ };
234
+ result.push(channel);
235
+ });
236
+ return result;
237
+ }
238
+
239
+ /**
240
+ * Given a set of image layer loader creator functions,
241
+ * create loader objects for an initial layer or set of layers,
242
+ * which will be selected based on default values predefined in
243
+ * the image data file or otherwise by a heuristic
244
+ * (the midpoint of the layers array).
245
+ * @param {object[]} rasterLayers A list of layer metadata objects with
246
+ * shape { name, type, url, createLoader }.
247
+ * @param {(string[]|null)} rasterRenderLayers A list of default raster layers. Optional.
248
+ */
249
+ export async function initializeRasterLayersAndChannels(
250
+ rasterLayers,
251
+ rasterRenderLayers,
252
+ usePhysicalSizeScaling,
253
+ ) {
254
+ const nextImageLoaders = [];
255
+ let nextImageMetaAndLayers = [];
256
+ const autoImageLayerDefPromises = [];
257
+
258
+ // Start all loader creators immediately.
259
+ // Reference: https://eslint.org/docs/rules/no-await-in-loop
260
+ const loaders = await Promise.all(rasterLayers.map(layer => layer.loaderCreator()));
261
+
262
+ for (let i = 0; i < rasterLayers.length; i++) {
263
+ const layer = rasterLayers[i];
264
+ const loader = loaders[i];
265
+ nextImageLoaders[i] = loader;
266
+ nextImageMetaAndLayers[i] = layer;
267
+ }
268
+ if (usePhysicalSizeScaling) {
269
+ nextImageMetaAndLayers = getMetaWithTransformMatrices(nextImageMetaAndLayers, nextImageLoaders);
270
+ }
271
+ // No layers were pre-defined so set up the default image layers.
272
+ if (!rasterRenderLayers) {
273
+ // Midpoint of images list as default image to show.
274
+ const layerIndex = Math.floor(rasterLayers.length / 2);
275
+ const loader = nextImageLoaders[layerIndex];
276
+ const autoImageLayerDefPromise = initializeLayerChannels(loader)
277
+ .then(channels => Promise.resolve({
278
+ type: nextImageMetaAndLayers[layerIndex]?.metadata?.isBitmask ? 'bitmask' : 'raster',
279
+ index: layerIndex,
280
+ ...DEFAULT_RASTER_LAYER_PROPS,
281
+ channels: channels.map((channel, j) => ({
282
+ ...channel,
283
+ ...(nextImageMetaAndLayers[layerIndex].channels
284
+ ? nextImageMetaAndLayers[layerIndex].channels[j] : []),
285
+ })),
286
+ modelMatrix: nextImageMetaAndLayers[layerIndex]?.metadata?.transform?.matrix,
287
+ transparentColor: layerIndex > 0 ? [0, 0, 0] : null,
288
+ }));
289
+ autoImageLayerDefPromises.push(autoImageLayerDefPromise);
290
+ } else {
291
+ // The renderLayers parameter is a list of layer names to show by default.
292
+ const globalIndicesOfRenderLayers = rasterRenderLayers
293
+ .map(imageName => rasterLayers.findIndex(image => image.name === imageName));
294
+ for (let i = 0; i < globalIndicesOfRenderLayers.length; i++) {
295
+ const layerIndex = globalIndicesOfRenderLayers[i];
296
+ const loader = nextImageLoaders[layerIndex];
297
+ const autoImageLayerDefPromise = initializeLayerChannels(loader)
298
+ // eslint-disable-next-line no-loop-func
299
+ .then(channels => Promise.resolve({
300
+ type: nextImageMetaAndLayers[layerIndex]?.metadata?.isBitmask ? 'bitmask' : 'raster',
301
+ index: layerIndex,
302
+ ...DEFAULT_RASTER_LAYER_PROPS,
303
+ channels: channels.map((channel, j) => ({
304
+ ...channel,
305
+ ...(nextImageMetaAndLayers[layerIndex].channels
306
+ ? nextImageMetaAndLayers[layerIndex].channels[j] : []),
307
+ })),
308
+ domainType: 'Min/Max',
309
+ modelMatrix: nextImageMetaAndLayers[layerIndex]?.metadata?.transform?.matrix,
310
+ transparentColor: i > 0 ? [0, 0, 0] : null,
311
+ }));
312
+ autoImageLayerDefPromises.push(autoImageLayerDefPromise);
313
+ }
314
+ }
315
+
316
+ const autoImageLayerDefs = await Promise.all(autoImageLayerDefPromises);
317
+ return [autoImageLayerDefs, nextImageLoaders, nextImageMetaAndLayers];
318
+ }
@@ -0,0 +1,9 @@
1
+ import { square } from './spatial';
2
+
3
+ describe('Spatial.js', () => {
4
+ describe('square()', () => {
5
+ it('gives the right coordinates', () => {
6
+ expect(square(0, 0, 50)).toEqual([[0, 50], [50, 0], [0, -50], [-50, 0]]);
7
+ });
8
+ });
9
+ });
@@ -1,109 +0,0 @@
1
- import { Matrix4 } from 'math.gl';
2
- import { viv } from '@vitessce/gl';
3
- async function getSingleSelectionStats2D({ loader, selection }) {
4
- const data = Array.isArray(loader) ? loader[loader.length - 1] : loader;
5
- const raster = await data.getRaster({ selection });
6
- const selectionStats = viv.getChannelStats(raster.data);
7
- const { domain, contrastLimits: slider } = selectionStats;
8
- return { domain, slider };
9
- }
10
- async function getSingleSelectionStats3D({ loader, selection }) {
11
- const lowResSource = loader[loader.length - 1];
12
- const { shape, labels } = lowResSource;
13
- // eslint-disable-next-line no-bitwise
14
- const sizeZ = shape[labels.indexOf('z')] >> (loader.length - 1);
15
- const raster0 = await lowResSource.getRaster({
16
- selection: { ...selection, z: 0 },
17
- });
18
- const rasterMid = await lowResSource.getRaster({
19
- selection: { ...selection, z: Math.floor(sizeZ / 2) },
20
- });
21
- const rasterTop = await lowResSource.getRaster({
22
- selection: { ...selection, z: Math.max(0, sizeZ - 1) },
23
- });
24
- const stats0 = viv.getChannelStats(raster0.data);
25
- const statsMid = viv.getChannelStats(rasterMid.data);
26
- const statsTop = viv.getChannelStats(rasterTop.data);
27
- return {
28
- domain: [
29
- Math.min(stats0.domain[0], statsMid.domain[0], statsTop.domain[0]),
30
- Math.max(stats0.domain[1], statsMid.domain[1], statsTop.domain[1]),
31
- ],
32
- slider: [
33
- Math.min(stats0.contrastLimits[0], statsMid.contrastLimits[0], statsTop.contrastLimits[0]),
34
- Math.max(stats0.contrastLimits[1], statsMid.contrastLimits[1], statsTop.contrastLimits[1]),
35
- ],
36
- };
37
- }
38
- /**
39
- * Get bounding cube for a given loader i.e [[0, width], [0, height], [0, depth]]
40
- * @param {Object} loader PixelSource|PixelSource[]
41
- * @param {[]} selection Selection for stats.
42
- * @param {boolean} use3d Whether or not to get 3d stats.
43
- * @returns {Object} { domains, sliders }
44
- */
45
- export const getSingleSelectionStats = async ({ loader, selection, use3d }) => {
46
- const getStats = use3d
47
- ? getSingleSelectionStats3D
48
- : getSingleSelectionStats2D;
49
- return getStats({ loader, selection });
50
- };
51
- export const getMultiSelectionStats = async ({ loader, selections, use3d }) => {
52
- const stats = await Promise.all(selections.map(selection => getSingleSelectionStats({ loader, selection, use3d })));
53
- const domains = stats.map(stat => stat.domain);
54
- const sliders = stats.map(stat => stat.slider);
55
- return { domains, sliders };
56
- };
57
- /**
58
- * Get physical size scaling Matrix4
59
- * @param {Object} loader PixelSource
60
- * @returns {Object} matrix
61
- */
62
- export function getPhysicalSizeScalingMatrix(loader) {
63
- const { x, y, z } = loader?.meta?.physicalSizes ?? {};
64
- if (x?.size && y?.size && z?.size) {
65
- const min = Math.min(z.size, x.size, y.size);
66
- const ratio = [x.size / min, y.size / min, z.size / min];
67
- return new Matrix4().scale(ratio);
68
- }
69
- return new Matrix4().identity();
70
- }
71
- /**
72
- * Get bounding cube for a given loader
73
- * @param {Object} loader PixelSource|PixelSource[]
74
- * @returns {Array} [0, width], [0, height], [0, depth]]
75
- */
76
- export function getBoundingCube(loader) {
77
- const source = Array.isArray(loader) ? loader[0] : loader;
78
- const { shape, labels } = source;
79
- const physicalSizeScalingMatrix = getPhysicalSizeScalingMatrix(source);
80
- const xSlice = [0, physicalSizeScalingMatrix[0] * shape[labels.indexOf('x')]];
81
- const ySlice = [0, physicalSizeScalingMatrix[5] * shape[labels.indexOf('y')]];
82
- const zSlice = [
83
- 0,
84
- physicalSizeScalingMatrix[10] * shape[labels.indexOf('z')],
85
- ];
86
- return [xSlice, ySlice, zSlice];
87
- }
88
- export function abbreviateNumber(value) {
89
- // Return an abbreviated representation of value, in 5 characters or less.
90
- const maxLength = 5;
91
- let maxNaiveDigits = maxLength;
92
- /* eslint-disable no-plusplus */
93
- if (!Number.isInteger(value)) {
94
- --maxNaiveDigits;
95
- } // Wasted on "."
96
- if (value < 1) {
97
- --maxNaiveDigits;
98
- } // Wasted on "0."
99
- /* eslint-disable no-plusplus */
100
- const naive = Intl.NumberFormat('en-US', {
101
- maximumSignificantDigits: maxNaiveDigits,
102
- useGrouping: false,
103
- }).format(value);
104
- if (naive.length <= maxLength)
105
- return naive;
106
- // "e+9" consumes 3 characters, so if we even had two significant digits,
107
- // it would take take us to six characters, including the decimal point.
108
- return value.toExponential(0);
109
- }