@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitessce/vit-s",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"author": "Gehlenborg Lab",
|
|
5
5
|
"homepage": "http://vitessce.io",
|
|
6
6
|
"repository": {
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
"react-grid-layout-with-lodash": "^1.3.5",
|
|
29
29
|
"uuid": "^9.0.0",
|
|
30
30
|
"zustand": "^3.5.10",
|
|
31
|
-
"
|
|
32
|
-
"@vitessce/
|
|
33
|
-
"@vitessce/schemas": "3.1
|
|
34
|
-
"@vitessce/
|
|
31
|
+
"react-aria": "^3.28.0",
|
|
32
|
+
"@vitessce/constants-internal": "3.2.1",
|
|
33
|
+
"@vitessce/schemas": "3.2.1",
|
|
34
|
+
"@vitessce/plugins": "3.2.1",
|
|
35
|
+
"@vitessce/utils": "3.2.1"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@testing-library/jest-dom": "^5.16.4",
|
package/src/data-hook-utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|