@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/config",
3
- "version": "3.0.0",
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.0.0",
20
- "@vitessce/utils": "3.0.0"
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 -r ../../ --dir ."
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
- async composeViewsConfig() { /* eslint-disable-line class-methods-use-this */
26
- return [
27
- ['description'],
28
- ['spatial'],
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
- ['description'],
65
- ['spatial'],
66
- ['layerController'],
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') || key.toLowerCase().includes('cell_type'));
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
- views.push(['obsSets']);
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
- views.push(['scatterplot', { mapping: 'UMAP' }]);
206
+ possibleViews.push(['scatterplot', { mapping: 'UMAP' }]);
165
207
  }
166
208
  if (key.toLowerCase().includes('obsm/x_tsne')) {
167
- views.push(['scatterplot', { mapping: 't-SNE' }]);
209
+ possibleViews.push(['scatterplot', { mapping: 't-SNE' }]);
168
210
  }
169
211
  if (key.toLowerCase().includes('obsm/x_pca')) {
170
- views.push(['scatterplot', { mapping: 'PCA' }]);
212
+ possibleViews.push(['scatterplot', { mapping: 'PCA' }]);
171
213
  }
172
214
  if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
173
- views.push(['layerController']);
215
+ possibleViews.push(['layerController']);
174
216
  }
175
217
  if (key.toLowerCase().includes(('obsm/x_spatial'))) {
176
- views.push(['spatial']);
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
- views.push(['heatmap']);
182
- views.push(['featureList']);
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 setMetadataSummary() {
189
- if (Object.keys(this.metadataSummary).length > 0) {
190
- return this.metadataSummary;
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 parseMetadataFile = (metadataFile) => {
194
- if (!metadataFile.metadata) {
195
- throw new Error('Could not generate config: .zmetadata file is not valid.');
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
- const obsmKeys = Object.keys(metadataFile.metadata)
199
- .filter(key => key.startsWith('obsm/X_'))
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
- const obsKeysArr = Object.keys(metadataFile.metadata)
203
- .filter(key => key.startsWith('obs/')).map(key => key.split('/.za')[0]);
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
- function uniq(a) {
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
- const X = Object.keys(metadataFile.metadata).filter(key => key.startsWith('X'));
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
- const out = {
213
- obsm: obsmKeys,
214
- obs: obsKeys,
215
- X: X.length > 0,
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
- return out;
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 response.json();
340
+ return this.setMetadataSummaryWithZmetadata(response);
226
341
  }
227
- return Promise.reject(response);
228
- })
229
- .then(responseJson => parseMetadataFile(responseJson))
230
- .catch((error) => {
231
- if (error.status === 404) {
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
- });
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
- coords.push([x, y, width, height]);
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
- async function generateConfig(url, vc) {
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
- const dataset = vc
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' && configInstance instanceof OmeTiffAutoConfig) {
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
- CoordinationType.SPATIAL_ZOOM,
347
- CoordinationType.SPATIAL_TARGET_X,
348
- CoordinationType.SPATIAL_TARGET_Y,
349
- CoordinationType.SPATIAL_SEGMENTATION_LAYER,
567
+ ct.SPATIAL_SEGMENTATION_LAYER,
350
568
  ],
351
- [-5.5, 16000, 20000, spatialSegmentationLayerValue],
569
+ [spatialSegmentationLayerValue],
352
570
  );
353
571
  }
354
572
 
355
573
  return views;
356
574
  }
357
575
 
358
- export async function generateConfigs(fileUrls) {
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(generateConfig(url, vc));
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
- const coord = calculateCoordinates(flattenedViews.length);
375
-
376
- for (let i = 0; i < flattenedViews.length; i++) {
377
- flattenedViews[i].setXYWH(...coord[i]);
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
  }