@vitessce/config 2.0.3 → 3.0.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 +15964 -0
- package/dist-tsc/VitessceAutoConfig.d.ts +2 -0
- package/dist-tsc/VitessceAutoConfig.d.ts.map +1 -0
- package/dist-tsc/VitessceAutoConfig.js +378 -0
- package/dist-tsc/VitessceAutoConfig.test.d.ts +2 -0
- package/dist-tsc/VitessceAutoConfig.test.d.ts.map +1 -0
- package/dist-tsc/VitessceAutoConfig.test.js +513 -0
- package/dist-tsc/VitessceConfig.d.ts +257 -0
- package/dist-tsc/VitessceConfig.d.ts.map +1 -0
- package/dist-tsc/VitessceConfig.js +468 -0
- package/dist-tsc/VitessceConfig.test.d.ts +2 -0
- package/dist-tsc/VitessceConfig.test.d.ts.map +1 -0
- package/dist-tsc/VitessceConfig.test.js +448 -0
- package/dist-tsc/index.d.ts +3 -0
- package/dist-tsc/index.d.ts.map +1 -0
- package/dist-tsc/index.js +2 -1
- package/dist-tsc/utils.d.ts +12 -0
- package/dist-tsc/utils.d.ts.map +1 -0
- package/dist-tsc/utils.js +47 -0
- package/package.json +15 -6
- package/src/VitessceAutoConfig.js +445 -0
- package/src/VitessceAutoConfig.test.js +532 -0
- package/src/VitessceConfig.test.js +2 -1
- package/src/index.js +2 -1
- package/dist/index.mjs +0 -9365
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { CoordinationType, FileType } from '@vitessce/constants-internal';
|
|
2
|
+
import {
|
|
3
|
+
VitessceConfig,
|
|
4
|
+
} from './VitessceConfig.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractAutoConfig {
|
|
8
|
+
async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
|
|
9
|
+
throw new Error('The composeViewsConfig() method has not been implemented.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async composeFileConfig() { /* eslint-disable-line class-methods-use-this */
|
|
13
|
+
throw new Error('The composeFileConfig() method has not been implemented.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class OmeTiffAutoConfig extends AbstractAutoConfig {
|
|
17
|
+
constructor(fileUrl) {
|
|
18
|
+
super();
|
|
19
|
+
this.fileUrl = fileUrl;
|
|
20
|
+
this.fileType = FileType.RASTER_JSON;
|
|
21
|
+
this.fileName = fileUrl.split('/').at(-1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
|
|
26
|
+
return [
|
|
27
|
+
['description'],
|
|
28
|
+
['spatial'],
|
|
29
|
+
['layerController'],
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async composeFileConfig() {
|
|
34
|
+
return {
|
|
35
|
+
fileType: this.fileType,
|
|
36
|
+
options: {
|
|
37
|
+
images: [
|
|
38
|
+
{
|
|
39
|
+
metadata: {
|
|
40
|
+
isBitmask: false,
|
|
41
|
+
},
|
|
42
|
+
name: this.fileName,
|
|
43
|
+
type: 'ome-tiff',
|
|
44
|
+
url: this.fileUrl,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
schemaVersion: '0.0.2',
|
|
48
|
+
usePhysicalSizeScaling: false,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class OmeZarrAutoConfig extends AbstractAutoConfig {
|
|
55
|
+
constructor(fileUrl) {
|
|
56
|
+
super();
|
|
57
|
+
this.fileUrl = fileUrl;
|
|
58
|
+
this.fileType = FileType.RASTER_OME_ZARR;
|
|
59
|
+
this.fileName = fileUrl.split('/').at(-1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
|
|
63
|
+
return [
|
|
64
|
+
['description'],
|
|
65
|
+
['spatial'],
|
|
66
|
+
['layerController'],
|
|
67
|
+
['status'],
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async composeFileConfig() {
|
|
72
|
+
return {
|
|
73
|
+
fileType: this.fileType,
|
|
74
|
+
type: 'raster',
|
|
75
|
+
url: this.fileUrl,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
81
|
+
constructor(fileUrl) {
|
|
82
|
+
super();
|
|
83
|
+
this.fileUrl = fileUrl;
|
|
84
|
+
this.fileType = FileType.ANNDATA_ZARR;
|
|
85
|
+
this.fileName = fileUrl.split('/').at(-1);
|
|
86
|
+
this.metadataSummary = {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async composeFileConfig() {
|
|
90
|
+
this.metadataSummary = await this.setMetadataSummary();
|
|
91
|
+
|
|
92
|
+
const options = {
|
|
93
|
+
obsEmbedding: [],
|
|
94
|
+
obsFeatureMatrix: {
|
|
95
|
+
path: 'X',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
this.metadataSummary.obsm.forEach((key) => {
|
|
100
|
+
if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
|
|
101
|
+
options.obsSegmentations = { path: key };
|
|
102
|
+
}
|
|
103
|
+
if (key.toLowerCase().includes(('obsm/x_spatial'))) {
|
|
104
|
+
options.obsLocations = { path: key };
|
|
105
|
+
}
|
|
106
|
+
if (key.toLowerCase().includes('obsm/x_umap')) {
|
|
107
|
+
options.obsEmbedding.push({ path: key, embeddingType: 'UMAP' });
|
|
108
|
+
}
|
|
109
|
+
if (key.toLowerCase().includes('obsm/x_tsne')) {
|
|
110
|
+
options.obsEmbedding.push({ path: key, embeddingType: 't-SNE' });
|
|
111
|
+
}
|
|
112
|
+
if (key.toLowerCase().includes('obsm/x_pca')) {
|
|
113
|
+
options.obsEmbedding.push({ path: key, embeddingType: 'PCA' });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const supportedObsSetsKeys = [
|
|
118
|
+
'cluster', 'subcluster', 'cell_type', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
this.metadataSummary.obs.forEach((key) => {
|
|
122
|
+
supportedObsSetsKeys.forEach((supportedKey) => {
|
|
123
|
+
if (key.toLowerCase() === ['obs', supportedKey].join('/')) {
|
|
124
|
+
if (!('obsSets' in options)) {
|
|
125
|
+
options.obsSets = [
|
|
126
|
+
{
|
|
127
|
+
name: 'Cell Type',
|
|
128
|
+
path: [key],
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
} else {
|
|
132
|
+
options.obsSets[0].path.push(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
options,
|
|
140
|
+
fileType: this.fileType,
|
|
141
|
+
url: this.fileUrl,
|
|
142
|
+
coordinationValues: {
|
|
143
|
+
obsType: 'cell',
|
|
144
|
+
featureType: 'gene',
|
|
145
|
+
featureValueType: 'expression',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async composeViewsConfig() {
|
|
151
|
+
this.metadataSummary = await this.setMetadataSummary();
|
|
152
|
+
|
|
153
|
+
const views = [];
|
|
154
|
+
|
|
155
|
+
const hasCellSetData = this.metadataSummary.obs
|
|
156
|
+
.filter(key => key.toLowerCase().includes('cluster') || key.toLowerCase().includes('cell_type'));
|
|
157
|
+
|
|
158
|
+
if (hasCellSetData.length > 0) {
|
|
159
|
+
views.push(['obsSets']);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.metadataSummary.obsm.forEach((key) => {
|
|
163
|
+
if (key.toLowerCase().includes('obsm/x_umap')) {
|
|
164
|
+
views.push(['scatterplot', { mapping: 'UMAP' }]);
|
|
165
|
+
}
|
|
166
|
+
if (key.toLowerCase().includes('obsm/x_tsne')) {
|
|
167
|
+
views.push(['scatterplot', { mapping: 't-SNE' }]);
|
|
168
|
+
}
|
|
169
|
+
if (key.toLowerCase().includes('obsm/x_pca')) {
|
|
170
|
+
views.push(['scatterplot', { mapping: 'PCA' }]);
|
|
171
|
+
}
|
|
172
|
+
if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
|
|
173
|
+
views.push(['layerController']);
|
|
174
|
+
}
|
|
175
|
+
if (key.toLowerCase().includes(('obsm/x_spatial'))) {
|
|
176
|
+
views.push(['spatial']);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (this.metadataSummary.X) {
|
|
181
|
+
views.push(['heatmap']);
|
|
182
|
+
views.push(['featureList']);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return views;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async setMetadataSummaryWithZmetadata(response) { /* eslint-disable-line class-methods-use-this */
|
|
189
|
+
const metadataFile = await response.json();
|
|
190
|
+
if (!metadataFile.metadata) {
|
|
191
|
+
throw new Error('Could not generate config: .zmetadata file is not valid.');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
195
|
+
.filter(key => key.startsWith('obsm/X_'))
|
|
196
|
+
.map(key => key.split('/.zarray')[0]);
|
|
197
|
+
|
|
198
|
+
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
199
|
+
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
200
|
+
|
|
201
|
+
function uniq(a) {
|
|
202
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
203
|
+
}
|
|
204
|
+
const obsKeys = uniq(obsKeysArr);
|
|
205
|
+
|
|
206
|
+
const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
// Array of keys in obsm that are found by the fetches above
|
|
210
|
+
obsm: obsmKeys,
|
|
211
|
+
// Array of keys in obs that are found by the fetches above
|
|
212
|
+
obs: obsKeys,
|
|
213
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
214
|
+
X: X.length > 0,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async setMetadataSummaryWithoutZmetadata() {
|
|
219
|
+
const knownMetadataFileSuffixes = [
|
|
220
|
+
'/obsm/X_pca/.zarray',
|
|
221
|
+
'/obsm/X_umap/.zarray',
|
|
222
|
+
'/obsm/X_tsne/.zarray',
|
|
223
|
+
'/obsm/X_spatial/.zarray',
|
|
224
|
+
'/obsm/X_segmentations/.zarray',
|
|
225
|
+
'/obs/.zattrs',
|
|
226
|
+
'/X/.zarray',
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const getObsmKey = (url) => {
|
|
230
|
+
// Get the substring "X_pca" from a URL like
|
|
231
|
+
// http://example.com/foo/adata.zarr/obsm/X_pca/.zarray
|
|
232
|
+
const obsmKeyStartIndex = `${this.fileUrl}/`.length;
|
|
233
|
+
const obsmKeyEndIndex = url.length - '/.zarray'.length;
|
|
234
|
+
return url.substring(obsmKeyStartIndex, obsmKeyEndIndex);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const promises = knownMetadataFileSuffixes.map(suffix => fetch(`${this.fileUrl}${suffix}`));
|
|
238
|
+
|
|
239
|
+
const fetchResults = await Promise.all(promises);
|
|
240
|
+
const okFetchResults = fetchResults.filter(j => j.ok);
|
|
241
|
+
const metadataSummary = {
|
|
242
|
+
// Array of keys in obsm that are found by the fetches above
|
|
243
|
+
obsm: [],
|
|
244
|
+
// Array of keys in obs that are found by the fetches above
|
|
245
|
+
obs: [],
|
|
246
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
247
|
+
X: false,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const obsPromiseResult = okFetchResults.find(
|
|
251
|
+
r => r.url === (`${this.fileUrl}/obs/.zattrs`),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const isObsValid = obsAttr => Object.keys(obsAttr).includes('column-order')
|
|
255
|
+
&& Object.keys(obsAttr).includes('encoding-version')
|
|
256
|
+
&& Object.keys(obsAttr).includes('encoding-type')
|
|
257
|
+
&& obsAttr['encoding-type'] === 'dataframe'
|
|
258
|
+
&& (obsAttr['encoding-version'] === '0.1.0' || obsAttr['encoding-version'] === '0.2.0');
|
|
259
|
+
|
|
260
|
+
if (obsPromiseResult) {
|
|
261
|
+
const obsAttrs = await obsPromiseResult.json();
|
|
262
|
+
if (isObsValid(obsAttrs)) {
|
|
263
|
+
obsAttrs['column-order'].forEach(key => metadataSummary.obs.push(`obs/${key}`));
|
|
264
|
+
} else {
|
|
265
|
+
throw new Error('Could not generate config: /obs/.zattrs file is not valid.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
okFetchResults
|
|
270
|
+
.forEach((r) => {
|
|
271
|
+
if (r.url.startsWith(`${this.fileUrl}/obsm`)) {
|
|
272
|
+
const obsmKey = getObsmKey(r.url);
|
|
273
|
+
if (obsmKey) {
|
|
274
|
+
metadataSummary.obsm.push(obsmKey);
|
|
275
|
+
}
|
|
276
|
+
} else if (r.url.startsWith(`${this.fileUrl}/X`)) {
|
|
277
|
+
metadataSummary.X = true;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return metadataSummary;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async setMetadataSummary() {
|
|
285
|
+
if (Object.keys(this.metadataSummary).length > 0) {
|
|
286
|
+
return this.metadataSummary;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const metadataExtension = '.zmetadata';
|
|
290
|
+
const url = [this.fileUrl, metadataExtension].join('/');
|
|
291
|
+
return fetch(url).then((response) => {
|
|
292
|
+
if (response.ok) {
|
|
293
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
294
|
+
}
|
|
295
|
+
if (response.status === 404) {
|
|
296
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const configClasses = [
|
|
304
|
+
{
|
|
305
|
+
extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
|
|
306
|
+
class: OmeTiffAutoConfig,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
|
|
310
|
+
class: AnndataZarrAutoConfig,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
extensions: ['ome.zarr'],
|
|
314
|
+
class: OmeZarrAutoConfig,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
function getFileType(url) {
|
|
319
|
+
const match = configClasses.find(obj => obj.extensions.filter(
|
|
320
|
+
ext => url.endsWith(ext),
|
|
321
|
+
).length === 1);
|
|
322
|
+
if (!match) {
|
|
323
|
+
throw new Error(`Could not generate config for URL: ${url}. This file type is not supported.`);
|
|
324
|
+
}
|
|
325
|
+
return match.class;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function calculateCoordinates(viewsNumb) {
|
|
329
|
+
const rows = Math.ceil(Math.sqrt(viewsNumb));
|
|
330
|
+
const cols = Math.ceil(viewsNumb / rows);
|
|
331
|
+
const width = 12 / cols;
|
|
332
|
+
const height = 12 / rows;
|
|
333
|
+
const coords = [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < viewsNumb; i++) {
|
|
336
|
+
const row = Math.floor(i / cols);
|
|
337
|
+
const col = i % cols;
|
|
338
|
+
const x = col * width;
|
|
339
|
+
const y = row * height;
|
|
340
|
+
coords.push([x, y, width, height]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return coords;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function generateConfig(url, vc) {
|
|
347
|
+
let ConfigClassName;
|
|
348
|
+
try {
|
|
349
|
+
ConfigClassName = getFileType(url);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
return Promise.reject(err);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const configInstance = new ConfigClassName(url);
|
|
355
|
+
let fileConfig;
|
|
356
|
+
let viewsConfig;
|
|
357
|
+
try {
|
|
358
|
+
fileConfig = await configInstance.composeFileConfig();
|
|
359
|
+
viewsConfig = await configInstance.composeViewsConfig();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error(error);
|
|
362
|
+
return Promise.reject(error);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const dataset = vc
|
|
366
|
+
.addDataset(configInstance.fileName)
|
|
367
|
+
.addFile(fileConfig);
|
|
368
|
+
|
|
369
|
+
let layerControllerView = false;
|
|
370
|
+
let spatialView = false;
|
|
371
|
+
|
|
372
|
+
const views = [];
|
|
373
|
+
|
|
374
|
+
viewsConfig.forEach((v) => {
|
|
375
|
+
const view = vc.addView(dataset, ...v);
|
|
376
|
+
if (v[0] === 'layerController') {
|
|
377
|
+
layerControllerView = view;
|
|
378
|
+
}
|
|
379
|
+
if (v[0] === 'spatial') {
|
|
380
|
+
spatialView = view;
|
|
381
|
+
}
|
|
382
|
+
// this piece of code can be removed once these props are added by default to layerController
|
|
383
|
+
// see this issue: https://github.com/vitessce/vitessce/issues/1454
|
|
384
|
+
if (v[0] === 'layerController' && configInstance instanceof OmeTiffAutoConfig) {
|
|
385
|
+
view.setProps({
|
|
386
|
+
disable3d: [],
|
|
387
|
+
disableChannelsIfRgbDetected: true,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// transpose the heatmap by default
|
|
391
|
+
if (v[0] === 'heatmap' && configInstance instanceof AnndataZarrAutoConfig) {
|
|
392
|
+
view.setProps({ transpose: true });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
views.push(view);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
|
|
399
|
+
const spatialSegmentationLayerValue = {
|
|
400
|
+
opacity: 1,
|
|
401
|
+
radius: 0,
|
|
402
|
+
visible: true,
|
|
403
|
+
stroked: false,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
vc.linkViews(
|
|
407
|
+
[spatialView, layerControllerView],
|
|
408
|
+
[
|
|
409
|
+
CoordinationType.SPATIAL_ZOOM,
|
|
410
|
+
CoordinationType.SPATIAL_TARGET_X,
|
|
411
|
+
CoordinationType.SPATIAL_TARGET_Y,
|
|
412
|
+
CoordinationType.SPATIAL_SEGMENTATION_LAYER,
|
|
413
|
+
],
|
|
414
|
+
[-5.5, 16000, 20000, spatialSegmentationLayerValue],
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return views;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function generateConfigs(fileUrls) {
|
|
422
|
+
const vc = new VitessceConfig({
|
|
423
|
+
schemaVersion: '1.0.15',
|
|
424
|
+
name: 'An automatically generated config. Adjust values and add layout components if needed.',
|
|
425
|
+
description: 'Populate with text relevant to this visualisation.',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const allViews = [];
|
|
429
|
+
|
|
430
|
+
fileUrls.forEach((url) => {
|
|
431
|
+
allViews.push(generateConfig(url, vc));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return Promise.all(allViews).then((views) => {
|
|
435
|
+
const flattenedViews = views.flat();
|
|
436
|
+
|
|
437
|
+
const coord = calculateCoordinates(flattenedViews.length);
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < flattenedViews.length; i++) {
|
|
440
|
+
flattenedViews[i].setXYWH(...coord[i]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return vc.toJSON();
|
|
444
|
+
});
|
|
445
|
+
}
|