@vitessce/config 3.0.0 → 3.1.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.
- package/dist/index.js +589 -83
- package/dist-tsc/VitessceAutoConfig.d.ts +13 -1
- package/dist-tsc/VitessceAutoConfig.d.ts.map +1 -1
- package/dist-tsc/VitessceAutoConfig.js +318 -87
- package/dist-tsc/VitessceAutoConfig.test.js +599 -71
- package/dist-tsc/VitessceConfig.d.ts +46 -0
- package/dist-tsc/VitessceConfig.d.ts.map +1 -1
- package/dist-tsc/VitessceConfig.js +353 -0
- package/dist-tsc/VitessceConfig.test.js +329 -1
- package/dist-tsc/constants.d.ts +247 -0
- package/dist-tsc/constants.d.ts.map +1 -0
- package/dist-tsc/constants.js +87 -0
- package/dist-tsc/index.d.ts +2 -1
- package/dist-tsc/index.js +2 -1
- package/package.json +4 -4
- package/src/VitessceAutoConfig.js +368 -99
- package/src/VitessceAutoConfig.test.js +626 -76
- package/src/VitessceConfig.js +393 -0
- package/src/VitessceConfig.test.js +339 -0
- package/src/constants.js +94 -0
- package/src/index.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitessce/config",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"author": "Gehlenborg Lab",
|
|
5
5
|
"homepage": "http://vitessce.io",
|
|
6
6
|
"repository": {
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"dist-tsc"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@vitessce/constants-internal": "3.
|
|
20
|
-
"@vitessce/utils": "3.
|
|
19
|
+
"@vitessce/constants-internal": "3.1.0",
|
|
20
|
+
"@vitessce/utils": "3.1.0"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"bundle": "pnpm exec vite build -c ../../scripts/vite.config.js",
|
|
24
|
-
"test": "pnpm exec vitest --run
|
|
24
|
+
"test": "pnpm exec vitest --run"
|
|
25
25
|
},
|
|
26
26
|
"module": "dist/index.js",
|
|
27
27
|
"exports": {
|
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { CoordinationType, FileType } from '@vitessce/constants-internal';
|
|
1
|
+
import { CoordinationType as ct, FileType } from '@vitessce/constants-internal';
|
|
2
2
|
import {
|
|
3
3
|
VitessceConfig,
|
|
4
4
|
} from './VitessceConfig.js';
|
|
5
5
|
|
|
6
|
+
import { HINTS_CONFIG, HINT_TYPE_TO_FILE_TYPE_MAP } from './constants.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} hintsConfig. The hints config for the given dataset.
|
|
10
|
+
*
|
|
11
|
+
* @param {Array} possibleViews. All views for a given file type, supported by the dataset.
|
|
12
|
+
* The array contains arrays of strings, each of size >= 1, where the first element of each string
|
|
13
|
+
* is a VitessceConfig view name ('description', 'spatial', 'layerController', etc.)
|
|
14
|
+
*
|
|
15
|
+
* @returns {Array of strings} the intersection of VitessceConfig view names in
|
|
16
|
+
* possibleViews and requiredViews.
|
|
17
|
+
*/
|
|
18
|
+
const filterViews = (hintsConfig, possibleViews) => {
|
|
19
|
+
const requiredViews = Object.keys(hintsConfig.views);
|
|
20
|
+
|
|
21
|
+
if (requiredViews.length === 0) {
|
|
22
|
+
return possibleViews;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resultViews = [];
|
|
26
|
+
|
|
27
|
+
requiredViews.forEach((requiredView) => {
|
|
28
|
+
const match = possibleViews.find(possibleView => possibleView[0] === requiredView);
|
|
29
|
+
if (match) resultViews.push(match);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (resultViews.length === 0) {
|
|
33
|
+
throw new Error('No views found that are compatible with the supplied dataset URLs and hint.');
|
|
34
|
+
}
|
|
35
|
+
return resultViews;
|
|
36
|
+
};
|
|
6
37
|
|
|
7
38
|
class AbstractAutoConfig {
|
|
8
39
|
async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
|
|
@@ -13,6 +44,7 @@ class AbstractAutoConfig {
|
|
|
13
44
|
throw new Error('The composeFileConfig() method has not been implemented.');
|
|
14
45
|
}
|
|
15
46
|
}
|
|
47
|
+
|
|
16
48
|
class OmeTiffAutoConfig extends AbstractAutoConfig {
|
|
17
49
|
constructor(fileUrl) {
|
|
18
50
|
super();
|
|
@@ -21,13 +53,11 @@ class OmeTiffAutoConfig extends AbstractAutoConfig {
|
|
|
21
53
|
this.fileName = fileUrl.split('/').at(-1);
|
|
22
54
|
}
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
['description'],
|
|
28
|
-
|
|
29
|
-
['layerController'],
|
|
30
|
-
];
|
|
56
|
+
async composeViewsConfig(hintsConfig) { /* eslint-disable-line class-methods-use-this */
|
|
57
|
+
return filterViews(
|
|
58
|
+
hintsConfig,
|
|
59
|
+
[['description'], ['spatial'], ['layerController']],
|
|
60
|
+
);
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
async composeFileConfig() {
|
|
@@ -59,13 +89,11 @@ class OmeZarrAutoConfig extends AbstractAutoConfig {
|
|
|
59
89
|
this.fileName = fileUrl.split('/').at(-1);
|
|
60
90
|
}
|
|
61
91
|
|
|
62
|
-
async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
['spatial'],
|
|
66
|
-
|
|
67
|
-
['status'],
|
|
68
|
-
];
|
|
92
|
+
async composeViewsConfig(hintsConfig) { /* eslint-disable-line class-methods-use-this */
|
|
93
|
+
return filterViews(
|
|
94
|
+
hintsConfig,
|
|
95
|
+
[['description'], ['spatial'], ['layerController']],
|
|
96
|
+
);
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
async composeFileConfig() {
|
|
@@ -115,7 +143,7 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
115
143
|
});
|
|
116
144
|
|
|
117
145
|
const supportedObsSetsKeys = [
|
|
118
|
-
'cluster', 'subcluster', 'cell_type', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
|
|
146
|
+
'cluster', 'clusters', 'subcluster', 'cell_type', 'celltype', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
|
|
119
147
|
];
|
|
120
148
|
|
|
121
149
|
this.metadataSummary.obs.forEach((key) => {
|
|
@@ -135,6 +163,19 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
135
163
|
});
|
|
136
164
|
});
|
|
137
165
|
|
|
166
|
+
// if length of path is 1, storing the value as an array doesn't work
|
|
167
|
+
// es-lint-disable-next-line max-len
|
|
168
|
+
// Example: https://s3.amazonaws.com/vitessce-data/0.0.33/main/human-lymph-node-10x-visium/human_lymph_node_10x_visium.h5ad.zarr
|
|
169
|
+
options.obsSets = options.obsSets?.map((obsSet) => {
|
|
170
|
+
if (obsSet.path.length === 1) {
|
|
171
|
+
return {
|
|
172
|
+
...obsSet,
|
|
173
|
+
path: obsSet.path[0],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return obsSet;
|
|
177
|
+
});
|
|
178
|
+
|
|
138
179
|
return {
|
|
139
180
|
options,
|
|
140
181
|
fileType: this.fileType,
|
|
@@ -147,93 +188,164 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
147
188
|
};
|
|
148
189
|
}
|
|
149
190
|
|
|
150
|
-
async composeViewsConfig() {
|
|
191
|
+
async composeViewsConfig(hintsConfig) {
|
|
151
192
|
this.metadataSummary = await this.setMetadataSummary();
|
|
152
|
-
|
|
153
|
-
const views = [];
|
|
193
|
+
const possibleViews = [];
|
|
154
194
|
|
|
155
195
|
const hasCellSetData = this.metadataSummary.obs
|
|
156
|
-
.filter(key => key.toLowerCase().includes('cluster')
|
|
196
|
+
.filter(key => key.toLowerCase().includes('cluster')
|
|
197
|
+
|| key.toLowerCase().includes('cell_type')
|
|
198
|
+
|| key.toLowerCase().includes('celltype'));
|
|
157
199
|
|
|
158
200
|
if (hasCellSetData.length > 0) {
|
|
159
|
-
|
|
201
|
+
possibleViews.push(['obsSets']);
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
this.metadataSummary.obsm.forEach((key) => {
|
|
163
205
|
if (key.toLowerCase().includes('obsm/x_umap')) {
|
|
164
|
-
|
|
206
|
+
possibleViews.push(['scatterplot', { mapping: 'UMAP' }]);
|
|
165
207
|
}
|
|
166
208
|
if (key.toLowerCase().includes('obsm/x_tsne')) {
|
|
167
|
-
|
|
209
|
+
possibleViews.push(['scatterplot', { mapping: 't-SNE' }]);
|
|
168
210
|
}
|
|
169
211
|
if (key.toLowerCase().includes('obsm/x_pca')) {
|
|
170
|
-
|
|
212
|
+
possibleViews.push(['scatterplot', { mapping: 'PCA' }]);
|
|
171
213
|
}
|
|
172
214
|
if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
|
|
173
|
-
|
|
215
|
+
possibleViews.push(['layerController']);
|
|
174
216
|
}
|
|
175
217
|
if (key.toLowerCase().includes(('obsm/x_spatial'))) {
|
|
176
|
-
|
|
218
|
+
possibleViews.push(['spatial']);
|
|
177
219
|
}
|
|
178
220
|
});
|
|
179
221
|
|
|
222
|
+
possibleViews.push(['obsSetSizes']);
|
|
223
|
+
possibleViews.push(['obsSetFeatureValueDistribution']);
|
|
224
|
+
|
|
180
225
|
if (this.metadataSummary.X) {
|
|
181
|
-
|
|
182
|
-
|
|
226
|
+
possibleViews.push(['heatmap']);
|
|
227
|
+
possibleViews.push(['featureList']);
|
|
183
228
|
}
|
|
184
229
|
|
|
230
|
+
const views = filterViews(hintsConfig, possibleViews);
|
|
185
231
|
return views;
|
|
186
232
|
}
|
|
187
233
|
|
|
188
|
-
async
|
|
189
|
-
|
|
190
|
-
|
|
234
|
+
async setMetadataSummaryWithZmetadata(response) { /* eslint-disable-line class-methods-use-this */
|
|
235
|
+
const metadataFile = await response.json();
|
|
236
|
+
if (!metadataFile.metadata) {
|
|
237
|
+
throw new Error('Could not generate config: .zmetadata file is not valid.');
|
|
191
238
|
}
|
|
192
239
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
240
|
+
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
241
|
+
.filter(key => key.startsWith('obsm/X_'))
|
|
242
|
+
.map(key => key.split('/.zarray')[0]);
|
|
197
243
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.map(key => key.split('/.zarray')[0]);
|
|
244
|
+
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
245
|
+
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
201
246
|
|
|
202
|
-
|
|
203
|
-
|
|
247
|
+
function uniq(a) {
|
|
248
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
249
|
+
}
|
|
250
|
+
const obsKeys = uniq(obsKeysArr);
|
|
204
251
|
|
|
205
|
-
|
|
206
|
-
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
207
|
-
}
|
|
208
|
-
const obsKeys = uniq(obsKeysArr);
|
|
252
|
+
const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
|
|
209
253
|
|
|
210
|
-
|
|
254
|
+
return {
|
|
255
|
+
// Array of keys in obsm that are found by the fetches above
|
|
256
|
+
obsm: obsmKeys,
|
|
257
|
+
// Array of keys in obs that are found by the fetches above
|
|
258
|
+
obs: obsKeys,
|
|
259
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
260
|
+
X: X.length > 0,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
211
263
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
264
|
+
async setMetadataSummaryWithoutZmetadata() {
|
|
265
|
+
const knownMetadataFileSuffixes = [
|
|
266
|
+
'/obsm/X_pca/.zarray',
|
|
267
|
+
'/obsm/X_umap/.zarray',
|
|
268
|
+
'/obsm/X_tsne/.zarray',
|
|
269
|
+
'/obsm/X_spatial/.zarray',
|
|
270
|
+
'/obsm/X_segmentations/.zarray',
|
|
271
|
+
'/obs/.zattrs',
|
|
272
|
+
'/X/.zarray',
|
|
273
|
+
'/X/data/.zarray', // for https://s3.amazonaws.com/vitessce-data/0.0.33/main/human-lymph-node-10x-visium/human_lymph_node_10x_visium.h5ad.zarr
|
|
274
|
+
];
|
|
217
275
|
|
|
218
|
-
|
|
276
|
+
const getObsmKey = (url) => {
|
|
277
|
+
// Get the substring "X_pca" from a URL like
|
|
278
|
+
// http://example.com/foo/adata.zarr/obsm/X_pca/.zarray
|
|
279
|
+
const obsmKeyStartIndex = `${this.fileUrl}/`.length;
|
|
280
|
+
const obsmKeyEndIndex = url.length - '/.zarray'.length;
|
|
281
|
+
return url.substring(obsmKeyStartIndex, obsmKeyEndIndex);
|
|
219
282
|
};
|
|
220
283
|
|
|
284
|
+
const promises = knownMetadataFileSuffixes.map(suffix => fetch(`${this.fileUrl}${suffix}`));
|
|
285
|
+
|
|
286
|
+
const fetchResults = await Promise.all(promises);
|
|
287
|
+
const okFetchResults = fetchResults.filter(j => j.ok);
|
|
288
|
+
const metadataSummary = {
|
|
289
|
+
// Array of keys in obsm that are found by the fetches above
|
|
290
|
+
obsm: [],
|
|
291
|
+
// Array of keys in obs that are found by the fetches above
|
|
292
|
+
obs: [],
|
|
293
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
294
|
+
X: false,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const obsPromiseResult = okFetchResults.find(
|
|
298
|
+
r => r.url === (`${this.fileUrl}/obs/.zattrs`),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const isObsValid = obsAttr => Object.keys(obsAttr).includes('column-order')
|
|
302
|
+
&& Object.keys(obsAttr).includes('encoding-version')
|
|
303
|
+
&& Object.keys(obsAttr).includes('encoding-type')
|
|
304
|
+
&& obsAttr['encoding-type'] === 'dataframe'
|
|
305
|
+
&& (obsAttr['encoding-version'] === '0.1.0' || obsAttr['encoding-version'] === '0.2.0');
|
|
306
|
+
|
|
307
|
+
if (obsPromiseResult) {
|
|
308
|
+
const obsAttrs = await obsPromiseResult.json();
|
|
309
|
+
if (isObsValid(obsAttrs)) {
|
|
310
|
+
obsAttrs['column-order'].forEach(key => metadataSummary.obs.push(`obs/${key}`));
|
|
311
|
+
} else {
|
|
312
|
+
throw new Error('Could not generate config: /obs/.zattrs file is not valid.');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
okFetchResults
|
|
317
|
+
.forEach((r) => {
|
|
318
|
+
if (r.url.startsWith(`${this.fileUrl}/obsm`)) {
|
|
319
|
+
const obsmKey = getObsmKey(r.url);
|
|
320
|
+
if (obsmKey) {
|
|
321
|
+
metadataSummary.obsm.push(obsmKey);
|
|
322
|
+
}
|
|
323
|
+
} else if (r.url.startsWith(`${this.fileUrl}/X`)) {
|
|
324
|
+
metadataSummary.X = true;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return metadataSummary;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async setMetadataSummary() {
|
|
332
|
+
if (Object.keys(this.metadataSummary).length > 0) {
|
|
333
|
+
return this.metadataSummary;
|
|
334
|
+
}
|
|
335
|
+
|
|
221
336
|
const metadataExtension = '.zmetadata';
|
|
222
337
|
const url = [this.fileUrl, metadataExtension].join('/');
|
|
223
338
|
return fetch(url).then((response) => {
|
|
224
339
|
if (response.ok) {
|
|
225
|
-
return
|
|
340
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
226
341
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
return Promise.reject(error);
|
|
236
|
-
});
|
|
342
|
+
if (response.status === 404) {
|
|
343
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
344
|
+
}
|
|
345
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
346
|
+
}).catch((error) => {
|
|
347
|
+
throw new Error(`Could not generate config for URL ${this.fileUrl}: ${error}`);
|
|
348
|
+
});
|
|
237
349
|
}
|
|
238
350
|
}
|
|
239
351
|
|
|
@@ -241,27 +353,20 @@ const configClasses = [
|
|
|
241
353
|
{
|
|
242
354
|
extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
|
|
243
355
|
class: OmeTiffAutoConfig,
|
|
356
|
+
name: 'OME-TIFF',
|
|
244
357
|
},
|
|
245
358
|
{
|
|
246
359
|
extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
|
|
247
360
|
class: AnndataZarrAutoConfig,
|
|
361
|
+
name: 'AnnData-Zarr',
|
|
248
362
|
},
|
|
249
363
|
{
|
|
250
364
|
extensions: ['ome.zarr'],
|
|
251
365
|
class: OmeZarrAutoConfig,
|
|
366
|
+
name: 'OME-Zarr',
|
|
252
367
|
},
|
|
253
368
|
];
|
|
254
369
|
|
|
255
|
-
function getFileType(url) {
|
|
256
|
-
const match = configClasses.find(obj => obj.extensions.filter(
|
|
257
|
-
ext => url.endsWith(ext),
|
|
258
|
-
).length === 1);
|
|
259
|
-
if (!match) {
|
|
260
|
-
throw new Error(`Could not generate config for URL: ${url}. This file type is not supported.`);
|
|
261
|
-
}
|
|
262
|
-
return match.class;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
370
|
function calculateCoordinates(viewsNumb) {
|
|
266
371
|
const rows = Math.ceil(Math.sqrt(viewsNumb));
|
|
267
372
|
const cols = Math.ceil(viewsNumb / rows);
|
|
@@ -274,16 +379,142 @@ function calculateCoordinates(viewsNumb) {
|
|
|
274
379
|
const col = i % cols;
|
|
275
380
|
const x = col * width;
|
|
276
381
|
const y = row * height;
|
|
277
|
-
|
|
382
|
+
|
|
383
|
+
// The coordinates have to be integer values:
|
|
384
|
+
coords.push([
|
|
385
|
+
Math.floor(x),
|
|
386
|
+
Math.floor(y),
|
|
387
|
+
// Ensure width/height is at least 1.
|
|
388
|
+
Math.max(1, Math.floor(width)),
|
|
389
|
+
Math.max(1, Math.floor(height)),
|
|
390
|
+
]);
|
|
278
391
|
}
|
|
279
392
|
|
|
280
393
|
return coords;
|
|
281
394
|
}
|
|
282
395
|
|
|
283
|
-
|
|
396
|
+
const spatialSegmentationLayerValue = {
|
|
397
|
+
radius: 65,
|
|
398
|
+
stroked: true,
|
|
399
|
+
visible: true,
|
|
400
|
+
opacity: 1,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Inserts the spatial coordination space into the VitessceConfig views.
|
|
405
|
+
* @param {Array of VitessceConfigView} of views
|
|
406
|
+
* @param {VitessceConfig} vc instance
|
|
407
|
+
*/
|
|
408
|
+
function insertCoordinationSpaceForSpatial(views, vc) {
|
|
409
|
+
const [
|
|
410
|
+
spatialSegmentationLayer,
|
|
411
|
+
spatialImageLayer,
|
|
412
|
+
spatialZoom,
|
|
413
|
+
spatialTargetX,
|
|
414
|
+
spatialTargetY,
|
|
415
|
+
] = vc.addCoordination(
|
|
416
|
+
ct.SPATIAL_SEGMENTATION_LAYER,
|
|
417
|
+
ct.SPATIAL_IMAGE_LAYER,
|
|
418
|
+
ct.SPATIAL_ZOOM,
|
|
419
|
+
ct.SPATIAL_TARGET_X,
|
|
420
|
+
ct.SPATIAL_TARGET_Y,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Note: this always assumes the segmentation is polygon-based.
|
|
424
|
+
// In the future, we may want to support both polygon-based and bitmask-based.
|
|
425
|
+
spatialSegmentationLayer.setValue(spatialSegmentationLayerValue);
|
|
426
|
+
// Note: this always assumes the image is RGB.
|
|
427
|
+
// In the future, we may want to support both RGB and multi-channel.
|
|
428
|
+
spatialImageLayer.setValue([
|
|
429
|
+
{
|
|
430
|
+
type: 'raster',
|
|
431
|
+
index: 0,
|
|
432
|
+
colormap: null,
|
|
433
|
+
transparentColor: null,
|
|
434
|
+
opacity: 1,
|
|
435
|
+
domainType: 'Min/Max',
|
|
436
|
+
channels: [
|
|
437
|
+
{
|
|
438
|
+
selection: {
|
|
439
|
+
c: 0,
|
|
440
|
+
},
|
|
441
|
+
color: [
|
|
442
|
+
255,
|
|
443
|
+
0,
|
|
444
|
+
0,
|
|
445
|
+
],
|
|
446
|
+
visible: true,
|
|
447
|
+
slider: [
|
|
448
|
+
0,
|
|
449
|
+
255,
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
selection: {
|
|
454
|
+
c: 1,
|
|
455
|
+
},
|
|
456
|
+
color: [
|
|
457
|
+
0,
|
|
458
|
+
255,
|
|
459
|
+
0,
|
|
460
|
+
],
|
|
461
|
+
visible: true,
|
|
462
|
+
slider: [
|
|
463
|
+
0,
|
|
464
|
+
255,
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
selection: {
|
|
469
|
+
c: 2,
|
|
470
|
+
},
|
|
471
|
+
color: [
|
|
472
|
+
0,
|
|
473
|
+
0,
|
|
474
|
+
255,
|
|
475
|
+
],
|
|
476
|
+
visible: true,
|
|
477
|
+
slider: [
|
|
478
|
+
0,
|
|
479
|
+
255,
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
views.forEach((view) => {
|
|
487
|
+
if (view.view.component === 'spatial' || view.view.component === 'layerController') {
|
|
488
|
+
view.useCoordination(spatialImageLayer);
|
|
489
|
+
view.useCoordination(spatialSegmentationLayer);
|
|
490
|
+
view.useCoordination(spatialZoom);
|
|
491
|
+
view.useCoordination(spatialTargetX);
|
|
492
|
+
view.useCoordination(spatialTargetY);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Returns the type of the file, based on the file extension.
|
|
499
|
+
* @param {string} url of the file.
|
|
500
|
+
* @returns {object} An element from the `configClasses` array, which will be an object with
|
|
501
|
+
* the properties `extensions: string[]`, `class` (an AutoConfig class definition) and
|
|
502
|
+
* `name`: string.
|
|
503
|
+
*/
|
|
504
|
+
function getFileType(url) {
|
|
505
|
+
const match = configClasses.find(obj => obj.extensions.filter(
|
|
506
|
+
ext => url.endsWith(ext),
|
|
507
|
+
).length === 1);
|
|
508
|
+
if (!match) {
|
|
509
|
+
throw new Error('One or more of the URLs provided point to unsupported file types.');
|
|
510
|
+
}
|
|
511
|
+
return match;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function generateViewDefinition(url, vc, dataset, hintsConfig) {
|
|
284
515
|
let ConfigClassName;
|
|
285
516
|
try {
|
|
286
|
-
ConfigClassName = getFileType(url);
|
|
517
|
+
ConfigClassName = getFileType(url).class;
|
|
287
518
|
} catch (err) {
|
|
288
519
|
return Promise.reject(err);
|
|
289
520
|
}
|
|
@@ -293,19 +524,16 @@ async function generateConfig(url, vc) {
|
|
|
293
524
|
let viewsConfig;
|
|
294
525
|
try {
|
|
295
526
|
fileConfig = await configInstance.composeFileConfig();
|
|
296
|
-
viewsConfig = await configInstance.composeViewsConfig();
|
|
527
|
+
viewsConfig = await configInstance.composeViewsConfig(hintsConfig);
|
|
297
528
|
} catch (error) {
|
|
298
529
|
console.error(error);
|
|
299
530
|
return Promise.reject(error);
|
|
300
531
|
}
|
|
301
532
|
|
|
302
|
-
|
|
303
|
-
.addDataset(configInstance.fileName)
|
|
304
|
-
.addFile(fileConfig);
|
|
533
|
+
dataset.addFile(fileConfig);
|
|
305
534
|
|
|
306
535
|
let layerControllerView = false;
|
|
307
536
|
let spatialView = false;
|
|
308
|
-
|
|
309
537
|
const views = [];
|
|
310
538
|
|
|
311
539
|
viewsConfig.forEach((v) => {
|
|
@@ -318,7 +546,7 @@ async function generateConfig(url, vc) {
|
|
|
318
546
|
}
|
|
319
547
|
// this piece of code can be removed once these props are added by default to layerController
|
|
320
548
|
// see this issue: https://github.com/vitessce/vitessce/issues/1454
|
|
321
|
-
if (v[0] === 'layerController'
|
|
549
|
+
if (v[0] === 'layerController') {
|
|
322
550
|
view.setProps({
|
|
323
551
|
disable3d: [],
|
|
324
552
|
disableChannelsIfRgbDetected: true,
|
|
@@ -333,29 +561,48 @@ async function generateConfig(url, vc) {
|
|
|
333
561
|
});
|
|
334
562
|
|
|
335
563
|
if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
|
|
336
|
-
const spatialSegmentationLayerValue = {
|
|
337
|
-
opacity: 1,
|
|
338
|
-
radius: 0,
|
|
339
|
-
visible: true,
|
|
340
|
-
stroked: false,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
564
|
vc.linkViews(
|
|
344
565
|
[spatialView, layerControllerView],
|
|
345
566
|
[
|
|
346
|
-
|
|
347
|
-
CoordinationType.SPATIAL_TARGET_X,
|
|
348
|
-
CoordinationType.SPATIAL_TARGET_Y,
|
|
349
|
-
CoordinationType.SPATIAL_SEGMENTATION_LAYER,
|
|
567
|
+
ct.SPATIAL_SEGMENTATION_LAYER,
|
|
350
568
|
],
|
|
351
|
-
[
|
|
569
|
+
[spatialSegmentationLayerValue],
|
|
352
570
|
);
|
|
353
571
|
}
|
|
354
572
|
|
|
355
573
|
return views;
|
|
356
574
|
}
|
|
357
575
|
|
|
358
|
-
|
|
576
|
+
/**
|
|
577
|
+
* Returns the hints that are available for the given file URLs, depending on their types.
|
|
578
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
579
|
+
* @returns the hints available for these file URLs
|
|
580
|
+
*/
|
|
581
|
+
export function getHintOptions(fileUrls) {
|
|
582
|
+
const fileTypes = {};
|
|
583
|
+
|
|
584
|
+
fileUrls.forEach((url) => {
|
|
585
|
+
const match = getFileType(url);
|
|
586
|
+
// hints config only has OME-TIFF, since settings are the same for both OME-TIFF and OME-Zarr
|
|
587
|
+
if (match.name === 'OME-Zarr') {
|
|
588
|
+
fileTypes['OME-TIFF'] = true;
|
|
589
|
+
} else {
|
|
590
|
+
fileTypes[match.name] = true;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const datasetType = Object.keys(fileTypes).sort().join(',');
|
|
595
|
+
|
|
596
|
+
return HINT_TYPE_TO_FILE_TYPE_MAP?.[datasetType] || [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
*
|
|
601
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
602
|
+
* @param {String} the hints config to be used for the dataset. Null by default
|
|
603
|
+
* @returns ViewConfig as JSON
|
|
604
|
+
*/
|
|
605
|
+
export async function generateConfig(fileUrls, hintTitle = null) {
|
|
359
606
|
const vc = new VitessceConfig({
|
|
360
607
|
schemaVersion: '1.0.15',
|
|
361
608
|
name: 'An automatically generated config. Adjust values and add layout components if needed.',
|
|
@@ -364,19 +611,41 @@ export async function generateConfigs(fileUrls) {
|
|
|
364
611
|
|
|
365
612
|
const allViews = [];
|
|
366
613
|
|
|
614
|
+
const dataset = vc.addDataset('An automatically generated view config for dataset. Adjust values and add layout components if needed.');
|
|
615
|
+
|
|
616
|
+
const hintsConfig = !hintTitle ? { views: {} } : HINTS_CONFIG?.[hintTitle];
|
|
617
|
+
if (!hintsConfig) {
|
|
618
|
+
throw new Error(`Hints config not found for the supplied hint: ${hintTitle}.`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const useHints = Object.keys(hintsConfig?.views)?.length > 0;
|
|
622
|
+
|
|
367
623
|
fileUrls.forEach((url) => {
|
|
368
|
-
allViews.push(
|
|
624
|
+
allViews.push(generateViewDefinition(url, vc, dataset, hintsConfig));
|
|
369
625
|
});
|
|
370
626
|
|
|
371
627
|
return Promise.all(allViews).then((views) => {
|
|
372
628
|
const flattenedViews = views.flat();
|
|
373
629
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
630
|
+
// If coordinationSpaceRequired field is set to true in the HINTS_CONFIG,
|
|
631
|
+
// then insert coordination space.
|
|
632
|
+
// NOTE: the user needs to manually add the coordination values for the image and
|
|
633
|
+
// segmentation layers which will be visualized in the spatial/layer controller views.
|
|
634
|
+
if (hintsConfig?.coordinationSpaceRequired) {
|
|
635
|
+
insertCoordinationSpaceForSpatial(flattenedViews, vc);
|
|
378
636
|
}
|
|
379
637
|
|
|
638
|
+
if (!useHints) {
|
|
639
|
+
const coord = calculateCoordinates(flattenedViews.length);
|
|
640
|
+
for (let i = 0; i < flattenedViews.length; i++) {
|
|
641
|
+
flattenedViews[i].setXYWH(...coord[i]);
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
flattenedViews.forEach((vitessceConfigView) => {
|
|
645
|
+
const coordinates = Object.values(hintsConfig.views[vitessceConfigView.view.component]);
|
|
646
|
+
vitessceConfigView.setXYWH(...coordinates);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
380
649
|
return vc.toJSON();
|
|
381
650
|
});
|
|
382
651
|
}
|