@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.
@@ -0,0 +1,2 @@
1
+ export function generateConfigs(fileUrls: any): Promise<object>;
2
+ //# sourceMappingURL=VitessceAutoConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VitessceAutoConfig.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.js"],"names":[],"mappings":"AAoaA,gEAwBC"}
@@ -0,0 +1,378 @@
1
+ import { CoordinationType, FileType } from '@vitessce/constants-internal';
2
+ import { VitessceConfig, } from './VitessceConfig.js';
3
+ class AbstractAutoConfig {
4
+ async composeViewsConfig() {
5
+ throw new Error('The composeViewsConfig() method has not been implemented.');
6
+ }
7
+ async composeFileConfig() {
8
+ throw new Error('The composeFileConfig() method has not been implemented.');
9
+ }
10
+ }
11
+ class OmeTiffAutoConfig extends AbstractAutoConfig {
12
+ constructor(fileUrl) {
13
+ super();
14
+ this.fileUrl = fileUrl;
15
+ this.fileType = FileType.RASTER_JSON;
16
+ this.fileName = fileUrl.split('/').at(-1);
17
+ }
18
+ async composeViewsConfig() {
19
+ return [
20
+ ['description'],
21
+ ['spatial'],
22
+ ['layerController'],
23
+ ];
24
+ }
25
+ async composeFileConfig() {
26
+ return {
27
+ fileType: this.fileType,
28
+ options: {
29
+ images: [
30
+ {
31
+ metadata: {
32
+ isBitmask: false,
33
+ },
34
+ name: this.fileName,
35
+ type: 'ome-tiff',
36
+ url: this.fileUrl,
37
+ },
38
+ ],
39
+ schemaVersion: '0.0.2',
40
+ usePhysicalSizeScaling: false,
41
+ },
42
+ };
43
+ }
44
+ }
45
+ class OmeZarrAutoConfig extends AbstractAutoConfig {
46
+ constructor(fileUrl) {
47
+ super();
48
+ this.fileUrl = fileUrl;
49
+ this.fileType = FileType.RASTER_OME_ZARR;
50
+ this.fileName = fileUrl.split('/').at(-1);
51
+ }
52
+ async composeViewsConfig() {
53
+ return [
54
+ ['description'],
55
+ ['spatial'],
56
+ ['layerController'],
57
+ ['status'],
58
+ ];
59
+ }
60
+ async composeFileConfig() {
61
+ return {
62
+ fileType: this.fileType,
63
+ type: 'raster',
64
+ url: this.fileUrl,
65
+ };
66
+ }
67
+ }
68
+ class AnndataZarrAutoConfig extends AbstractAutoConfig {
69
+ constructor(fileUrl) {
70
+ super();
71
+ this.fileUrl = fileUrl;
72
+ this.fileType = FileType.ANNDATA_ZARR;
73
+ this.fileName = fileUrl.split('/').at(-1);
74
+ this.metadataSummary = {};
75
+ }
76
+ async composeFileConfig() {
77
+ this.metadataSummary = await this.setMetadataSummary();
78
+ const options = {
79
+ obsEmbedding: [],
80
+ obsFeatureMatrix: {
81
+ path: 'X',
82
+ },
83
+ };
84
+ this.metadataSummary.obsm.forEach((key) => {
85
+ if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
86
+ options.obsSegmentations = { path: key };
87
+ }
88
+ if (key.toLowerCase().includes(('obsm/x_spatial'))) {
89
+ options.obsLocations = { path: key };
90
+ }
91
+ if (key.toLowerCase().includes('obsm/x_umap')) {
92
+ options.obsEmbedding.push({ path: key, embeddingType: 'UMAP' });
93
+ }
94
+ if (key.toLowerCase().includes('obsm/x_tsne')) {
95
+ options.obsEmbedding.push({ path: key, embeddingType: 't-SNE' });
96
+ }
97
+ if (key.toLowerCase().includes('obsm/x_pca')) {
98
+ options.obsEmbedding.push({ path: key, embeddingType: 'PCA' });
99
+ }
100
+ });
101
+ const supportedObsSetsKeys = [
102
+ 'cluster', 'subcluster', 'cell_type', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
103
+ ];
104
+ this.metadataSummary.obs.forEach((key) => {
105
+ supportedObsSetsKeys.forEach((supportedKey) => {
106
+ if (key.toLowerCase() === ['obs', supportedKey].join('/')) {
107
+ if (!('obsSets' in options)) {
108
+ options.obsSets = [
109
+ {
110
+ name: 'Cell Type',
111
+ path: [key],
112
+ },
113
+ ];
114
+ }
115
+ else {
116
+ options.obsSets[0].path.push(key);
117
+ }
118
+ }
119
+ });
120
+ });
121
+ return {
122
+ options,
123
+ fileType: this.fileType,
124
+ url: this.fileUrl,
125
+ coordinationValues: {
126
+ obsType: 'cell',
127
+ featureType: 'gene',
128
+ featureValueType: 'expression',
129
+ },
130
+ };
131
+ }
132
+ async composeViewsConfig() {
133
+ this.metadataSummary = await this.setMetadataSummary();
134
+ const views = [];
135
+ const hasCellSetData = this.metadataSummary.obs
136
+ .filter(key => key.toLowerCase().includes('cluster') || key.toLowerCase().includes('cell_type'));
137
+ if (hasCellSetData.length > 0) {
138
+ views.push(['obsSets']);
139
+ }
140
+ this.metadataSummary.obsm.forEach((key) => {
141
+ if (key.toLowerCase().includes('obsm/x_umap')) {
142
+ views.push(['scatterplot', { mapping: 'UMAP' }]);
143
+ }
144
+ if (key.toLowerCase().includes('obsm/x_tsne')) {
145
+ views.push(['scatterplot', { mapping: 't-SNE' }]);
146
+ }
147
+ if (key.toLowerCase().includes('obsm/x_pca')) {
148
+ views.push(['scatterplot', { mapping: 'PCA' }]);
149
+ }
150
+ if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
151
+ views.push(['layerController']);
152
+ }
153
+ if (key.toLowerCase().includes(('obsm/x_spatial'))) {
154
+ views.push(['spatial']);
155
+ }
156
+ });
157
+ if (this.metadataSummary.X) {
158
+ views.push(['heatmap']);
159
+ views.push(['featureList']);
160
+ }
161
+ return views;
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
+ }
244
+ async setMetadataSummary() {
245
+ if (Object.keys(this.metadataSummary).length > 0) {
246
+ return this.metadataSummary;
247
+ }
248
+ const metadataExtension = '.zmetadata';
249
+ const url = [this.fileUrl, metadataExtension].join('/');
250
+ return fetch(url).then((response) => {
251
+ if (response.ok) {
252
+ return this.setMetadataSummaryWithZmetadata(response);
253
+ }
254
+ if (response.status === 404) {
255
+ return this.setMetadataSummaryWithoutZmetadata();
256
+ }
257
+ throw new Error(`Could not generate config: ${response.statusText}`);
258
+ });
259
+ }
260
+ }
261
+ const configClasses = [
262
+ {
263
+ extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
264
+ class: OmeTiffAutoConfig,
265
+ },
266
+ {
267
+ extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
268
+ class: AnndataZarrAutoConfig,
269
+ },
270
+ {
271
+ extensions: ['ome.zarr'],
272
+ class: OmeZarrAutoConfig,
273
+ },
274
+ ];
275
+ function getFileType(url) {
276
+ const match = configClasses.find(obj => obj.extensions.filter(ext => url.endsWith(ext)).length === 1);
277
+ if (!match) {
278
+ throw new Error(`Could not generate config for URL: ${url}. This file type is not supported.`);
279
+ }
280
+ return match.class;
281
+ }
282
+ function calculateCoordinates(viewsNumb) {
283
+ const rows = Math.ceil(Math.sqrt(viewsNumb));
284
+ const cols = Math.ceil(viewsNumb / rows);
285
+ const width = 12 / cols;
286
+ const height = 12 / rows;
287
+ const coords = [];
288
+ for (let i = 0; i < viewsNumb; i++) {
289
+ const row = Math.floor(i / cols);
290
+ const col = i % cols;
291
+ const x = col * width;
292
+ const y = row * height;
293
+ coords.push([x, y, width, height]);
294
+ }
295
+ return coords;
296
+ }
297
+ async function generateConfig(url, vc) {
298
+ let ConfigClassName;
299
+ try {
300
+ ConfigClassName = getFileType(url);
301
+ }
302
+ catch (err) {
303
+ return Promise.reject(err);
304
+ }
305
+ const configInstance = new ConfigClassName(url);
306
+ let fileConfig;
307
+ let viewsConfig;
308
+ try {
309
+ fileConfig = await configInstance.composeFileConfig();
310
+ viewsConfig = await configInstance.composeViewsConfig();
311
+ }
312
+ catch (error) {
313
+ console.error(error);
314
+ return Promise.reject(error);
315
+ }
316
+ const dataset = vc
317
+ .addDataset(configInstance.fileName)
318
+ .addFile(fileConfig);
319
+ let layerControllerView = false;
320
+ let spatialView = false;
321
+ const views = [];
322
+ viewsConfig.forEach((v) => {
323
+ const view = vc.addView(dataset, ...v);
324
+ if (v[0] === 'layerController') {
325
+ layerControllerView = view;
326
+ }
327
+ if (v[0] === 'spatial') {
328
+ spatialView = view;
329
+ }
330
+ // this piece of code can be removed once these props are added by default to layerController
331
+ // see this issue: https://github.com/vitessce/vitessce/issues/1454
332
+ if (v[0] === 'layerController' && configInstance instanceof OmeTiffAutoConfig) {
333
+ view.setProps({
334
+ disable3d: [],
335
+ disableChannelsIfRgbDetected: true,
336
+ });
337
+ }
338
+ // transpose the heatmap by default
339
+ if (v[0] === 'heatmap' && configInstance instanceof AnndataZarrAutoConfig) {
340
+ view.setProps({ transpose: true });
341
+ }
342
+ views.push(view);
343
+ });
344
+ if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
345
+ const spatialSegmentationLayerValue = {
346
+ opacity: 1,
347
+ radius: 0,
348
+ visible: true,
349
+ stroked: false,
350
+ };
351
+ vc.linkViews([spatialView, layerControllerView], [
352
+ CoordinationType.SPATIAL_ZOOM,
353
+ CoordinationType.SPATIAL_TARGET_X,
354
+ CoordinationType.SPATIAL_TARGET_Y,
355
+ CoordinationType.SPATIAL_SEGMENTATION_LAYER,
356
+ ], [-5.5, 16000, 20000, spatialSegmentationLayerValue]);
357
+ }
358
+ return views;
359
+ }
360
+ export async function generateConfigs(fileUrls) {
361
+ const vc = new VitessceConfig({
362
+ schemaVersion: '1.0.15',
363
+ name: 'An automatically generated config. Adjust values and add layout components if needed.',
364
+ description: 'Populate with text relevant to this visualisation.',
365
+ });
366
+ const allViews = [];
367
+ fileUrls.forEach((url) => {
368
+ allViews.push(generateConfig(url, vc));
369
+ });
370
+ return Promise.all(allViews).then((views) => {
371
+ const flattenedViews = views.flat();
372
+ const coord = calculateCoordinates(flattenedViews.length);
373
+ for (let i = 0; i < flattenedViews.length; i++) {
374
+ flattenedViews[i].setXYWH(...coord[i]);
375
+ }
376
+ return vc.toJSON();
377
+ });
378
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=VitessceAutoConfig.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VitessceAutoConfig.test.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.test.js"],"names":[],"mappings":""}