@vitessce/vit-s 3.1.2 → 3.2.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.
Files changed (46) hide show
  1. package/dist/index.js +1673 -130
  2. package/dist-tsc/data-hook-utils.d.ts +1 -0
  3. package/dist-tsc/data-hook-utils.d.ts.map +1 -1
  4. package/dist-tsc/data-hook-utils.js +12 -4
  5. package/dist-tsc/data-hooks-multilevel-utils.d.ts +77 -0
  6. package/dist-tsc/data-hooks-multilevel-utils.d.ts.map +1 -0
  7. package/dist-tsc/data-hooks-multilevel-utils.js +418 -0
  8. package/dist-tsc/data-hooks-multilevel-utils.test.d.ts +2 -0
  9. package/dist-tsc/data-hooks-multilevel-utils.test.d.ts.map +1 -0
  10. package/dist-tsc/data-hooks-multilevel-utils.test.js +101 -0
  11. package/dist-tsc/data-hooks-multilevel.d.ts +8 -0
  12. package/dist-tsc/data-hooks-multilevel.d.ts.map +1 -0
  13. package/dist-tsc/data-hooks-multilevel.js +125 -0
  14. package/dist-tsc/data-hooks.d.ts +7 -0
  15. package/dist-tsc/data-hooks.d.ts.map +1 -1
  16. package/dist-tsc/data-hooks.js +75 -6
  17. package/dist-tsc/hooks.js +1 -1
  18. package/dist-tsc/index.d.ts +3 -2
  19. package/dist-tsc/index.js +3 -2
  20. package/dist-tsc/shared-mui/components.d.ts.map +1 -1
  21. package/dist-tsc/shared-mui/components.js +3 -2
  22. package/dist-tsc/state/hooks.d.ts +80 -21
  23. package/dist-tsc/state/hooks.d.ts.map +1 -1
  24. package/dist-tsc/state/hooks.js +428 -103
  25. package/dist-tsc/state/hooks.test.d.ts +2 -0
  26. package/dist-tsc/state/hooks.test.d.ts.map +1 -0
  27. package/dist-tsc/state/hooks.test.js +149 -0
  28. package/dist-tsc/state/spatial-reducers.d.ts +25 -0
  29. package/dist-tsc/state/spatial-reducers.d.ts.map +1 -0
  30. package/dist-tsc/state/spatial-reducers.js +251 -0
  31. package/dist-tsc/state/spatial-reducers.test.d.ts +2 -0
  32. package/dist-tsc/state/spatial-reducers.test.d.ts.map +1 -0
  33. package/dist-tsc/state/spatial-reducers.test.js +1382 -0
  34. package/package.json +5 -5
  35. package/src/data-hook-utils.js +19 -3
  36. package/src/data-hooks-multilevel-utils.js +571 -0
  37. package/src/data-hooks-multilevel-utils.test.js +110 -0
  38. package/src/data-hooks-multilevel.js +226 -0
  39. package/src/data-hooks.js +156 -6
  40. package/src/hooks.js +1 -1
  41. package/src/index.js +25 -0
  42. package/src/shared-mui/components.js +9 -4
  43. package/src/state/hooks.js +485 -104
  44. package/src/state/hooks.test.js +182 -0
  45. package/src/state/spatial-reducers.js +291 -0
  46. package/src/state/spatial-reducers.test.js +1432 -0
@@ -1,11 +1,16 @@
1
+ /* eslint-disable max-len */
1
2
  /* eslint-disable react-refresh/only-export-components */
2
3
  import { useRef, useCallback, useMemo } from 'react';
3
4
  import create from 'zustand';
4
5
  import createContext from 'zustand/context';
5
6
  import shallow from 'zustand/shallow';
6
- import { isMatch, cloneDeep } from 'lodash-es';
7
+ import { isMatch, merge, cloneDeep } from 'lodash-es';
7
8
  import { CoordinationType } from '@vitessce/constants-internal';
8
9
  import { fromEntries, capitalize } from '@vitessce/utils';
10
+ import {
11
+ removeImageChannelInMetaCoordinationScopesHelper,
12
+ addImageChannelInMetaCoordinationScopesHelper,
13
+ } from './spatial-reducers.js';
9
14
 
10
15
  // Reference: https://github.com/pmndrs/zustand#react-context
11
16
  // Reference: https://github.com/pmndrs/zustand/blob/e47ea03/tests/context.test.tsx#L60
@@ -27,6 +32,92 @@ const {
27
32
  export const AuxiliaryProvider = AuxiliaryProviderLocal;
28
33
  export const useAuxiliaryStore = useAuxiliaryStoreLocal;
29
34
 
35
+ /**
36
+ * Get the "computed" coordinationScopes after accounting for
37
+ * meta-coordination.
38
+ * @param {*} coordinationScopes The coordinationScopes for a view.
39
+ * @param {*} coordinationSpace The coordinationSpace for a config.
40
+ * @returns {string|undefined} The coordinationScopesBy after meta-coordination.
41
+ */
42
+ export function getScopes(coordinationScopes, metaSpace) {
43
+ let result = { ...coordinationScopes };
44
+ // Check if there is a matching meta-scope.
45
+ if (metaSpace) {
46
+ // Determine if there is a meta-scope that would take precedence.
47
+ const metaScopes = coordinationScopes[CoordinationType.META_COORDINATION_SCOPES];
48
+ if (metaScopes && metaSpace) {
49
+ // The view.coordinationScopes.metaCoordinationScopes might be an array or a string.
50
+ // Convert to an array.
51
+ const metaScopesArr = Array.isArray(metaScopes) ? metaScopes : [metaScopes];
52
+ metaScopesArr.forEach((metaScope) => {
53
+ // Merge the original coordinationScopes with the matching meta-coordinationScopes
54
+ // from the coordinationSpace.
55
+ result = merge(result, metaSpace[metaScope]);
56
+ });
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Get the "computed" coordinationScopesBy after accounting for
64
+ * meta-coordination.
65
+ * @param {*} coordinationScopes The coordinationScopes for a view.
66
+ * @param {*} coordinationScopesBy The coordinationScopesBy for a view.
67
+ * @param {*} coordinationSpace The coordinationSpace for a config.
68
+ * @returns {string|undefined} The coordinationScopesBy after meta-coordination.
69
+ */
70
+ export function getScopesBy(coordinationScopes, coordinationScopesBy, metaSpaceBy) {
71
+ let result = { ...coordinationScopesBy };
72
+ // Check if there is a matching meta-scope.
73
+ if (metaSpaceBy) {
74
+ // Determine if there is a meta-scope that would take precedence.
75
+ const metaScopesBy = coordinationScopes[CoordinationType.META_COORDINATION_SCOPES_BY];
76
+ if (metaSpaceBy && metaScopesBy) {
77
+ // The view.coordinationScopes.metaCoordinationScopes might be an array or a string.
78
+ // Convert to an array.
79
+ const metaScopesArr = Array.isArray(metaScopesBy) ? metaScopesBy : [metaScopesBy];
80
+ metaScopesArr.forEach((metaScope) => {
81
+ // Merge the original coordinationScopesBy with the matching meta-coordinationScopesBy
82
+ // from the coordinationSpace.
83
+ result = merge(result, metaSpaceBy[metaScope]);
84
+ });
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Get the matching parameter scope.
92
+ * @param {string} parameter A coordination type.
93
+ * @param {*} coordinationScopes The coordinationScopes for a view.
94
+ * @returns {string|undefined} The coordination scope that matches.
95
+ */
96
+ export function getParameterScope(parameter, coordinationScopes) {
97
+ return coordinationScopes[parameter];
98
+ }
99
+
100
+ /**
101
+ * Get the matching parameter scope.
102
+ * @param {string} parameter A coordination type.
103
+ * @param {*} coordinationScopes The coordinationScopes for a view.
104
+ * @param {*} coordinationScopesBy The coordinationScopesBy for a view.
105
+ * @returns {string|undefined} The coordination scope that matches.
106
+ */
107
+ export function getParameterScopeBy(
108
+ parameter, byType, typeScope, coordinationScopes, coordinationScopesBy,
109
+ ) {
110
+ const parameterScopeGlobal = coordinationScopes[parameter];
111
+ // Set parameterScope to the non-meta-value first.
112
+ const parameterScopeByType = coordinationScopesBy?.[byType]?.[parameter];
113
+
114
+ if (parameterScopeByType && parameterScopeByType[typeScope]) {
115
+ return parameterScopeByType[typeScope];
116
+ }
117
+ // console.error(`coordination scope for ${parameter} was not found.`);
118
+ return parameterScopeGlobal;
119
+ }
120
+
30
121
 
31
122
  /**
32
123
  * The useViewConfigStore hook is initialized via the zustand
@@ -52,18 +143,63 @@ export const createViewConfigStore = (initialLoaders, initialConfig) => create(s
52
143
  // (although technically also part of state):
53
144
  setViewConfig: viewConfig => set({ viewConfig, initialViewConfig: viewConfig }),
54
145
  setLoaders: loaders => set({ loaders }),
55
- setCoordinationValue: ({ parameter, scope, value }) => set(state => ({
56
- viewConfig: {
57
- ...state.viewConfig,
58
- coordinationSpace: {
59
- ...state.viewConfig.coordinationSpace,
60
- [parameter]: {
61
- ...state.viewConfig.coordinationSpace[parameter],
62
- [scope]: value,
146
+ setCoordinationValue: ({
147
+ parameter, value, coordinationScopes,
148
+ byType, typeScope, coordinationScopesBy,
149
+ }) => set((state) => {
150
+ const { coordinationSpace } = state.viewConfig;
151
+ let scope;
152
+ if (!byType) {
153
+ scope = getParameterScope(parameter, coordinationScopes);
154
+ } else {
155
+ scope = getParameterScopeBy(
156
+ parameter, byType, typeScope, coordinationScopes, coordinationScopesBy,
157
+ );
158
+ if (!scope) {
159
+ // Fall back to using the view-level scope.
160
+ scope = getParameterScope(parameter, coordinationScopes);
161
+ }
162
+ }
163
+ return {
164
+ viewConfig: {
165
+ ...state.viewConfig,
166
+ coordinationSpace: {
167
+ ...coordinationSpace,
168
+ [parameter]: {
169
+ ...coordinationSpace[parameter],
170
+ [scope]: value,
171
+ },
63
172
  },
64
173
  },
65
- },
66
- })),
174
+ };
175
+ }),
176
+ removeImageChannelInMetaCoordinationScopes: (coordinationScopesRaw, layerScope, channelScope) => set((state) => {
177
+ const { coordinationSpace } = state.viewConfig;
178
+ return {
179
+ viewConfig: {
180
+ ...state.viewConfig,
181
+ coordinationSpace: removeImageChannelInMetaCoordinationScopesHelper(
182
+ coordinationScopesRaw,
183
+ layerScope,
184
+ channelScope,
185
+ coordinationSpace,
186
+ ),
187
+ },
188
+ };
189
+ }),
190
+ addImageChannelInMetaCoordinationScopes: (coordinationScopesRaw, layerScope) => set((state) => {
191
+ const { coordinationSpace } = state.viewConfig;
192
+ return {
193
+ viewConfig: {
194
+ ...state.viewConfig,
195
+ coordinationSpace: addImageChannelInMetaCoordinationScopesHelper(
196
+ coordinationScopesRaw,
197
+ layerScope,
198
+ coordinationSpace,
199
+ ),
200
+ },
201
+ };
202
+ }),
67
203
  removeComponent: uid => set((state) => {
68
204
  const newLayout = state.viewConfig.layout.filter(c => c.uid !== uid);
69
205
  return {
@@ -229,9 +365,12 @@ export function useCoordination(parameters, coordinationScopes) {
229
365
  const values = useViewConfigStore((state) => {
230
366
  const { coordinationSpace } = state.viewConfig;
231
367
  return fromEntries(parameters.map((parameter) => {
232
- if (coordinationSpace && coordinationSpace[parameter]) {
233
- const value = coordinationSpace[parameter][coordinationScopes[parameter]];
234
- return [parameter, value];
368
+ if (coordinationSpace) {
369
+ const parameterScope = getParameterScope(parameter, coordinationScopes);
370
+ if (coordinationSpace && coordinationSpace[parameter]) {
371
+ const value = coordinationSpace[parameter][parameterScope];
372
+ return [parameter, value];
373
+ }
235
374
  }
236
375
  return [parameter, undefined];
237
376
  }));
@@ -241,7 +380,7 @@ export function useCoordination(parameters, coordinationScopes) {
241
380
  const setterName = `set${capitalize(parameter)}`;
242
381
  const setterFunc = value => setCoordinationValue({
243
382
  parameter,
244
- scope: coordinationScopes[parameter],
383
+ coordinationScopes,
245
384
  value,
246
385
  });
247
386
  return [setterName, setterFunc];
@@ -251,8 +390,136 @@ export function useCoordination(parameters, coordinationScopes) {
251
390
  return [values, setters];
252
391
  }
253
392
 
393
+ export function useMultiCoordinationScopes(parameter, coordinationScopes) {
394
+ return useMemo(() => {
395
+ const scopes = coordinationScopes[parameter];
396
+ return Array.isArray(scopes) ? scopes : [scopes];
397
+ }, [parameter, coordinationScopes]);
398
+ }
399
+
400
+ export function useMultiCoordinationScopesNonNull(parameter, coordinationScopes) {
401
+ const scopes = getParameterScope(parameter, coordinationScopes);
402
+
403
+ // Return array of coordination scopes,
404
+ // but filter out any whose value is null / falsey.
405
+ const parameterSpace = useViewConfigStore((state) => {
406
+ const { coordinationSpace } = state.viewConfig;
407
+ return coordinationSpace?.[parameter];
408
+ }, shallow);
409
+ const nonNullScopes = useMemo(() => {
410
+ // Convert a single scope to an array of scopes to be consistent.
411
+ const scopesArr = Array.isArray(scopes) ? scopes : [scopes];
412
+ return scopesArr.filter((scope) => {
413
+ if (parameterSpace) {
414
+ const value = parameterSpace[scope];
415
+ return value !== null;
416
+ }
417
+ return false;
418
+ });
419
+ }, [parameter, coordinationScopes, scopes, parameterSpace]);
420
+ return nonNullScopes;
421
+ }
422
+
423
+ export function useMultiCoordinationScopesSecondary(
424
+ parameter, byType, coordinationScopes, coordinationScopesBy,
425
+ ) {
426
+ return useMemo(() => {
427
+ const scopes = getParameterScope(byType, coordinationScopes);
428
+ if (scopes && coordinationScopesBy?.[byType]?.[parameter]) {
429
+ const scopesArr = Array.isArray(scopes) ? scopes : [scopes];
430
+ return [scopesArr, fromEntries(scopesArr.map((scope) => {
431
+ const secondaryScopes = coordinationScopesBy[byType][parameter][scope];
432
+ const secondaryScopesArr = Array.isArray(secondaryScopes)
433
+ ? secondaryScopes
434
+ : [secondaryScopes];
435
+ return [scope, secondaryScopesArr];
436
+ }))];
437
+ }
438
+ // Fallback from fine-grained to coarse-grained.
439
+ if (scopes && !coordinationScopesBy?.[byType]?.[parameter] && coordinationScopes?.[parameter]) {
440
+ const scopesArr = Array.isArray(scopes) ? scopes : [scopes];
441
+ return [scopesArr, fromEntries(scopesArr.map((scope) => {
442
+ const secondaryScopes = coordinationScopes?.[parameter];
443
+ const secondaryScopesArr = Array.isArray(secondaryScopes)
444
+ ? secondaryScopes
445
+ : [secondaryScopes];
446
+ return [scope, secondaryScopesArr];
447
+ }))];
448
+ }
449
+ return [[], {}];
450
+ }, [parameter, byType, coordinationScopes, coordinationScopesBy]);
451
+ }
452
+
453
+ export function useMultiCoordinationScopesSecondaryNonNull(
454
+ parameter, byType, coordinationScopes, coordinationScopesBy,
455
+ ) {
456
+ const scopes = getParameterScope(byType, coordinationScopes);
457
+
458
+ const parameterSpace = useViewConfigStore((state) => {
459
+ const { coordinationSpace } = state.viewConfig;
460
+ return coordinationSpace?.[parameter];
461
+ }, shallow);
462
+ const byTypeSpace = useViewConfigStore((state) => {
463
+ const { coordinationSpace } = state.viewConfig;
464
+ return coordinationSpace?.[byType];
465
+ }, shallow);
466
+
467
+ return useMemo(() => {
468
+ if (scopes && coordinationScopesBy?.[byType]?.[parameter]) {
469
+ const scopesArr = Array.isArray(scopes) ? scopes : [scopes];
470
+ const scopesNonNull = scopesArr.filter((scope) => {
471
+ if (byTypeSpace) {
472
+ const value = byTypeSpace[scope];
473
+ return value !== null;
474
+ }
475
+ return false;
476
+ });
477
+ return [scopesNonNull, fromEntries(scopesNonNull.map((scope) => {
478
+ const secondaryScopes = coordinationScopesBy[byType][parameter][scope];
479
+ const secondaryScopesArr = Array.isArray(secondaryScopes)
480
+ ? secondaryScopes
481
+ : [secondaryScopes];
482
+ const secondaryScopesNonNull = secondaryScopesArr.filter((innerScope) => {
483
+ if (parameterSpace) {
484
+ const value = parameterSpace[innerScope];
485
+ return value !== null;
486
+ }
487
+ return false;
488
+ });
489
+ return [scope, secondaryScopesNonNull];
490
+ }))];
491
+ }
492
+ // Fallback from fine-grained to coarse-grained.
493
+ if (scopes && !coordinationScopesBy?.[byType]?.[parameter] && coordinationScopes?.[parameter]) {
494
+ const scopesArr = Array.isArray(scopes) ? scopes : [scopes];
495
+ const scopesNonNull = scopesArr.filter((scope) => {
496
+ if (byTypeSpace) {
497
+ const value = byTypeSpace[scope];
498
+ return value !== null;
499
+ }
500
+ return false;
501
+ });
502
+ return [scopesNonNull, fromEntries(scopesNonNull.map((scope) => {
503
+ const secondaryScopes = coordinationScopes?.[parameter];
504
+ const secondaryScopesArr = Array.isArray(secondaryScopes)
505
+ ? secondaryScopes
506
+ : [secondaryScopes];
507
+ const secondaryScopesNonNull = secondaryScopesArr.filter((innerScope) => {
508
+ if (parameterSpace) {
509
+ const value = parameterSpace[innerScope];
510
+ return value !== null;
511
+ }
512
+ return false;
513
+ });
514
+ return [scope, secondaryScopesNonNull];
515
+ }))];
516
+ }
517
+ return [[], {}];
518
+ }, [parameter, byType, scopes, coordinationScopes, coordinationScopesBy, parameterSpace, byTypeSpace]);
519
+ }
520
+
254
521
  export function useMultiCoordinationValues(parameter, coordinationScopes) {
255
- const scopes = coordinationScopes[parameter];
522
+ const scopes = getParameterScope(parameter, coordinationScopes);
256
523
 
257
524
  // Mapping from dataset coordination scope name to dataset uid
258
525
  const vals = useViewConfigStore((state) => {
@@ -300,66 +567,198 @@ export function useComplexCoordination(
300
567
  ) {
301
568
  const setCoordinationValue = useViewConfigStore(state => state.setCoordinationValue);
302
569
 
303
- const typeScopes = coordinationScopes[byType];
304
- const byTypeScopes = coordinationScopesBy[byType];
305
-
306
- // Convert a single scope to an array of scopes to be consistent.
307
- const typeScopesArr = Array.isArray(typeScopes) ? typeScopes : [typeScopes];
308
-
309
- const values = useViewConfigStore((state) => {
570
+ const parameterSpaces = useViewConfigStore((state) => {
310
571
  const { coordinationSpace } = state.viewConfig;
311
- return fromEntries(typeScopesArr.map((datasetScope) => {
312
- const datasetValues = fromEntries(parameters.map((parameter) => {
313
- if (coordinationSpace && coordinationSpace[parameter]) {
314
- let value;
315
- const parameterSpace = coordinationSpace[parameter];
316
- const parameterScopeGlobal = coordinationScopes[parameter];
317
- const parameterScopeByDataset = byTypeScopes?.[parameter];
318
- if (parameterScopeByDataset && parameterScopeByDataset[datasetScope]) {
319
- value = parameterSpace[parameterScopeByDataset[datasetScope]];
320
- } else if (parameterScopeGlobal) {
321
- value = parameterSpace[parameterScopeGlobal];
322
- } else {
323
- console.error(`coordination scope for ${parameter} was not found.`);
572
+ return parameters.map(parameter => coordinationSpace[parameter]);
573
+ }, shallow);
574
+
575
+ const values = useMemo(() => {
576
+ const typeScopes = getParameterScope(byType, coordinationScopes);
577
+ if (typeScopes) {
578
+ // Convert a single scope to an array of scopes to be consistent.
579
+ const typeScopesArr = Array.isArray(typeScopes) ? typeScopes : [typeScopes];
580
+ return fromEntries(typeScopesArr.map((datasetScope) => {
581
+ const datasetValues = fromEntries(parameters.map((parameter, i) => {
582
+ if (parameterSpaces[i]) {
583
+ const parameterSpace = parameterSpaces[i];
584
+ const parameterScope = getParameterScopeBy(
585
+ parameter,
586
+ byType,
587
+ datasetScope,
588
+ coordinationScopes,
589
+ coordinationScopesBy,
590
+ );
591
+ if (parameterScope) {
592
+ const value = parameterSpace[parameterScope];
593
+ return [parameter, value];
594
+ }
595
+ // Fall back to global scope for this parameter.
596
+ const globalParameterScope = getParameterScope(parameter, coordinationScopes);
597
+ const globalValue = parameterSpace[globalParameterScope];
598
+ return [parameter, globalValue];
324
599
  }
325
- return [parameter, value];
326
- }
327
- return [parameter, undefined];
600
+ return [parameter, undefined];
601
+ }));
602
+ return [datasetScope, datasetValues];
328
603
  }));
329
- return [datasetScope, datasetValues];
330
- }));
604
+ }
605
+ return {};
606
+ }, [byType, coordinationScopes, coordinationScopesBy, parameterSpaces]);
607
+
608
+ const setters = useMemo(() => {
609
+ const typeScopes = getParameterScope(byType, coordinationScopes);
610
+ if (typeScopes) {
611
+ // Convert a single scope to an array of scopes to be consistent.
612
+ const typeScopesArr = Array.isArray(typeScopes) ? typeScopes : [typeScopes];
613
+ return fromEntries(typeScopesArr.map((datasetScope) => {
614
+ const datasetSetters = fromEntries(parameters.map((parameter) => {
615
+ const setterName = `set${capitalize(parameter)}`;
616
+ const setterFunc = value => setCoordinationValue({
617
+ parameter,
618
+ byType,
619
+ typeScope: datasetScope,
620
+ coordinationScopes,
621
+ coordinationScopesBy,
622
+ value,
623
+ });
624
+ return [setterName, setterFunc];
625
+ }));
626
+ return [datasetScope, datasetSetters];
627
+ }));
628
+ }
629
+ return {};
630
+ // eslint-disable-next-line react-hooks/exhaustive-deps
631
+ // parameters is assumed to be a constant array.
632
+ }, [coordinationScopes]);
633
+
634
+ return [values, setters];
635
+ }
636
+
637
+ /**
638
+ * Get the "computed" (i.e., after accounting for meta-coordination)
639
+ * value for coordinationScopes.
640
+ * @param {object} coordinationScopes The original coordinationScopes passed to the view.
641
+ * @returns {object} The coordinationScopes after filling in with meta-coordinationScopes.
642
+ */
643
+ export function useCoordinationScopes(coordinationScopes) {
644
+ const metaSpace = useViewConfigStore((state) => {
645
+ const { coordinationSpace } = state.viewConfig;
646
+ return coordinationSpace?.[CoordinationType.META_COORDINATION_SCOPES];
331
647
  }, shallow);
648
+ const vals = useMemo(() => {
649
+ const scopes = getScopes(
650
+ coordinationScopes,
651
+ metaSpace,
652
+ );
653
+ // Prevent infinite loop, delete metaCoordinationScopes now that they are computed.
654
+ delete scopes[CoordinationType.META_COORDINATION_SCOPES];
655
+ return scopes;
656
+ }, [coordinationScopes, metaSpace]);
657
+ return vals;
658
+ }
332
659
 
333
- const setters = useMemo(() => fromEntries(typeScopesArr.map((datasetScope) => {
334
- const datasetSetters = fromEntries(parameters.map((parameter) => {
335
- const setterName = `set${capitalize(parameter)}`;
336
- let setterFunc;
337
- const parameterScopeGlobal = coordinationScopes[parameter];
338
- const parameterScopeByDataset = byTypeScopes?.[parameter];
339
- if (parameterScopeByDataset && parameterScopeByDataset[datasetScope]) {
340
- setterFunc = value => setCoordinationValue({
341
- parameter,
342
- scope: parameterScopeByDataset[datasetScope],
343
- value,
660
+ /**
661
+ * Get the "computed" (i.e., after accounting for meta-coordination)
662
+ * value for coordinationScopesBy.
663
+ * @param {object} coordinationScopes The original coordinationScopes passed to the view.
664
+ * @param {object} coordinationScopesBy The original coordinationScopesBy passed to the view.
665
+ * @returns {object} The coordinationScopesBy after filling in with meta-coordinationScopesBy.
666
+ */
667
+ export function useCoordinationScopesBy(coordinationScopes, coordinationScopesBy) {
668
+ const metaSpaceBy = useViewConfigStore((state) => {
669
+ const { coordinationSpace } = state.viewConfig;
670
+ return coordinationSpace?.[CoordinationType.META_COORDINATION_SCOPES_BY];
671
+ }, shallow);
672
+ const vals = useMemo(() => {
673
+ const scopesBy = getScopesBy(
674
+ coordinationScopes,
675
+ coordinationScopesBy,
676
+ metaSpaceBy,
677
+ );
678
+ return scopesBy;
679
+ }, [coordinationScopes, coordinationScopesBy, metaSpaceBy]);
680
+ return vals;
681
+ }
682
+
683
+ /**
684
+ * Use a second level of complex coordination.
685
+ * @param {string[]} parameters Array of coordination types.
686
+ * @param {object} coordinationScopesBy The coordinationScopesBy object from the view definition.
687
+ * @param {string} primaryType The first-level coordination type, such as spatialImageLayer.
688
+ * @param {string} secondaryType The second-level coordination type, such as spatialImageChannel.
689
+ * @returns The results of useComplexCoordination.
690
+ */
691
+ export function useComplexCoordinationSecondary(
692
+ parameters, coordinationScopes, coordinationScopesBy, primaryType, secondaryType,
693
+ ) {
694
+ const coordinationScopesFake = useMemo(() => {
695
+ if (coordinationScopesBy?.[primaryType]?.[secondaryType]) {
696
+ return {
697
+ ...coordinationScopes, // TODO: this is needed for falling back to view-level coordination scopes, but does it affect performance?
698
+ [secondaryType]: Object.values(coordinationScopesBy[primaryType][secondaryType]).flat(),
699
+ };
700
+ }
701
+ // First fallback: try coarser (_Channel -> _Layer -> view)
702
+ // coordination values when finer ones are null/undefined.
703
+ if (coordinationScopes?.[secondaryType] && Array.isArray(coordinationScopes[secondaryType])) {
704
+ return {
705
+ ...coordinationScopes, // TODO: this is needed for falling back to view-level coordination scopes, but does it affect performance?
706
+ [secondaryType]: coordinationScopes[secondaryType],
707
+ };
708
+ }
709
+ // Finally, fall back to empty array.
710
+ return { [secondaryType]: [] };
711
+ }, [coordinationScopesBy, primaryType, secondaryType]);
712
+ const [flatValues, flatSetters] = useComplexCoordination(
713
+ parameters, coordinationScopesFake, coordinationScopesBy, secondaryType,
714
+ );
715
+ const nestedValues = useMemo(() => {
716
+ // Re-nest
717
+ const result = {};
718
+ if (coordinationScopesBy?.[primaryType]?.[secondaryType]) {
719
+ Object.entries(coordinationScopesBy[primaryType][secondaryType])
720
+ .forEach(([layerScope, channelScopes]) => {
721
+ result[layerScope] = {};
722
+ channelScopes.forEach((channelScope) => {
723
+ result[layerScope][channelScope] = flatValues[channelScope];
724
+ });
344
725
  });
345
- } else if (parameterScopeGlobal) {
346
- setterFunc = value => setCoordinationValue({
347
- parameter,
348
- scope: parameterScopeGlobal,
349
- value,
726
+ } else if (coordinationScopes?.[secondaryType] && Array.isArray(coordinationScopes[secondaryType])) {
727
+ // Re-nesting for fallback case.
728
+ const layerScopes = coordinationScopes[primaryType];
729
+ layerScopes.forEach((layerScope) => {
730
+ // eslint-disable-next-line prefer-destructuring
731
+ result[layerScope] = flatValues;
732
+ });
733
+ }
734
+ return result;
735
+ }, [flatValues]);
736
+ const nestedSetters = useMemo(() => {
737
+ // Re-nest
738
+ const result = {};
739
+ if (coordinationScopesBy?.[primaryType]?.[secondaryType]) {
740
+ Object.entries(coordinationScopesBy[primaryType][secondaryType])
741
+ .forEach(([layerScope, channelScopes]) => {
742
+ result[layerScope] = {};
743
+ channelScopes.forEach((channelScope) => {
744
+ result[layerScope][channelScope] = flatSetters[channelScope];
745
+ });
350
746
  });
351
- } else {
352
- console.error(`coordination scope for ${parameter} was not found.`);
353
- }
354
- return [setterName, setterFunc];
355
- }));
356
- return [datasetScope, datasetSetters];
357
- // eslint-disable-next-line react-hooks/exhaustive-deps
358
- })), [parameters, coordinationScopes]);
747
+ } else if (coordinationScopes?.[secondaryType] && Array.isArray(coordinationScopes[secondaryType])) {
748
+ // Re-nesting for fallback case.
749
+ const layerScopes = coordinationScopes[primaryType];
750
+ layerScopes.forEach((layerScope) => {
751
+ // eslint-disable-next-line prefer-destructuring
752
+ result[layerScope] = flatSetters;
753
+ });
754
+ }
755
+ return result;
756
+ }, [flatSetters]);
359
757
 
360
- return [values, setters];
758
+ return [nestedValues, nestedSetters];
361
759
  }
362
760
 
761
+
363
762
  /**
364
763
  * Use coordination values and coordination setter functions corresponding to
365
764
  * dataset-specific coordination scopes for each coordination type.
@@ -510,44 +909,6 @@ export function useMatchingLoader(loaders, dataset, dataType, viewCoordinationVa
510
909
  ), [loaders, dataset, dataType, viewCoordinationValues]);
511
910
  }
512
911
 
513
- /**
514
- * Find a specific loader instance for a particular dataset, data type, and view
515
- * coordination values (mapping from coordination types to coordination values).
516
- * Uses lodash/isMatch to perform matching against the file definition's
517
- * coordination value mapping.
518
- * TODO: can this function be removed?
519
- * @param {object} loaders The value returned by useLoaders.
520
- * @param {string} dataset The dataset UID.
521
- * @param {string} dataType The data type for the matching file.
522
- * @param {object} viewCoordinationValues Current coordination values
523
- * from the view. Match these against a subset of file definition coordination
524
- * values.
525
- * @returns The matching loader instance or `null`.
526
- */
527
- export function useMatchingLoaders(loaders, dataset, dataType, viewCoordinationValuesObj) {
528
- return useMemo(() => {
529
- if (!loaders[dataset]) {
530
- return null;
531
- }
532
- const loaderInternMap = loaders[dataset].loaders[dataType];
533
- if (!loaderInternMap) {
534
- return null;
535
- }
536
- const loaderKeys = Array.from(loaderInternMap.keys());
537
- function getLoader(viewCoordinationValues) {
538
- const matchingKey = loaderKeys
539
- .find(fileCoordinationValues => isMatch(fileCoordinationValues, viewCoordinationValues));
540
- return loaderInternMap.get(matchingKey);
541
- }
542
- return fromEntries(
543
- Object.entries(viewCoordinationValuesObj).map(([key, viewCoordinationValues]) => ([
544
- key,
545
- getLoader(viewCoordinationValues),
546
- ])),
547
- );
548
- }, [loaders, dataset, dataType, viewCoordinationValuesObj]);
549
- }
550
-
551
912
  /**
552
913
  * Obtain the view config layout array from
553
914
  * the global app state.
@@ -568,6 +929,26 @@ export function useRemoveComponent() {
568
929
  return useViewConfigStore(state => state.removeComponent);
569
930
  }
570
931
 
932
+ /**
933
+ * Obtain the component removal function from
934
+ * the global app state.
935
+ * @returns {function} The remove component function
936
+ * in the `useViewInfoStore` store.
937
+ */
938
+ export function useRemoveImageChannelInMetaCoordinationScopes() {
939
+ return useViewConfigStore(state => state.removeImageChannelInMetaCoordinationScopes);
940
+ }
941
+
942
+ /**
943
+ * Obtain the component removal function from
944
+ * the global app state.
945
+ * @returns {function} The remove component function
946
+ * in the `useViewInfoStore` store.
947
+ */
948
+ export function useAddImageChannelInMetaCoordinationScopes() {
949
+ return useViewConfigStore(state => state.addImageChannelInMetaCoordinationScopes);
950
+ }
951
+
571
952
  /**
572
953
  * Obtain the component prop setter function from
573
954
  * the global app state.