@vitessce/config 3.6.3 → 3.6.4

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.
Files changed (33) hide show
  1. package/dist-tsc/generate-config-anndata.d.ts +8 -0
  2. package/dist-tsc/generate-config-anndata.d.ts.map +1 -0
  3. package/dist-tsc/generate-config-anndata.js +66 -0
  4. package/dist-tsc/generate-config-helpers.d.ts +9 -0
  5. package/dist-tsc/generate-config-helpers.d.ts.map +1 -0
  6. package/dist-tsc/generate-config-helpers.js +17 -0
  7. package/dist-tsc/generate-config-ome.d.ts +4 -0
  8. package/dist-tsc/generate-config-ome.d.ts.map +1 -0
  9. package/dist-tsc/generate-config-ome.js +19 -0
  10. package/dist-tsc/generate-config-spatialdata.d.ts +5 -0
  11. package/dist-tsc/generate-config-spatialdata.d.ts.map +1 -0
  12. package/dist-tsc/generate-config-spatialdata.js +87 -0
  13. package/dist-tsc/generate-config.d.ts +41 -0
  14. package/dist-tsc/generate-config.d.ts.map +1 -0
  15. package/dist-tsc/generate-config.js +241 -0
  16. package/dist-tsc/generate-config.test.d.ts +2 -0
  17. package/dist-tsc/generate-config.test.d.ts.map +1 -0
  18. package/dist-tsc/generate-config.test.js +258 -0
  19. package/dist-tsc/json-fixtures/mouse_liver.anndata.json +274 -0
  20. package/dist-tsc/json-fixtures/mouse_liver.labels.ome.json +50 -0
  21. package/dist-tsc/json-fixtures/mouse_liver.ome.json +50 -0
  22. package/dist-tsc/json-fixtures/mouse_liver.spatialdata.json +374 -0
  23. package/package.json +6 -4
  24. package/src/generate-config-anndata.js +75 -0
  25. package/src/generate-config-helpers.js +19 -0
  26. package/src/generate-config-ome.js +21 -0
  27. package/src/generate-config-spatialdata.js +101 -0
  28. package/src/generate-config.js +265 -0
  29. package/src/generate-config.test.js +274 -0
  30. package/src/json-fixtures/mouse_liver.anndata.json +274 -0
  31. package/src/json-fixtures/mouse_liver.labels.ome.json +50 -0
  32. package/src/json-fixtures/mouse_liver.ome.json +50 -0
  33. package/src/json-fixtures/mouse_liver.spatialdata.json +374 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/config",
3
- "version": "3.6.3",
3
+ "version": "3.6.4",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -16,9 +16,11 @@
16
16
  "dist-tsc"
17
17
  ],
18
18
  "dependencies": {
19
- "@vitessce/constants-internal": "3.6.3",
20
- "@vitessce/utils": "3.6.3",
21
- "@vitessce/globals": "3.6.3"
19
+ "zarrita": "0.5.2",
20
+ "@vitessce/constants-internal": "3.6.4",
21
+ "@vitessce/utils": "3.6.4",
22
+ "@vitessce/globals": "3.6.4",
23
+ "@vitessce/zarr-utils": "3.6.4"
22
24
  },
23
25
  "scripts": {
24
26
  "bundle": "pnpm exec vite build -c ../../scripts/vite.config.js",
@@ -0,0 +1,75 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { AbstractAutoConfig } from './generate-config-helpers.js';
3
+
4
+ export class AnnDataAutoConfig extends AbstractAutoConfig {
5
+ getOptions() {
6
+ const { zmetadata } = this;
7
+ const options = {
8
+ obsEmbedding: [],
9
+ obsSets: [],
10
+ };
11
+
12
+ zmetadata.forEach(({ path, attrs }) => {
13
+ const lowerPath = path.toLowerCase();
14
+ const relPath = path.substring(1);
15
+ // Gene expression matrix.
16
+ if (['/x'].includes(lowerPath)) {
17
+ options.obsFeatureMatrix = {
18
+ path: relPath,
19
+
20
+ // TODO: Also check the shape of X.
21
+ // If X is very large, try to initialize initial-filtering properties
22
+ // (will require that /var contains a boolean column however.)
23
+ };
24
+ }
25
+
26
+ // Spatial coordinates.
27
+ if (['/obsm/x_spatial', '/obsm/spatial'].includes(lowerPath)) {
28
+ // TODO: use obsSpots instead of obsLocations here?
29
+ options.obsLocations = {
30
+ path: relPath,
31
+ };
32
+ }
33
+
34
+ // Embedding arrays.
35
+ if (['/obsm/x_umap', '/obsm/umap'].includes(lowerPath)) {
36
+ options.obsEmbedding.push({ path: relPath, embeddingType: 'UMAP' });
37
+ }
38
+ if (['/obsm/x_tsne', '/obsm/tsne'].includes(lowerPath)) {
39
+ options.obsEmbedding.push({ path: relPath, embeddingType: 't-SNE' });
40
+ }
41
+ if (['/obsm/x_pca', '/obsm/pca'].includes(lowerPath)) {
42
+ options.obsEmbedding.push({ path: relPath, embeddingType: 'PCA' });
43
+ }
44
+
45
+ // Cell set columns.
46
+ // TODO: use all categorical/string columns of obs instead of this fixed set?
47
+ const supportedObsSetsPaths = [
48
+ 'cluster', 'clusters', 'subcluster', 'cell_type', 'celltype',
49
+ 'leiden', 'louvain', 'disease', 'organism', 'self_reported_ethnicity',
50
+ 'tissue', 'sex',
51
+ ].map(colname => `/obs/${colname}`);
52
+ if (supportedObsSetsPaths.includes(lowerPath)) {
53
+ const name = relPath.split('/').at(-1);
54
+ options.obsSets.push({ path: relPath, name });
55
+ }
56
+ });
57
+
58
+ return options;
59
+ }
60
+
61
+ addFiles(vc, dataset) {
62
+ const { url, fileType } = this;
63
+ dataset.addFile({
64
+ url,
65
+ fileType,
66
+ options: this.getOptions(),
67
+ // TODO: coordination values?
68
+ });
69
+ }
70
+
71
+ // eslint-disable-next-line class-methods-use-this
72
+ addViews(vc, layoutOption) {
73
+ // TODO
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ /* eslint-disable no-unused-vars */
2
+ export class AbstractAutoConfig {
3
+ constructor(parsedStore) {
4
+ const { url, fileType, zmetadata } = parsedStore;
5
+ this.url = url;
6
+ this.fileType = fileType;
7
+ this.zmetadata = zmetadata;
8
+ }
9
+
10
+ // eslint-disable-next-line class-methods-use-this
11
+ addFiles(vc, dataset) {
12
+ throw new Error('The addFiles() method has not been implemented.');
13
+ }
14
+
15
+ // eslint-disable-next-line class-methods-use-this
16
+ addViews(vc, layoutOption) {
17
+ throw new Error('The addViews() method has not been implemented.');
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { AbstractAutoConfig } from './generate-config-helpers.js';
3
+
4
+ // TODO: split into separate classes for OME-TIFF and OME-Zarr?
5
+ // TODO: split into separate classes for image and obsSegmentations?
6
+ export class OmeAutoConfig extends AbstractAutoConfig {
7
+ addFiles(vc, dataset) {
8
+ const { url, fileType } = this;
9
+ dataset.addFile({
10
+ url,
11
+ fileType,
12
+ // TODO: options?
13
+ // TODO: coordination values?
14
+ });
15
+ }
16
+
17
+ // eslint-disable-next-line class-methods-use-this
18
+ addViews(vc, layoutOption) {
19
+ // TODO
20
+ }
21
+ }
@@ -0,0 +1,101 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { AbstractAutoConfig } from './generate-config-helpers.js';
3
+
4
+ export class SpatialDataAutoConfig extends AbstractAutoConfig {
5
+ getOptions() {
6
+ const { zmetadata } = this;
7
+ const options = {};
8
+
9
+ const availableElements = zmetadata.filter(({ path }) => {
10
+ const relPath = path.substring(1);
11
+ return relPath.match(/^(tables|table|images|labels|shapes|points)\/([^/]*)$/);
12
+ });
13
+
14
+ availableElements.forEach(({ path, attrs }) => {
15
+ const relPath = path.substring(1);
16
+
17
+ const firstCoordinateSystem = attrs
18
+ ?.multiscales?.[0]
19
+ ?.coordinateTransformations?.[0]
20
+ ?.output?.name;
21
+
22
+ // Handle image elements.
23
+ if (relPath.match(/^(images)\/([^/]*)$/)) {
24
+ options.image = {
25
+ path: relPath,
26
+ coordinateSystem: firstCoordinateSystem,
27
+ // TODO: support a fileUid property in the schema?
28
+ };
29
+ }
30
+ // Handle labels elements.
31
+ if (relPath.match(/^(labels)\/([^/]*)$/)) {
32
+ options.labels = {
33
+ path: relPath,
34
+ coordinateSystem: firstCoordinateSystem,
35
+ // TODO: support a fileUid property in the schema?
36
+ };
37
+
38
+ // TODO: check which table annotates these labels.
39
+ }
40
+
41
+ // Handle shapes elements.
42
+ if (relPath.match(/^(shapes)\/([^/]*)$/)) {
43
+ // TODO: check if shapes are circles or polygons
44
+ // to determine which Vitessce data type to use.
45
+ options.obsSpots = {
46
+ path: relPath,
47
+ coordinateSystem: firstCoordinateSystem,
48
+ };
49
+
50
+ // TODO: check which table annotates these shapes.
51
+ }
52
+
53
+ // Handle table elements.
54
+ if (relPath.match(/^(tables|table)\/([^/]*)$/)) {
55
+ // Identify all sub-paths within this table element.
56
+ const tableEls = zmetadata.filter(({ path: subpath }) => subpath.startsWith(path));
57
+
58
+ // Check if the table contains an X array.
59
+ const hasX = tableEls.find(el => el.path === `${path}/X`);
60
+ if (hasX) {
61
+ options.obsFeatureMatrix = {
62
+ path: hasX.path.substring(1),
63
+ // region: null,
64
+ };
65
+ }
66
+
67
+ // Check if the table contains an obs dataframe.
68
+ const hasObs = tableEls.find(el => el.path === `${path}/obs`);
69
+ if (hasObs) {
70
+ const columnOrder = hasObs.attrs?.['column-order'];
71
+ // Use the columns of this dataframe to configure the obsSets.
72
+ options.obsSets = {
73
+ // region: null,
74
+ tablePath: relPath,
75
+ obsSets: columnOrder.map(c => ({
76
+ path: `${hasObs.path.substring(1)}/${c}`,
77
+ name: c,
78
+ })),
79
+ };
80
+ }
81
+ }
82
+ });
83
+
84
+ return options;
85
+ }
86
+
87
+ addFiles(vc, dataset) {
88
+ const { url, fileType } = this;
89
+ dataset.addFile({
90
+ url,
91
+ fileType,
92
+ options: this.getOptions(),
93
+ // TODO: coordination values?
94
+ });
95
+ }
96
+
97
+ // eslint-disable-next-line class-methods-use-this
98
+ addViews(vc, layoutOption) {
99
+ // TODO
100
+ }
101
+ }
@@ -0,0 +1,265 @@
1
+ // TODO: ts-check
2
+ import { FileType } from '@vitessce/constants-internal';
3
+ import { withConsolidated, FetchStore, ZipFileStore, open as zarrOpen, root as zarrRoot } from 'zarrita';
4
+ import { VitessceConfig } from './VitessceConfig.js';
5
+ // Classes for different types of objects
6
+ import { AnnDataAutoConfig } from './generate-config-anndata.js';
7
+ import { SpatialDataAutoConfig } from './generate-config-spatialdata.js';
8
+ import { OmeAutoConfig } from './generate-config-ome.js';
9
+
10
+ const fileTypeToExtensions = {
11
+ [FileType.IMAGE_OME_TIFF]: ['.ome.tif', '.ome.tiff', '.ome.tf2', '.ome.tf8'],
12
+ [FileType.IMAGE_OME_ZARR]: ['.ome.zarr'],
13
+ [FileType.IMAGE_OME_ZARR_ZIP]: ['.ome.zarr.zip'],
14
+ [FileType.ANNDATA_ZARR]: ['.ad.zarr', '.h5ad.zarr', '.adata.zarr', '.anndata.zarr'],
15
+ [FileType.ANNDATA_ZARR_ZIP]: ['.ad.zarr.zip', '.h5ad.zarr.zip', '.adata.zarr.zip', '.anndata.zarr.zip'],
16
+ // TODO: how to handle h5ad-based AnnData (since needs reference JSON file).
17
+ // Perhaps just assume one H5AD+one JSON (or .ref.json) file correspond to each other?
18
+ [FileType.SPATIALDATA_ZARR]: ['.sd.zarr', '.sdata.zarr', '.spatialdata.zarr'],
19
+ [FileType.SPATIALDATA_ZARR_ZIP]: ['.sd.zarr.zip', '.sdata.zarr.zip', '.spatialdata.zarr.zip'],
20
+ };
21
+
22
+ const fileTypeToClass = {
23
+ // OME-TIFF
24
+ [FileType.IMAGE_OME_TIFF]: OmeAutoConfig,
25
+ [FileType.OBS_SEGMENTATIONS_OME_TIFF]: OmeAutoConfig,
26
+ // OME-Zarr
27
+ [FileType.IMAGE_OME_ZARR]: OmeAutoConfig,
28
+ [FileType.IMAGE_OME_ZARR_ZIP]: OmeAutoConfig,
29
+ [FileType.OBS_SEGMENTATIONS_OME_ZARR]: OmeAutoConfig,
30
+ [FileType.OBS_SEGMENTATIONS_OME_ZARR_ZIP]: OmeAutoConfig,
31
+ // AnnData
32
+ [FileType.ANNDATA_ZARR]: AnnDataAutoConfig,
33
+ [FileType.ANNDATA_ZARR_ZIP]: AnnDataAutoConfig,
34
+ // SpatialData
35
+ [FileType.SPATIALDATA_ZARR]: SpatialDataAutoConfig,
36
+ [FileType.SPATIALDATA_ZARR_ZIP]: SpatialDataAutoConfig,
37
+ };
38
+
39
+ // This list contains file types that can be mapped to a regular Zarr store
40
+ // (e.g., FetchStore or ZipStore).
41
+ const ZARR_FILETYPES = [
42
+ FileType.ANNDATA_ZARR,
43
+ FileType.ANNDATA_ZARR_ZIP,
44
+ FileType.SPATIALDATA_ZARR,
45
+ FileType.SPATIALDATA_ZARR_ZIP,
46
+ FileType.IMAGE_OME_ZARR,
47
+ FileType.IMAGE_OME_ZARR_ZIP,
48
+ FileType.OBS_SEGMENTATIONS_OME_ZARR,
49
+ FileType.OBS_SEGMENTATIONS_OME_ZARR_ZIP,
50
+ ];
51
+
52
+ function urlToFileType(url) {
53
+ const match = Object.entries(fileTypeToExtensions).find(
54
+ // eslint-disable-next-line no-unused-vars
55
+ ([fileType, extensions]) => extensions.some(ext => url.endsWith(ext)),
56
+ );
57
+ if (match) {
58
+ return match[0];
59
+ }
60
+ throw new Error('The file extension contained in the URL did not map to a supported fileType.');
61
+ }
62
+
63
+ /**
64
+ *
65
+ * @param {{ fileType, url }} parsedUrl
66
+ * @returns {Readable}
67
+ */
68
+ function getStore(parsedUrl) {
69
+ const { fileType, url } = parsedUrl;
70
+ if (!ZARR_FILETYPES.includes(fileType)) {
71
+ return null;
72
+ }
73
+ return fileType.endsWith('.zip')
74
+ ? ZipFileStore.fromUrl(url)
75
+ : new FetchStore(url);
76
+ }
77
+
78
+ /**
79
+ * Ensure that each object { url, fileType, [store] }
80
+ * contains a `store`.
81
+ * @param {object[]} parsedUrls
82
+ * @returns {object[]}
83
+ */
84
+ function ensureStores(parsedUrls) {
85
+ return parsedUrls.map((parsedUrl) => {
86
+ if (parsedUrl.store) {
87
+ return parsedUrl;
88
+ }
89
+ const store = getStore(parsedUrl);
90
+ return {
91
+ ...parsedUrl,
92
+ store,
93
+ };
94
+ });
95
+ }
96
+
97
+ /**
98
+ *
99
+ * @param {string} s A single string, like this
100
+ * `http://example.com/my_zarr.zarr#anndata.zarr;
101
+ * http://example.com/my_tiff.ome.tif`
102
+ * @returns {{ url: string, fileType: string}[]} The URLs with file types.
103
+ */
104
+ export function parseUrls(s) {
105
+ const urlsWithHashes = s.split(';');
106
+ return urlsWithHashes.map((urlWithHash) => {
107
+ const parts = urlWithHash.split('#');
108
+ if (parts.length === 1) {
109
+ const [url] = parts;
110
+ return {
111
+ url,
112
+ fileType: urlToFileType(url),
113
+ };
114
+ } if (parts.length === 2) {
115
+ const [url, fileType] = parts;
116
+ return {
117
+ url,
118
+ fileType,
119
+ };
120
+ }
121
+ throw new Error('Only expected zero or one # character per URL, but received more.');
122
+ });
123
+ }
124
+
125
+
126
+ export async function parsedUrlToZmetadata(parsedUrl) {
127
+ const { store: initialStore } = parsedUrl;
128
+
129
+ if (!initialStore) {
130
+ return null;
131
+ }
132
+
133
+ let store;
134
+ let promises = [];
135
+
136
+ try {
137
+ try {
138
+ store = await withConsolidated(initialStore);
139
+ } catch {
140
+ // Try again with `zmetadata` rather than `.zmetadata`.
141
+ // Reference: https://github.com/zarr-developers/zarr-python/issues/1121
142
+ store = await withConsolidated(initialStore, { metadataKey: 'zmetadata' });
143
+ }
144
+ // Is consolidated.
145
+ const contents = store.contents();
146
+ promises = contents.map(async (value) => {
147
+ const item = await zarrOpen(zarrRoot(store).resolve(value.path));
148
+ return {
149
+ ...value,
150
+ attrs: item.attrs,
151
+ };
152
+ });
153
+ } catch {
154
+ store = initialStore;
155
+ // Is not consolidated.
156
+ const keysToTry = [
157
+ // Note: OME-NGFF metadata is stored in the root attrs.
158
+ '/',
159
+ // AnnData keys
160
+ '/X',
161
+ '/layers',
162
+ '/obs',
163
+ '/var',
164
+ '/obsm',
165
+ '/obsm/spatial',
166
+ '/obsm/X_spatial',
167
+ '/obsm/pca',
168
+ '/obsm/X_pca',
169
+ '/obsm/tsne',
170
+ '/obsm/X_tsne',
171
+ '/obsm/umap',
172
+ '/obsm/X_umap',
173
+ // TODO: second round of getting metadata for
174
+ // columns listed in /obs and /var .attrs['column-order'] ?
175
+
176
+ // SpatialData keys
177
+ // Note: For spatialData, we assume the store is always consolidated.
178
+ // TODO: throw error if spatialdata + not consolidated?
179
+ ];
180
+ promises = keysToTry.map(async (k) => {
181
+ try {
182
+ const item = await zarrOpen(zarrRoot(store).resolve(k));
183
+ return {
184
+ path: k,
185
+ kind: item.kind,
186
+ attrs: item.attrs,
187
+ };
188
+ } catch {
189
+ return null;
190
+ }
191
+ });
192
+ }
193
+
194
+ return (await Promise.all(promises))
195
+ .filter(entry => entry !== null);
196
+ }
197
+
198
+ /**
199
+ *
200
+ * @param {{ url, fileType, store }[]} parsedUrls
201
+ * @return {string[]} The layoutOptions.
202
+ */
203
+ export function parsedUrlsToLayoutOptions(parsedUrls) {
204
+ // eslint-disable-next-line no-unused-vars
205
+ const parsedStores = ensureStores(parsedUrls);
206
+
207
+ // TODO: implement
208
+ }
209
+
210
+ /**
211
+ *
212
+ * @param {{ url, fileType, store }[]} parsedUrls
213
+ * @param {string|null} layoutOption
214
+ */
215
+ export async function generateConfig(parsedUrls, layoutOption = null) {
216
+ // Map each URL to a Zarr store.
217
+ const parsedStores = ensureStores(parsedUrls);
218
+
219
+ // Obtain Zarr consolidated_metadata for each store.
220
+ const zmetadataStores = await Promise.all(
221
+ parsedStores.map(async parsedStore => ({
222
+ ...parsedStore,
223
+ zmetadata: await parsedUrlToZmetadata(parsedStore),
224
+ })),
225
+ );
226
+
227
+ // Create configuration instance.
228
+ const vc = new VitessceConfig({
229
+ schemaVersion: '1.0.17',
230
+ name: 'Automatically-generated configuration.',
231
+ // TODO: write a description based on what is known
232
+ // (fileType(s) and maybe layoutOption).
233
+ description: 'Populate with a description of this visualization.',
234
+ });
235
+
236
+ // Create datasets.
237
+ // TODO: cases in which more than one dataset should be created?
238
+ const dataset = vc.addDataset('Main dataset');
239
+
240
+ zmetadataStores.forEach((parsedStore) => {
241
+ const { fileType } = parsedStore;
242
+ const AutoConfigClass = fileTypeToClass[fileType];
243
+ const autoConfig = new AutoConfigClass(parsedStore);
244
+
245
+ autoConfig.addFiles(vc, dataset);
246
+ // TODO: add all files, then add all views (in two separate loops)?
247
+ autoConfig.addViews(vc, layoutOption);
248
+ });
249
+
250
+ const stores = Object.fromEntries(
251
+ // Here, we use `parsedUrls` rather than `parsedStores`
252
+ // so that we do not provide more stores than intended
253
+ // (i.e., we do not provide stores which were solely created
254
+ // to obtain zmetadata).
255
+ parsedUrls
256
+ .filter(d => d.store)
257
+ .map(d => ([d.url, d.store])),
258
+ );
259
+
260
+ // Return both the config and the `stores` url-to-store dict.
261
+ return {
262
+ config: vc,
263
+ stores,
264
+ };
265
+ }