@vitessce/vit-s 4.0.3 → 4.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/vit-s",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@tanstack/react-query": "^5.80.2",
20
- "@zarrita/storage": "0.1.4",
20
+ "@zarrita/storage": "0.2.0",
21
21
  "clsx": "^1.1.1",
22
22
  "d3-array": "^2.4.0",
23
23
  "fast-deep-equal": "^3.1.3",
@@ -30,18 +30,18 @@
30
30
  "react-aria": "^3.47.0",
31
31
  "react-error-boundary": "^5.0.0",
32
32
  "@placemarkio/flat-drop-files": "^1.0.2",
33
- "zarrita": "0.6.1",
34
- "@vitessce/styles": "4.0.3",
35
- "@vitessce/abstract": "4.0.3",
36
- "@vitessce/schemas": "4.0.3",
37
- "@vitessce/plugins": "4.0.3",
38
- "@vitessce/constants-internal": "4.0.3",
39
- "@vitessce/utils": "4.0.3",
40
- "@vitessce/sets-utils": "4.0.3",
41
- "@vitessce/zarr-utils": "4.0.3",
42
- "@vitessce/error": "4.0.3",
43
- "@vitessce/config": "4.0.3",
44
- "@vitessce/globals": "4.0.3"
33
+ "zarrita": "0.7.5",
34
+ "@vitessce/styles": "4.0.5",
35
+ "@vitessce/error": "4.0.5",
36
+ "@vitessce/plugins": "4.0.5",
37
+ "@vitessce/constants-internal": "4.0.5",
38
+ "@vitessce/schemas": "4.0.5",
39
+ "@vitessce/abstract": "4.0.5",
40
+ "@vitessce/sets-utils": "4.0.5",
41
+ "@vitessce/utils": "4.0.5",
42
+ "@vitessce/zarr-utils": "4.0.5",
43
+ "@vitessce/config": "4.0.5",
44
+ "@vitessce/globals": "4.0.5"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/jest-dom": "^6.6.3",
@@ -50,7 +50,7 @@
50
50
  "react-dom": "19.2.6",
51
51
  "vite": "^7.0.0",
52
52
  "vitest": "^3.1.4",
53
- "@vitessce/types": "4.0.3"
53
+ "@vitessce/types": "4.0.5"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
package/src/VitS.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from '@tanstack/react-query';
15
15
  import { isEqual } from 'lodash-es';
16
16
  import { buildConfigSchema, latestConfigSchema } from '@vitessce/schemas';
17
+ import { MatrixTooLargeError } from '@vitessce/error';
17
18
  import {
18
19
  log,
19
20
  setLogLevel, setDebugMode,
@@ -248,7 +249,12 @@ export function VitS(props) {
248
249
  defaultOptions: {
249
250
  queries: {
250
251
  refetchOnWindowFocus: false,
251
- retry: 2,
252
+ // A matrix that does not fit the browser's budget will not fit on a
253
+ // retry either; surface that message immediately rather than after
254
+ // two more attempts and their backoff.
255
+ retry: (failureCount, error) => (
256
+ failureCount < 2 && !(error instanceof MatrixTooLargeError)
257
+ ),
252
258
  },
253
259
  },
254
260
  // Reference: https://tkdodo.eu/blog/react-query-error-handling#the-global-callbacks
@@ -589,3 +589,22 @@ export function useObsLabelsMultiLevel(
589
589
  depth, DataType.OBS_LABELS,
590
590
  );
591
591
  }
592
+
593
+ /**
594
+ * Wrapper around useDataTypeMultiLevel.
595
+ * @param {object} loaders
596
+ * @param {string} dataset
597
+ * @param {boolean} isRequired
598
+ * @param {object} matchOnObj
599
+ * @param {number} depth
600
+ * @returns {array} [data, status, errors]
601
+ */
602
+ export function useObsColorsMultiLevel(
603
+ loaders, dataset, isRequired, matchOnObj,
604
+ depth,
605
+ ) {
606
+ return useDataTypeMultiLevel(
607
+ loaders, dataset, isRequired, matchOnObj,
608
+ depth, DataType.OBS_COLORS,
609
+ );
610
+ }
@@ -10,6 +10,7 @@ import {
10
10
  useObsLocationsMultiLevel,
11
11
  useObsSetsMultiLevel,
12
12
  useObsLabelsMultiLevel,
13
+ useObsColorsMultiLevel,
13
14
  } from './data-hooks-multilevel-utils.js';
14
15
 
15
16
 
@@ -319,3 +320,34 @@ export function useSegmentationMultiObsSets(
319
320
  );
320
321
  return [indicesData, indicesDataStatus, indicesDataErrors];
321
322
  }
323
+
324
+ /**
325
+ * Wrapper around useObsColorsMultiLevel.
326
+ * @param {object} coordinationScopes
327
+ * @param {object} coordinationScopesBy
328
+ * @param {object} loaders
329
+ * @param {string} dataset
330
+ * @returns {array} [data, status, errors]
331
+ */
332
+ export function useSegmentationMultiObsColors(
333
+ coordinationScopes, coordinationScopesBy, loaders, dataset,
334
+ ) {
335
+ const obsTypeCoordination = useComplexCoordinationSecondary(
336
+ [
337
+ CoordinationType.OBS_TYPE,
338
+ ],
339
+ coordinationScopes,
340
+ coordinationScopesBy,
341
+ CoordinationType.SEGMENTATION_LAYER,
342
+ CoordinationType.SEGMENTATION_CHANNEL,
343
+ );
344
+ const matchOnObj = useMemo(() => obsTypeCoordination[0],
345
+ // imageCoordination reference changes each render,
346
+ // use coordinationScopes and coordinationScopesBy which are
347
+ // indirect dependencies here.
348
+ [coordinationScopes, coordinationScopesBy]);
349
+ const [colorsData, colorsDataStatus, colorsDataErrors] = useObsColorsMultiLevel(
350
+ loaders, dataset, false, matchOnObj, 2,
351
+ );
352
+ return [colorsData, colorsDataStatus, colorsDataErrors];
353
+ }
package/src/hooks.js CHANGED
@@ -5,6 +5,7 @@ import { debounce, every } from 'lodash-es';
5
5
  import { extent } from 'd3-array';
6
6
  import { useQuery } from '@tanstack/react-query';
7
7
  import { capitalize } from '@vitessce/utils';
8
+ import { getObsIndexMap } from '@vitessce/sets-utils';
8
9
  import { STATUS, AsyncFunctionType } from '@vitessce/constants-internal';
9
10
  import { VITESSCE_CONTAINER } from './classNames.js';
10
11
  import { useGridResize, useEmitGridResize } from './state/hooks.js';
@@ -258,6 +259,10 @@ export function useUint8FeatureSelection(expressionData) {
258
259
  }, [expressionData]);
259
260
  }
260
261
 
262
+ // Marks an instance index that equals the matrix row index, so that no
263
+ // per-observation mapping needs to be built.
264
+ const IDENTITY_MAPPING = 'identity';
265
+
261
266
  export function useExpressionValueGetter(
262
267
  { instanceObsIndex, matrixObsIndex, expressionData },
263
268
  ) {
@@ -267,18 +272,32 @@ export function useExpressionValueGetter(
267
272
  // we need a way to look up an obsFeatureMatrix obsIndex index
268
273
  // given an obsEmbedding obsIndex index.
269
274
  const toMatrixIndexMap = useMemo(() => {
270
- if (instanceObsIndex && matrixObsIndex) {
271
- const matrixIndexMap = new Map(matrixObsIndex.map((key, i) => ([key, i])));
272
- return instanceObsIndex.map(key => matrixIndexMap.get(key));
275
+ if (!instanceObsIndex || !matrixObsIndex) {
276
+ return null;
273
277
  }
274
- return null;
278
+ if (instanceObsIndex === matrixObsIndex) {
279
+ // The same array (the usual case when both come from one data source)
280
+ // is already aligned by position.
281
+ return IDENTITY_MAPPING;
282
+ }
283
+ // The map is shared with other consumers of this index via getObsIndexMap.
284
+ const matrixIndexMap = getObsIndexMap(matrixObsIndex);
285
+ const mapping = new Int32Array(instanceObsIndex.length);
286
+ for (let i = 0; i < instanceObsIndex.length; i += 1) {
287
+ const rowIndex = matrixIndexMap.get(instanceObsIndex[i]);
288
+ mapping[i] = rowIndex === undefined ? -1 : rowIndex;
289
+ }
290
+ return mapping;
275
291
  }, [instanceObsIndex, matrixObsIndex]);
276
292
 
277
293
  // Set up a getter function for gene expression values, to be used
278
294
  // by the DeckGL layer to obtain values for instanced attributes.
279
295
  const getExpressionValue = useCallback((entry, { index: instanceIndex }) => {
280
296
  if (toMatrixIndexMap && expressionData && expressionData[0]) {
281
- const rowIndex = toMatrixIndexMap[instanceIndex];
297
+ const rowIndex = toMatrixIndexMap === IDENTITY_MAPPING
298
+ ? instanceIndex
299
+ : toMatrixIndexMap[instanceIndex];
300
+ // An observation absent from the matrix (-1) reads as undefined, as before.
282
301
  const val = expressionData[0][rowIndex];
283
302
  return val;
284
303
  }
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useExpressionValueGetter } from './hooks.js';
4
+
5
+ describe('useExpressionValueGetter', () => {
6
+ const expressionData = [new Uint8Array([10, 20, 30])];
7
+
8
+ it('reads by position when the instance and matrix indices are the same array', () => {
9
+ const obsIndex = ['a', 'b', 'c'];
10
+ const { result } = renderHook(() => useExpressionValueGetter({
11
+ instanceObsIndex: obsIndex, matrixObsIndex: obsIndex, expressionData,
12
+ }));
13
+ expect(result.current(null, { index: 0 })).toEqual(10);
14
+ expect(result.current(null, { index: 2 })).toEqual(30);
15
+ });
16
+
17
+ it('maps through observation IDs when the indices differ', () => {
18
+ const { result } = renderHook(() => useExpressionValueGetter({
19
+ instanceObsIndex: ['c', 'unknown', 'a'],
20
+ matrixObsIndex: ['a', 'b', 'c'],
21
+ expressionData,
22
+ }));
23
+ expect(result.current(null, { index: 0 })).toEqual(30);
24
+ // An observation absent from the matrix reads as undefined, as before.
25
+ expect(result.current(null, { index: 1 })).toEqual(undefined);
26
+ expect(result.current(null, { index: 2 })).toEqual(10);
27
+ });
28
+
29
+ it('returns 0 without indices or without data', () => {
30
+ const { result: noIndices } = renderHook(() => useExpressionValueGetter({
31
+ instanceObsIndex: null, matrixObsIndex: ['a'], expressionData,
32
+ }));
33
+ expect(noIndices.current(null, { index: 0 })).toEqual(0);
34
+ const { result: noData } = renderHook(() => useExpressionValueGetter({
35
+ instanceObsIndex: ['a'], matrixObsIndex: ['a'], expressionData: null,
36
+ }));
37
+ expect(noData.current(null, { index: 0 })).toEqual(0);
38
+ });
39
+ });
package/src/index.js CHANGED
@@ -93,6 +93,7 @@ export {
93
93
  useSegmentationMultiObsFeatureMatrixIndices,
94
94
  useSegmentationMultiObsLocations,
95
95
  useSegmentationMultiObsSets,
96
+ useSegmentationMultiObsColors,
96
97
  } from './data-hooks-multilevel.js';
97
98
  export {
98
99
  useHasLoader,
@@ -107,3 +108,15 @@ export { logConfig } from './view-config-utils.js';
107
108
  export { useAsyncFunction, usePageModeView } from './contexts.js';
108
109
  export { createLoaders } from './vitessce-grid-utils.js';
109
110
  export { createOnDrop } from './on-drop.js';
111
+ export {
112
+ useMemoCustomComparison,
113
+ shallowDiff,
114
+ shallowDiffByLayer,
115
+ shallowDiffByLayerWithKeys,
116
+ shallowDiffByChannel,
117
+ shallowDiffByChannelWithKeys,
118
+ shallowDiffByLayerCoordination,
119
+ shallowDiffByLayerCoordinationWithKeys,
120
+ shallowDiffByChannelCoordination,
121
+ shallowDiffByChannelCoordinationWithKeys,
122
+ } from './use-memo-custom-comparison.js';
@@ -0,0 +1,61 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable max-len */
3
+ import { useRef } from 'react';
4
+
5
+ /**
6
+ * A variant of useMemo that accepts a custom equality function
7
+ * to determine whether the dependencies have changed.
8
+ * @template T
9
+ * @param {() => T} factory - Function that computes the memoized value.
10
+ * @param {any} dependencies - The useMemo dependencies.
11
+ * @param {(prevDeps: any, nextDeps: any) => boolean} customIsEqual - Custom equality function,
12
+ * which receives the previous and next dependencies and returns true if they are considered equal.
13
+ * @returns {T} The memoized value.
14
+ */
15
+ export function useMemoCustomComparison(factory, dependencies, customIsEqual) {
16
+ const ref = useRef(/** @type {{ deps: any; value: T } | undefined} */ (undefined));
17
+
18
+ if (ref.current === undefined || !customIsEqual(ref.current.deps, dependencies)) {
19
+ ref.current = { deps: dependencies, value: factory() };
20
+ }
21
+
22
+ return ref.current.value;
23
+ }
24
+
25
+
26
+ // Comparison utilties inspired by componentDidUpdate in spatial-beta/Spatial.js:
27
+ export const shallowDiff = (prevDeps, nextDeps, depName) => prevDeps[depName] !== nextDeps[depName];
28
+ export const shallowDiffByLayer = (prevDeps, nextDeps, depName, scopeName) => (
29
+ prevDeps?.[depName]?.[scopeName] !== nextDeps?.[depName]?.[scopeName]
30
+ );
31
+ // Rather than checking equality of the entire object,
32
+ // here, we only shallowly compare the specific properties that are relevant.
33
+ export const shallowDiffByLayerWithKeys = (prevDeps, nextDeps, depName, scopeName, keys) => keys.some(
34
+ k => (prevDeps?.[depName]?.[scopeName]?.[k] !== nextDeps?.[depName]?.[scopeName]?.[k]),
35
+ );
36
+ export const shallowDiffByChannel = (prevDeps, nextDeps, depName, firstName, secondName) => (
37
+ prevDeps?.[depName]?.[firstName]?.[secondName]
38
+ !== nextDeps?.[depName]?.[firstName]?.[secondName]
39
+ );
40
+ export const shallowDiffByChannelWithKeys = (prevDeps, nextDeps, depName, firstName, secondName, keys) => keys.some(
41
+ k => (
42
+ prevDeps?.[depName]?.[firstName]?.[secondName]?.[k]
43
+ !== nextDeps?.[depName]?.[firstName]?.[secondName]?.[k]
44
+ ),
45
+ );
46
+ export const shallowDiffByLayerCoordination = (prevDeps, nextDeps, depName, layerScope) => (
47
+ prevDeps?.[depName]?.[0]?.[layerScope]
48
+ !== nextDeps?.[depName]?.[0]?.[layerScope]
49
+ );
50
+ export const shallowDiffByLayerCoordinationWithKeys = (prevDeps, nextDeps, depName, layerScope, keys) => keys.some(
51
+ k => prevDeps?.[depName]?.[0]?.[layerScope]?.[k]
52
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[k],
53
+ );
54
+ export const shallowDiffByChannelCoordination = (prevDeps, nextDeps, depName, layerScope, channelScope) => (
55
+ prevDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]
56
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]
57
+ );
58
+ export const shallowDiffByChannelCoordinationWithKeys = (prevDeps, nextDeps, depName, layerScope, channelScope, keys) => keys.some(
59
+ k => prevDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]?.[k]
60
+ !== nextDeps?.[depName]?.[0]?.[layerScope]?.[channelScope]?.[k],
61
+ );