@vitessce/vit-s 3.1.3 → 3.2.1
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/dist/index.js +1801 -132
- package/dist-tsc/data-hook-utils.d.ts +1 -0
- package/dist-tsc/data-hook-utils.d.ts.map +1 -1
- package/dist-tsc/data-hook-utils.js +12 -4
- package/dist-tsc/data-hooks-multilevel-utils.d.ts +77 -0
- package/dist-tsc/data-hooks-multilevel-utils.d.ts.map +1 -0
- package/dist-tsc/data-hooks-multilevel-utils.js +418 -0
- package/dist-tsc/data-hooks-multilevel-utils.test.d.ts +2 -0
- package/dist-tsc/data-hooks-multilevel-utils.test.d.ts.map +1 -0
- package/dist-tsc/data-hooks-multilevel-utils.test.js +101 -0
- package/dist-tsc/data-hooks-multilevel.d.ts +8 -0
- package/dist-tsc/data-hooks-multilevel.d.ts.map +1 -0
- package/dist-tsc/data-hooks-multilevel.js +125 -0
- package/dist-tsc/data-hooks.d.ts +7 -0
- package/dist-tsc/data-hooks.d.ts.map +1 -1
- package/dist-tsc/data-hooks.js +75 -6
- package/dist-tsc/hooks.js +1 -1
- package/dist-tsc/index.d.ts +3 -2
- package/dist-tsc/index.js +3 -2
- package/dist-tsc/shared-mui/components.d.ts.map +1 -1
- package/dist-tsc/shared-mui/components.js +3 -2
- package/dist-tsc/shared-plot-options/CellColorEncodingOption.d.ts.map +1 -1
- package/dist-tsc/shared-plot-options/CellColorEncodingOption.js +2 -1
- package/dist-tsc/state/hooks.d.ts +80 -21
- package/dist-tsc/state/hooks.d.ts.map +1 -1
- package/dist-tsc/state/hooks.js +428 -103
- package/dist-tsc/state/hooks.test.d.ts +2 -0
- package/dist-tsc/state/hooks.test.d.ts.map +1 -0
- package/dist-tsc/state/hooks.test.js +149 -0
- package/dist-tsc/state/spatial-reducers.d.ts +25 -0
- package/dist-tsc/state/spatial-reducers.d.ts.map +1 -0
- package/dist-tsc/state/spatial-reducers.js +251 -0
- package/dist-tsc/state/spatial-reducers.test.d.ts +2 -0
- package/dist-tsc/state/spatial-reducers.test.d.ts.map +1 -0
- package/dist-tsc/state/spatial-reducers.test.js +1382 -0
- package/package.json +6 -5
- package/src/data-hook-utils.js +19 -3
- package/src/data-hooks-multilevel-utils.js +571 -0
- package/src/data-hooks-multilevel-utils.test.js +110 -0
- package/src/data-hooks-multilevel.js +226 -0
- package/src/data-hooks.js +156 -6
- package/src/hooks.js +1 -1
- package/src/index.js +25 -0
- package/src/shared-mui/components.js +9 -4
- package/src/shared-plot-options/CellColorEncodingOption.js +2 -1
- package/src/state/hooks.js +485 -104
- package/src/state/hooks.test.js +182 -0
- package/src/state/spatial-reducers.js +291 -0
- package/src/state/spatial-reducers.test.js +1432 -0
|
@@ -16,6 +16,7 @@ export function warn(error: AbstractLoaderError, setWarning: any): void;
|
|
|
16
16
|
* keys are coordination type names and keys are values.
|
|
17
17
|
*/
|
|
18
18
|
export function initCoordinationSpace(values: object, setters: object, initialValues: object): void;
|
|
19
|
+
export function dataQueryFn(ctx: any): Promise<any>;
|
|
19
20
|
/**
|
|
20
21
|
* Get data from a cells data type loader,
|
|
21
22
|
* updating "ready" and URL state appropriately.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-hook-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-utils.js"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,4BAFW,mBAAmB,yBAS7B;AAED;;;;;;;;;;GAUG;AACH,8CATW,MAAM,WAGN,MAAM,iBAGN,MAAM,QAiBhB;
|
|
1
|
+
{"version":3,"file":"data-hook-utils.d.ts","sourceRoot":"","sources":["../src/data-hook-utils.js"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,4BAFW,mBAAmB,yBAS7B;AAED;;;;;;;;;;GAUG;AACH,8CATW,MAAM,WAGN,MAAM,iBAGN,MAAM,QAiBhB;AAGD,oDAuBC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,oDAhBW,MAAM,WAEN,MAAM,cAEN,OAAO,uBAEP,MAAM,6BAGN,MAAM,uBAkDhB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,yDAhBW,MAAM,WAEN,MAAM,cAEN,OAAO,uBAEP,MAAM,6BAGN,MAAM,0BA+EhB;AAED,+FAGC"}
|
|
@@ -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 { getMatchingLoader, useMatchingLoader, useSetWarning, } from './state/hooks.js';
|
|
6
6
|
import { AbstractLoaderError, LoaderNotFoundError, } from './errors/index.js';
|
|
7
7
|
/**
|
|
@@ -43,7 +43,7 @@ export function initCoordinationSpace(values, setters, initialValues) {
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
async function dataQueryFn(ctx) {
|
|
46
|
+
export async function dataQueryFn(ctx) {
|
|
47
47
|
const { placeholderObject, loaders } = ctx.meta;
|
|
48
48
|
// This ordering of the queryKey must match.
|
|
49
49
|
const [dataset, dataType, matchOn, isRequired] = ctx.queryKey;
|
|
@@ -99,6 +99,9 @@ export function useDataType(dataType, loaders, dataset, isRequired, coordination
|
|
|
99
99
|
placeholderData: placeholderObject,
|
|
100
100
|
// Include the hook name in the queryKey to prevent the case in which an identical queryKey
|
|
101
101
|
// in a different hook would cause an accidental cache hit.
|
|
102
|
+
// Note: same key structure/suffix as
|
|
103
|
+
// useDataTypeMulti() and getQueryKeyScopeTuplesAux()
|
|
104
|
+
// for shared caching.
|
|
102
105
|
queryKey: [dataset, dataType, matchOn, isRequired, 'useDataType'],
|
|
103
106
|
// Query function should return an object
|
|
104
107
|
// { data, dataKey } where dataKey is the loaded gene selection.
|
|
@@ -152,6 +155,9 @@ export function useDataTypeMulti(dataType, loaders, dataset, isRequired, coordin
|
|
|
152
155
|
placeholderData: placeholderObject,
|
|
153
156
|
// Query key should match useDataType for shared cacheing
|
|
154
157
|
// and correctness of dataQueryFn.
|
|
158
|
+
// Note: same key structure/suffix as
|
|
159
|
+
// useDataType() and getQueryKeyScopeTuplesAux()
|
|
160
|
+
// for shared caching.
|
|
155
161
|
queryKey: [dataset, dataType, matchOn, isRequired, 'useDataType'],
|
|
156
162
|
// Query function should return an object
|
|
157
163
|
// { data, dataKey } where dataKey is the loaded gene selection.
|
|
@@ -182,8 +188,10 @@ export function useDataTypeMulti(dataType, loaders, dataset, isRequired, coordin
|
|
|
182
188
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
189
|
}, [anyError, setWarning]);
|
|
184
190
|
// Convert data to object keyed by scopeKey.
|
|
185
|
-
const data = useMemo(() => fromEntries(matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.data]))), [
|
|
186
|
-
|
|
191
|
+
const data = useMemo(() => fromEntries(matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.data]))), [matchOnObj, dataStatus]);
|
|
192
|
+
// Convert data to object keyed by scopeKey.
|
|
193
|
+
const urls = useMemo(() => fromEntries(matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.urls]))), [dataQueries]);
|
|
194
|
+
return [data, dataStatus, urls];
|
|
187
195
|
}
|
|
188
196
|
export function useHasLoader(loaders, dataset, dataType, matchOn) {
|
|
189
197
|
const loader = useMatchingLoader(loaders, dataset, dataType, matchOn);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
3
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
4
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
5
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
6
|
+
* the first level of keys might be for image layer scopes,
|
|
7
|
+
* and the second level of keys might be for channel scopes.
|
|
8
|
+
* @param {object} selections
|
|
9
|
+
* @param {object} matchOnObj
|
|
10
|
+
* @param {number} depth
|
|
11
|
+
* @param {string} dataset
|
|
12
|
+
* @param {string} dataType
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export function getFeatureSelectionQueryKeyScopeTuples(selections: object, matchOnObj: object, depth: number, dataset: string, dataType: string, isRequired: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
18
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
19
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
20
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
21
|
+
* the first level of keys might be for image layer scopes,
|
|
22
|
+
* and the second level of keys might be for channel scopes.
|
|
23
|
+
* @param {object} matchOnObj
|
|
24
|
+
* @param {number} depth
|
|
25
|
+
* @param {string} dataset
|
|
26
|
+
* @param {string} dataType
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
export function getMatrixIndicesQueryKeyScopeTuples(matchOnObj: object, depth: number, dataset: string, dataType: string, isRequired: any): any;
|
|
30
|
+
/**
|
|
31
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
32
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
33
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
34
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
35
|
+
* the first level of keys might be for image layer scopes,
|
|
36
|
+
* and the second level of keys might be for channel scopes.
|
|
37
|
+
* @param {object} matchOnObj
|
|
38
|
+
* @param {number} depth
|
|
39
|
+
* @param {string} dataset
|
|
40
|
+
* @param {string} dataType
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
export function getQueryKeyScopeTuples(matchOnObj: object, depth: number, dataset: string, dataType: string, isRequired: any): any;
|
|
44
|
+
/**
|
|
45
|
+
* For a list of paths into a nested object,
|
|
46
|
+
* initialize the object if the object keys do not yet exist.
|
|
47
|
+
* For the first level, the object is initialized to the return
|
|
48
|
+
* value of getBaseValue. For example, this allows initializing
|
|
49
|
+
* to an empty array (without reusing the same array object reference).
|
|
50
|
+
* @param {string[]} levelScopes
|
|
51
|
+
* @param {object} currObj
|
|
52
|
+
* @param {function} getBaseValue
|
|
53
|
+
* @returns The value at the end of the path specified by `levelScopes`.
|
|
54
|
+
*/
|
|
55
|
+
export function initializeNestedObject(levelScopes: string[], currObj: object, getBaseValue: Function): any;
|
|
56
|
+
/**
|
|
57
|
+
* Nest query results.
|
|
58
|
+
* @param {array} queryKeyScopeTuples
|
|
59
|
+
* @param {array} flatQueryResults Return value of useQueries,
|
|
60
|
+
* after .map() to get inner data elements.
|
|
61
|
+
* @returns The nested object.
|
|
62
|
+
*/
|
|
63
|
+
export function nestFeatureSelectionQueryResults(queryKeyScopeTuples: array, flatQueryResults: array): {};
|
|
64
|
+
/**
|
|
65
|
+
* Nest query results.
|
|
66
|
+
* @param {array} queryKeyScopeTuples
|
|
67
|
+
* @param {array} flatQueryResults Return value of useQueries,
|
|
68
|
+
* after .map() to get inner data elements.
|
|
69
|
+
* @returns The nested object.
|
|
70
|
+
*/
|
|
71
|
+
export function nestQueryResults(queryKeyScopeTuples: array, flatQueryResults: array): {};
|
|
72
|
+
export function useFeatureSelectionMultiLevel(loaders: any, dataset: any, isRequired: any, matchOnObj: any, selections: any, depth: any): any[];
|
|
73
|
+
export function useObsFeatureMatrixIndicesMultiLevel(loaders: any, dataset: any, isRequired: any, matchOnObj: any, depth: any): any[];
|
|
74
|
+
export function useObsLocationsMultiLevel(loaders: any, dataset: any, isRequired: any, matchOnObj: any, depth: any): any[];
|
|
75
|
+
export function useObsSetsMultiLevel(loaders: any, dataset: any, isRequired: any, matchOnObj: any, depth: any): any[];
|
|
76
|
+
export function useObsLabelsMultiLevel(loaders: any, dataset: any, isRequired: any, matchOnObj: any, depth: any): any[];
|
|
77
|
+
//# sourceMappingURL=data-hooks-multilevel-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-hooks-multilevel-utils.d.ts","sourceRoot":"","sources":["../src/data-hooks-multilevel-utils.js"],"names":[],"mappings":"AA+CA;;;;;;;;;;;;;GAaG;AACH,mEAPW,MAAM,cACN,MAAM,SACN,MAAM,WACN,MAAM,YACN,MAAM,wBAmBhB;AA8BD;;;;;;;;;;;;GAYG;AACH,gEANW,MAAM,SACN,MAAM,WACN,MAAM,YACN,MAAM,wBAiBhB;AA+BD;;;;;;;;;;;;GAYG;AACH,mDANW,MAAM,SACN,MAAM,WACN,MAAM,YACN,MAAM,wBAiBhB;AAuBD;;;;;;;;;;GAUG;AACH,oDALW,MAAM,EAAE,WACR,MAAM,+BAOhB;AAED;;;;;;GAMG;AACH,0GAUC;AAED;;;;;;GAMG;AACH,0FAYC;AA+DD,gJA6DC;AAuCD,sIAqDC;AAyDD,2HAQC;AAED,sHAQC;AAED,wHAQC"}
|
|
@@ -0,0 +1,418 @@
|
|
|
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 { getMatchingLoader, useSetWarning, } from './state/hooks.js';
|
|
6
|
+
import { LoaderNotFoundError, } from './errors/index.js';
|
|
7
|
+
import { warn, dataQueryFn, } from './data-hook-utils.js';
|
|
8
|
+
function getFeatureSelectionQueryKeyScopeTuplesAux(allSelections, allMatchOnObj, depth, dataset, dataType, isRequired, prevLevelScopes, currSelection, currMatchOn) {
|
|
9
|
+
// Base case
|
|
10
|
+
if (depth === 0) {
|
|
11
|
+
return currSelection
|
|
12
|
+
?.map((featureId, featureIndex) => ([
|
|
13
|
+
// queryKey:
|
|
14
|
+
// TODO: use same key suffix as useFeatureSelection for shared caching?
|
|
15
|
+
[dataset, dataType, currMatchOn, featureId, isRequired, 'useFeatureSelectionMultiLevel'],
|
|
16
|
+
// scope info (for rolling up later)
|
|
17
|
+
{ levelScopes: prevLevelScopes, featureIndex, numFeatures: currSelection.length },
|
|
18
|
+
])) || [];
|
|
19
|
+
}
|
|
20
|
+
// Recursive case
|
|
21
|
+
return Object.entries(currSelection)
|
|
22
|
+
?.flatMap(([levelScope, levelSelections]) => getFeatureSelectionQueryKeyScopeTuplesAux(allSelections, allMatchOnObj, depth - 1, dataset, dataType, isRequired, [...prevLevelScopes, levelScope], levelSelections, currMatchOn?.[levelScope])) || [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
26
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
27
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
28
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
29
|
+
* the first level of keys might be for image layer scopes,
|
|
30
|
+
* and the second level of keys might be for channel scopes.
|
|
31
|
+
* @param {object} selections
|
|
32
|
+
* @param {object} matchOnObj
|
|
33
|
+
* @param {number} depth
|
|
34
|
+
* @param {string} dataset
|
|
35
|
+
* @param {string} dataType
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
export function getFeatureSelectionQueryKeyScopeTuples(selections, matchOnObj, depth, dataset, dataType, isRequired) {
|
|
39
|
+
// Begin recursion.
|
|
40
|
+
return getFeatureSelectionQueryKeyScopeTuplesAux(selections, matchOnObj, depth, dataset, dataType, isRequired, [], selections, matchOnObj);
|
|
41
|
+
}
|
|
42
|
+
function getMatrixIndicesQueryKeyScopeTuplesAux(allMatchOnObj, depth, dataset, dataType, isRequired, prevLevelScopes, currMatchOn) {
|
|
43
|
+
// Base case
|
|
44
|
+
if (depth === 0) {
|
|
45
|
+
return ([[
|
|
46
|
+
// queryKey:
|
|
47
|
+
// Note: this uses the same key structure/suffix as
|
|
48
|
+
// useObsFeatureMatrixIndices for shared caching.
|
|
49
|
+
[dataset, dataType, currMatchOn, isRequired, 'useObsFeatureMatrixIndices'],
|
|
50
|
+
// scope info (for rolling up later)
|
|
51
|
+
{ levelScopes: prevLevelScopes },
|
|
52
|
+
]]);
|
|
53
|
+
}
|
|
54
|
+
// Recursive case
|
|
55
|
+
return Object.entries(currMatchOn)
|
|
56
|
+
?.flatMap(([levelScope, levelMatchOn]) => getMatrixIndicesQueryKeyScopeTuplesAux(allMatchOnObj, depth - 1, dataset, dataType, isRequired, [...prevLevelScopes, levelScope], levelMatchOn)) || [];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
60
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
61
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
62
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
63
|
+
* the first level of keys might be for image layer scopes,
|
|
64
|
+
* and the second level of keys might be for channel scopes.
|
|
65
|
+
* @param {object} matchOnObj
|
|
66
|
+
* @param {number} depth
|
|
67
|
+
* @param {string} dataset
|
|
68
|
+
* @param {string} dataType
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
export function getMatrixIndicesQueryKeyScopeTuples(matchOnObj, depth, dataset, dataType, isRequired) {
|
|
72
|
+
// Begin recursion.
|
|
73
|
+
return getMatrixIndicesQueryKeyScopeTuplesAux(matchOnObj, depth, dataset, dataType, isRequired, [], matchOnObj);
|
|
74
|
+
}
|
|
75
|
+
function getQueryKeyScopeTuplesAux(allMatchOnObj, depth, dataset, dataType, isRequired, prevLevelScopes, currMatchOn) {
|
|
76
|
+
// Base case
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
return ([[
|
|
79
|
+
// queryKey:
|
|
80
|
+
// Note: same key structure/suffix as
|
|
81
|
+
// useDataType() and useDataTypeMulti()
|
|
82
|
+
// for shared caching.
|
|
83
|
+
[dataset, dataType, currMatchOn, isRequired, 'useDataType'],
|
|
84
|
+
// scope info (for rolling up later)
|
|
85
|
+
{ levelScopes: prevLevelScopes },
|
|
86
|
+
]]);
|
|
87
|
+
}
|
|
88
|
+
// Recursive case
|
|
89
|
+
return Object.entries(currMatchOn)
|
|
90
|
+
?.flatMap(([levelScope, levelMatchOn]) => getQueryKeyScopeTuplesAux(allMatchOnObj, depth - 1, dataset, dataType, isRequired, [...prevLevelScopes, levelScope], levelMatchOn)) || [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get a flat list of tuples like (queryKey, scopeInfo)
|
|
94
|
+
* where scopeInfo is an object like { levelScopes, featureIndex, numFeatures }.
|
|
95
|
+
* Selections and matchOnObj are assumed to be objects with the same keys,
|
|
96
|
+
* both nested to the specified depth. For example, if depth is 2,
|
|
97
|
+
* the first level of keys might be for image layer scopes,
|
|
98
|
+
* and the second level of keys might be for channel scopes.
|
|
99
|
+
* @param {object} matchOnObj
|
|
100
|
+
* @param {number} depth
|
|
101
|
+
* @param {string} dataset
|
|
102
|
+
* @param {string} dataType
|
|
103
|
+
* @returns
|
|
104
|
+
*/
|
|
105
|
+
export function getQueryKeyScopeTuples(matchOnObj, depth, dataset, dataType, isRequired) {
|
|
106
|
+
// Begin recursion.
|
|
107
|
+
return getQueryKeyScopeTuplesAux(matchOnObj, depth, dataset, dataType, isRequired, [], matchOnObj);
|
|
108
|
+
}
|
|
109
|
+
function initializeNestedObjectAux(levelScopes, currObj, getBaseValue, currLevel) {
|
|
110
|
+
const currScope = levelScopes[currLevel];
|
|
111
|
+
const depthRemaining = levelScopes.length - currLevel;
|
|
112
|
+
// Base case.
|
|
113
|
+
if (depthRemaining === 0) {
|
|
114
|
+
return currObj;
|
|
115
|
+
}
|
|
116
|
+
// Recursive case.
|
|
117
|
+
if (!currObj[currScope]) {
|
|
118
|
+
if (depthRemaining === 1) {
|
|
119
|
+
// eslint-disable-next-line no-param-reassign
|
|
120
|
+
currObj[currScope] = getBaseValue();
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// eslint-disable-next-line no-param-reassign
|
|
124
|
+
currObj[currScope] = {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return initializeNestedObjectAux(levelScopes, currObj[currScope], getBaseValue, currLevel + 1);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* For a list of paths into a nested object,
|
|
131
|
+
* initialize the object if the object keys do not yet exist.
|
|
132
|
+
* For the first level, the object is initialized to the return
|
|
133
|
+
* value of getBaseValue. For example, this allows initializing
|
|
134
|
+
* to an empty array (without reusing the same array object reference).
|
|
135
|
+
* @param {string[]} levelScopes
|
|
136
|
+
* @param {object} currObj
|
|
137
|
+
* @param {function} getBaseValue
|
|
138
|
+
* @returns The value at the end of the path specified by `levelScopes`.
|
|
139
|
+
*/
|
|
140
|
+
export function initializeNestedObject(levelScopes, currObj, getBaseValue) {
|
|
141
|
+
// Begin recursion.
|
|
142
|
+
return initializeNestedObjectAux(levelScopes, currObj, getBaseValue, 0);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Nest query results.
|
|
146
|
+
* @param {array} queryKeyScopeTuples
|
|
147
|
+
* @param {array} flatQueryResults Return value of useQueries,
|
|
148
|
+
* after .map() to get inner data elements.
|
|
149
|
+
* @returns The nested object.
|
|
150
|
+
*/
|
|
151
|
+
export function nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatQueryResults) {
|
|
152
|
+
const nestedData = {};
|
|
153
|
+
// eslint-disable-next-line no-unused-vars
|
|
154
|
+
queryKeyScopeTuples.forEach(([queryKey, { levelScopes, featureIndex, numFeatures }], i) => {
|
|
155
|
+
const getBaseValue = () => new Array(numFeatures);
|
|
156
|
+
const subObj = initializeNestedObject(levelScopes, nestedData, getBaseValue);
|
|
157
|
+
subObj[featureIndex] = flatQueryResults?.[i];
|
|
158
|
+
});
|
|
159
|
+
return nestedData;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Nest query results.
|
|
163
|
+
* @param {array} queryKeyScopeTuples
|
|
164
|
+
* @param {array} flatQueryResults Return value of useQueries,
|
|
165
|
+
* after .map() to get inner data elements.
|
|
166
|
+
* @returns The nested object.
|
|
167
|
+
*/
|
|
168
|
+
export function nestQueryResults(queryKeyScopeTuples, flatQueryResults) {
|
|
169
|
+
const nestedData = {};
|
|
170
|
+
// eslint-disable-next-line no-unused-vars
|
|
171
|
+
queryKeyScopeTuples.forEach(([queryKey, { levelScopes }], i) => {
|
|
172
|
+
const getBaseValue = () => ({});
|
|
173
|
+
const subObj = initializeNestedObject(levelScopes, nestedData, getBaseValue);
|
|
174
|
+
Object.entries(flatQueryResults?.[i] || {}).forEach(([key, value]) => {
|
|
175
|
+
subObj[key] = value;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
return nestedData;
|
|
179
|
+
}
|
|
180
|
+
async function featureSelectionQueryFn(ctx) {
|
|
181
|
+
// (copied from useFeatureSelection queryFn)
|
|
182
|
+
const { loaders } = ctx.meta;
|
|
183
|
+
const [dataset, dataType, matchOn, featureId, isRequired] = ctx.queryKey;
|
|
184
|
+
const loader = getMatchingLoader(loaders, dataset, dataType, matchOn);
|
|
185
|
+
if (loader) {
|
|
186
|
+
const implementsGeneSelection = typeof loader.loadGeneSelection === 'function';
|
|
187
|
+
if (implementsGeneSelection) {
|
|
188
|
+
const payload = await loader.loadGeneSelection({ selection: [featureId] });
|
|
189
|
+
if (!payload)
|
|
190
|
+
return null;
|
|
191
|
+
const { data } = payload;
|
|
192
|
+
const expressionData = data[0];
|
|
193
|
+
// Compute [min, max] extent of the data, and also a normalized version.
|
|
194
|
+
const dataExtent = extent(expressionData);
|
|
195
|
+
const [min, max] = dataExtent;
|
|
196
|
+
const ratio = 255 / (max - min);
|
|
197
|
+
const normData = new Uint8Array(expressionData.map(j => Math.floor((j - min) * ratio)));
|
|
198
|
+
return {
|
|
199
|
+
data: expressionData,
|
|
200
|
+
normData,
|
|
201
|
+
dataExtent,
|
|
202
|
+
dataKey: featureId,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Loader does not implement loadGeneSelection.
|
|
206
|
+
const payload = await loader.load();
|
|
207
|
+
if (!payload)
|
|
208
|
+
return null;
|
|
209
|
+
const { data } = payload;
|
|
210
|
+
const { obsIndex, featureIndex, obsFeatureMatrix } = data;
|
|
211
|
+
// Compute expressionData
|
|
212
|
+
const geneIndex = featureIndex.indexOf(featureId);
|
|
213
|
+
const numGenes = featureIndex.length;
|
|
214
|
+
const numCells = obsIndex.length;
|
|
215
|
+
const expressionData = new Float32Array(numCells);
|
|
216
|
+
for (let cellIndex = 0; cellIndex < numCells; cellIndex += 1) {
|
|
217
|
+
expressionData[cellIndex] = obsFeatureMatrix.data[cellIndex * numGenes + geneIndex];
|
|
218
|
+
}
|
|
219
|
+
// Compute [min, max] extent of the data, and also a normalized version.
|
|
220
|
+
const dataExtent = extent(expressionData);
|
|
221
|
+
const [min, max] = dataExtent;
|
|
222
|
+
const ratio = 255 / (max - min);
|
|
223
|
+
const normData = new Uint8Array(expressionData.map(j => Math.floor((j - min) * ratio)));
|
|
224
|
+
return {
|
|
225
|
+
data: expressionData,
|
|
226
|
+
normData,
|
|
227
|
+
dataExtent,
|
|
228
|
+
dataKey: featureId,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// No loader was found.
|
|
232
|
+
if (isRequired) {
|
|
233
|
+
throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
return { data: null, dataKey: null };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
export function useFeatureSelectionMultiLevel(loaders, dataset, isRequired, matchOnObj, selections, depth) {
|
|
240
|
+
const setWarning = useSetWarning();
|
|
241
|
+
// Create a flat list of tuples (queryKey, scopeInfo).
|
|
242
|
+
const queryKeyScopeTuples = useMemo(() => getFeatureSelectionQueryKeyScopeTuples(selections, matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired), [selections, matchOnObj, depth, dataset]);
|
|
243
|
+
const featureQueries = useQueries({
|
|
244
|
+
queries: queryKeyScopeTuples.map(([queryKey]) => ({
|
|
245
|
+
structuralSharing: false,
|
|
246
|
+
placeholderData: null,
|
|
247
|
+
queryKey,
|
|
248
|
+
queryFn: featureSelectionQueryFn,
|
|
249
|
+
meta: { loaders },
|
|
250
|
+
})),
|
|
251
|
+
});
|
|
252
|
+
const anyLoading = featureQueries.some(q => q.isFetching);
|
|
253
|
+
const anyError = featureQueries.some(q => q.isError);
|
|
254
|
+
// eslint-disable-next-line no-nested-ternary
|
|
255
|
+
const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
|
|
256
|
+
const flatGeneData = featureQueries.map(q => q.data?.data || null);
|
|
257
|
+
const flatLoadedGeneName = featureQueries.map(q => q.data?.dataKey || null);
|
|
258
|
+
const flatExtents = featureQueries.map(q => q.data?.dataExtent || null);
|
|
259
|
+
const flatNormData = featureQueries.map(q => q.data?.normData || null);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
featureQueries
|
|
262
|
+
.map(q => q.error)
|
|
263
|
+
.filter(e => Boolean(e))
|
|
264
|
+
.forEach((error) => {
|
|
265
|
+
warn(error, setWarning);
|
|
266
|
+
});
|
|
267
|
+
// Deliberate dependency omissions: use indirect dependencies for efficiency.
|
|
268
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
269
|
+
}, [anyError, setWarning]);
|
|
270
|
+
// Need to re-nest the geneData and the loadedGeneName info.
|
|
271
|
+
const [geneData, loadedGeneName, extents, normData] = useMemo(() => {
|
|
272
|
+
const nestedGeneData = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatGeneData);
|
|
273
|
+
const nestedLoadedGeneName = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatLoadedGeneName);
|
|
274
|
+
const nestedExtents = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatExtents);
|
|
275
|
+
const nestedNormData = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatNormData);
|
|
276
|
+
return [nestedGeneData, nestedLoadedGeneName, nestedExtents, nestedNormData];
|
|
277
|
+
// We do not want this useMemo to execute on every re-render, only when the
|
|
278
|
+
// featureQueries results change. Unfortunately, the featureQueries array
|
|
279
|
+
// reference is not stable on each re-render, so we use dataUpdatedAt instead.
|
|
280
|
+
// We use .reduce to ensure the number of dependencies is stable
|
|
281
|
+
// (i.e., a single number, despite possibly different numbers of queries).
|
|
282
|
+
// Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
|
|
283
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
284
|
+
}, [featureQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
|
|
285
|
+
return [geneData, loadedGeneName, extents, normData, dataStatus];
|
|
286
|
+
}
|
|
287
|
+
async function matrixIndicesQueryFn(ctx) {
|
|
288
|
+
// (copied from useFeatureSelection queryFn)
|
|
289
|
+
const { loaders } = ctx.meta;
|
|
290
|
+
const [dataset, dataType, matchOn, isRequired] = ctx.queryKey;
|
|
291
|
+
const loader = getMatchingLoader(loaders, dataset, dataType, matchOn);
|
|
292
|
+
if (loader) {
|
|
293
|
+
const implementsLoadAttrs = typeof loader.loadAttrs === 'function';
|
|
294
|
+
if (implementsLoadAttrs) {
|
|
295
|
+
const payload = await loader.loadAttrs();
|
|
296
|
+
if (!payload)
|
|
297
|
+
return null;
|
|
298
|
+
const { data: payloadData } = payload;
|
|
299
|
+
return {
|
|
300
|
+
data: {
|
|
301
|
+
obsIndex: payloadData.rows,
|
|
302
|
+
featureIndex: payloadData.cols,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Loader does not implement loadAttrs.
|
|
307
|
+
const payload = await loader.load();
|
|
308
|
+
if (!payload)
|
|
309
|
+
return null;
|
|
310
|
+
const { data: payloadData } = payload;
|
|
311
|
+
return {
|
|
312
|
+
data: {
|
|
313
|
+
obsIndex: payloadData.obsIndex,
|
|
314
|
+
featureIndex: payloadData.featureIndex,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// No loader was found.
|
|
319
|
+
if (isRequired) {
|
|
320
|
+
throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
return { data: null, dataKey: null };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export function useObsFeatureMatrixIndicesMultiLevel(loaders, dataset, isRequired, matchOnObj, depth) {
|
|
327
|
+
const setWarning = useSetWarning();
|
|
328
|
+
// Create a flat list of tuples (queryKey, scopeInfo).
|
|
329
|
+
const queryKeyScopeTuples = useMemo(() => getMatrixIndicesQueryKeyScopeTuples(matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired), [matchOnObj, depth, dataset]);
|
|
330
|
+
const indicesQueries = useQueries({
|
|
331
|
+
queries: queryKeyScopeTuples.map(([queryKey]) => ({
|
|
332
|
+
structuralSharing: false,
|
|
333
|
+
placeholderData: null,
|
|
334
|
+
queryKey,
|
|
335
|
+
queryFn: matrixIndicesQueryFn,
|
|
336
|
+
meta: { loaders },
|
|
337
|
+
})),
|
|
338
|
+
});
|
|
339
|
+
const anyLoading = indicesQueries.some(q => q.isFetching);
|
|
340
|
+
const anyError = indicesQueries.some(q => q.isError);
|
|
341
|
+
// eslint-disable-next-line no-nested-ternary
|
|
342
|
+
const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
|
|
343
|
+
const flatIndicesData = indicesQueries.map(q => q.data?.data || null);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
indicesQueries
|
|
346
|
+
.map(q => q.error)
|
|
347
|
+
.filter(e => Boolean(e))
|
|
348
|
+
.forEach((error) => {
|
|
349
|
+
warn(error, setWarning);
|
|
350
|
+
});
|
|
351
|
+
// Deliberate dependency omissions: use indirect dependencies for efficiency.
|
|
352
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
353
|
+
}, [anyError, setWarning]);
|
|
354
|
+
// Need to re-nest the geneData and the loadedGeneName info.
|
|
355
|
+
const indicesData = useMemo(() => {
|
|
356
|
+
const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
|
|
357
|
+
return nestedIndicesData;
|
|
358
|
+
// We do not want this useMemo to execute on every re-render, only when the
|
|
359
|
+
// featureQueries results change. Unfortunately, the featureQueries array
|
|
360
|
+
// reference is not stable on each re-render, so we use dataUpdatedAt instead.
|
|
361
|
+
// We use .reduce to ensure the number of dependencies is stable
|
|
362
|
+
// (i.e., a single number, despite possibly different numbers of queries).
|
|
363
|
+
// Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
|
|
364
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
365
|
+
}, [indicesQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
|
|
366
|
+
return [indicesData, dataStatus];
|
|
367
|
+
}
|
|
368
|
+
function useDataTypeMultiLevel(loaders, dataset, isRequired, matchOnObj, depth, dataType) {
|
|
369
|
+
const setWarning = useSetWarning();
|
|
370
|
+
// Create a flat list of tuples (queryKey, scopeInfo).
|
|
371
|
+
const queryKeyScopeTuples = useMemo(() => getQueryKeyScopeTuples(matchOnObj, depth, dataset, dataType, isRequired), [matchOnObj, depth, dataset]);
|
|
372
|
+
const locationsQueries = useQueries({
|
|
373
|
+
queries: queryKeyScopeTuples.map(([queryKey]) => ({
|
|
374
|
+
structuralSharing: false,
|
|
375
|
+
placeholderData: null,
|
|
376
|
+
queryKey,
|
|
377
|
+
queryFn: dataQueryFn,
|
|
378
|
+
meta: { loaders },
|
|
379
|
+
})),
|
|
380
|
+
});
|
|
381
|
+
const anyLoading = locationsQueries.some(q => q.isFetching);
|
|
382
|
+
const anyError = locationsQueries.some(q => q.isError);
|
|
383
|
+
// eslint-disable-next-line no-nested-ternary
|
|
384
|
+
const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
|
|
385
|
+
const flatIndicesData = locationsQueries.map(q => q.data?.data || null);
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
locationsQueries
|
|
388
|
+
.map(q => q.error)
|
|
389
|
+
.filter(e => Boolean(e))
|
|
390
|
+
.forEach((error) => {
|
|
391
|
+
warn(error, setWarning);
|
|
392
|
+
});
|
|
393
|
+
// Deliberate dependency omissions: use indirect dependencies for efficiency.
|
|
394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
395
|
+
}, [anyError, setWarning]);
|
|
396
|
+
// Need to re-nest the geneData and the loadedGeneName info.
|
|
397
|
+
const locationsData = useMemo(() => {
|
|
398
|
+
const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
|
|
399
|
+
return nestedIndicesData;
|
|
400
|
+
// We do not want this useMemo to execute on every re-render, only when the
|
|
401
|
+
// featureQueries results change. Unfortunately, the featureQueries array
|
|
402
|
+
// reference is not stable on each re-render, so we use dataUpdatedAt instead.
|
|
403
|
+
// We use .reduce to ensure the number of dependencies is stable
|
|
404
|
+
// (i.e., a single number, despite possibly different numbers of queries).
|
|
405
|
+
// Reference: https://github.com/TanStack/query/issues/3049#issuecomment-1253201068
|
|
406
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
407
|
+
}, [locationsQueries.reduce((a, h) => a + h.dataUpdatedAt, 0)]);
|
|
408
|
+
return [locationsData, dataStatus];
|
|
409
|
+
}
|
|
410
|
+
export function useObsLocationsMultiLevel(loaders, dataset, isRequired, matchOnObj, depth) {
|
|
411
|
+
return useDataTypeMultiLevel(loaders, dataset, isRequired, matchOnObj, depth, DataType.OBS_LOCATIONS);
|
|
412
|
+
}
|
|
413
|
+
export function useObsSetsMultiLevel(loaders, dataset, isRequired, matchOnObj, depth) {
|
|
414
|
+
return useDataTypeMultiLevel(loaders, dataset, isRequired, matchOnObj, depth, DataType.OBS_SETS);
|
|
415
|
+
}
|
|
416
|
+
export function useObsLabelsMultiLevel(loaders, dataset, isRequired, matchOnObj, depth) {
|
|
417
|
+
return useDataTypeMultiLevel(loaders, dataset, isRequired, matchOnObj, depth, DataType.OBS_LABELS);
|
|
418
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-hooks-multilevel-utils.test.d.ts","sourceRoot":"","sources":["../src/data-hooks-multilevel-utils.test.js"],"names":[],"mappings":""}
|