@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.
@@ -1,2 +1,14 @@
1
- export function generateConfigs(fileUrls: any): Promise<object>;
1
+ /**
2
+ * Returns the hints that are available for the given file URLs, depending on their types.
3
+ * @param {Array} fileUrls containing urls of files to be loaded into Vitessce
4
+ * @returns the hints available for these file URLs
5
+ */
6
+ export function getHintOptions(fileUrls: any[]): any;
7
+ /**
8
+ *
9
+ * @param {Array} fileUrls containing urls of files to be loaded into Vitessce
10
+ * @param {String} the hints config to be used for the dataset. Null by default
11
+ * @returns ViewConfig as JSON
12
+ */
13
+ export function generateConfig(fileUrls: any[], hintTitle?: null): Promise<object>;
2
14
  //# sourceMappingURL=VitessceAutoConfig.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VitessceAutoConfig.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.js"],"names":[],"mappings":"AAoaA,gEAwBC"}
1
+ {"version":3,"file":"VitessceAutoConfig.d.ts","sourceRoot":"","sources":["../src/VitessceAutoConfig.js"],"names":[],"mappings":"AA+jBA;;;;GAIG;AACH,qDAgBC;AAED;;;;;GAKG;AACH,mFA8CC"}
@@ -1,5 +1,32 @@
1
- import { CoordinationType, FileType } from '@vitessce/constants-internal';
1
+ import { CoordinationType as ct, FileType } from '@vitessce/constants-internal';
2
2
  import { VitessceConfig, } from './VitessceConfig.js';
3
+ import { HINTS_CONFIG, HINT_TYPE_TO_FILE_TYPE_MAP } from './constants.js';
4
+ /**
5
+ * @param {Object} hintsConfig. The hints config for the given dataset.
6
+ *
7
+ * @param {Array} possibleViews. All views for a given file type, supported by the dataset.
8
+ * The array contains arrays of strings, each of size >= 1, where the first element of each string
9
+ * is a VitessceConfig view name ('description', 'spatial', 'layerController', etc.)
10
+ *
11
+ * @returns {Array of strings} the intersection of VitessceConfig view names in
12
+ * possibleViews and requiredViews.
13
+ */
14
+ const filterViews = (hintsConfig, possibleViews) => {
15
+ const requiredViews = Object.keys(hintsConfig.views);
16
+ if (requiredViews.length === 0) {
17
+ return possibleViews;
18
+ }
19
+ const resultViews = [];
20
+ requiredViews.forEach((requiredView) => {
21
+ const match = possibleViews.find(possibleView => possibleView[0] === requiredView);
22
+ if (match)
23
+ resultViews.push(match);
24
+ });
25
+ if (resultViews.length === 0) {
26
+ throw new Error('No views found that are compatible with the supplied dataset URLs and hint.');
27
+ }
28
+ return resultViews;
29
+ };
3
30
  class AbstractAutoConfig {
4
31
  async composeViewsConfig() {
5
32
  throw new Error('The composeViewsConfig() method has not been implemented.');
@@ -15,12 +42,8 @@ class OmeTiffAutoConfig extends AbstractAutoConfig {
15
42
  this.fileType = FileType.RASTER_JSON;
16
43
  this.fileName = fileUrl.split('/').at(-1);
17
44
  }
18
- async composeViewsConfig() {
19
- return [
20
- ['description'],
21
- ['spatial'],
22
- ['layerController'],
23
- ];
45
+ async composeViewsConfig(hintsConfig) {
46
+ return filterViews(hintsConfig, [['description'], ['spatial'], ['layerController']]);
24
47
  }
25
48
  async composeFileConfig() {
26
49
  return {
@@ -49,13 +72,8 @@ class OmeZarrAutoConfig extends AbstractAutoConfig {
49
72
  this.fileType = FileType.RASTER_OME_ZARR;
50
73
  this.fileName = fileUrl.split('/').at(-1);
51
74
  }
52
- async composeViewsConfig() {
53
- return [
54
- ['description'],
55
- ['spatial'],
56
- ['layerController'],
57
- ['status'],
58
- ];
75
+ async composeViewsConfig(hintsConfig) {
76
+ return filterViews(hintsConfig, [['description'], ['spatial'], ['layerController']]);
59
77
  }
60
78
  async composeFileConfig() {
61
79
  return {
@@ -99,7 +117,7 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
99
117
  }
100
118
  });
101
119
  const supportedObsSetsKeys = [
102
- 'cluster', 'subcluster', 'cell_type', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
120
+ 'cluster', 'clusters', 'subcluster', 'cell_type', 'celltype', 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity', 'tissue', 'sex',
103
121
  ];
104
122
  this.metadataSummary.obs.forEach((key) => {
105
123
  supportedObsSetsKeys.forEach((supportedKey) => {
@@ -118,6 +136,18 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
118
136
  }
119
137
  });
120
138
  });
139
+ // if length of path is 1, storing the value as an array doesn't work
140
+ // es-lint-disable-next-line max-len
141
+ // Example: https://s3.amazonaws.com/vitessce-data/0.0.33/main/human-lymph-node-10x-visium/human_lymph_node_10x_visium.h5ad.zarr
142
+ options.obsSets = options.obsSets?.map((obsSet) => {
143
+ if (obsSet.path.length === 1) {
144
+ return {
145
+ ...obsSet,
146
+ path: obsSet.path[0],
147
+ };
148
+ }
149
+ return obsSet;
150
+ });
121
151
  return {
122
152
  options,
123
153
  fileType: this.fileType,
@@ -129,35 +159,40 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
129
159
  },
130
160
  };
131
161
  }
132
- async composeViewsConfig() {
162
+ async composeViewsConfig(hintsConfig) {
133
163
  this.metadataSummary = await this.setMetadataSummary();
134
- const views = [];
164
+ const possibleViews = [];
135
165
  const hasCellSetData = this.metadataSummary.obs
136
- .filter(key => key.toLowerCase().includes('cluster') || key.toLowerCase().includes('cell_type'));
166
+ .filter(key => key.toLowerCase().includes('cluster')
167
+ || key.toLowerCase().includes('cell_type')
168
+ || key.toLowerCase().includes('celltype'));
137
169
  if (hasCellSetData.length > 0) {
138
- views.push(['obsSets']);
170
+ possibleViews.push(['obsSets']);
139
171
  }
140
172
  this.metadataSummary.obsm.forEach((key) => {
141
173
  if (key.toLowerCase().includes('obsm/x_umap')) {
142
- views.push(['scatterplot', { mapping: 'UMAP' }]);
174
+ possibleViews.push(['scatterplot', { mapping: 'UMAP' }]);
143
175
  }
144
176
  if (key.toLowerCase().includes('obsm/x_tsne')) {
145
- views.push(['scatterplot', { mapping: 't-SNE' }]);
177
+ possibleViews.push(['scatterplot', { mapping: 't-SNE' }]);
146
178
  }
147
179
  if (key.toLowerCase().includes('obsm/x_pca')) {
148
- views.push(['scatterplot', { mapping: 'PCA' }]);
180
+ possibleViews.push(['scatterplot', { mapping: 'PCA' }]);
149
181
  }
150
182
  if (key.toLowerCase().includes(('obsm/x_segmentations'))) {
151
- views.push(['layerController']);
183
+ possibleViews.push(['layerController']);
152
184
  }
153
185
  if (key.toLowerCase().includes(('obsm/x_spatial'))) {
154
- views.push(['spatial']);
186
+ possibleViews.push(['spatial']);
155
187
  }
156
188
  });
189
+ possibleViews.push(['obsSetSizes']);
190
+ possibleViews.push(['obsSetFeatureValueDistribution']);
157
191
  if (this.metadataSummary.X) {
158
- views.push(['heatmap']);
159
- views.push(['featureList']);
192
+ possibleViews.push(['heatmap']);
193
+ possibleViews.push(['featureList']);
160
194
  }
195
+ const views = filterViews(hintsConfig, possibleViews);
161
196
  return views;
162
197
  }
163
198
  async setMetadataSummaryWithZmetadata(response) {
@@ -193,6 +228,7 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
193
228
  '/obsm/X_segmentations/.zarray',
194
229
  '/obs/.zattrs',
195
230
  '/X/.zarray',
231
+ '/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
196
232
  ];
197
233
  const getObsmKey = (url) => {
198
234
  // Get the substring "X_pca" from a URL like
@@ -255,6 +291,8 @@ class AnndataZarrAutoConfig extends AbstractAutoConfig {
255
291
  return this.setMetadataSummaryWithoutZmetadata();
256
292
  }
257
293
  throw new Error(`Could not generate config: ${response.statusText}`);
294
+ }).catch((error) => {
295
+ throw new Error(`Could not generate config for URL ${this.fileUrl}: ${error}`);
258
296
  });
259
297
  }
260
298
  }
@@ -262,23 +300,19 @@ const configClasses = [
262
300
  {
263
301
  extensions: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
264
302
  class: OmeTiffAutoConfig,
303
+ name: 'OME-TIFF',
265
304
  },
266
305
  {
267
306
  extensions: ['.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
268
307
  class: AnndataZarrAutoConfig,
308
+ name: 'AnnData-Zarr',
269
309
  },
270
310
  {
271
311
  extensions: ['ome.zarr'],
272
312
  class: OmeZarrAutoConfig,
313
+ name: 'OME-Zarr',
273
314
  },
274
315
  ];
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
316
  function calculateCoordinates(viewsNumb) {
283
317
  const rows = Math.ceil(Math.sqrt(viewsNumb));
284
318
  const cols = Math.ceil(viewsNumb / rows);
@@ -290,14 +324,120 @@ function calculateCoordinates(viewsNumb) {
290
324
  const col = i % cols;
291
325
  const x = col * width;
292
326
  const y = row * height;
293
- coords.push([x, y, width, height]);
327
+ // The coordinates have to be integer values:
328
+ coords.push([
329
+ Math.floor(x),
330
+ Math.floor(y),
331
+ // Ensure width/height is at least 1.
332
+ Math.max(1, Math.floor(width)),
333
+ Math.max(1, Math.floor(height)),
334
+ ]);
294
335
  }
295
336
  return coords;
296
337
  }
297
- async function generateConfig(url, vc) {
338
+ const spatialSegmentationLayerValue = {
339
+ radius: 65,
340
+ stroked: true,
341
+ visible: true,
342
+ opacity: 1,
343
+ };
344
+ /**
345
+ * Inserts the spatial coordination space into the VitessceConfig views.
346
+ * @param {Array of VitessceConfigView} of views
347
+ * @param {VitessceConfig} vc instance
348
+ */
349
+ function insertCoordinationSpaceForSpatial(views, vc) {
350
+ const [spatialSegmentationLayer, spatialImageLayer, spatialZoom, spatialTargetX, spatialTargetY,] = vc.addCoordination(ct.SPATIAL_SEGMENTATION_LAYER, ct.SPATIAL_IMAGE_LAYER, ct.SPATIAL_ZOOM, ct.SPATIAL_TARGET_X, ct.SPATIAL_TARGET_Y);
351
+ // Note: this always assumes the segmentation is polygon-based.
352
+ // In the future, we may want to support both polygon-based and bitmask-based.
353
+ spatialSegmentationLayer.setValue(spatialSegmentationLayerValue);
354
+ // Note: this always assumes the image is RGB.
355
+ // In the future, we may want to support both RGB and multi-channel.
356
+ spatialImageLayer.setValue([
357
+ {
358
+ type: 'raster',
359
+ index: 0,
360
+ colormap: null,
361
+ transparentColor: null,
362
+ opacity: 1,
363
+ domainType: 'Min/Max',
364
+ channels: [
365
+ {
366
+ selection: {
367
+ c: 0,
368
+ },
369
+ color: [
370
+ 255,
371
+ 0,
372
+ 0,
373
+ ],
374
+ visible: true,
375
+ slider: [
376
+ 0,
377
+ 255,
378
+ ],
379
+ },
380
+ {
381
+ selection: {
382
+ c: 1,
383
+ },
384
+ color: [
385
+ 0,
386
+ 255,
387
+ 0,
388
+ ],
389
+ visible: true,
390
+ slider: [
391
+ 0,
392
+ 255,
393
+ ],
394
+ },
395
+ {
396
+ selection: {
397
+ c: 2,
398
+ },
399
+ color: [
400
+ 0,
401
+ 0,
402
+ 255,
403
+ ],
404
+ visible: true,
405
+ slider: [
406
+ 0,
407
+ 255,
408
+ ],
409
+ },
410
+ ],
411
+ },
412
+ ]);
413
+ views.forEach((view) => {
414
+ if (view.view.component === 'spatial' || view.view.component === 'layerController') {
415
+ view.useCoordination(spatialImageLayer);
416
+ view.useCoordination(spatialSegmentationLayer);
417
+ view.useCoordination(spatialZoom);
418
+ view.useCoordination(spatialTargetX);
419
+ view.useCoordination(spatialTargetY);
420
+ }
421
+ });
422
+ }
423
+ /**
424
+ * Returns the type of the file, based on the file extension.
425
+ * @param {string} url of the file.
426
+ * @returns {object} An element from the `configClasses` array, which will be an object with
427
+ * the properties `extensions: string[]`, `class` (an AutoConfig class definition) and
428
+ * `name`: string.
429
+ */
430
+ function getFileType(url) {
431
+ const match = configClasses.find(obj => obj.extensions.filter(ext => url.endsWith(ext)).length === 1);
432
+ if (!match) {
433
+ throw new Error('One or more of the URLs provided point to unsupported file types.');
434
+ }
435
+ return match;
436
+ }
437
+ async function generateViewDefinition(url, vc, dataset, hintsConfig) {
298
438
  let ConfigClassName;
299
439
  try {
300
- ConfigClassName = getFileType(url);
440
+ ConfigClassName = getFileType(url).class;
301
441
  }
302
442
  catch (err) {
303
443
  return Promise.reject(err);
@@ -307,15 +447,13 @@ async function generateConfig(url, vc) {
307
447
  let viewsConfig;
308
448
  try {
309
449
  fileConfig = await configInstance.composeFileConfig();
310
- viewsConfig = await configInstance.composeViewsConfig();
450
+ viewsConfig = await configInstance.composeViewsConfig(hintsConfig);
311
451
  }
312
452
  catch (error) {
313
453
  console.error(error);
314
454
  return Promise.reject(error);
315
455
  }
316
- const dataset = vc
317
- .addDataset(configInstance.fileName)
318
- .addFile(fileConfig);
456
+ dataset.addFile(fileConfig);
319
457
  let layerControllerView = false;
320
458
  let spatialView = false;
321
459
  const views = [];
@@ -329,7 +467,7 @@ async function generateConfig(url, vc) {
329
467
  }
330
468
  // this piece of code can be removed once these props are added by default to layerController
331
469
  // see this issue: https://github.com/vitessce/vitessce/issues/1454
332
- if (v[0] === 'layerController' && configInstance instanceof OmeTiffAutoConfig) {
470
+ if (v[0] === 'layerController') {
333
471
  view.setProps({
334
472
  disable3d: [],
335
473
  disableChannelsIfRgbDetected: true,
@@ -342,36 +480,74 @@ async function generateConfig(url, vc) {
342
480
  views.push(view);
343
481
  });
344
482
  if (layerControllerView && spatialView && configInstance instanceof AnndataZarrAutoConfig) {
345
- const spatialSegmentationLayerValue = {
346
- opacity: 1,
347
- radius: 0,
348
- visible: true,
349
- stroked: false,
350
- };
351
483
  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]);
484
+ ct.SPATIAL_SEGMENTATION_LAYER,
485
+ ], [spatialSegmentationLayerValue]);
357
486
  }
358
487
  return views;
359
488
  }
360
- export async function generateConfigs(fileUrls) {
489
+ /**
490
+ * Returns the hints that are available for the given file URLs, depending on their types.
491
+ * @param {Array} fileUrls containing urls of files to be loaded into Vitessce
492
+ * @returns the hints available for these file URLs
493
+ */
494
+ export function getHintOptions(fileUrls) {
495
+ const fileTypes = {};
496
+ fileUrls.forEach((url) => {
497
+ const match = getFileType(url);
498
+ // hints config only has OME-TIFF, since settings are the same for both OME-TIFF and OME-Zarr
499
+ if (match.name === 'OME-Zarr') {
500
+ fileTypes['OME-TIFF'] = true;
501
+ }
502
+ else {
503
+ fileTypes[match.name] = true;
504
+ }
505
+ });
506
+ const datasetType = Object.keys(fileTypes).sort().join(',');
507
+ return HINT_TYPE_TO_FILE_TYPE_MAP?.[datasetType] || [];
508
+ }
509
+ /**
510
+ *
511
+ * @param {Array} fileUrls containing urls of files to be loaded into Vitessce
512
+ * @param {String} the hints config to be used for the dataset. Null by default
513
+ * @returns ViewConfig as JSON
514
+ */
515
+ export async function generateConfig(fileUrls, hintTitle = null) {
361
516
  const vc = new VitessceConfig({
362
517
  schemaVersion: '1.0.15',
363
518
  name: 'An automatically generated config. Adjust values and add layout components if needed.',
364
519
  description: 'Populate with text relevant to this visualisation.',
365
520
  });
366
521
  const allViews = [];
522
+ const dataset = vc.addDataset('An automatically generated view config for dataset. Adjust values and add layout components if needed.');
523
+ const hintsConfig = !hintTitle ? { views: {} } : HINTS_CONFIG?.[hintTitle];
524
+ if (!hintsConfig) {
525
+ throw new Error(`Hints config not found for the supplied hint: ${hintTitle}.`);
526
+ }
527
+ const useHints = Object.keys(hintsConfig?.views)?.length > 0;
367
528
  fileUrls.forEach((url) => {
368
- allViews.push(generateConfig(url, vc));
529
+ allViews.push(generateViewDefinition(url, vc, dataset, hintsConfig));
369
530
  });
370
531
  return Promise.all(allViews).then((views) => {
371
532
  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]);
533
+ // If coordinationSpaceRequired field is set to true in the HINTS_CONFIG,
534
+ // then insert coordination space.
535
+ // NOTE: the user needs to manually add the coordination values for the image and
536
+ // segmentation layers which will be visualized in the spatial/layer controller views.
537
+ if (hintsConfig?.coordinationSpaceRequired) {
538
+ insertCoordinationSpaceForSpatial(flattenedViews, vc);
539
+ }
540
+ if (!useHints) {
541
+ const coord = calculateCoordinates(flattenedViews.length);
542
+ for (let i = 0; i < flattenedViews.length; i++) {
543
+ flattenedViews[i].setXYWH(...coord[i]);
544
+ }
545
+ }
546
+ else {
547
+ flattenedViews.forEach((vitessceConfigView) => {
548
+ const coordinates = Object.values(hintsConfig.views[vitessceConfigView.view.component]);
549
+ vitessceConfigView.setXYWH(...coordinates);
550
+ });
375
551
  }
376
552
  return vc.toJSON();
377
553
  });