@vitessce/vit-s 3.1.3 → 3.2.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 (46) hide show
  1. package/dist/index.js +1673 -130
  2. package/dist-tsc/data-hook-utils.d.ts +1 -0
  3. package/dist-tsc/data-hook-utils.d.ts.map +1 -1
  4. package/dist-tsc/data-hook-utils.js +12 -4
  5. package/dist-tsc/data-hooks-multilevel-utils.d.ts +77 -0
  6. package/dist-tsc/data-hooks-multilevel-utils.d.ts.map +1 -0
  7. package/dist-tsc/data-hooks-multilevel-utils.js +418 -0
  8. package/dist-tsc/data-hooks-multilevel-utils.test.d.ts +2 -0
  9. package/dist-tsc/data-hooks-multilevel-utils.test.d.ts.map +1 -0
  10. package/dist-tsc/data-hooks-multilevel-utils.test.js +101 -0
  11. package/dist-tsc/data-hooks-multilevel.d.ts +8 -0
  12. package/dist-tsc/data-hooks-multilevel.d.ts.map +1 -0
  13. package/dist-tsc/data-hooks-multilevel.js +125 -0
  14. package/dist-tsc/data-hooks.d.ts +7 -0
  15. package/dist-tsc/data-hooks.d.ts.map +1 -1
  16. package/dist-tsc/data-hooks.js +75 -6
  17. package/dist-tsc/hooks.js +1 -1
  18. package/dist-tsc/index.d.ts +3 -2
  19. package/dist-tsc/index.js +3 -2
  20. package/dist-tsc/shared-mui/components.d.ts.map +1 -1
  21. package/dist-tsc/shared-mui/components.js +3 -2
  22. package/dist-tsc/state/hooks.d.ts +80 -21
  23. package/dist-tsc/state/hooks.d.ts.map +1 -1
  24. package/dist-tsc/state/hooks.js +428 -103
  25. package/dist-tsc/state/hooks.test.d.ts +2 -0
  26. package/dist-tsc/state/hooks.test.d.ts.map +1 -0
  27. package/dist-tsc/state/hooks.test.js +149 -0
  28. package/dist-tsc/state/spatial-reducers.d.ts +25 -0
  29. package/dist-tsc/state/spatial-reducers.d.ts.map +1 -0
  30. package/dist-tsc/state/spatial-reducers.js +251 -0
  31. package/dist-tsc/state/spatial-reducers.test.d.ts +2 -0
  32. package/dist-tsc/state/spatial-reducers.test.d.ts.map +1 -0
  33. package/dist-tsc/state/spatial-reducers.test.js +1382 -0
  34. package/package.json +5 -5
  35. package/src/data-hook-utils.js +19 -3
  36. package/src/data-hooks-multilevel-utils.js +571 -0
  37. package/src/data-hooks-multilevel-utils.test.js +110 -0
  38. package/src/data-hooks-multilevel.js +226 -0
  39. package/src/data-hooks.js +156 -6
  40. package/src/hooks.js +1 -1
  41. package/src/index.js +25 -0
  42. package/src/shared-mui/components.js +9 -4
  43. package/src/state/hooks.js +485 -104
  44. package/src/state/hooks.test.js +182 -0
  45. package/src/state/spatial-reducers.js +291 -0
  46. package/src/state/spatial-reducers.test.js +1432 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/vit-s",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -28,10 +28,10 @@
28
28
  "react-grid-layout-with-lodash": "^1.3.5",
29
29
  "uuid": "^9.0.0",
30
30
  "zustand": "^3.5.10",
31
- "@vitessce/constants-internal": "3.1.3",
32
- "@vitessce/plugins": "3.1.3",
33
- "@vitessce/schemas": "3.1.3",
34
- "@vitessce/utils": "3.1.3"
31
+ "@vitessce/plugins": "3.2.0",
32
+ "@vitessce/schemas": "3.2.0",
33
+ "@vitessce/constants-internal": "3.2.0",
34
+ "@vitessce/utils": "3.2.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@testing-library/jest-dom": "^5.16.4",
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo } from 'react';
2
+ import { useQuery, useQueries } from '@tanstack/react-query';
2
3
  import { capitalize, fromEntries } from '@vitessce/utils';
3
4
  import { STATUS } from '@vitessce/constants-internal';
4
- import { useQuery, useQueries } from '@tanstack/react-query';
5
5
  import {
6
6
  getMatchingLoader,
7
7
  useMatchingLoader,
@@ -54,7 +54,7 @@ export function initCoordinationSpace(values, setters, initialValues) {
54
54
  }
55
55
 
56
56
 
57
- async function dataQueryFn(ctx) {
57
+ export async function dataQueryFn(ctx) {
58
58
  const { placeholderObject, loaders } = ctx.meta;
59
59
  // This ordering of the queryKey must match.
60
60
  const [dataset, dataType, matchOn, isRequired] = ctx.queryKey;
@@ -112,6 +112,9 @@ export function useDataType(
112
112
  placeholderData: placeholderObject,
113
113
  // Include the hook name in the queryKey to prevent the case in which an identical queryKey
114
114
  // in a different hook would cause an accidental cache hit.
115
+ // Note: same key structure/suffix as
116
+ // useDataTypeMulti() and getQueryKeyScopeTuplesAux()
117
+ // for shared caching.
115
118
  queryKey: [dataset, dataType, matchOn, isRequired, 'useDataType'],
116
119
  // Query function should return an object
117
120
  // { data, dataKey } where dataKey is the loaded gene selection.
@@ -178,6 +181,9 @@ export function useDataTypeMulti(
178
181
  placeholderData: placeholderObject,
179
182
  // Query key should match useDataType for shared cacheing
180
183
  // and correctness of dataQueryFn.
184
+ // Note: same key structure/suffix as
185
+ // useDataType() and getQueryKeyScopeTuplesAux()
186
+ // for shared caching.
181
187
  queryKey: [dataset, dataType, matchOn, isRequired, 'useDataType'],
182
188
  // Query function should return an object
183
189
  // { data, dataKey } where dataKey is the loaded gene selection.
@@ -218,10 +224,20 @@ export function useDataTypeMulti(
218
224
  // Convert data to object keyed by scopeKey.
219
225
  const data = useMemo(() => fromEntries(
220
226
  matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.data])),
227
+ // Deliberate dependency omissions: dataQueries and matchOnEntries,
228
+ // since dataQueries changes every re-render. We use the in-direct
229
+ // dependency of matchOnObj and the derived primitive value dataStatus.
230
+ // eslint-disable-next-line react-hooks/exhaustive-deps
231
+ ), [matchOnObj, dataStatus]);
232
+
233
+ // Convert data to object keyed by scopeKey.
234
+ const urls = useMemo(() => fromEntries(
235
+ matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.urls])),
221
236
  // Deliberate dependency omissions: matchOnEntries, since dataQueries depends on it.
222
237
  // eslint-disable-next-line react-hooks/exhaustive-deps
223
238
  ), [dataQueries]);
224
- return [data, dataStatus];
239
+
240
+ return [data, dataStatus, urls];
225
241
  }
226
242
 
227
243
  export function useHasLoader(loaders, dataset, dataType, matchOn) {
@@ -0,0 +1,571 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { useQueries } from '@tanstack/react-query';
3
+ import { extent } from 'd3-array';
4
+ import { DataType, STATUS } from '@vitessce/constants-internal';
5
+ import {
6
+ getMatchingLoader,
7
+ useSetWarning,
8
+ } from './state/hooks.js';
9
+ import {
10
+ LoaderNotFoundError,
11
+ } from './errors/index.js';
12
+ import {
13
+ warn,
14
+ dataQueryFn,
15
+ } from './data-hook-utils.js';
16
+
17
+
18
+ function getFeatureSelectionQueryKeyScopeTuplesAux(
19
+ allSelections, allMatchOnObj, depth, dataset, dataType, isRequired,
20
+ prevLevelScopes, currSelection, currMatchOn,
21
+ ) {
22
+ // Base case
23
+ if (depth === 0) {
24
+ return currSelection
25
+ ?.map((featureId, featureIndex) => ([
26
+ // queryKey:
27
+ // TODO: use same key suffix as useFeatureSelection for shared caching?
28
+ [dataset, dataType, currMatchOn, featureId, isRequired, 'useFeatureSelectionMultiLevel'],
29
+ // scope info (for rolling up later)
30
+ { levelScopes: prevLevelScopes, featureIndex, numFeatures: currSelection.length },
31
+ ])) || [];
32
+ }
33
+ // Recursive case
34
+ return Object.entries(currSelection)
35
+ ?.flatMap(([levelScope, levelSelections]) => getFeatureSelectionQueryKeyScopeTuplesAux(
36
+ allSelections,
37
+ allMatchOnObj,
38
+ depth - 1,
39
+ dataset,
40
+ dataType,
41
+ isRequired,
42
+ [...prevLevelScopes, levelScope],
43
+ levelSelections,
44
+ currMatchOn?.[levelScope],
45
+ )) || [];
46
+ }
47
+
48
+ /**
49
+ * Get a flat list of tuples like (queryKey, scopeInfo)
50
+ * where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
51
+ * Selections and matchOnObj are assumed to be objects with the same keys,
52
+ * both nested to the specified depth. For example, if depth is 2,
53
+ * the first level of keys might be for image layer scopes,
54
+ * and the second level of keys might be for channel scopes.
55
+ * @param {object} selections
56
+ * @param {object} matchOnObj
57
+ * @param {number} depth
58
+ * @param {string} dataset
59
+ * @param {string} dataType
60
+ * @returns
61
+ */
62
+ export function getFeatureSelectionQueryKeyScopeTuples(
63
+ selections, matchOnObj, depth,
64
+ dataset, dataType, isRequired,
65
+ ) {
66
+ // Begin recursion.
67
+ return getFeatureSelectionQueryKeyScopeTuplesAux(
68
+ selections,
69
+ matchOnObj,
70
+ depth,
71
+ dataset,
72
+ dataType,
73
+ isRequired,
74
+ [],
75
+ selections,
76
+ matchOnObj,
77
+ );
78
+ }
79
+
80
+ function getMatrixIndicesQueryKeyScopeTuplesAux(
81
+ allMatchOnObj, depth, dataset, dataType, isRequired,
82
+ prevLevelScopes, currMatchOn,
83
+ ) {
84
+ // Base case
85
+ if (depth === 0) {
86
+ return ([[
87
+ // queryKey:
88
+ // Note: this uses the same key structure/suffix as
89
+ // useObsFeatureMatrixIndices for shared caching.
90
+ [dataset, dataType, currMatchOn, isRequired, 'useObsFeatureMatrixIndices'],
91
+ // scope info (for rolling up later)
92
+ { levelScopes: prevLevelScopes },
93
+ ]]);
94
+ }
95
+ // Recursive case
96
+ return Object.entries(currMatchOn)
97
+ ?.flatMap(([levelScope, levelMatchOn]) => getMatrixIndicesQueryKeyScopeTuplesAux(
98
+ allMatchOnObj,
99
+ depth - 1,
100
+ dataset,
101
+ dataType,
102
+ isRequired,
103
+ [...prevLevelScopes, levelScope],
104
+ levelMatchOn,
105
+ )) || [];
106
+ }
107
+
108
+ /**
109
+ * Get a flat list of tuples like (queryKey, scopeInfo)
110
+ * where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
111
+ * Selections and matchOnObj are assumed to be objects with the same keys,
112
+ * both nested to the specified depth. For example, if depth is 2,
113
+ * the first level of keys might be for image layer scopes,
114
+ * and the second level of keys might be for channel scopes.
115
+ * @param {object} matchOnObj
116
+ * @param {number} depth
117
+ * @param {string} dataset
118
+ * @param {string} dataType
119
+ * @returns
120
+ */
121
+ export function getMatrixIndicesQueryKeyScopeTuples(
122
+ matchOnObj, depth,
123
+ dataset, dataType, isRequired,
124
+ ) {
125
+ // Begin recursion.
126
+ return getMatrixIndicesQueryKeyScopeTuplesAux(
127
+ matchOnObj,
128
+ depth,
129
+ dataset,
130
+ dataType,
131
+ isRequired,
132
+ [],
133
+ matchOnObj,
134
+ );
135
+ }
136
+
137
+ function getQueryKeyScopeTuplesAux(
138
+ allMatchOnObj, depth, dataset, dataType, isRequired,
139
+ prevLevelScopes, currMatchOn,
140
+ ) {
141
+ // Base case
142
+ if (depth === 0) {
143
+ return ([[
144
+ // queryKey:
145
+ // Note: same key structure/suffix as
146
+ // useDataType() and useDataTypeMulti()
147
+ // for shared caching.
148
+ [dataset, dataType, currMatchOn, isRequired, 'useDataType'],
149
+ // scope info (for rolling up later)
150
+ { levelScopes: prevLevelScopes },
151
+ ]]);
152
+ }
153
+ // Recursive case
154
+ return Object.entries(currMatchOn)
155
+ ?.flatMap(([levelScope, levelMatchOn]) => getQueryKeyScopeTuplesAux(
156
+ allMatchOnObj,
157
+ depth - 1,
158
+ dataset,
159
+ dataType,
160
+ isRequired,
161
+ [...prevLevelScopes, levelScope],
162
+ levelMatchOn,
163
+ )) || [];
164
+ }
165
+
166
+ /**
167
+ * Get a flat list of tuples like (queryKey, scopeInfo)
168
+ * where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
169
+ * Selections and matchOnObj are assumed to be objects with the same keys,
170
+ * both nested to the specified depth. For example, if depth is 2,
171
+ * the first level of keys might be for image layer scopes,
172
+ * and the second level of keys might be for channel scopes.
173
+ * @param {object} matchOnObj
174
+ * @param {number} depth
175
+ * @param {string} dataset
176
+ * @param {string} dataType
177
+ * @returns
178
+ */
179
+ export function getQueryKeyScopeTuples(
180
+ matchOnObj, depth,
181
+ dataset, dataType, isRequired,
182
+ ) {
183
+ // Begin recursion.
184
+ return getQueryKeyScopeTuplesAux(
185
+ matchOnObj,
186
+ depth,
187
+ dataset,
188
+ dataType,
189
+ isRequired,
190
+ [],
191
+ matchOnObj,
192
+ );
193
+ }
194
+
195
+
196
+ function initializeNestedObjectAux(levelScopes, currObj, getBaseValue, currLevel) {
197
+ const currScope = levelScopes[currLevel];
198
+ const depthRemaining = levelScopes.length - currLevel;
199
+ // Base case.
200
+ if (depthRemaining === 0) {
201
+ return currObj;
202
+ }
203
+ // Recursive case.
204
+ if (!currObj[currScope]) {
205
+ if (depthRemaining === 1) {
206
+ // eslint-disable-next-line no-param-reassign
207
+ currObj[currScope] = getBaseValue();
208
+ } else {
209
+ // eslint-disable-next-line no-param-reassign
210
+ currObj[currScope] = {};
211
+ }
212
+ }
213
+ return initializeNestedObjectAux(levelScopes, currObj[currScope], getBaseValue, currLevel + 1);
214
+ }
215
+
216
+ /**
217
+ * For a list of paths into a nested object,
218
+ * initialize the object if the object keys do not yet exist.
219
+ * For the first level, the object is initialized to the return
220
+ * value of getBaseValue. For example, this allows initializing
221
+ * to an empty array (without reusing the same array object reference).
222
+ * @param {string[]} levelScopes
223
+ * @param {object} currObj
224
+ * @param {function} getBaseValue
225
+ * @returns The value at the end of the path specified by `levelScopes`.
226
+ */
227
+ export function initializeNestedObject(levelScopes, currObj, getBaseValue) {
228
+ // Begin recursion.
229
+ return initializeNestedObjectAux(levelScopes, currObj, getBaseValue, 0);
230
+ }
231
+
232
+ /**
233
+ * Nest query results.
234
+ * @param {array} queryKeyScopeTuples
235
+ * @param {array} flatQueryResults Return value of useQueries,
236
+ * after .map() to get inner data elements.
237
+ * @returns The nested object.
238
+ */
239
+ export function nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatQueryResults) {
240
+ const nestedData = {};
241
+
242
+ // eslint-disable-next-line no-unused-vars
243
+ queryKeyScopeTuples.forEach(([queryKey, { levelScopes, featureIndex, numFeatures }], i) => {
244
+ const getBaseValue = () => new Array(numFeatures);
245
+ const subObj = initializeNestedObject(levelScopes, nestedData, getBaseValue);
246
+ subObj[featureIndex] = flatQueryResults?.[i];
247
+ });
248
+ return nestedData;
249
+ }
250
+
251
+ /**
252
+ * Nest query results.
253
+ * @param {array} queryKeyScopeTuples
254
+ * @param {array} flatQueryResults Return value of useQueries,
255
+ * after .map() to get inner data elements.
256
+ * @returns The nested object.
257
+ */
258
+ export function nestQueryResults(queryKeyScopeTuples, flatQueryResults) {
259
+ const nestedData = {};
260
+
261
+ // eslint-disable-next-line no-unused-vars
262
+ queryKeyScopeTuples.forEach(([queryKey, { levelScopes }], i) => {
263
+ const getBaseValue = () => ({});
264
+ const subObj = initializeNestedObject(levelScopes, nestedData, getBaseValue);
265
+ Object.entries(flatQueryResults?.[i] || {}).forEach(([key, value]) => {
266
+ subObj[key] = value;
267
+ });
268
+ });
269
+ return nestedData;
270
+ }
271
+
272
+ async function featureSelectionQueryFn(ctx) {
273
+ // (copied from useFeatureSelection queryFn)
274
+ const { loaders } = ctx.meta;
275
+ const [dataset, dataType, matchOn, featureId, isRequired] = ctx.queryKey;
276
+ const loader = getMatchingLoader(loaders, dataset, dataType, matchOn);
277
+ if (loader) {
278
+ const implementsGeneSelection = typeof loader.loadGeneSelection === 'function';
279
+ if (implementsGeneSelection) {
280
+ const payload = await loader.loadGeneSelection({ selection: [featureId] });
281
+ if (!payload) return null;
282
+ const { data } = payload;
283
+ const expressionData = data[0];
284
+ // Compute [min, max] extent of the data, and also a normalized version.
285
+ const dataExtent = extent(expressionData);
286
+ const [min, max] = dataExtent;
287
+ const ratio = 255 / (max - min);
288
+ const normData = new Uint8Array(
289
+ expressionData.map(j => Math.floor((j - min) * ratio)),
290
+ );
291
+ return {
292
+ data: expressionData,
293
+ normData,
294
+ dataExtent,
295
+ dataKey: featureId,
296
+ };
297
+ }
298
+ // Loader does not implement loadGeneSelection.
299
+ const payload = await loader.load();
300
+ if (!payload) return null;
301
+ const { data } = payload;
302
+ const { obsIndex, featureIndex, obsFeatureMatrix } = data;
303
+ // Compute expressionData
304
+ const geneIndex = featureIndex.indexOf(featureId);
305
+ const numGenes = featureIndex.length;
306
+ const numCells = obsIndex.length;
307
+ const expressionData = new Float32Array(numCells);
308
+ for (let cellIndex = 0; cellIndex < numCells; cellIndex += 1) {
309
+ expressionData[cellIndex] = obsFeatureMatrix.data[cellIndex * numGenes + geneIndex];
310
+ }
311
+ // Compute [min, max] extent of the data, and also a normalized version.
312
+ const dataExtent = extent(expressionData);
313
+ const [min, max] = dataExtent;
314
+ const ratio = 255 / (max - min);
315
+ const normData = new Uint8Array(
316
+ expressionData.map(j => Math.floor((j - min) * ratio)),
317
+ );
318
+ return {
319
+ data: expressionData,
320
+ normData,
321
+ dataExtent,
322
+ dataKey: featureId,
323
+ };
324
+ }
325
+ // No loader was found.
326
+ if (isRequired) {
327
+ throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
328
+ } else {
329
+ return { data: null, dataKey: null };
330
+ }
331
+ }
332
+
333
+ export function useFeatureSelectionMultiLevel(
334
+ loaders, dataset, isRequired, matchOnObj, selections,
335
+ depth,
336
+ ) {
337
+ const setWarning = useSetWarning();
338
+
339
+ // Create a flat list of tuples (queryKey, scopeInfo).
340
+ const queryKeyScopeTuples = useMemo(() => getFeatureSelectionQueryKeyScopeTuples(
341
+ selections, matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired,
342
+ ), [selections, matchOnObj, depth, dataset]);
343
+
344
+ const featureQueries = useQueries({
345
+ queries: queryKeyScopeTuples.map(([queryKey]) => ({
346
+ structuralSharing: false,
347
+ placeholderData: null,
348
+ queryKey,
349
+ queryFn: featureSelectionQueryFn,
350
+ meta: { loaders },
351
+ })),
352
+ });
353
+
354
+ const anyLoading = featureQueries.some(q => q.isFetching);
355
+ const anyError = featureQueries.some(q => q.isError);
356
+ // eslint-disable-next-line no-nested-ternary
357
+ const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
358
+ const flatGeneData = featureQueries.map(q => q.data?.data || null);
359
+ const flatLoadedGeneName = featureQueries.map(q => q.data?.dataKey || null);
360
+ const flatExtents = featureQueries.map(q => q.data?.dataExtent || null);
361
+ const flatNormData = featureQueries.map(q => q.data?.normData || null);
362
+
363
+ useEffect(() => {
364
+ featureQueries
365
+ .map(q => q.error)
366
+ .filter(e => Boolean(e))
367
+ .forEach((error) => {
368
+ warn(error, setWarning);
369
+ });
370
+ // Deliberate dependency omissions: use indirect dependencies for efficiency.
371
+ // eslint-disable-next-line react-hooks/exhaustive-deps
372
+ }, [anyError, setWarning]);
373
+
374
+ // Need to re-nest the geneData and the loadedGeneName info.
375
+ const [geneData, loadedGeneName, extents, normData] = useMemo(() => {
376
+ const nestedGeneData = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatGeneData);
377
+ const nestedLoadedGeneName = nestFeatureSelectionQueryResults(
378
+ queryKeyScopeTuples, flatLoadedGeneName,
379
+ );
380
+ const nestedExtents = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatExtents);
381
+ const nestedNormData = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatNormData);
382
+ return [nestedGeneData, nestedLoadedGeneName, nestedExtents, nestedNormData];
383
+
384
+ // We do not want this useMemo to execute on every re-render, only when the
385
+ // featureQueries results change. Unfortunately, the featureQueries array
386
+ // reference is not stable on each re-render, so we use dataUpdatedAt instead.
387
+ // We use .reduce to ensure the number of dependencies is stable
388
+ // (i.e., a single number, despite possibly different numbers of queries).
389
+ // Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
390
+ // eslint-disable-next-line react-hooks/exhaustive-deps
391
+ }, [featureQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
392
+
393
+ return [geneData, loadedGeneName, extents, normData, dataStatus];
394
+ }
395
+
396
+ async function matrixIndicesQueryFn(ctx) {
397
+ // (copied from useFeatureSelection queryFn)
398
+ const { loaders } = ctx.meta;
399
+ const [dataset, dataType, matchOn, isRequired] = ctx.queryKey;
400
+ const loader = getMatchingLoader(loaders, dataset, dataType, matchOn);
401
+ if (loader) {
402
+ const implementsLoadAttrs = typeof loader.loadAttrs === 'function';
403
+ if (implementsLoadAttrs) {
404
+ const payload = await loader.loadAttrs();
405
+ if (!payload) return null;
406
+ const { data: payloadData } = payload;
407
+ return {
408
+ data: {
409
+ obsIndex: payloadData.rows,
410
+ featureIndex: payloadData.cols,
411
+ },
412
+ };
413
+ }
414
+ // Loader does not implement loadAttrs.
415
+ const payload = await loader.load();
416
+ if (!payload) return null;
417
+ const { data: payloadData } = payload;
418
+ return {
419
+ data: {
420
+ obsIndex: payloadData.obsIndex,
421
+ featureIndex: payloadData.featureIndex,
422
+ },
423
+ };
424
+ }
425
+ // No loader was found.
426
+ if (isRequired) {
427
+ throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
428
+ } else {
429
+ return { data: null, dataKey: null };
430
+ }
431
+ }
432
+
433
+ export function useObsFeatureMatrixIndicesMultiLevel(
434
+ loaders, dataset, isRequired, matchOnObj,
435
+ depth,
436
+ ) {
437
+ const setWarning = useSetWarning();
438
+
439
+ // Create a flat list of tuples (queryKey, scopeInfo).
440
+ const queryKeyScopeTuples = useMemo(() => getMatrixIndicesQueryKeyScopeTuples(
441
+ matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired,
442
+ ), [matchOnObj, depth, dataset]);
443
+
444
+ const indicesQueries = useQueries({
445
+ queries: queryKeyScopeTuples.map(([queryKey]) => ({
446
+ structuralSharing: false,
447
+ placeholderData: null,
448
+ queryKey,
449
+ queryFn: matrixIndicesQueryFn,
450
+ meta: { loaders },
451
+ })),
452
+ });
453
+
454
+ const anyLoading = indicesQueries.some(q => q.isFetching);
455
+ const anyError = indicesQueries.some(q => q.isError);
456
+ // eslint-disable-next-line no-nested-ternary
457
+ const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
458
+ const flatIndicesData = indicesQueries.map(q => q.data?.data || null);
459
+
460
+ useEffect(() => {
461
+ indicesQueries
462
+ .map(q => q.error)
463
+ .filter(e => Boolean(e))
464
+ .forEach((error) => {
465
+ warn(error, setWarning);
466
+ });
467
+ // Deliberate dependency omissions: use indirect dependencies for efficiency.
468
+ // eslint-disable-next-line react-hooks/exhaustive-deps
469
+ }, [anyError, setWarning]);
470
+
471
+ // Need to re-nest the geneData and the loadedGeneName info.
472
+ const indicesData = useMemo(() => {
473
+ const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
474
+ return nestedIndicesData;
475
+
476
+ // We do not want this useMemo to execute on every re-render, only when the
477
+ // featureQueries results change. Unfortunately, the featureQueries array
478
+ // reference is not stable on each re-render, so we use dataUpdatedAt instead.
479
+ // We use .reduce to ensure the number of dependencies is stable
480
+ // (i.e., a single number, despite possibly different numbers of queries).
481
+ // Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
482
+ // eslint-disable-next-line react-hooks/exhaustive-deps
483
+ }, [indicesQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
484
+
485
+ return [indicesData, dataStatus];
486
+ }
487
+
488
+ function useDataTypeMultiLevel(
489
+ loaders, dataset, isRequired, matchOnObj,
490
+ depth, dataType,
491
+ ) {
492
+ const setWarning = useSetWarning();
493
+
494
+ // Create a flat list of tuples (queryKey, scopeInfo).
495
+ const queryKeyScopeTuples = useMemo(() => getQueryKeyScopeTuples(
496
+ matchOnObj, depth, dataset, dataType, isRequired,
497
+ ), [matchOnObj, depth, dataset]);
498
+
499
+ const locationsQueries = useQueries({
500
+ queries: queryKeyScopeTuples.map(([queryKey]) => ({
501
+ structuralSharing: false,
502
+ placeholderData: null,
503
+ queryKey,
504
+ queryFn: dataQueryFn,
505
+ meta: { loaders },
506
+ })),
507
+ });
508
+
509
+ const anyLoading = locationsQueries.some(q => q.isFetching);
510
+ const anyError = locationsQueries.some(q => q.isError);
511
+ // eslint-disable-next-line no-nested-ternary
512
+ const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
513
+ const flatIndicesData = locationsQueries.map(q => q.data?.data || null);
514
+
515
+ useEffect(() => {
516
+ locationsQueries
517
+ .map(q => q.error)
518
+ .filter(e => Boolean(e))
519
+ .forEach((error) => {
520
+ warn(error, setWarning);
521
+ });
522
+ // Deliberate dependency omissions: use indirect dependencies for efficiency.
523
+ // eslint-disable-next-line react-hooks/exhaustive-deps
524
+ }, [anyError, setWarning]);
525
+
526
+ // Need to re-nest the geneData and the loadedGeneName info.
527
+ const locationsData = useMemo(() => {
528
+ const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
529
+ return nestedIndicesData;
530
+
531
+ // We do not want this useMemo to execute on every re-render, only when the
532
+ // featureQueries results change. Unfortunately, the featureQueries array
533
+ // reference is not stable on each re-render, so we use dataUpdatedAt instead.
534
+ // We use .reduce to ensure the number of dependencies is stable
535
+ // (i.e., a single number, despite possibly different numbers of queries).
536
+ // Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
537
+ // eslint-disable-next-line react-hooks/exhaustive-deps
538
+ }, [locationsQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
539
+
540
+ return [locationsData, dataStatus];
541
+ }
542
+
543
+ export function useObsLocationsMultiLevel(
544
+ loaders, dataset, isRequired, matchOnObj,
545
+ depth,
546
+ ) {
547
+ return useDataTypeMultiLevel(
548
+ loaders, dataset, isRequired, matchOnObj,
549
+ depth, DataType.OBS_LOCATIONS,
550
+ );
551
+ }
552
+
553
+ export function useObsSetsMultiLevel(
554
+ loaders, dataset, isRequired, matchOnObj,
555
+ depth,
556
+ ) {
557
+ return useDataTypeMultiLevel(
558
+ loaders, dataset, isRequired, matchOnObj,
559
+ depth, DataType.OBS_SETS,
560
+ );
561
+ }
562
+
563
+ export function useObsLabelsMultiLevel(
564
+ loaders, dataset, isRequired, matchOnObj,
565
+ depth,
566
+ ) {
567
+ return useDataTypeMultiLevel(
568
+ loaders, dataset, isRequired, matchOnObj,
569
+ depth, DataType.OBS_LABELS,
570
+ );
571
+ }