@vitessce/config 2.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,468 @@
1
+ import { CoordinationType } from '@vitessce/constants-internal';
2
+ import { fromEntries, getNextScope } from '@vitessce/utils';
3
+ /**
4
+ * Class representing a file within a Vitessce config dataset.
5
+ */
6
+ export class VitessceConfigDatasetFile {
7
+ /**
8
+ * Construct a new file definition instance.
9
+ * @param {string} url The URL to the file.
10
+ * @param {string} dataType The type of data contained in the file.
11
+ * @param {string} fileType The file type.
12
+ * @param {object|array|null} options An optional object or array
13
+ * which may provide additional parameters to the loader class
14
+ * corresponding to the specified fileType.
15
+ */
16
+ constructor(url, fileType, coordinationValues, options) {
17
+ this.file = {
18
+ url,
19
+ fileType,
20
+ ...(coordinationValues ? { coordinationValues } : {}),
21
+ ...(options ? { options } : {}),
22
+ };
23
+ }
24
+ /**
25
+ * @returns {object} This dataset file as a JSON object.
26
+ */
27
+ toJSON() {
28
+ return this.file;
29
+ }
30
+ }
31
+ /**
32
+ * Class representing a dataset within a Vitessce config.
33
+ */
34
+ export class VitessceConfigDataset {
35
+ /**
36
+ * Construct a new dataset definition instance.
37
+ * @param {string} uid The unique ID for the dataset.
38
+ * @param {string} name The name of the dataset.
39
+ * @param {string} description A description for the dataset.
40
+ */
41
+ constructor(uid, name, description) {
42
+ this.dataset = {
43
+ uid,
44
+ name,
45
+ description,
46
+ files: [],
47
+ };
48
+ }
49
+ /**
50
+ * Add a file definition to the dataset.
51
+ * @param {object} params An object with named arguments.
52
+ * @param {string|undefined} params.url The URL to the file.
53
+ * @param {string} params.fileType The file type.
54
+ * @param {object|undefined} params.coordinationValues The coordination values.
55
+ * @param {object|array|undefined} params.options An optional object or array
56
+ * which may provide additional parameters to the loader class
57
+ * corresponding to the specified fileType.
58
+ * @returns {VitessceConfigDataset} This, to allow chaining.
59
+ */
60
+ addFile(params, ...args) {
61
+ let url;
62
+ let fileType;
63
+ let coordinationValues;
64
+ let options;
65
+ if (args.length > 0) {
66
+ // Old behavior.
67
+ url = params;
68
+ // eslint-disable-next-line no-unused-vars
69
+ let dataType;
70
+ if (args.length === 2) {
71
+ [dataType, fileType] = args;
72
+ }
73
+ else if (args.length === 3) {
74
+ // eslint-disable-next-line no-unused-vars
75
+ [dataType, fileType, options] = args;
76
+ }
77
+ }
78
+ else if (typeof params === 'object') {
79
+ ({
80
+ url, fileType, options, coordinationValues,
81
+ } = params);
82
+ }
83
+ else {
84
+ throw new Error('Expected addFile argument to be an object.');
85
+ }
86
+ this.dataset.files.push(new VitessceConfigDatasetFile(url, fileType, coordinationValues, options));
87
+ return this;
88
+ }
89
+ /**
90
+ * @returns {object} This dataset as a JSON object.
91
+ */
92
+ toJSON() {
93
+ return {
94
+ ...this.dataset,
95
+ files: this.dataset.files.map(f => f.toJSON()),
96
+ };
97
+ }
98
+ }
99
+ /**
100
+ * Class representing a view within a Vitessce layout.
101
+ */
102
+ export class VitessceConfigView {
103
+ /**
104
+ * Construct a new view instance.
105
+ * @param {string} component The name of the Vitessce component type.
106
+ * @param {object} coordinationScopes A mapping from coordination type
107
+ * names to coordination scope names.
108
+ * @param {number} x The x-coordinate of the view in the layout.
109
+ * @param {number} y The y-coordinate of the view in the layout.
110
+ * @param {number} w The width of the view in the layout.
111
+ * @param {number} h The height of the view in the layout.
112
+ */
113
+ constructor(component, coordinationScopes, x, y, w, h) {
114
+ this.view = {
115
+ component,
116
+ coordinationScopes,
117
+ x,
118
+ y,
119
+ w,
120
+ h,
121
+ };
122
+ }
123
+ /**
124
+ * Attach coordination scopes to this view.
125
+ * @param {...VitessceConfigCoordinationScope} args A variable number of
126
+ * coordination scope instances.
127
+ * @returns {VitessceConfigView} This, to allow chaining.
128
+ */
129
+ useCoordination(...args) {
130
+ const cScopes = args;
131
+ cScopes.forEach((cScope) => {
132
+ this.view.coordinationScopes[cScope.cType] = cScope.cScope;
133
+ });
134
+ return this;
135
+ }
136
+ /**
137
+ * Set the x, y, w, h values for this view.
138
+ * @param {number} x The x-coordinate of the view in the layout.
139
+ * @param {number} y The y-coordinate of the view in the layout.
140
+ * @param {number} w The width of the view in the layout.
141
+ * @param {number} h The height of the view in the layout.
142
+ * @returns {VitessceConfigView} This, to allow chaining.
143
+ */
144
+ setXYWH(x, y, w, h) {
145
+ this.view.x = x;
146
+ this.view.y = y;
147
+ this.view.w = w;
148
+ this.view.h = h;
149
+ return this;
150
+ }
151
+ /**
152
+ * Set props for this view.
153
+ * @returns {VitessceConfigView} This, to allow chaining.
154
+ */
155
+ setProps(props) {
156
+ this.view.props = {
157
+ ...(this.view.props || {}),
158
+ ...props,
159
+ };
160
+ return this;
161
+ }
162
+ /**
163
+ * @returns {object} This view as a JSON object.
164
+ */
165
+ toJSON() {
166
+ return this.view;
167
+ }
168
+ }
169
+ /**
170
+ * Class representing a horizontal concatenation of views.
171
+ */
172
+ export class VitessceConfigViewHConcat {
173
+ constructor(views) {
174
+ this.views = views;
175
+ }
176
+ }
177
+ /**
178
+ * Class representing a vertical concatenation of views.
179
+ */
180
+ export class VitessceConfigViewVConcat {
181
+ constructor(views) {
182
+ this.views = views;
183
+ }
184
+ }
185
+ /**
186
+ * A helper function to create a horizontal concatenation of views.
187
+ * @param {...(VitessceConfigView|VitessceConfigViewHConcat|VitessceConfigViewVConcat)} views A
188
+ * variable number of views or view concatenations.
189
+ * @returns {VitessceConfigViewHConcat} A new horizontal view concatenation instance.
190
+ */
191
+ export function hconcat(...views) {
192
+ const vcvhc = new VitessceConfigViewHConcat(views);
193
+ return vcvhc;
194
+ }
195
+ /**
196
+ * A helper function to create a vertical concatenation of views.
197
+ * @param {...(VitessceConfigView|VitessceConfigViewHConcat|VitessceConfigViewVConcat)} views A
198
+ * variable number of views or view concatenations.
199
+ * @returns {VitessceConfigViewVConcat} A new vertical view concatenation instance.
200
+ */
201
+ export function vconcat(...views) {
202
+ const vcvvc = new VitessceConfigViewVConcat(views);
203
+ return vcvvc;
204
+ }
205
+ /**
206
+ * Class representing a coordination scope in the coordination space.
207
+ */
208
+ export class VitessceConfigCoordinationScope {
209
+ /**
210
+ * Construct a new coordination scope instance.
211
+ * @param {string} cType The coordination type for this coordination scope.
212
+ * @param {string} cScope The name of the coordination scope.
213
+ */
214
+ constructor(cType, cScope) {
215
+ this.cType = cType;
216
+ this.cScope = cScope;
217
+ this.cValue = null;
218
+ }
219
+ /**
220
+ * Set the coordination value of the coordination scope.
221
+ * @param {any} cValue The value to set.
222
+ * @returns {VitessceConfigCoordinationScope} This, to allow chaining.
223
+ */
224
+ setValue(cValue) {
225
+ this.cValue = cValue;
226
+ return this;
227
+ }
228
+ }
229
+ /**
230
+ * Class representing a Vitessce view config.
231
+ */
232
+ export class VitessceConfig {
233
+ /**
234
+ * Construct a new view config instance.
235
+ * @param {object} params An object with named arguments.
236
+ * @param {string} params.schemaVersion The view config schema version. Required.
237
+ * @param {string} params.name A name for the config. Optional.
238
+ * @param {string|undefined} params.description A description for the config. Optional.
239
+ */
240
+ constructor(params, ...args) {
241
+ let name;
242
+ let description;
243
+ let schemaVersion;
244
+ if (typeof params === 'string') {
245
+ // Old behavior for backwards compatibility.
246
+ schemaVersion = '1.0.7';
247
+ name = params || '';
248
+ if (args.length === 1) {
249
+ [description] = args;
250
+ }
251
+ else if (args.length > 1) {
252
+ throw new Error('Expected only one VitessceConfig constructor argument.');
253
+ }
254
+ }
255
+ else if (typeof params === 'object') {
256
+ ({ schemaVersion, name, description } = params);
257
+ if (!name) {
258
+ throw new Error('Expected params.name argument in VitessceConfig constructor');
259
+ }
260
+ if (!schemaVersion) {
261
+ throw new Error('Expected params.schemaVersion argument in VitessceConfig constructor');
262
+ }
263
+ }
264
+ else {
265
+ throw new Error('Expected VitessceConfig constructor argument to be an object.');
266
+ }
267
+ this.config = {
268
+ version: schemaVersion,
269
+ name,
270
+ description,
271
+ datasets: [],
272
+ coordinationSpace: {},
273
+ layout: [],
274
+ initStrategy: 'auto',
275
+ };
276
+ }
277
+ /**
278
+ * Add a new dataset to the config.
279
+ * @param {string} name A name for the dataset. Optional.
280
+ * @param {string} description A description for the dataset. Optional.
281
+ * @param {object} options Extra parameters to be used internally. Optional.
282
+ * @param {string} options.uid Override the automatically-generated dataset ID.
283
+ * Intended for internal usage by the VitessceConfig.fromJSON code.
284
+ * @returns {VitessceConfigDataset} A new dataset instance.
285
+ */
286
+ addDataset(name = undefined, description = undefined, options = undefined) {
287
+ const { uid } = options || {};
288
+ const prevDatasetUids = this.config.datasets.map(d => d.dataset.uid);
289
+ const nextUid = (uid || getNextScope(prevDatasetUids));
290
+ const newDataset = new VitessceConfigDataset(nextUid, name, description);
291
+ this.config.datasets.push(newDataset);
292
+ const [newScope] = this.addCoordination(CoordinationType.DATASET);
293
+ newScope.setValue(nextUid);
294
+ return newDataset;
295
+ }
296
+ /**
297
+ * Add a new view to the config.
298
+ * @param {VitessceConfigDataset} dataset The dataset instance which defines the data
299
+ * that will be displayed in the view.
300
+ * @param {string} component A component name, such as "scatterplot" or "spatial".
301
+ * @param {object} options Extra options for the component.
302
+ * @param {number} options.x The x-coordinate for the view in the grid layout.
303
+ * @param {number} options.y The y-coordinate for the view in the grid layout.
304
+ * @param {number} options.w The width for the view in the grid layout.
305
+ * @param {number} options.h The height for the view in the grid layout.
306
+ * @param {number} options.mapping A convenience parameter for setting the EMBEDDING_TYPE
307
+ * coordination value. Only applicable if the component is "scatterplot".
308
+ * @returns {VitessceConfigView} A new view instance.
309
+ */
310
+ addView(dataset, component, options) {
311
+ const { x = 0, y = 0, w = 1, h = 1, mapping = null, } = options || {};
312
+ const datasetMatches = (this.config.coordinationSpace[CoordinationType.DATASET]
313
+ ? Object.entries(this.config.coordinationSpace[CoordinationType.DATASET])
314
+ // eslint-disable-next-line no-unused-vars
315
+ .filter(([scopeName, datasetScope]) => datasetScope.cValue === dataset.dataset.uid)
316
+ .map(([scopeName]) => scopeName)
317
+ : []);
318
+ let datasetScope;
319
+ if (datasetMatches.length === 1) {
320
+ [datasetScope] = datasetMatches;
321
+ }
322
+ else {
323
+ throw new Error('No coordination scope matching the dataset parameter could be found in the coordination space.');
324
+ }
325
+ const coordinationScopes = {
326
+ [CoordinationType.DATASET]: datasetScope,
327
+ };
328
+ const newView = new VitessceConfigView(component, coordinationScopes, x, y, w, h);
329
+ if (mapping) {
330
+ const [etScope] = this.addCoordination(CoordinationType.EMBEDDING_TYPE);
331
+ etScope.setValue(mapping);
332
+ newView.useCoordination(etScope);
333
+ }
334
+ this.config.layout.push(newView);
335
+ return newView;
336
+ }
337
+ /**
338
+ * Get an array of new coordination scope instances corresponding to coordination types
339
+ * of interest.
340
+ * @param {...string} args A variable number of coordination type names.
341
+ * @returns {VitessceConfigCoordinationScope[]} An array of coordination scope instances.
342
+ */
343
+ addCoordination(...args) {
344
+ const cTypes = args;
345
+ const result = [];
346
+ cTypes.forEach((cType) => {
347
+ const prevScopes = (this.config.coordinationSpace[cType]
348
+ ? Object.keys(this.config.coordinationSpace[cType])
349
+ : []);
350
+ const scope = new VitessceConfigCoordinationScope(cType, getNextScope(prevScopes));
351
+ if (!this.config.coordinationSpace[scope.cType]) {
352
+ this.config.coordinationSpace[scope.cType] = {};
353
+ }
354
+ this.config.coordinationSpace[scope.cType][scope.cScope] = scope;
355
+ result.push(scope);
356
+ });
357
+ return result;
358
+ }
359
+ /**
360
+ * A convenience function for setting up new coordination scopes across a set of views.
361
+ * @param {VitessceConfigView[]} views An array of view objects to link together.
362
+ * @param {string[]} cTypes The coordination types on which to coordinate the views.
363
+ * @param {any[]} cValues Initial values corresponding to each coordination type.
364
+ * Should have the same length as the cTypes array. Optional.
365
+ * @returns {VitessceConfig} This, to allow chaining.
366
+ */
367
+ linkViews(views, cTypes, cValues = null) {
368
+ const cScopes = this.addCoordination(...cTypes);
369
+ views.forEach((view) => {
370
+ cScopes.forEach((cScope) => {
371
+ view.useCoordination(cScope);
372
+ });
373
+ });
374
+ if (Array.isArray(cValues) && cValues.length === cTypes.length) {
375
+ cScopes.forEach((cScope, i) => {
376
+ cScope.setValue(cValues[i]);
377
+ });
378
+ }
379
+ return this;
380
+ }
381
+ /**
382
+ * Set the layout of views.
383
+ * @param {VitessceConfigView|VitessceConfigViewHConcat|VitessceConfigViewVConcat} viewConcat A
384
+ * view or a concatenation of views.
385
+ * @returns {VitessceConfig} This, to allow chaining.
386
+ */
387
+ layout(viewConcat) {
388
+ function layoutAux(obj, xMin, xMax, yMin, yMax) {
389
+ const w = xMax - xMin;
390
+ const h = yMax - yMin;
391
+ if (obj instanceof VitessceConfigView) {
392
+ obj.setXYWH(xMin, yMin, w, h);
393
+ }
394
+ else if (obj instanceof VitessceConfigViewHConcat) {
395
+ const { views } = obj;
396
+ const numViews = views.length;
397
+ views.forEach((view, i) => {
398
+ layoutAux(view, xMin + (w / numViews) * i, xMin + (w / numViews) * (i + 1), yMin, yMax);
399
+ });
400
+ }
401
+ else if (obj instanceof VitessceConfigViewVConcat) {
402
+ const { views } = obj;
403
+ const numViews = views.length;
404
+ views.forEach((view, i) => {
405
+ layoutAux(view, xMin, xMax, yMin + (h / numViews) * i, yMin + (h / numViews) * (i + 1));
406
+ });
407
+ }
408
+ }
409
+ layoutAux(viewConcat, 0, 12, 0, 12);
410
+ return this;
411
+ }
412
+ /**
413
+ * Convert this instance to a JSON object that can be passed to the Vitessce component.
414
+ * @returns {object} The view config as a JSON object.
415
+ */
416
+ toJSON() {
417
+ return {
418
+ ...this.config,
419
+ datasets: this.config.datasets.map(d => d.toJSON()),
420
+ coordinationSpace: fromEntries(Object.entries(this.config.coordinationSpace).map(([cType, cScopes]) => ([
421
+ cType,
422
+ fromEntries(Object.entries(cScopes).map(([cScopeName, cScope]) => ([
423
+ cScopeName,
424
+ cScope.cValue,
425
+ ]))),
426
+ ]))),
427
+ layout: this.config.layout.map(c => c.toJSON()),
428
+ };
429
+ }
430
+ /**
431
+ * Create a VitessceConfig instance from an existing view config, to enable
432
+ * manipulation with the JavaScript API.
433
+ * @param {object} config An existing Vitessce view config as a JSON object.
434
+ * @returns {VitessceConfig} A new config instance, with values set to match
435
+ * the config parameter.
436
+ */
437
+ static fromJSON(config) {
438
+ const { name, description, version: schemaVersion } = config;
439
+ const vc = new VitessceConfig({ schemaVersion, name, description });
440
+ config.datasets.forEach((d) => {
441
+ const newDataset = vc.addDataset(d.name, d.description, { uid: d.uid });
442
+ d.files.forEach((f) => {
443
+ newDataset.addFile({
444
+ url: f.url,
445
+ fileType: f.fileType,
446
+ coordinationValues: f.coordinationValues,
447
+ options: f.options,
448
+ });
449
+ });
450
+ });
451
+ Object.keys(config.coordinationSpace).forEach((cType) => {
452
+ if (cType !== CoordinationType.DATASET) {
453
+ const cObj = config.coordinationSpace[cType];
454
+ vc.config.coordinationSpace[cType] = {};
455
+ Object.entries(cObj).forEach(([cScopeName, cScopeValue]) => {
456
+ const scope = new VitessceConfigCoordinationScope(cType, cScopeName);
457
+ scope.setValue(cScopeValue);
458
+ vc.config.coordinationSpace[cType][cScopeName] = scope;
459
+ });
460
+ }
461
+ });
462
+ config.layout.forEach((c) => {
463
+ const newView = new VitessceConfigView(c.component, c.coordinationScopes, c.x, c.y, c.w, c.h);
464
+ vc.config.layout.push(newView);
465
+ });
466
+ return vc;
467
+ }
468
+ }
@@ -0,0 +1,447 @@
1
+ import { CoordinationType } from '@vitessce/constants-internal';
2
+ import { VitessceConfig, hconcat, vconcat, } from './VitessceConfig';
3
+ describe('src/api/VitessceConfig.js', () => {
4
+ describe('VitessceConfig', () => {
5
+ it('can be instantiated in the old way for backwards compatibility', () => {
6
+ const config = new VitessceConfig('My config');
7
+ const configJSON = config.toJSON();
8
+ expect(configJSON).toEqual({
9
+ coordinationSpace: {},
10
+ datasets: [],
11
+ initStrategy: 'auto',
12
+ layout: [],
13
+ name: 'My config',
14
+ version: '1.0.7',
15
+ });
16
+ });
17
+ it('can be instantiated', () => {
18
+ const config = new VitessceConfig({ schemaVersion: '1.0.4', name: 'My config' });
19
+ const configJSON = config.toJSON();
20
+ expect(configJSON).toEqual({
21
+ coordinationSpace: {},
22
+ datasets: [],
23
+ initStrategy: 'auto',
24
+ layout: [],
25
+ name: 'My config',
26
+ version: '1.0.4',
27
+ });
28
+ });
29
+ it('can add a dataset', () => {
30
+ const config = new VitessceConfig({ schemaVersion: '1.0.4', name: 'My config' });
31
+ config.addDataset('My dataset');
32
+ const configJSON = config.toJSON();
33
+ expect(configJSON).toEqual({
34
+ coordinationSpace: {
35
+ dataset: {
36
+ A: 'A',
37
+ },
38
+ },
39
+ datasets: [{
40
+ name: 'My dataset',
41
+ uid: 'A',
42
+ files: [],
43
+ }],
44
+ initStrategy: 'auto',
45
+ layout: [],
46
+ name: 'My config',
47
+ version: '1.0.4',
48
+ });
49
+ });
50
+ it('can add a file to a dataset in the old way for backwards compatibility', () => {
51
+ const config = new VitessceConfig({
52
+ schemaVersion: '1.0.4',
53
+ name: 'My config',
54
+ description: 'My config description',
55
+ });
56
+ // Positional arguments.
57
+ config.addDataset('My dataset', 'My dataset description').addFile('http://example.com/cells.json', 'cells', 'cells.json');
58
+ const configJSON = config.toJSON();
59
+ expect(configJSON).toEqual({
60
+ coordinationSpace: {
61
+ dataset: {
62
+ A: 'A',
63
+ },
64
+ },
65
+ datasets: [{
66
+ name: 'My dataset',
67
+ description: 'My dataset description',
68
+ uid: 'A',
69
+ files: [{
70
+ url: 'http://example.com/cells.json',
71
+ fileType: 'cells.json',
72
+ }],
73
+ }],
74
+ description: 'My config description',
75
+ initStrategy: 'auto',
76
+ layout: [],
77
+ name: 'My config',
78
+ version: '1.0.4',
79
+ });
80
+ });
81
+ it('can add a file to a dataset', () => {
82
+ const config = new VitessceConfig({
83
+ schemaVersion: '1.0.4',
84
+ name: 'My config',
85
+ description: 'My config description',
86
+ });
87
+ // Named arguments.
88
+ config.addDataset('My dataset', 'My dataset description').addFile({
89
+ url: 'http://example.com/cells.json',
90
+ fileType: 'cells.json',
91
+ });
92
+ const configJSON = config.toJSON();
93
+ expect(configJSON).toEqual({
94
+ coordinationSpace: {
95
+ dataset: {
96
+ A: 'A',
97
+ },
98
+ },
99
+ datasets: [{
100
+ name: 'My dataset',
101
+ description: 'My dataset description',
102
+ uid: 'A',
103
+ files: [{
104
+ url: 'http://example.com/cells.json',
105
+ fileType: 'cells.json',
106
+ }],
107
+ }],
108
+ description: 'My config description',
109
+ initStrategy: 'auto',
110
+ layout: [],
111
+ name: 'My config',
112
+ version: '1.0.4',
113
+ });
114
+ });
115
+ it('can add a view', () => {
116
+ const config = new VitessceConfig({
117
+ schemaVersion: '1.0.4',
118
+ name: 'My config',
119
+ });
120
+ const dataset = config.addDataset('My dataset');
121
+ config.addView(dataset, 'description');
122
+ config.addView(dataset, 'scatterplot', { mapping: 'PCA' });
123
+ const configJSON = config.toJSON();
124
+ expect(configJSON).toEqual({
125
+ coordinationSpace: {
126
+ dataset: {
127
+ A: 'A',
128
+ },
129
+ embeddingType: {
130
+ A: 'PCA',
131
+ },
132
+ },
133
+ datasets: [{
134
+ name: 'My dataset',
135
+ uid: 'A',
136
+ files: [],
137
+ }],
138
+ initStrategy: 'auto',
139
+ layout: [
140
+ {
141
+ component: 'description',
142
+ coordinationScopes: {
143
+ dataset: 'A',
144
+ },
145
+ x: 0,
146
+ y: 0,
147
+ w: 1,
148
+ h: 1,
149
+ },
150
+ {
151
+ component: 'scatterplot',
152
+ coordinationScopes: {
153
+ dataset: 'A',
154
+ embeddingType: 'A',
155
+ },
156
+ x: 0,
157
+ y: 0,
158
+ w: 1,
159
+ h: 1,
160
+ },
161
+ ],
162
+ name: 'My config',
163
+ version: '1.0.4',
164
+ });
165
+ });
166
+ it('can add a coordination scope', () => {
167
+ const config = new VitessceConfig({
168
+ schemaVersion: '1.0.4',
169
+ name: 'My config',
170
+ });
171
+ const dataset = config.addDataset('My dataset');
172
+ const pca = config.addView(dataset, 'scatterplot', { mapping: 'PCA' });
173
+ const tsne = config.addView(dataset, 'scatterplot', { mapping: 't-SNE' });
174
+ const [ezScope, etxScope, etyScope] = config.addCoordination(CoordinationType.EMBEDDING_ZOOM, CoordinationType.EMBEDDING_TARGET_X, CoordinationType.EMBEDDING_TARGET_Y);
175
+ pca.useCoordination(ezScope, etxScope, etyScope);
176
+ tsne.useCoordination(ezScope, etxScope, etyScope);
177
+ ezScope.setValue(10);
178
+ etxScope.setValue(11);
179
+ etyScope.setValue(12);
180
+ const configJSON = config.toJSON();
181
+ expect(configJSON).toEqual({
182
+ coordinationSpace: {
183
+ dataset: {
184
+ A: 'A',
185
+ },
186
+ embeddingType: {
187
+ A: 'PCA',
188
+ B: 't-SNE',
189
+ },
190
+ embeddingZoom: {
191
+ A: 10,
192
+ },
193
+ embeddingTargetX: {
194
+ A: 11,
195
+ },
196
+ embeddingTargetY: {
197
+ A: 12,
198
+ },
199
+ },
200
+ datasets: [{
201
+ name: 'My dataset',
202
+ uid: 'A',
203
+ files: [],
204
+ }],
205
+ initStrategy: 'auto',
206
+ layout: [
207
+ {
208
+ component: 'scatterplot',
209
+ coordinationScopes: {
210
+ dataset: 'A',
211
+ embeddingType: 'A',
212
+ embeddingTargetX: 'A',
213
+ embeddingTargetY: 'A',
214
+ embeddingZoom: 'A',
215
+ },
216
+ x: 0,
217
+ y: 0,
218
+ w: 1,
219
+ h: 1,
220
+ },
221
+ {
222
+ component: 'scatterplot',
223
+ coordinationScopes: {
224
+ dataset: 'A',
225
+ embeddingType: 'B',
226
+ embeddingTargetX: 'A',
227
+ embeddingTargetY: 'A',
228
+ embeddingZoom: 'A',
229
+ },
230
+ x: 0,
231
+ y: 0,
232
+ w: 1,
233
+ h: 1,
234
+ },
235
+ ],
236
+ name: 'My config',
237
+ version: '1.0.4',
238
+ });
239
+ });
240
+ it('can add a coordination scope using the link views convenience function', () => {
241
+ const config = new VitessceConfig({
242
+ schemaVersion: '1.0.4',
243
+ name: 'My config',
244
+ });
245
+ const dataset = config.addDataset('My dataset');
246
+ const pca = config.addView(dataset, 'scatterplot', { mapping: 'PCA' });
247
+ const tsne = config.addView(dataset, 'scatterplot', { mapping: 't-SNE' });
248
+ config.linkViews([pca, tsne], [
249
+ CoordinationType.EMBEDDING_ZOOM,
250
+ ]);
251
+ config.linkViews([pca, tsne], [
252
+ CoordinationType.EMBEDDING_TARGET_X,
253
+ CoordinationType.EMBEDDING_TARGET_Y,
254
+ ], [
255
+ 2,
256
+ 3,
257
+ ]);
258
+ const configJSON = config.toJSON();
259
+ expect(configJSON).toEqual({
260
+ coordinationSpace: {
261
+ dataset: {
262
+ A: 'A',
263
+ },
264
+ embeddingType: {
265
+ A: 'PCA',
266
+ B: 't-SNE',
267
+ },
268
+ embeddingZoom: {
269
+ A: null,
270
+ },
271
+ embeddingTargetX: {
272
+ A: 2,
273
+ },
274
+ embeddingTargetY: {
275
+ A: 3,
276
+ },
277
+ },
278
+ datasets: [{
279
+ name: 'My dataset',
280
+ uid: 'A',
281
+ files: [],
282
+ }],
283
+ initStrategy: 'auto',
284
+ layout: [
285
+ {
286
+ component: 'scatterplot',
287
+ coordinationScopes: {
288
+ dataset: 'A',
289
+ embeddingType: 'A',
290
+ embeddingTargetX: 'A',
291
+ embeddingTargetY: 'A',
292
+ embeddingZoom: 'A',
293
+ },
294
+ x: 0,
295
+ y: 0,
296
+ w: 1,
297
+ h: 1,
298
+ },
299
+ {
300
+ component: 'scatterplot',
301
+ coordinationScopes: {
302
+ dataset: 'A',
303
+ embeddingType: 'B',
304
+ embeddingTargetX: 'A',
305
+ embeddingTargetY: 'A',
306
+ embeddingZoom: 'A',
307
+ },
308
+ x: 0,
309
+ y: 0,
310
+ w: 1,
311
+ h: 1,
312
+ },
313
+ ],
314
+ name: 'My config',
315
+ version: '1.0.4',
316
+ });
317
+ });
318
+ it('can create a layout', () => {
319
+ const config = new VitessceConfig({
320
+ schemaVersion: '1.0.4',
321
+ name: 'My config',
322
+ });
323
+ const dataset = config.addDataset('My dataset');
324
+ const v1 = config.addView(dataset, 'spatial');
325
+ const v2 = config.addView(dataset, 'scatterplot', { mapping: 'PCA' });
326
+ const v3 = config.addView(dataset, 'status');
327
+ config.layout(hconcat(v1, vconcat(v2, v3)));
328
+ const configJSON = config.toJSON();
329
+ expect(configJSON).toEqual({
330
+ coordinationSpace: {
331
+ dataset: {
332
+ A: 'A',
333
+ },
334
+ embeddingType: {
335
+ A: 'PCA',
336
+ },
337
+ },
338
+ datasets: [{
339
+ name: 'My dataset',
340
+ uid: 'A',
341
+ files: [],
342
+ }],
343
+ initStrategy: 'auto',
344
+ layout: [
345
+ {
346
+ component: 'spatial',
347
+ coordinationScopes: {
348
+ dataset: 'A',
349
+ },
350
+ x: 0,
351
+ y: 0,
352
+ w: 6,
353
+ h: 12,
354
+ },
355
+ {
356
+ component: 'scatterplot',
357
+ coordinationScopes: {
358
+ dataset: 'A',
359
+ embeddingType: 'A',
360
+ },
361
+ x: 6,
362
+ y: 0,
363
+ w: 6,
364
+ h: 6,
365
+ },
366
+ {
367
+ component: 'status',
368
+ coordinationScopes: {
369
+ dataset: 'A',
370
+ },
371
+ x: 6,
372
+ y: 6,
373
+ w: 6,
374
+ h: 6,
375
+ },
376
+ ],
377
+ name: 'My config',
378
+ version: '1.0.4',
379
+ });
380
+ });
381
+ it('can load a view config from JSON', () => {
382
+ const config = new VitessceConfig({
383
+ schemaVersion: '1.0.4',
384
+ name: 'My config',
385
+ });
386
+ const dataset = config.addDataset('My dataset');
387
+ const v1 = config.addView(dataset, 'spatial');
388
+ const v2 = config.addView(dataset, 'scatterplot', { mapping: 'PCA' });
389
+ const v3 = config.addView(dataset, 'status');
390
+ config.layout(hconcat(v1, vconcat(v2, v3)));
391
+ const origConfigJSON = config.toJSON();
392
+ const loadedConfig = VitessceConfig.fromJSON(origConfigJSON);
393
+ const loadedConfigJSON = loadedConfig.toJSON();
394
+ expect(loadedConfigJSON).toEqual({
395
+ coordinationSpace: {
396
+ dataset: {
397
+ A: 'A',
398
+ },
399
+ embeddingType: {
400
+ A: 'PCA',
401
+ },
402
+ },
403
+ datasets: [{
404
+ name: 'My dataset',
405
+ uid: 'A',
406
+ files: [],
407
+ }],
408
+ initStrategy: 'auto',
409
+ layout: [
410
+ {
411
+ component: 'spatial',
412
+ coordinationScopes: {
413
+ dataset: 'A',
414
+ },
415
+ x: 0,
416
+ y: 0,
417
+ w: 6,
418
+ h: 12,
419
+ },
420
+ {
421
+ component: 'scatterplot',
422
+ coordinationScopes: {
423
+ dataset: 'A',
424
+ embeddingType: 'A',
425
+ },
426
+ x: 6,
427
+ y: 0,
428
+ w: 6,
429
+ h: 6,
430
+ },
431
+ {
432
+ component: 'status',
433
+ coordinationScopes: {
434
+ dataset: 'A',
435
+ },
436
+ x: 6,
437
+ y: 6,
438
+ w: 6,
439
+ h: 6,
440
+ },
441
+ ],
442
+ name: 'My config',
443
+ version: '1.0.4',
444
+ });
445
+ });
446
+ });
447
+ });
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { VitessceConfig, vconcat, hconcat } from './VitessceConfig';
package/dist/utils.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Generate a new scope name which does not
3
+ * conflict / overlap with a previous scope name.
4
+ * Really these just need to be unique within the coordination object.
5
+ * So in theory they could be String(Math.random()) or uuidv4() or something.
6
+ * However it may be good to make them more human-readable and memorable
7
+ * since eventually we will want to expose a UI to update the coordination.
8
+ * @param {string[]} prevScopes Previous scope names.
9
+ * @returns {string} The new scope name.
10
+ */
11
+ export function getNextScope(prevScopes) {
12
+ // Keep an ordered list of valid characters.
13
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
14
+ // Store the value of the next character for each position
15
+ // in the new string.
16
+ // For example, [0] -> "A", [1] -> "B", [0, 1] -> "AB"
17
+ const nextCharIndices = [0];
18
+ // Generate a new scope name,
19
+ // potentially conflicting with an existing name.
20
+ // Reference: https://stackoverflow.com/a/12504061
21
+ function next() {
22
+ const r = [];
23
+ nextCharIndices.forEach((charIndex) => {
24
+ r.unshift(chars[charIndex]);
25
+ });
26
+ let increment = true;
27
+ for (let i = 0; i < nextCharIndices.length; i++) {
28
+ const val = ++nextCharIndices[i];
29
+ if (val >= chars.length) {
30
+ nextCharIndices[i] = 0;
31
+ }
32
+ else {
33
+ increment = false;
34
+ break;
35
+ }
36
+ }
37
+ if (increment) {
38
+ nextCharIndices.push(0);
39
+ }
40
+ return r.join('');
41
+ }
42
+ let nextScope;
43
+ do {
44
+ nextScope = next();
45
+ } while (prevScopes.includes(nextScope));
46
+ return nextScope;
47
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@vitessce/config",
3
+ "version": "2.0.0-beta.0",
4
+ "author": "Gehlenborg Lab",
5
+ "homepage": "http://vitessce.io",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vitessce/vitessce.git"
9
+ },
10
+ "license": "MIT",
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "@vitessce/constants-internal": "2.0.0-beta.0",
17
+ "@vitessce/utils": "2.0.0-beta.0"
18
+ },
19
+ "scripts": {
20
+ "start": "tsc --watch",
21
+ "build": "tsc",
22
+ "test": "pnpm exec vitest --run -r ../../ --dir ."
23
+ }
24
+ }