@vitessce/config 3.0.0 → 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 +75 -25
- package/dist-tsc/VitessceAutoConfig.d.ts.map +1 -1
- package/dist-tsc/VitessceAutoConfig.js +85 -30
- package/dist-tsc/VitessceAutoConfig.test.js +79 -8
- package/dist-tsc/VitessceConfig.test.js +1 -0
- package/package.json +4 -4
- package/src/VitessceAutoConfig.js +97 -34
- package/src/VitessceAutoConfig.test.js +79 -12
- package/src/VitessceConfig.test.js +1 -0
package/dist/index.js
CHANGED
|
@@ -15751,41 +15751,91 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
15751
15751
|
}
|
|
15752
15752
|
return views;
|
|
15753
15753
|
}
|
|
15754
|
+
async setMetadataSummaryWithZmetadata(response) {
|
|
15755
|
+
const metadataFile = await response.json();
|
|
15756
|
+
if (!metadataFile.metadata) {
|
|
15757
|
+
throw new Error("Could not generate config: .zmetadata file is not valid.");
|
|
15758
|
+
}
|
|
15759
|
+
const obsmKeys = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("obsm/X_")).map((key) => key.split("/.zarray")[0]);
|
|
15760
|
+
const obsKeysArr = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("obs/")).map((key) => key.split("/.za")[0]);
|
|
15761
|
+
function uniq(a) {
|
|
15762
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
15763
|
+
}
|
|
15764
|
+
const obsKeys = uniq(obsKeysArr);
|
|
15765
|
+
const X = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("X"));
|
|
15766
|
+
return {
|
|
15767
|
+
// Array of keys in obsm that are found by the fetches above
|
|
15768
|
+
obsm: obsmKeys,
|
|
15769
|
+
// Array of keys in obs that are found by the fetches above
|
|
15770
|
+
obs: obsKeys,
|
|
15771
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
15772
|
+
X: X.length > 0
|
|
15773
|
+
};
|
|
15774
|
+
}
|
|
15775
|
+
async setMetadataSummaryWithoutZmetadata() {
|
|
15776
|
+
const knownMetadataFileSuffixes = [
|
|
15777
|
+
"/obsm/X_pca/.zarray",
|
|
15778
|
+
"/obsm/X_umap/.zarray",
|
|
15779
|
+
"/obsm/X_tsne/.zarray",
|
|
15780
|
+
"/obsm/X_spatial/.zarray",
|
|
15781
|
+
"/obsm/X_segmentations/.zarray",
|
|
15782
|
+
"/obs/.zattrs",
|
|
15783
|
+
"/X/.zarray"
|
|
15784
|
+
];
|
|
15785
|
+
const getObsmKey = (url) => {
|
|
15786
|
+
const obsmKeyStartIndex = `${this.fileUrl}/`.length;
|
|
15787
|
+
const obsmKeyEndIndex = url.length - "/.zarray".length;
|
|
15788
|
+
return url.substring(obsmKeyStartIndex, obsmKeyEndIndex);
|
|
15789
|
+
};
|
|
15790
|
+
const promises = knownMetadataFileSuffixes.map((suffix) => fetch(`${this.fileUrl}${suffix}`));
|
|
15791
|
+
const fetchResults = await Promise.all(promises);
|
|
15792
|
+
const okFetchResults = fetchResults.filter((j) => j.ok);
|
|
15793
|
+
const metadataSummary = {
|
|
15794
|
+
// Array of keys in obsm that are found by the fetches above
|
|
15795
|
+
obsm: [],
|
|
15796
|
+
// Array of keys in obs that are found by the fetches above
|
|
15797
|
+
obs: [],
|
|
15798
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
15799
|
+
X: false
|
|
15800
|
+
};
|
|
15801
|
+
const obsPromiseResult = okFetchResults.find(
|
|
15802
|
+
(r) => r.url === `${this.fileUrl}/obs/.zattrs`
|
|
15803
|
+
);
|
|
15804
|
+
const isObsValid = (obsAttr) => Object.keys(obsAttr).includes("column-order") && Object.keys(obsAttr).includes("encoding-version") && Object.keys(obsAttr).includes("encoding-type") && obsAttr["encoding-type"] === "dataframe" && (obsAttr["encoding-version"] === "0.1.0" || obsAttr["encoding-version"] === "0.2.0");
|
|
15805
|
+
if (obsPromiseResult) {
|
|
15806
|
+
const obsAttrs = await obsPromiseResult.json();
|
|
15807
|
+
if (isObsValid(obsAttrs)) {
|
|
15808
|
+
obsAttrs["column-order"].forEach((key) => metadataSummary.obs.push(`obs/${key}`));
|
|
15809
|
+
} else {
|
|
15810
|
+
throw new Error("Could not generate config: /obs/.zattrs file is not valid.");
|
|
15811
|
+
}
|
|
15812
|
+
}
|
|
15813
|
+
okFetchResults.forEach((r) => {
|
|
15814
|
+
if (r.url.startsWith(`${this.fileUrl}/obsm`)) {
|
|
15815
|
+
const obsmKey = getObsmKey(r.url);
|
|
15816
|
+
if (obsmKey) {
|
|
15817
|
+
metadataSummary.obsm.push(obsmKey);
|
|
15818
|
+
}
|
|
15819
|
+
} else if (r.url.startsWith(`${this.fileUrl}/X`)) {
|
|
15820
|
+
metadataSummary.X = true;
|
|
15821
|
+
}
|
|
15822
|
+
});
|
|
15823
|
+
return metadataSummary;
|
|
15824
|
+
}
|
|
15754
15825
|
async setMetadataSummary() {
|
|
15755
15826
|
if (Object.keys(this.metadataSummary).length > 0) {
|
|
15756
15827
|
return this.metadataSummary;
|
|
15757
15828
|
}
|
|
15758
|
-
const parseMetadataFile = (metadataFile) => {
|
|
15759
|
-
if (!metadataFile.metadata) {
|
|
15760
|
-
throw new Error("Could not generate config: .zmetadata file is not valid.");
|
|
15761
|
-
}
|
|
15762
|
-
const obsmKeys = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("obsm/X_")).map((key) => key.split("/.zarray")[0]);
|
|
15763
|
-
const obsKeysArr = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("obs/")).map((key) => key.split("/.za")[0]);
|
|
15764
|
-
function uniq(a) {
|
|
15765
|
-
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
15766
|
-
}
|
|
15767
|
-
const obsKeys = uniq(obsKeysArr);
|
|
15768
|
-
const X = Object.keys(metadataFile.metadata).filter((key) => key.startsWith("X"));
|
|
15769
|
-
const out = {
|
|
15770
|
-
obsm: obsmKeys,
|
|
15771
|
-
obs: obsKeys,
|
|
15772
|
-
X: X.length > 0
|
|
15773
|
-
};
|
|
15774
|
-
return out;
|
|
15775
|
-
};
|
|
15776
15829
|
const metadataExtension = ".zmetadata";
|
|
15777
15830
|
const url = [this.fileUrl, metadataExtension].join("/");
|
|
15778
15831
|
return fetch(url).then((response) => {
|
|
15779
15832
|
if (response.ok) {
|
|
15780
|
-
return
|
|
15833
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
15781
15834
|
}
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
if (error.status === 404) {
|
|
15785
|
-
const errorMssg = `Could not generate config. File ${metadataExtension} not found in supplied file URL. Check docs for more explanation.`;
|
|
15786
|
-
return Promise.reject(new Error(errorMssg));
|
|
15835
|
+
if (response.status === 404) {
|
|
15836
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
15787
15837
|
}
|
|
15788
|
-
|
|
15838
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
15789
15839
|
});
|
|
15790
15840
|
}
|
|
15791
15841
|
}
|
|
@@ -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":"AAoaA,gEAwBC"}
|
|
@@ -160,46 +160,101 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
160
160
|
}
|
|
161
161
|
return views;
|
|
162
162
|
}
|
|
163
|
+
async setMetadataSummaryWithZmetadata(response) {
|
|
164
|
+
const metadataFile = await response.json();
|
|
165
|
+
if (!metadataFile.metadata) {
|
|
166
|
+
throw new Error('Could not generate config: .zmetadata file is not valid.');
|
|
167
|
+
}
|
|
168
|
+
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
169
|
+
.filter(key => key.startsWith('obsm/X_'))
|
|
170
|
+
.map(key => key.split('/.zarray')[0]);
|
|
171
|
+
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
172
|
+
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
173
|
+
function uniq(a) {
|
|
174
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
175
|
+
}
|
|
176
|
+
const obsKeys = uniq(obsKeysArr);
|
|
177
|
+
const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
|
|
178
|
+
return {
|
|
179
|
+
// Array of keys in obsm that are found by the fetches above
|
|
180
|
+
obsm: obsmKeys,
|
|
181
|
+
// Array of keys in obs that are found by the fetches above
|
|
182
|
+
obs: obsKeys,
|
|
183
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
184
|
+
X: X.length > 0,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async setMetadataSummaryWithoutZmetadata() {
|
|
188
|
+
const knownMetadataFileSuffixes = [
|
|
189
|
+
'/obsm/X_pca/.zarray',
|
|
190
|
+
'/obsm/X_umap/.zarray',
|
|
191
|
+
'/obsm/X_tsne/.zarray',
|
|
192
|
+
'/obsm/X_spatial/.zarray',
|
|
193
|
+
'/obsm/X_segmentations/.zarray',
|
|
194
|
+
'/obs/.zattrs',
|
|
195
|
+
'/X/.zarray',
|
|
196
|
+
];
|
|
197
|
+
const getObsmKey = (url) => {
|
|
198
|
+
// Get the substring "X_pca" from a URL like
|
|
199
|
+
// http://example.com/foo/adata.zarr/obsm/X_pca/.zarray
|
|
200
|
+
const obsmKeyStartIndex = `${this.fileUrl}/`.length;
|
|
201
|
+
const obsmKeyEndIndex = url.length - '/.zarray'.length;
|
|
202
|
+
return url.substring(obsmKeyStartIndex, obsmKeyEndIndex);
|
|
203
|
+
};
|
|
204
|
+
const promises = knownMetadataFileSuffixes.map(suffix => fetch(`${this.fileUrl}${suffix}`));
|
|
205
|
+
const fetchResults = await Promise.all(promises);
|
|
206
|
+
const okFetchResults = fetchResults.filter(j => j.ok);
|
|
207
|
+
const metadataSummary = {
|
|
208
|
+
// Array of keys in obsm that are found by the fetches above
|
|
209
|
+
obsm: [],
|
|
210
|
+
// Array of keys in obs that are found by the fetches above
|
|
211
|
+
obs: [],
|
|
212
|
+
// Boolean indicating whether the X array was found by the fetches above
|
|
213
|
+
X: false,
|
|
214
|
+
};
|
|
215
|
+
const obsPromiseResult = okFetchResults.find(r => r.url === (`${this.fileUrl}/obs/.zattrs`));
|
|
216
|
+
const isObsValid = obsAttr => Object.keys(obsAttr).includes('column-order')
|
|
217
|
+
&& Object.keys(obsAttr).includes('encoding-version')
|
|
218
|
+
&& Object.keys(obsAttr).includes('encoding-type')
|
|
219
|
+
&& obsAttr['encoding-type'] === 'dataframe'
|
|
220
|
+
&& (obsAttr['encoding-version'] === '0.1.0' || obsAttr['encoding-version'] === '0.2.0');
|
|
221
|
+
if (obsPromiseResult) {
|
|
222
|
+
const obsAttrs = await obsPromiseResult.json();
|
|
223
|
+
if (isObsValid(obsAttrs)) {
|
|
224
|
+
obsAttrs['column-order'].forEach(key => metadataSummary.obs.push(`obs/${key}`));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
throw new Error('Could not generate config: /obs/.zattrs file is not valid.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
okFetchResults
|
|
231
|
+
.forEach((r) => {
|
|
232
|
+
if (r.url.startsWith(`${this.fileUrl}/obsm`)) {
|
|
233
|
+
const obsmKey = getObsmKey(r.url);
|
|
234
|
+
if (obsmKey) {
|
|
235
|
+
metadataSummary.obsm.push(obsmKey);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (r.url.startsWith(`${this.fileUrl}/X`)) {
|
|
239
|
+
metadataSummary.X = true;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return metadataSummary;
|
|
243
|
+
}
|
|
163
244
|
async setMetadataSummary() {
|
|
164
245
|
if (Object.keys(this.metadataSummary).length > 0) {
|
|
165
246
|
return this.metadataSummary;
|
|
166
247
|
}
|
|
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
248
|
const metadataExtension = '.zmetadata';
|
|
189
249
|
const url = [this.fileUrl, metadataExtension].join('/');
|
|
190
250
|
return fetch(url).then((response) => {
|
|
191
251
|
if (response.ok) {
|
|
192
|
-
return
|
|
252
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
193
253
|
}
|
|
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));
|
|
254
|
+
if (response.status === 404) {
|
|
255
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
201
256
|
}
|
|
202
|
-
|
|
257
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
203
258
|
});
|
|
204
259
|
}
|
|
205
260
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
1
2
|
import { generateConfigs } from './VitessceAutoConfig.js';
|
|
2
3
|
describe('src/VitessceAutoConfig.js', () => {
|
|
3
4
|
it('generates config for OME-TIFF file correctly', async () => {
|
|
@@ -150,7 +151,7 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
150
151
|
expect(config).toEqual(expectedConfig);
|
|
151
152
|
});
|
|
152
153
|
it('generates config for Anndata-ZARR file correctly', async () => {
|
|
153
|
-
const urls = ['http://localhost:
|
|
154
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/.anndata.zarr'];
|
|
154
155
|
const expectedName = urls[0].split('/').at(-1);
|
|
155
156
|
const expectedConfig = {
|
|
156
157
|
version: '1.0.15',
|
|
@@ -258,7 +259,7 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
258
259
|
expect(config).toEqual(expectedConfig);
|
|
259
260
|
});
|
|
260
261
|
it('generates empty config for Anndata-ZARR file with empty .zmetadata', async () => {
|
|
261
|
-
const urls = ['http://localhost:
|
|
262
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/emptymeta.h5ad.zarr'];
|
|
262
263
|
const expectedName = urls[0].split('/').at(-1);
|
|
263
264
|
const expectedConfig = {
|
|
264
265
|
version: '1.0.15',
|
|
@@ -299,8 +300,13 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
299
300
|
expect(config).toEqual(expectedConfig);
|
|
300
301
|
});
|
|
301
302
|
it('raises an error for Anndata-ZARR file with misconfigured .zmetadata', async () => {
|
|
302
|
-
const urls = ['http://localhost:
|
|
303
|
-
|
|
303
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/invalidmeta.adata.zarr'];
|
|
304
|
+
// References:
|
|
305
|
+
// - https://vitest.dev/api/expect.html#tothrowerror
|
|
306
|
+
// - https://vitest.dev/api/expect.html#rejects
|
|
307
|
+
await expect(() => generateConfigs(urls))
|
|
308
|
+
.rejects
|
|
309
|
+
.toThrowError('Could not generate config: .zmetadata file is not valid.');
|
|
304
310
|
});
|
|
305
311
|
it('generates config for multiple files correctly', async () => {
|
|
306
312
|
const urls = ['somefile.ome.tif', 'anoterfile.ome.zarr'];
|
|
@@ -431,12 +437,77 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
431
437
|
const config = await generateConfigs(urls);
|
|
432
438
|
expect(config).toEqual(expectedConfig);
|
|
433
439
|
});
|
|
434
|
-
it('
|
|
435
|
-
const urls = ['http://localhost:
|
|
436
|
-
|
|
440
|
+
it('Does the parsing when .zmetadata file not present in folder', async () => {
|
|
441
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/anndata-0.8/anndata-csr.adata.zarr'];
|
|
442
|
+
const expectedName = urls[0].split('/').at(-1);
|
|
443
|
+
const expectedConfig = {
|
|
444
|
+
version: '1.0.15',
|
|
445
|
+
name: 'An automatically generated config. Adjust values and add layout components if needed.',
|
|
446
|
+
description: 'Populate with text relevant to this visualisation.',
|
|
447
|
+
datasets: [
|
|
448
|
+
{
|
|
449
|
+
uid: 'A',
|
|
450
|
+
name: expectedName,
|
|
451
|
+
files: [
|
|
452
|
+
{
|
|
453
|
+
url: urls[0],
|
|
454
|
+
fileType: 'anndata.zarr',
|
|
455
|
+
coordinationValues: {
|
|
456
|
+
obsType: 'cell',
|
|
457
|
+
featureType: 'gene',
|
|
458
|
+
featureValueType: 'expression',
|
|
459
|
+
},
|
|
460
|
+
options: {
|
|
461
|
+
obsEmbedding: [
|
|
462
|
+
{
|
|
463
|
+
path: 'obsm/X_umap',
|
|
464
|
+
embeddingType: 'UMAP',
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
obsFeatureMatrix: {
|
|
468
|
+
path: 'X',
|
|
469
|
+
},
|
|
470
|
+
obsSets: [
|
|
471
|
+
{
|
|
472
|
+
name: 'Cell Type',
|
|
473
|
+
path: [
|
|
474
|
+
'obs/leiden',
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
coordinationSpace: {
|
|
484
|
+
dataset: {
|
|
485
|
+
A: 'A',
|
|
486
|
+
},
|
|
487
|
+
embeddingType: {
|
|
488
|
+
A: 'UMAP',
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
layout: [
|
|
492
|
+
{
|
|
493
|
+
component: 'scatterplot',
|
|
494
|
+
coordinationScopes: {
|
|
495
|
+
dataset: 'A',
|
|
496
|
+
embeddingType: 'A',
|
|
497
|
+
},
|
|
498
|
+
x: 0,
|
|
499
|
+
y: 0,
|
|
500
|
+
w: 12,
|
|
501
|
+
h: 12,
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
initStrategy: 'auto',
|
|
505
|
+
};
|
|
506
|
+
const config = await generateConfigs(urls);
|
|
507
|
+
expect(config).toEqual(expectedConfig);
|
|
437
508
|
});
|
|
438
509
|
it('raises an error when URL with unsupported file format is passed', async () => {
|
|
439
|
-
const urls = ['http://localhost:
|
|
510
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/anndata-0.7/somefile.zarr'];
|
|
440
511
|
await generateConfigs(urls).catch(e => expect(e.message).toContain('This file type is not supported.'));
|
|
441
512
|
});
|
|
442
513
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitessce/config",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
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.0.
|
|
20
|
-
"@vitessce/utils": "3.0.
|
|
19
|
+
"@vitessce/constants-internal": "3.0.1",
|
|
20
|
+
"@vitessce/utils": "3.0.1"
|
|
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": {
|
|
@@ -185,55 +185,118 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
|
|
|
185
185
|
return views;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
async
|
|
189
|
-
|
|
190
|
-
|
|
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.');
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
194
|
+
const obsmKeys = Object.keys(metadataFile.metadata)
|
|
195
|
+
.filter(key => key.startsWith('obsm/X_'))
|
|
196
|
+
.map(key => key.split('/.zarray')[0]);
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
.map(key => key.split('/.zarray')[0]);
|
|
198
|
+
const obsKeysArr = Object.keys(metadataFile.metadata)
|
|
199
|
+
.filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
function uniq(a) {
|
|
202
|
+
return a.sort().filter((item, pos, ary) => !pos || item !== ary[pos - 1]);
|
|
203
|
+
}
|
|
204
|
+
const obsKeys = uniq(obsKeysArr);
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
217
|
|
|
210
|
-
|
|
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
|
+
];
|
|
211
228
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
};
|
|
217
236
|
|
|
218
|
-
|
|
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,
|
|
219
248
|
};
|
|
220
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
|
+
|
|
221
289
|
const metadataExtension = '.zmetadata';
|
|
222
290
|
const url = [this.fileUrl, metadataExtension].join('/');
|
|
223
291
|
return fetch(url).then((response) => {
|
|
224
292
|
if (response.ok) {
|
|
225
|
-
return
|
|
293
|
+
return this.setMetadataSummaryWithZmetadata(response);
|
|
226
294
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const errorMssg = `Could not generate config. File ${metadataExtension} not found in supplied file URL. Check docs for more explanation.`;
|
|
233
|
-
return Promise.reject(new Error(errorMssg));
|
|
234
|
-
}
|
|
235
|
-
return Promise.reject(error);
|
|
236
|
-
});
|
|
295
|
+
if (response.status === 404) {
|
|
296
|
+
return this.setMetadataSummaryWithoutZmetadata();
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Could not generate config: ${response.statusText}`);
|
|
299
|
+
});
|
|
237
300
|
}
|
|
238
301
|
}
|
|
239
302
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
1
2
|
import { generateConfigs } from './VitessceAutoConfig.js';
|
|
2
3
|
|
|
3
4
|
describe('src/VitessceAutoConfig.js', () => {
|
|
@@ -155,7 +156,7 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
it('generates config for Anndata-ZARR file correctly', async () => {
|
|
158
|
-
const urls = ['http://localhost:
|
|
159
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/.anndata.zarr'];
|
|
159
160
|
const expectedName = urls[0].split('/').at(-1);
|
|
160
161
|
const expectedConfig = {
|
|
161
162
|
version: '1.0.15',
|
|
@@ -264,7 +265,7 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
264
265
|
});
|
|
265
266
|
|
|
266
267
|
it('generates empty config for Anndata-ZARR file with empty .zmetadata', async () => {
|
|
267
|
-
const urls = ['http://localhost:
|
|
268
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/emptymeta.h5ad.zarr'];
|
|
268
269
|
const expectedName = urls[0].split('/').at(-1);
|
|
269
270
|
const expectedConfig = {
|
|
270
271
|
version: '1.0.15',
|
|
@@ -307,11 +308,14 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
307
308
|
});
|
|
308
309
|
|
|
309
310
|
it('raises an error for Anndata-ZARR file with misconfigured .zmetadata', async () => {
|
|
310
|
-
const urls = ['http://localhost:
|
|
311
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/partials/invalidmeta.adata.zarr'];
|
|
311
312
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
// References:
|
|
314
|
+
// - https://vitest.dev/api/expect.html#tothrowerror
|
|
315
|
+
// - https://vitest.dev/api/expect.html#rejects
|
|
316
|
+
await expect(() => generateConfigs(urls))
|
|
317
|
+
.rejects
|
|
318
|
+
.toThrowError('Could not generate config: .zmetadata file is not valid.');
|
|
315
319
|
});
|
|
316
320
|
|
|
317
321
|
it('generates config for multiple files correctly', async () => {
|
|
@@ -447,16 +451,79 @@ describe('src/VitessceAutoConfig.js', () => {
|
|
|
447
451
|
});
|
|
448
452
|
|
|
449
453
|
|
|
450
|
-
it('
|
|
451
|
-
const urls = ['http://localhost:
|
|
454
|
+
it('Does the parsing when .zmetadata file not present in folder', async () => {
|
|
455
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/anndata-0.8/anndata-csr.adata.zarr'];
|
|
456
|
+
const expectedName = urls[0].split('/').at(-1);
|
|
457
|
+
const expectedConfig = {
|
|
458
|
+
version: '1.0.15',
|
|
459
|
+
name: 'An automatically generated config. Adjust values and add layout components if needed.',
|
|
460
|
+
description: 'Populate with text relevant to this visualisation.',
|
|
461
|
+
datasets: [
|
|
462
|
+
{
|
|
463
|
+
uid: 'A',
|
|
464
|
+
name: expectedName,
|
|
465
|
+
files: [
|
|
466
|
+
{
|
|
467
|
+
url: urls[0],
|
|
468
|
+
fileType: 'anndata.zarr',
|
|
469
|
+
coordinationValues: {
|
|
470
|
+
obsType: 'cell',
|
|
471
|
+
featureType: 'gene',
|
|
472
|
+
featureValueType: 'expression',
|
|
473
|
+
},
|
|
474
|
+
options: {
|
|
475
|
+
obsEmbedding: [
|
|
476
|
+
{
|
|
477
|
+
path: 'obsm/X_umap',
|
|
478
|
+
embeddingType: 'UMAP',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
obsFeatureMatrix: {
|
|
482
|
+
path: 'X',
|
|
483
|
+
},
|
|
484
|
+
obsSets: [
|
|
485
|
+
{
|
|
486
|
+
name: 'Cell Type',
|
|
487
|
+
path: [
|
|
488
|
+
'obs/leiden',
|
|
489
|
+
],
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
coordinationSpace: {
|
|
498
|
+
dataset: {
|
|
499
|
+
A: 'A',
|
|
500
|
+
},
|
|
501
|
+
embeddingType: {
|
|
502
|
+
A: 'UMAP',
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
layout: [
|
|
506
|
+
{
|
|
507
|
+
component: 'scatterplot',
|
|
508
|
+
coordinationScopes: {
|
|
509
|
+
dataset: 'A',
|
|
510
|
+
embeddingType: 'A',
|
|
511
|
+
},
|
|
512
|
+
x: 0,
|
|
513
|
+
y: 0,
|
|
514
|
+
w: 12,
|
|
515
|
+
h: 12,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
initStrategy: 'auto',
|
|
519
|
+
};
|
|
452
520
|
|
|
453
|
-
await generateConfigs(urls)
|
|
454
|
-
|
|
455
|
-
);
|
|
521
|
+
const config = await generateConfigs(urls);
|
|
522
|
+
expect(config).toEqual(expectedConfig);
|
|
456
523
|
});
|
|
457
524
|
|
|
458
525
|
it('raises an error when URL with unsupported file format is passed', async () => {
|
|
459
|
-
const urls = ['http://localhost:
|
|
526
|
+
const urls = ['http://localhost:4204/@fixtures/zarr/anndata-0.7/somefile.zarr'];
|
|
460
527
|
|
|
461
528
|
await generateConfigs(urls).catch(
|
|
462
529
|
e => expect(e.message).toContain('This file type is not supported.'),
|