@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
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Returns the hints that are available for the given file URLs, depending on their types.
|
|
3
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
4
|
+
* @returns the hints available for these file URLs
|
|
5
|
+
*/
|
|
6
|
+
export function getHintOptions(fileUrls: any[]): any;
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
10
|
+
* @param {String} the hints config to be used for the dataset. Null by default
|
|
11
|
+
* @returns ViewConfig as JSON
|
|
12
|
+
*/
|
|
13
|
+
export function generateConfig(fileUrls: any[], hintTitle?: null): Promise<object>;
|
|
2
14
|
//# sourceMappingURL=VitessceAutoConfig.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitessceAutoConfig.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VitessceAutoConfig.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.js"],"names":[],"mappings":"AA+jBA;;;;GAIG;AACH,qDAgBC;AAED;;;;;GAKG;AACH,mFA8CC"}
|
|
@@ -1,5 +1,32 @@
|
|
|
1
|
-
import { CoordinationType, FileType } from '@vitessce/constants-internal';
|
|
1
|
+
import { CoordinationType as ct, FileType } from '@vitessce/constants-internal';
|
|
2
2
|
import { VitessceConfig, } from './VitessceConfig.js';
|
|
3
|
+
import { HINTS_CONFIG, HINT_TYPE_TO_FILE_TYPE_MAP } from './constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object} hintsConfig. The hints config for the given dataset.
|
|
6
|
+
*
|
|
7
|
+
* @param {Array} possibleViews. All views for a given file type, supported by the dataset.
|
|
8
|
+
* The array contains arrays of strings, each of size >= 1, where the first element of each string
|
|
9
|
+
* is a VitessceConfig view name ('description', 'spatial', 'layerController', etc.)
|
|
10
|
+
*
|
|
11
|
+
* @returns {Array of strings} the intersection of VitessceConfig view names in
|
|
12
|
+
* possibleViews and requiredViews.
|
|
13
|
+
*/
|
|
14
|
+
const filterViews = (hintsConfig, possibleViews) => {
|
|
15
|
+
const requiredViews = Object.keys(hintsConfig.views);
|
|
16
|
+
if (requiredViews.length === 0) {
|
|
17
|
+
return possibleViews;
|
|
18
|
+
}
|
|
19
|
+
const resultViews = [];
|
|
20
|
+
requiredViews.forEach((requiredView) => {
|
|
21
|
+
const match = possibleViews.find(possibleView => possibleView[0] === requiredView);
|
|
22
|
+
if (match)
|
|
23
|
+
resultViews.push(match);
|
|
24
|
+
});
|
|
25
|
+
if (resultViews.length === 0) {
|
|
26
|
+
throw new Error('No views found that are compatible with the supplied dataset URLs and hint.');
|
|
27
|
+
}
|
|
28
|
+
return resultViews;
|
|
29
|
+
};
|
|
3
30
|
class AbstractAutoConfig {
|
|
4
31
|
async composeViewsConfig() {
|
|
5
32
|
throw new Error('The composeViewsConfig() method has not been implemented.');
|
|
@@ -15,12 +42,8 @@ class OmeTiffAutoConfig extends AbstractAutoConfig {
|
|
|
15
42
|
this.fileType = FileType.RASTER_JSON;
|
|
16
43
|
this.fileName = fileUrl.split('/').at(-1);
|
|
17
44
|
}
|
|
18
|
-
async composeViewsConfig() {
|
|
19
|
-
return [
|
|
20
|
-
['description'],
|
|
21
|
-
['spatial'],
|
|
22
|
-
['layerController'],
|
|
23
|
-
];
|
|
45
|
+
async composeViewsConfig(hintsConfig) {
|
|
46
|
+
return filterViews(hintsConfig, [['description'], ['spatial'], ['layerController']]);
|
|
24
47
|
}
|
|
25
48
|
async composeFileConfig() {
|
|
26
49
|
return {
|
|
@@ -49,13 +72,8 @@ class OmeZarrAutoConfig extends AbstractAutoConfig {
|
|
|
49
72
|
this.fileType = FileType.RASTER_OME_ZARR;
|
|
50
73
|
this.fileName = fileUrl.split('/').at(-1);
|
|
51
74
|
}
|
|
52
|
-
async composeViewsConfig() {
|
|
53
|
-
return [
|
|
54
|
-
['description'],
|
|
55
|
-
['spatial'],
|
|
56
|
-
['layerController'],
|
|
57
|
-
['status'],
|
|
58
|
-
];
|
|
75
|
+
async composeViewsConfig(hintsConfig) {
|
|
76
|
+
return filterViews(hintsConfig, [['description'], ['spatial'], ['layerController']]);
|
|
59
77
|
}
|
|
60
78
|
async composeFileConfig() {
|
|
61
79
|
return {
|
|
@@ -99,7 +117,7 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
99
117
|
}
|
|
100
118
|
});
|
|
101
119
|
const supportedObsSetsKeys = [
|
|
102
|
-
'cluster', 'subcluster', 'cell_type', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
|
|
120
|
+
'cluster', 'clusters', 'subcluster', 'cell_type', 'celltype', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
|
|
103
121
|
];
|
|
104
122
|
this.metadataSummary.obs.forEach((key) => {
|
|
105
123
|
supportedObsSetsKeys.forEach((supportedKey) => {
|
|
@@ -118,6 +136,18 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
118
136
|
}
|
|
119
137
|
});
|
|
120
138
|
});
|
|
139
|
+
// if length of path is 1, storing the value as an array doesn't work
|
|
140
|
+
// es-lint-disable-next-line max-len
|
|
141
|
+
// Example: https://s3.amazonaws.com/vitessce-data/0.0.33/main/human-lymph-node-10x-visium/human_lymph_node_10x_visium.h5ad.zarr
|
|
142
|
+
options.obsSets = options.obsSets?.map((obsSet) => {
|
|
143
|
+
if (obsSet.path.length === 1) {
|
|
144
|
+
return {
|
|
145
|
+
...obsSet,
|
|
146
|
+
path: obsSet.path[0],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return obsSet;
|
|
150
|
+
});
|
|
121
151
|
return {
|
|
122
152
|
options,
|
|
123
153
|
fileType: this.fileType,
|
|
@@ -129,77 +159,140 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
129
159
|
},
|
|
130
160
|
};
|
|
131
161
|
}
|
|
132
|
-
async composeViewsConfig() {
|
|
162
|
+
async composeViewsConfig(hintsConfig) {
|
|
133
163
|
this.metadataSummary = await this.setMetadataSummary();
|
|
134
|
-
const
|
|
164
|
+
const possibleViews = [];
|
|
135
165
|
const hasCellSetData = this.metadataSummary.obs
|
|
136
|
-
.filter(key => key.toLowerCase().includes('cluster')
|
|
166
|
+
.filter(key => key.toLowerCase().includes('cluster')
|
|
167
|
+
|| key.toLowerCase().includes('cell_type')
|
|
168
|
+
|| key.toLowerCase().includes('celltype'));
|
|
137
169
|
if (hasCellSetData.length > 0) {
|
|
138
|
-
|
|
170
|
+
possibleViews.push(['obsSets']);
|
|
139
171
|
}
|
|
140
172
|
this.metadataSummary.obsm.forEach((key) => {
|
|
141
173
|
if (key.toLowerCase().includes('obsm/x_umap')) {
|
|
142
|
-
|
|
174
|
+
possibleViews.push(['scatterplot', { mapping: 'UMAP' }]);
|
|
143
175
|
}
|
|
144
176
|
if (key.toLowerCase().includes('obsm/x_tsne')) {
|
|
145
|
-
|
|
177
|
+
possibleViews.push(['scatterplot', { mapping: 't-SNE' }]);
|
|
146
178
|
}
|
|
147
179
|
if (key.toLowerCase().includes('obsm/x_pca')) {
|
|
148
|
-
|
|
180
|
+
possibleViews.push(['scatterplot', { mapping: 'PCA' }]);
|
|
149
181
|
}
|
|
150
182
|
if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
|
|
151
|
-
|
|
183
|
+
possibleViews.push(['layerController']);
|
|
152
184
|
}
|
|
153
185
|
if (key.toLowerCase().includes(('obsm/x_spatial'))) {
|
|
154
|
-
|
|
186
|
+
possibleViews.push(['spatial']);
|
|
155
187
|
}
|
|
156
188
|
});
|
|
189
|
+
possibleViews.push(['obsSetSizes']);
|
|
190
|
+
possibleViews.push(['obsSetFeatureValueDistribution']);
|
|
157
191
|
if (this.metadataSummary.X) {
|
|
158
|
-
|
|
159
|
-
|
|
192
|
+
possibleViews.push(['heatmap']);
|
|
193
|
+
possibleViews.push(['featureList']);
|
|
160
194
|
}
|
|
195
|
+
const views = filterViews(hintsConfig, possibleViews);
|
|
161
196
|
return views;
|
|
162
197
|
}
|
|
198
|
+
async setMetadataSummaryWithZmetadata(response) {
|
|
199
|
+
const metadataFile = await response.json();
|
|
200
|
+
if (!metadataFile.metadata) {
|
|
201
|
+
throw new Error('Could not generate config: .zmetadata file is not valid.');
|
|
202
|
+
}
|
|
203
|
+
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
204
|
+
.filter(key => key.startsWith('obsm/X_'))
|
|
205
|
+
.map(key => key.split('/.zarray')[0]);
|
|
206
|
+
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
207
|
+
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
208
|
+
function uniq(a) {
|
|
209
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
210
|
+
}
|
|
211
|
+
const obsKeys = uniq(obsKeysArr);
|
|
212
|
+
const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
|
|
213
|
+
return {
|
|
214
|
+
// Array of keys in obsm that are found by the fetches above
|
|
215
|
+
obsm: obsmKeys,
|
|
216
|
+
// Array of keys in obs that are found by the fetches above
|
|
217
|
+
obs: obsKeys,
|
|
218
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
219
|
+
X: X.length > 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async setMetadataSummaryWithoutZmetadata() {
|
|
223
|
+
const knownMetadataFileSuffixes = [
|
|
224
|
+
'/obsm/X_pca/.zarray',
|
|
225
|
+
'/obsm/X_umap/.zarray',
|
|
226
|
+
'/obsm/X_tsne/.zarray',
|
|
227
|
+
'/obsm/X_spatial/.zarray',
|
|
228
|
+
'/obsm/X_segmentations/.zarray',
|
|
229
|
+
'/obs/.zattrs',
|
|
230
|
+
'/X/.zarray',
|
|
231
|
+
'/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
|
|
232
|
+
];
|
|
233
|
+
const getObsmKey = (url) => {
|
|
234
|
+
// Get the substring "X_pca" from a URL like
|
|
235
|
+
// http://example.com/foo/adata.zarr/obsm/X_pca/.zarray
|
|
236
|
+
const obsmKeyStartIndex = `${this.fileUrl}/`.length;
|
|
237
|
+
const obsmKeyEndIndex = url.length - '/.zarray'.length;
|
|
238
|
+
return url.substring(obsmKeyStartIndex, obsmKeyEndIndex);
|
|
239
|
+
};
|
|
240
|
+
const promises = knownMetadataFileSuffixes.map(suffix => fetch(`${this.fileUrl}${suffix}`));
|
|
241
|
+
const fetchResults = await Promise.all(promises);
|
|
242
|
+
const okFetchResults = fetchResults.filter(j => j.ok);
|
|
243
|
+
const metadataSummary = {
|
|
244
|
+
// Array of keys in obsm that are found by the fetches above
|
|
245
|
+
obsm: [],
|
|
246
|
+
// Array of keys in obs that are found by the fetches above
|
|
247
|
+
obs: [],
|
|
248
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
249
|
+
X: false,
|
|
250
|
+
};
|
|
251
|
+
const obsPromiseResult = okFetchResults.find(r => r.url === (`${this.fileUrl}/obs/.zattrs`));
|
|
252
|
+
const isObsValid = obsAttr => Object.keys(obsAttr).includes('column-order')
|
|
253
|
+
&& Object.keys(obsAttr).includes('encoding-version')
|
|
254
|
+
&& Object.keys(obsAttr).includes('encoding-type')
|
|
255
|
+
&& obsAttr['encoding-type'] === 'dataframe'
|
|
256
|
+
&& (obsAttr['encoding-version'] === '0.1.0' || obsAttr['encoding-version'] === '0.2.0');
|
|
257
|
+
if (obsPromiseResult) {
|
|
258
|
+
const obsAttrs = await obsPromiseResult.json();
|
|
259
|
+
if (isObsValid(obsAttrs)) {
|
|
260
|
+
obsAttrs['column-order'].forEach(key => metadataSummary.obs.push(`obs/${key}`));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
throw new Error('Could not generate config: /obs/.zattrs file is not valid.');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
okFetchResults
|
|
267
|
+
.forEach((r) => {
|
|
268
|
+
if (r.url.startsWith(`${this.fileUrl}/obsm`)) {
|
|
269
|
+
const obsmKey = getObsmKey(r.url);
|
|
270
|
+
if (obsmKey) {
|
|
271
|
+
metadataSummary.obsm.push(obsmKey);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (r.url.startsWith(`${this.fileUrl}/X`)) {
|
|
275
|
+
metadataSummary.X = true;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return metadataSummary;
|
|
279
|
+
}
|
|
163
280
|
async setMetadataSummary() {
|
|
164
281
|
if (Object.keys(this.metadataSummary).length > 0) {
|
|
165
282
|
return this.metadataSummary;
|
|
166
283
|
}
|
|
167
|
-
const parseMetadataFile = (metadataFile) => {
|
|
168
|
-
if (!metadataFile.metadata) {
|
|
169
|
-
throw new Error('Could not generate config: .zmetadata file is not valid.');
|
|
170
|
-
}
|
|
171
|
-
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
172
|
-
.filter(key => key.startsWith('obsm/X_'))
|
|
173
|
-
.map(key => key.split('/.zarray')[0]);
|
|
174
|
-
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
175
|
-
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
176
|
-
function uniq(a) {
|
|
177
|
-
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
178
|
-
}
|
|
179
|
-
const obsKeys = uniq(obsKeysArr);
|
|
180
|
-
const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
|
|
181
|
-
const out = {
|
|
182
|
-
obsm: obsmKeys,
|
|
183
|
-
obs: obsKeys,
|
|
184
|
-
X: X.length > 0,
|
|
185
|
-
};
|
|
186
|
-
return out;
|
|
187
|
-
};
|
|
188
284
|
const metadataExtension = '.zmetadata';
|
|
189
285
|
const url = [this.fileUrl, metadataExtension].join('/');
|
|
190
286
|
return fetch(url).then((response) => {
|
|
191
287
|
if (response.ok) {
|
|
192
|
-
return
|
|
288
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
193
289
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
.then(responseJson => parseMetadataFile(responseJson))
|
|
197
|
-
.catch((error) => {
|
|
198
|
-
if (error.status === 404) {
|
|
199
|
-
const errorMssg = `Could not generate config. File ${metadataExtension} not found in supplied file URL. Check docs for more explanation.`;
|
|
200
|
-
return Promise.reject(new Error(errorMssg));
|
|
290
|
+
if (response.status === 404) {
|
|
291
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
201
292
|
}
|
|
202
|
-
|
|
293
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
294
|
+
}).catch((error) => {
|
|
295
|
+
throw new Error(`Could not generate config for URL ${this.fileUrl}: ${error}`);
|
|
203
296
|
});
|
|
204
297
|
}
|
|
205
298
|
}
|
|
@@ -207,23 +300,19 @@ const configClasses = [
|
|
|
207
300
|
{
|
|
208
301
|
extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
|
|
209
302
|
class: OmeTiffAutoConfig,
|
|
303
|
+
name: 'OME-TIFF',
|
|
210
304
|
},
|
|
211
305
|
{
|
|
212
306
|
extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
|
|
213
307
|
class: AnndataZarrAutoConfig,
|
|
308
|
+
name: 'AnnData-Zarr',
|
|
214
309
|
},
|
|
215
310
|
{
|
|
216
311
|
extensions: ['ome.zarr'],
|
|
217
312
|
class: OmeZarrAutoConfig,
|
|
313
|
+
name: 'OME-Zarr',
|
|
218
314
|
},
|
|
219
315
|
];
|
|
220
|
-
function getFileType(url) {
|
|
221
|
-
const match = configClasses.find(obj => obj.extensions.filter(ext => url.endsWith(ext)).length === 1);
|
|
222
|
-
if (!match) {
|
|
223
|
-
throw new Error(`Could not generate config for URL: ${url}. This file type is not supported.`);
|
|
224
|
-
}
|
|
225
|
-
return match.class;
|
|
226
|
-
}
|
|
227
316
|
function calculateCoordinates(viewsNumb) {
|
|
228
317
|
const rows = Math.ceil(Math.sqrt(viewsNumb));
|
|
229
318
|
const cols = Math.ceil(viewsNumb / rows);
|
|
@@ -235,14 +324,120 @@ function calculateCoordinates(viewsNumb) {
|
|
|
235
324
|
const col = i % cols;
|
|
236
325
|
const x = col * width;
|
|
237
326
|
const y = row * height;
|
|
238
|
-
|
|
327
|
+
// The coordinates have to be integer values:
|
|
328
|
+
coords.push([
|
|
329
|
+
Math.floor(x),
|
|
330
|
+
Math.floor(y),
|
|
331
|
+
// Ensure width/height is at least 1.
|
|
332
|
+
Math.max(1, Math.floor(width)),
|
|
333
|
+
Math.max(1, Math.floor(height)),
|
|
334
|
+
]);
|
|
239
335
|
}
|
|
240
336
|
return coords;
|
|
241
337
|
}
|
|
242
|
-
|
|
338
|
+
const spatialSegmentationLayerValue = {
|
|
339
|
+
radius: 65,
|
|
340
|
+
stroked: true,
|
|
341
|
+
visible: true,
|
|
342
|
+
opacity: 1,
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Inserts the spatial coordination space into the VitessceConfig views.
|
|
346
|
+
* @param {Array of VitessceConfigView} of views
|
|
347
|
+
* @param {VitessceConfig} vc instance
|
|
348
|
+
*/
|
|
349
|
+
function insertCoordinationSpaceForSpatial(views, vc) {
|
|
350
|
+
const [spatialSegmentationLayer, spatialImageLayer, spatialZoom, spatialTargetX, spatialTargetY,] = vc.addCoordination(ct.SPATIAL_SEGMENTATION_LAYER, ct.SPATIAL_IMAGE_LAYER, ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, ct.SPATIAL_TARGET_Y);
|
|
351
|
+
// Note: this always assumes the segmentation is polygon-based.
|
|
352
|
+
// In the future, we may want to support both polygon-based and bitmask-based.
|
|
353
|
+
spatialSegmentationLayer.setValue(spatialSegmentationLayerValue);
|
|
354
|
+
// Note: this always assumes the image is RGB.
|
|
355
|
+
// In the future, we may want to support both RGB and multi-channel.
|
|
356
|
+
spatialImageLayer.setValue([
|
|
357
|
+
{
|
|
358
|
+
type: 'raster',
|
|
359
|
+
index: 0,
|
|
360
|
+
colormap: null,
|
|
361
|
+
transparentColor: null,
|
|
362
|
+
opacity: 1,
|
|
363
|
+
domainType: 'Min/Max',
|
|
364
|
+
channels: [
|
|
365
|
+
{
|
|
366
|
+
selection: {
|
|
367
|
+
c: 0,
|
|
368
|
+
},
|
|
369
|
+
color: [
|
|
370
|
+
255,
|
|
371
|
+
0,
|
|
372
|
+
0,
|
|
373
|
+
],
|
|
374
|
+
visible: true,
|
|
375
|
+
slider: [
|
|
376
|
+
0,
|
|
377
|
+
255,
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
selection: {
|
|
382
|
+
c: 1,
|
|
383
|
+
},
|
|
384
|
+
color: [
|
|
385
|
+
0,
|
|
386
|
+
255,
|
|
387
|
+
0,
|
|
388
|
+
],
|
|
389
|
+
visible: true,
|
|
390
|
+
slider: [
|
|
391
|
+
0,
|
|
392
|
+
255,
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
selection: {
|
|
397
|
+
c: 2,
|
|
398
|
+
},
|
|
399
|
+
color: [
|
|
400
|
+
0,
|
|
401
|
+
0,
|
|
402
|
+
255,
|
|
403
|
+
],
|
|
404
|
+
visible: true,
|
|
405
|
+
slider: [
|
|
406
|
+
0,
|
|
407
|
+
255,
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
]);
|
|
413
|
+
views.forEach((view) => {
|
|
414
|
+
if (view.view.component === 'spatial' || view.view.component === 'layerController') {
|
|
415
|
+
view.useCoordination(spatialImageLayer);
|
|
416
|
+
view.useCoordination(spatialSegmentationLayer);
|
|
417
|
+
view.useCoordination(spatialZoom);
|
|
418
|
+
view.useCoordination(spatialTargetX);
|
|
419
|
+
view.useCoordination(spatialTargetY);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Returns the type of the file, based on the file extension.
|
|
425
|
+
* @param {string} url of the file.
|
|
426
|
+
* @returns {object} An element from the `configClasses` array, which will be an object with
|
|
427
|
+
* the properties `extensions: string[]`, `class` (an AutoConfig class definition) and
|
|
428
|
+
* `name`: string.
|
|
429
|
+
*/
|
|
430
|
+
function getFileType(url) {
|
|
431
|
+
const match = configClasses.find(obj => obj.extensions.filter(ext => url.endsWith(ext)).length === 1);
|
|
432
|
+
if (!match) {
|
|
433
|
+
throw new Error('One or more of the URLs provided point to unsupported file types.');
|
|
434
|
+
}
|
|
435
|
+
return match;
|
|
436
|
+
}
|
|
437
|
+
async function generateViewDefinition(url, vc, dataset, hintsConfig) {
|
|
243
438
|
let ConfigClassName;
|
|
244
439
|
try {
|
|
245
|
-
ConfigClassName = getFileType(url);
|
|
440
|
+
ConfigClassName = getFileType(url).class;
|
|
246
441
|
}
|
|
247
442
|
catch (err) {
|
|
248
443
|
return Promise.reject(err);
|
|
@@ -252,15 +447,13 @@ async function generateConfig(url, vc) {
|
|
|
252
447
|
let viewsConfig;
|
|
253
448
|
try {
|
|
254
449
|
fileConfig = await configInstance.composeFileConfig();
|
|
255
|
-
viewsConfig = await configInstance.composeViewsConfig();
|
|
450
|
+
viewsConfig = await configInstance.composeViewsConfig(hintsConfig);
|
|
256
451
|
}
|
|
257
452
|
catch (error) {
|
|
258
453
|
console.error(error);
|
|
259
454
|
return Promise.reject(error);
|
|
260
455
|
}
|
|
261
|
-
|
|
262
|
-
.addDataset(configInstance.fileName)
|
|
263
|
-
.addFile(fileConfig);
|
|
456
|
+
dataset.addFile(fileConfig);
|
|
264
457
|
let layerControllerView = false;
|
|
265
458
|
let spatialView = false;
|
|
266
459
|
const views = [];
|
|
@@ -274,7 +467,7 @@ async function generateConfig(url, vc) {
|
|
|
274
467
|
}
|
|
275
468
|
// this piece of code can be removed once these props are added by default to layerController
|
|
276
469
|
// see this issue: https://github.com/vitessce/vitessce/issues/1454
|
|
277
|
-
if (v[0] === 'layerController'
|
|
470
|
+
if (v[0] === 'layerController') {
|
|
278
471
|
view.setProps({
|
|
279
472
|
disable3d: [],
|
|
280
473
|
disableChannelsIfRgbDetected: true,
|
|
@@ -287,36 +480,74 @@ async function generateConfig(url, vc) {
|
|
|
287
480
|
views.push(view);
|
|
288
481
|
});
|
|
289
482
|
if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
|
|
290
|
-
const spatialSegmentationLayerValue = {
|
|
291
|
-
opacity: 1,
|
|
292
|
-
radius: 0,
|
|
293
|
-
visible: true,
|
|
294
|
-
stroked: false,
|
|
295
|
-
};
|
|
296
483
|
vc.linkViews([spatialView, layerControllerView], [
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
CoordinationType.SPATIAL_TARGET_Y,
|
|
300
|
-
CoordinationType.SPATIAL_SEGMENTATION_LAYER,
|
|
301
|
-
], [-5.5, 16000, 20000, spatialSegmentationLayerValue]);
|
|
484
|
+
ct.SPATIAL_SEGMENTATION_LAYER,
|
|
485
|
+
], [spatialSegmentationLayerValue]);
|
|
302
486
|
}
|
|
303
487
|
return views;
|
|
304
488
|
}
|
|
305
|
-
|
|
489
|
+
/**
|
|
490
|
+
* Returns the hints that are available for the given file URLs, depending on their types.
|
|
491
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
492
|
+
* @returns the hints available for these file URLs
|
|
493
|
+
*/
|
|
494
|
+
export function getHintOptions(fileUrls) {
|
|
495
|
+
const fileTypes = {};
|
|
496
|
+
fileUrls.forEach((url) => {
|
|
497
|
+
const match = getFileType(url);
|
|
498
|
+
// hints config only has OME-TIFF, since settings are the same for both OME-TIFF and OME-Zarr
|
|
499
|
+
if (match.name === 'OME-Zarr') {
|
|
500
|
+
fileTypes['OME-TIFF'] = true;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
fileTypes[match.name] = true;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
const datasetType = Object.keys(fileTypes).sort().join(',');
|
|
507
|
+
return HINT_TYPE_TO_FILE_TYPE_MAP?.[datasetType] || [];
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
*
|
|
511
|
+
* @param {Array} fileUrls containing urls of files to be loaded into Vitessce
|
|
512
|
+
* @param {String} the hints config to be used for the dataset. Null by default
|
|
513
|
+
* @returns ViewConfig as JSON
|
|
514
|
+
*/
|
|
515
|
+
export async function generateConfig(fileUrls, hintTitle = null) {
|
|
306
516
|
const vc = new VitessceConfig({
|
|
307
517
|
schemaVersion: '1.0.15',
|
|
308
518
|
name: 'An automatically generated config. Adjust values and add layout components if needed.',
|
|
309
519
|
description: 'Populate with text relevant to this visualisation.',
|
|
310
520
|
});
|
|
311
521
|
const allViews = [];
|
|
522
|
+
const dataset = vc.addDataset('An automatically generated view config for dataset. Adjust values and add layout components if needed.');
|
|
523
|
+
const hintsConfig = !hintTitle ? { views: {} } : HINTS_CONFIG?.[hintTitle];
|
|
524
|
+
if (!hintsConfig) {
|
|
525
|
+
throw new Error(`Hints config not found for the supplied hint: ${hintTitle}.`);
|
|
526
|
+
}
|
|
527
|
+
const useHints = Object.keys(hintsConfig?.views)?.length > 0;
|
|
312
528
|
fileUrls.forEach((url) => {
|
|
313
|
-
allViews.push(
|
|
529
|
+
allViews.push(generateViewDefinition(url, vc, dataset, hintsConfig));
|
|
314
530
|
});
|
|
315
531
|
return Promise.all(allViews).then((views) => {
|
|
316
532
|
const flattenedViews = views.flat();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
533
|
+
// If coordinationSpaceRequired field is set to true in the HINTS_CONFIG,
|
|
534
|
+
// then insert coordination space.
|
|
535
|
+
// NOTE: the user needs to manually add the coordination values for the image and
|
|
536
|
+
// segmentation layers which will be visualized in the spatial/layer controller views.
|
|
537
|
+
if (hintsConfig?.coordinationSpaceRequired) {
|
|
538
|
+
insertCoordinationSpaceForSpatial(flattenedViews, vc);
|
|
539
|
+
}
|
|
540
|
+
if (!useHints) {
|
|
541
|
+
const coord = calculateCoordinates(flattenedViews.length);
|
|
542
|
+
for (let i = 0; i < flattenedViews.length; i++) {
|
|
543
|
+
flattenedViews[i].setXYWH(...coord[i]);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
flattenedViews.forEach((vitessceConfigView) => {
|
|
548
|
+
const coordinates = Object.values(hintsConfig.views[vitessceConfigView.view.component]);
|
|
549
|
+
vitessceConfigView.setXYWH(...coordinates);
|
|
550
|
+
});
|
|
320
551
|
}
|
|
321
552
|
return vc.toJSON();
|
|
322
553
|
});
|