@vitessce/config 3.0.1 → 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.1",
3
+ "version": "3.1.0",
4
4
  "author": "Gehlenborg Lab",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -16,8 +16,8 @@
16
16
  "dist-tsc"
17
17
  ],
18
18
  "dependencies": {
19
- "@vitessce/constants-internal": "3.0.1",
20
- "@vitessce/utils": "3.0.1"
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",
@@ -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,41 +188,46 @@ 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
 
@@ -224,6 +270,7 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
224
270
  '/obsm/X_segmentations/.zarray',
225
271
  '/obs/.zattrs',
226
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
227
274
  ];
228
275
 
229
276
  const getObsmKey = (url) => {
@@ -296,6 +343,8 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
296
343
  return this.setMetadataSummaryWithoutZmetadata();
297
344
  }
298
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}`);
299
348
  });
300
349
  }
301
350
  }
@@ -304,27 +353,20 @@ const configClasses = [
304
353
  {
305
354
  extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
306
355
  class: OmeTiffAutoConfig,
356
+ name: 'OME-TIFF',
307
357
  },
308
358
  {
309
359
  extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
310
360
  class: AnndataZarrAutoConfig,
361
+ name: 'AnnData-Zarr',
311
362
  },
312
363
  {
313
364
  extensions: ['ome.zarr'],
314
365
  class: OmeZarrAutoConfig,
366
+ name: 'OME-Zarr',
315
367
  },
316
368
  ];
317
369
 
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
370
  function calculateCoordinates(viewsNumb) {
329
371
  const rows = Math.ceil(Math.sqrt(viewsNumb));
330
372
  const cols = Math.ceil(viewsNumb / rows);
@@ -337,16 +379,142 @@ function calculateCoordinates(viewsNumb) {
337
379
  const col = i % cols;
338
380
  const x = col * width;
339
381
  const y = row * height;
340
- 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
+ ]);
341
391
  }
342
392
 
343
393
  return coords;
344
394
  }
345
395
 
346
- 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) {
347
515
  let ConfigClassName;
348
516
  try {
349
- ConfigClassName = getFileType(url);
517
+ ConfigClassName = getFileType(url).class;
350
518
  } catch (err) {
351
519
  return Promise.reject(err);
352
520
  }
@@ -356,19 +524,16 @@ async function generateConfig(url, vc) {
356
524
  let viewsConfig;
357
525
  try {
358
526
  fileConfig = await configInstance.composeFileConfig();
359
- viewsConfig = await configInstance.composeViewsConfig();
527
+ viewsConfig = await configInstance.composeViewsConfig(hintsConfig);
360
528
  } catch (error) {
361
529
  console.error(error);
362
530
  return Promise.reject(error);
363
531
  }
364
532
 
365
- const dataset = vc
366
- .addDataset(configInstance.fileName)
367
- .addFile(fileConfig);
533
+ dataset.addFile(fileConfig);
368
534
 
369
535
  let layerControllerView = false;
370
536
  let spatialView = false;
371
-
372
537
  const views = [];
373
538
 
374
539
  viewsConfig.forEach((v) => {
@@ -381,7 +546,7 @@ async function generateConfig(url, vc) {
381
546
  }
382
547
  // this piece of code can be removed once these props are added by default to layerController
383
548
  // see this issue: https://github.com/vitessce/vitessce/issues/1454
384
- if (v[0] === 'layerController' && configInstance instanceof OmeTiffAutoConfig) {
549
+ if (v[0] === 'layerController') {
385
550
  view.setProps({
386
551
  disable3d: [],
387
552
  disableChannelsIfRgbDetected: true,
@@ -396,29 +561,48 @@ async function generateConfig(url, vc) {
396
561
  });
397
562
 
398
563
  if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
399
- const spatialSegmentationLayerValue = {
400
- opacity: 1,
401
- radius: 0,
402
- visible: true,
403
- stroked: false,
404
- };
405
-
406
564
  vc.linkViews(
407
565
  [spatialView, layerControllerView],
408
566
  [
409
- CoordinationType.SPATIAL_ZOOM,
410
- CoordinationType.SPATIAL_TARGET_X,
411
- CoordinationType.SPATIAL_TARGET_Y,
412
- CoordinationType.SPATIAL_SEGMENTATION_LAYER,
567
+ ct.SPATIAL_SEGMENTATION_LAYER,
413
568
  ],
414
- [-5.5, 16000, 20000, spatialSegmentationLayerValue],
569
+ [spatialSegmentationLayerValue],
415
570
  );
416
571
  }
417
572
 
418
573
  return views;
419
574
  }
420
575
 
421
- 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) {
422
606
  const vc = new VitessceConfig({
423
607
  schemaVersion: '1.0.15',
424
608
  name: 'An automatically generated config. Adjust values and add layout components if needed.',
@@ -427,19 +611,41 @@ export async function generateConfigs(fileUrls) {
427
611
 
428
612
  const allViews = [];
429
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
+
430
623
  fileUrls.forEach((url) => {
431
- allViews.push(generateConfig(url, vc));
624
+ allViews.push(generateViewDefinition(url, vc, dataset, hintsConfig));
432
625
  });
433
626
 
434
627
  return Promise.all(allViews).then((views) => {
435
628
  const flattenedViews = views.flat();
436
629
 
437
- const coord = calculateCoordinates(flattenedViews.length);
438
-
439
- for (let i = 0; i < flattenedViews.length; i++) {
440
- 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);
441
636
  }
442
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
+ }
443
649
  return vc.toJSON();
444
650
  });
445
651
  }