@genome-spy/core 0.77.0 → 0.78.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 (48) hide show
  1. package/dist/bundle/{esm-CscjKVDc.js → esm-DAnOffpD.js} +1 -1
  2. package/dist/bundle/{esm-0dYHNV_D.js → esm-DNtC3H80.js} +1 -1
  3. package/dist/bundle/{esm-CRMf_I9V.js → esm-DVOHLB1e.js} +1 -1
  4. package/dist/bundle/esm-NIYEaYkc.js +1221 -0
  5. package/dist/bundle/index.es.js +2816 -2703
  6. package/dist/bundle/index.js +57 -54
  7. package/dist/schema.json +825 -112
  8. package/dist/src/genomeSpy/headlessBootstrap.d.ts.map +1 -1
  9. package/dist/src/genomeSpy/headlessBootstrap.js +2 -0
  10. package/dist/src/genomeSpy/interactionController.d.ts +4 -1
  11. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
  12. package/dist/src/genomeSpy/interactionController.js +57 -13
  13. package/dist/src/genomeSpyBase.d.ts.map +1 -1
  14. package/dist/src/genomeSpyBase.js +5 -1
  15. package/dist/src/scales/domainExpressions.d.ts +21 -0
  16. package/dist/src/scales/domainExpressions.d.ts.map +1 -0
  17. package/dist/src/scales/domainExpressions.js +43 -0
  18. package/dist/src/scales/domainPlanner.d.ts +12 -1
  19. package/dist/src/scales/domainPlanner.d.ts.map +1 -1
  20. package/dist/src/scales/domainPlanner.js +55 -36
  21. package/dist/src/scales/scaleInstanceManager.d.ts +1 -0
  22. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  23. package/dist/src/scales/scaleInstanceManager.js +5 -0
  24. package/dist/src/scales/scalePropsResolver.d.ts +6 -1
  25. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -1
  26. package/dist/src/scales/scalePropsResolver.js +35 -10
  27. package/dist/src/scales/scaleResolution.d.ts +16 -0
  28. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  29. package/dist/src/scales/scaleResolution.js +136 -16
  30. package/dist/src/scales/scaleRules.d.ts +10 -0
  31. package/dist/src/scales/scaleRules.d.ts.map +1 -1
  32. package/dist/src/scales/scaleRules.js +38 -1
  33. package/dist/src/scales/viewLevelScaleConfig.d.ts +45 -0
  34. package/dist/src/scales/viewLevelScaleConfig.d.ts.map +1 -0
  35. package/dist/src/scales/viewLevelScaleConfig.js +138 -0
  36. package/dist/src/spec/scale.d.ts +19 -6
  37. package/dist/src/spec/view.d.ts +11 -0
  38. package/dist/src/styles/genome-spy.css +4 -1
  39. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  40. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  41. package/dist/src/styles/genome-spy.css.js +4 -1
  42. package/dist/src/utils/ui/tooltip.d.ts +4 -0
  43. package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
  44. package/dist/src/utils/ui/tooltip.js +35 -10
  45. package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
  46. package/dist/src/view/containerMutationHelper.js +11 -3
  47. package/package.json +2 -2
  48. package/dist/bundle/esm-C49STiCR.js +0 -1248
@@ -9,7 +9,12 @@ import {
9
9
  getConfiguredNamedRange,
10
10
  isConfigRangeName,
11
11
  } from "../config/scaleConfig.js";
12
- import { applyLockedProperties, getDefaultScaleType } from "./scaleRules.js";
12
+ import { collectConfiguredDomainExprRefs } from "./domainExpressions.js";
13
+ import {
14
+ applyLockedProperties,
15
+ getDefaultScaleType,
16
+ validateScaleTypeCompatibility,
17
+ } from "./scaleRules.js";
13
18
  import { INDEX, LOCUS } from "./scaleResolutionConstants.js";
14
19
 
15
20
  /**
@@ -23,6 +28,7 @@ import { INDEX, LOCUS } from "./scaleResolutionConstants.js";
23
28
  * @param {Channel} options.channel
24
29
  * @param {import("../spec/channel.js").Type} options.dataType
25
30
  * @param {ScaleResolutionMember[]} options.orderedMembers
31
+ * @param {{ view: import("../view/view.js").default, config: Scale } | undefined} [options.viewLevelScaleConfig]
26
32
  * @param {boolean} options.isExplicitDomain
27
33
  * @param {import("../spec/config.js").GenomeSpyConfig[]} options.configScopes
28
34
  * @returns {Scale}
@@ -31,6 +37,7 @@ export function resolveScalePropsBase({
31
37
  channel,
32
38
  dataType,
33
39
  orderedMembers,
40
+ viewLevelScaleConfig,
34
41
  isExplicitDomain,
35
42
  configScopes,
36
43
  }) {
@@ -44,9 +51,11 @@ export function resolveScalePropsBase({
44
51
  )
45
52
  .filter((markType) => !!markType);
46
53
 
47
- const propArray = memberList
48
- .map((member) => member.channelDef.scale)
49
- .filter((props) => props !== undefined);
54
+ const propArray = viewLevelScaleConfig
55
+ ? [viewLevelScaleConfig.config]
56
+ : memberList
57
+ .map((member) => member.channelDef.scale)
58
+ .filter((props) => props !== undefined);
50
59
 
51
60
  // TODO: Disabled scale: https://vega.github.io/vega-lite/docs/scale.html#disable
52
61
  const mergedProps = mergeObjects(propArray, "scale", ["domain"]);
@@ -76,6 +85,13 @@ export function resolveScalePropsBase({
76
85
  props.type = getDefaultScaleType(channel, dataType);
77
86
  }
78
87
 
88
+ validateScaleTypeCompatibility(
89
+ channel,
90
+ dataType,
91
+ viewLevelScaleConfig ? props.type : undefined,
92
+ `View-level scales.${channel}.type`
93
+ );
94
+
79
95
  if (typeof props.range == "string") {
80
96
  if (!isConfigRangeName(props.range)) {
81
97
  throw new Error(
@@ -132,9 +148,15 @@ export function resolveScalePropsBase({
132
148
  }
133
149
 
134
150
  if (props.domainTransition === undefined) {
135
- const hasExprDrivenDomain = memberList.some((member) =>
136
- isExprRef(member.channelDef.scale?.domain)
137
- );
151
+ const hasExprDrivenDomain =
152
+ memberList.some(
153
+ (member) =>
154
+ collectConfiguredDomainExprRefs(
155
+ member.channelDef.scale?.domain
156
+ ).length > 0
157
+ ) ||
158
+ collectConfiguredDomainExprRefs(viewLevelScaleConfig?.config.domain)
159
+ .length > 0;
138
160
  props.domainTransition = !hasExprDrivenDomain;
139
161
  }
140
162
 
@@ -143,9 +165,12 @@ export function resolveScalePropsBase({
143
165
  props.range.some(isExprRef) &&
144
166
  memberList.length > 0
145
167
  ) {
146
- const rangeOwner = memberList.find(
147
- (member) => member.channelDef.scale?.range !== undefined
148
- )?.view;
168
+ const rangeOwner =
169
+ viewLevelScaleConfig?.config.range !== undefined
170
+ ? viewLevelScaleConfig.view
171
+ : memberList.find(
172
+ (member) => member.channelDef.scale?.range !== undefined
173
+ )?.view;
149
174
  if (rangeOwner) {
150
175
  /** @type {any} */
151
176
  (props).__rangeExprScope = rangeOwner;
@@ -68,6 +68,22 @@ export default class ScaleResolution implements ScaleResolutionApi {
68
68
  * @returns {() => boolean}
69
69
  */
70
70
  registerMember(member: ScaleResolutionMember): () => boolean;
71
+ /**
72
+ * @param {import("../view/view.js").default} view
73
+ * @param {import("../spec/scale.js").Scale} config
74
+ */
75
+ attachViewLevelScaleConfig(view: import("../view/view.js").default, config: import("../spec/scale.js").Scale): void;
76
+ /**
77
+ * @param {import("../view/view.js").default} view
78
+ */
79
+ clearViewLevelScaleConfig(view: import("../view/view.js").default): void;
80
+ /**
81
+ * @returns {{ view: import("../view/view.js").default, config: import("../spec/scale.js").Scale } | undefined}
82
+ */
83
+ getViewLevelScaleConfig(): {
84
+ view: import("../view/view.js").default;
85
+ config: import("../spec/scale.js").Scale;
86
+ } | undefined;
71
87
  dispose(): void;
72
88
  /**
73
89
  * @param {import("../data/collector.js").default} collector
@@ -1 +1 @@
1
- {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleResolution.js"],"names":[],"mappings":"AAoDA;;;;;;;;GAQG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH;IA8cI;;;;;;;;OAQG;IACH,uBALa,CAAC,eACH,QAAQ,CAAC,eAAe,CAAC,YACzB,MAAM,CAAC,GACL,CAAC,CAqBb;IAnaD;;;OAGG;IACH,sEAFW,OAAO,iBAAiB,EAAE,OAAO,EAuC3C;IApCG,8CAAsB;IACtB,0FAA0F;IAC1F,MADW,OAAO,oBAAoB,EAAE,IAAI,CAC5B;IAEhB,iEAAiE;IACjE,MADW,MAAM,CACI;IA2EzB,2BASC;IAwBD;;;;;;;OAOG;IACH,4KAEC;IAED;;;OAGG;IACH,+KAEC;IAiCD,sCA6CC;IA+MD;;;OAGG;IACH,uBAHW,qBAAqB,GACnB,MAAM,OAAO,CAazB;IAED,gBAMC;IAoFD;;;;OAIG;IACH,0CAJW,OAAO,sBAAsB,EAAE,OAAO,aACtC,QAAQ,CAAC,OAAO,qBAAqB,EAAE,aAAa,CAAC,GACnD,MAAM,IAAI,CAkCtB;IAED;;OAEG;IACH,qCAEC;IAED,+BAiBC;IAuED;;;;;;;;;;;OAWG;IACH,0BALa;QACR,QAAQ,EAAE,OAAO,kBAAkB,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,SAAS,CAAC;QACnE,oBAAoB,EAAE,OAAO,CAAA;KAC9B,CAeH;IAED;;;;;;;OAOG;IACH,wBAFa,OAAO,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,CAShE;IAgGD;;;;;OAKG;IACH,oBAYC;IAED;;;;;OAKG;IACH,0BA0BC;IAoID;;OAEG;IACH;eAnhCkC,OAAO,kBAAkB,EAAE,KAAK;MA0hCjE;IAED;;;;;;OAMG;IACH;eAniCkC,OAAO,kBAAkB,EAAE,KAAK;MA0iCjE;IAED;;;;OAIG;IACH;eAjjCkC,OAAO,kBAAkB,EAAE,KAAK;MA0jCjE;IAED,mBASC;IAED;;;;OAIG;IACH,+DAEC;IAED;;OAEG;IACH,oBAFa,mFAA6B,CASzC;IAED;;OAEG;IACH;;;;MAqBC;IAED;;;;OAIG;IACH,oBAEC;IAED;;OAEG;IACH,sBASC;IAED;;;;;;;OAOG;IACH,kBALW,MAAM,eACN,MAAM,OACN,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;OAMG;IACH,eAJW,mFAA6B,YAC7B,OAAO,gCAAgC,EAAE,aAAa,GAAG,OAAO,GAAG,MAAM,iBAKnF;IAED;;;;OAIG;IACH,qBAEC;IAED;;;;;OAKG;IACH,uBAEC;IAED;;;;;;;OAOG;IACH,wBAoBC;IAED;;;;;OAKG;IACH,uBAFW,MAAM,yDAUhB;IAED;;OAEG;IACH,iBAFW,MAAM,yDAIhB;IAED;;;OAGG;IACH,qBAHW,MAAM,+CAAmB,GACvB,MAAM,CAIlB;IAED;;;OAGG;IACH,8BAHW,kFAA4B,GAC1B,MAAM,EAAE,CAOpB;;CACJ;kCAxyC+B,CAAC,SAApB,6CAAkB;;;;UAGrB,OAAO,qBAAqB,EAAE,OAAO;aACrC,CAAC;gBACD,OAAO,oBAAoB,EAAE,mBAAmB;yBAChD,OAAO;;sBApCV,+BAA+B;sBAA/B,+BAA+B;wBAA/B,+BAA+B;wBAA/B,+BAA+B;6BAA/B,+BAA+B"}
1
+ {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleResolution.js"],"names":[],"mappings":"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"}
@@ -15,6 +15,7 @@ import ScaleInstanceManager from "./scaleInstanceManager.js";
15
15
  import { resolveScalePropsBase } from "./scalePropsResolver.js";
16
16
  import DomainPlanner from "./domainPlanner.js";
17
17
  import ScaleInteractionController from "./scaleInteractionController.js";
18
+ import { validateScaleTypeCompatibility } from "./scaleRules.js";
18
19
  import {
19
20
  INDEX,
20
21
  LOCUS,
@@ -24,11 +25,8 @@ import {
24
25
  } from "./scaleResolutionConstants.js";
25
26
 
26
27
  import { getAccessorDomainKey } from "../encoder/accessor.js";
27
- import {
28
- isPrimaryPositionalChannel,
29
- isSecondaryChannel,
30
- } from "../encoder/encoder.js";
31
- import { isExprRef } from "../paramRuntime/paramUtils.js";
28
+ import { isSecondaryChannel } from "../encoder/encoder.js";
29
+ import { collectConfiguredDomainExprRefs } from "./domainExpressions.js";
32
30
  import { NominalDomain } from "../utils/domainArray.js";
33
31
  import { shallowArrayEquals } from "../utils/arrayUtils.js";
34
32
  import createIndexer from "../utils/indexer.js";
@@ -143,6 +141,11 @@ export default class ScaleResolution {
143
141
  /** @type {import("../view/view.js").default | undefined} */
144
142
  #hostView;
145
143
 
144
+ /**
145
+ * @type {{ view: import("../view/view.js").default, config: import("../spec/scale.js").Scale } | undefined}
146
+ */
147
+ #viewLevelScaleConfig;
148
+
146
149
  #resolvingScaleProps = 0;
147
150
 
148
151
  #memberRegistrationBatchDepth = 0;
@@ -168,6 +171,8 @@ export default class ScaleResolution {
168
171
  getAllMembers: () => this.#members,
169
172
  getDataMembers: () =>
170
173
  this.#getActiveMembers(this.#dataDomainMembers),
174
+ getViewLevelConfiguredDomain: () =>
175
+ this.#getViewLevelConfiguredDomain(),
171
176
  getType: () => this.type,
172
177
  getLocusExtent: (assembly) => this.#getLocusExtent(assembly),
173
178
  fromComplexInterval: this.fromComplexInterval.bind(this),
@@ -400,6 +405,10 @@ export default class ScaleResolution {
400
405
  * @returns {boolean}
401
406
  */
402
407
  #hasConfiguredDomain() {
408
+ if (this.#viewLevelScaleConfig?.config.domain !== undefined) {
409
+ return true;
410
+ }
411
+
403
412
  for (const member of this.#members) {
404
413
  if (
405
414
  member.contributesToDomain &&
@@ -452,6 +461,8 @@ export default class ScaleResolution {
452
461
  const member = normalizeMember(newMember);
453
462
  const { channel, channelDef } = member;
454
463
 
464
+ this.#assertCanRegisterMember(member);
465
+
455
466
  // A convenience hack for cases where the new member should adapt
456
467
  // the scale type to the existing one. For example: SelectionRect
457
468
  // TODO: Add test
@@ -481,15 +492,12 @@ export default class ScaleResolution {
481
492
  explicitScaleType ??
482
493
  (type === INDEX || type === LOCUS ? type : undefined);
483
494
 
484
- if (
485
- effectiveScaleType &&
486
- [INDEX, LOCUS].includes(effectiveScaleType) &&
487
- !isPrimaryPositionalChannel(this.channel)
488
- ) {
489
- throw new Error(
490
- `Index and locus scales are only supported on positional channels (x/y). Channel "${this.channel}" resolves to scale type "${effectiveScaleType}".`
491
- );
492
- }
495
+ validateScaleTypeCompatibility(
496
+ this.channel,
497
+ type,
498
+ effectiveScaleType,
499
+ `encoding.${channel}.scale.type`
500
+ );
493
501
 
494
502
  if (name) {
495
503
  if (this.name !== undefined && name != this.name) {
@@ -587,6 +595,100 @@ export default class ScaleResolution {
587
595
  };
588
596
  }
589
597
 
598
+ /**
599
+ * @param {import("../view/view.js").default} view
600
+ * @param {import("../spec/scale.js").Scale} config
601
+ */
602
+ attachViewLevelScaleConfig(view, config) {
603
+ if (
604
+ this.#viewLevelScaleConfig &&
605
+ this.#viewLevelScaleConfig.view !== view
606
+ ) {
607
+ throw new Error(
608
+ `Multiple view-level scale configs target the same ${this.channel} scale resolution.`
609
+ );
610
+ }
611
+
612
+ for (const member of this.#members) {
613
+ this.#assertMemberHasNoScaleConfig(member);
614
+ }
615
+
616
+ this.#viewLevelScaleConfig = { view, config };
617
+ this.#invalidateMergedScaleProps();
618
+ this.#invalidateConfiguredDomain();
619
+ this.#refreshSelectionDomainParamSubscriptions();
620
+ this.#refreshConfiguredDomainExprSubscriptions();
621
+ if (this.#scaleManager.scale) {
622
+ this.#scaleManager.resetScale();
623
+ this.initializeScale();
624
+ }
625
+ }
626
+
627
+ /**
628
+ * @param {import("../view/view.js").default} view
629
+ */
630
+ clearViewLevelScaleConfig(view) {
631
+ if (this.#viewLevelScaleConfig?.view === view) {
632
+ this.#viewLevelScaleConfig = undefined;
633
+ this.#invalidateMergedScaleProps();
634
+ this.#invalidateConfiguredDomain();
635
+ this.#refreshSelectionDomainParamSubscriptions();
636
+ this.#refreshConfiguredDomainExprSubscriptions();
637
+ if (this.#scaleManager.scale) {
638
+ this.#scaleManager.resetScale();
639
+ this.initializeScale();
640
+ }
641
+ }
642
+ }
643
+
644
+ /**
645
+ * @returns {{ view: import("../view/view.js").default, config: import("../spec/scale.js").Scale } | undefined}
646
+ */
647
+ getViewLevelScaleConfig() {
648
+ return this.#viewLevelScaleConfig;
649
+ }
650
+
651
+ #getViewLevelConfiguredDomain() {
652
+ const viewLevelScaleConfig = this.#viewLevelScaleConfig;
653
+ if (!viewLevelScaleConfig) {
654
+ return undefined;
655
+ }
656
+
657
+ return {
658
+ view: viewLevelScaleConfig.view,
659
+ channel:
660
+ /** @type {import("../spec/channel.js").ChannelWithScale} */ (
661
+ this.channel
662
+ ),
663
+ type: this.type,
664
+ domain: viewLevelScaleConfig.config.domain,
665
+ };
666
+ }
667
+
668
+ /**
669
+ * @param {ScaleResolutionMember} member
670
+ */
671
+ #assertCanRegisterMember(member) {
672
+ if (!this.#viewLevelScaleConfig) {
673
+ return;
674
+ }
675
+
676
+ this.#assertMemberHasNoScaleConfig(member);
677
+ }
678
+
679
+ /**
680
+ * @param {ScaleResolutionMember} member
681
+ */
682
+ #assertMemberHasNoScaleConfig(member) {
683
+ if (member.channelDef.scale === undefined) {
684
+ return;
685
+ }
686
+
687
+ throw new Error(
688
+ `Cannot mix view-level scales.${this.channel} with encoding.${member.channel}.scale in the same scale resolution.`
689
+ );
690
+ }
691
+
590
692
  dispose() {
591
693
  this.#clearSelectionDomainParamSubscriptions();
592
694
  this.#clearConfiguredDomainExprSubscriptions();
@@ -658,11 +760,28 @@ export default class ScaleResolution {
658
760
  continue;
659
761
  }
660
762
  const domain = member.channelDef.scale?.domain;
661
- if (!isExprRef(domain)) {
763
+ const exprRefs = collectConfiguredDomainExprRefs(domain);
764
+ if (exprRefs.length === 0) {
662
765
  continue;
663
766
  }
664
767
 
665
- const expr = member.view.paramRuntime.createExpression(domain.expr);
768
+ for (const exprRef of exprRefs) {
769
+ const expr = member.view.paramRuntime.createExpression(
770
+ exprRef.expr
771
+ );
772
+ const unsubscribe = expr.subscribe(listener);
773
+ this.#configuredDomainExprUnsubscribers.push(unsubscribe);
774
+ }
775
+ }
776
+
777
+ const viewLevelDomain = this.#viewLevelScaleConfig?.config.domain;
778
+ const viewLevelExprRefs =
779
+ collectConfiguredDomainExprRefs(viewLevelDomain);
780
+ for (const exprRef of viewLevelExprRefs) {
781
+ const expr =
782
+ this.#viewLevelScaleConfig.view.paramRuntime.createExpression(
783
+ exprRef.expr
784
+ );
666
785
  const unsubscribe = expr.subscribe(listener);
667
786
  this.#configuredDomainExprUnsubscribers.push(unsubscribe);
668
787
  }
@@ -754,6 +873,7 @@ export default class ScaleResolution {
754
873
  channel: this.channel,
755
874
  dataType: this.type,
756
875
  orderedMembers: this.#getOrderedMembers(),
876
+ viewLevelScaleConfig: this.#viewLevelScaleConfig,
757
877
  isExplicitDomain: this.isDomainDefinedExplicitly(),
758
878
  configScopes: this.#resolutionView.getConfigScopes(),
759
879
  });
@@ -7,6 +7,16 @@
7
7
  * @returns {import("../spec/scale.js").ScaleType}
8
8
  */
9
9
  export function getDefaultScaleType(channel: Channel, dataType: import("../spec/channel.js").Type): import("../spec/scale.js").ScaleType;
10
+ /**
11
+ * Validates explicit scale type choices against the resolved data type and
12
+ * channel constraints.
13
+ *
14
+ * @param {Channel} channel
15
+ * @param {import("../spec/channel.js").Type} dataType
16
+ * @param {import("../spec/scale.js").ScaleType | undefined} scaleType
17
+ * @param {string} source
18
+ */
19
+ export function validateScaleTypeCompatibility(channel: Channel, dataType: import("../spec/channel.js").Type, scaleType: import("../spec/scale.js").ScaleType | undefined, source: string): void;
10
20
  /**
11
21
  * @param {import("../spec/scale.js").Scale} props
12
22
  * @param {Channel} channel
@@ -1 +1 @@
1
- {"version":3,"file":"scaleRules.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleRules.js"],"names":[],"mappings":"AAcA;;GAEG;AAEH;;;;GAIG;AACH,6CAJW,OAAO,YACP,OAAO,oBAAoB,EAAE,IAAI,GAC/B,OAAO,kBAAkB,EAAE,SAAS,CA2EhD;AAED;;;GAGG;AACH,6CAHW,OAAO,kBAAkB,EAAE,KAAK,WAChC,OAAO,QAgBjB;sBArGY,OAAO,oBAAoB,EAAE,OAAO"}
1
+ {"version":3,"file":"scaleRules.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleRules.js"],"names":[],"mappings":"AAcA;;GAEG;AAEH;;;;GAIG;AACH,6CAJW,OAAO,YACP,OAAO,oBAAoB,EAAE,IAAI,GAC/B,OAAO,kBAAkB,EAAE,SAAS,CA0EhD;AAED;;;;;;;;GAQG;AACH,wDALW,OAAO,YACP,OAAO,oBAAoB,EAAE,IAAI,aACjC,OAAO,kBAAkB,EAAE,SAAS,GAAG,SAAS,UAChD,MAAM,QA6BhB;AAED;;;GAGG;AACH,6CAHW,OAAO,kBAAkB,EAAE,KAAK,WAChC,OAAO,QAgBjB;sBA1IY,OAAO,oBAAoB,EAAE,OAAO"}
@@ -37,7 +37,6 @@ export function getDefaultScaleType(channel, dataType) {
37
37
  if (isPrimaryPositionalChannel(channel)) {
38
38
  return dataType;
39
39
  } else {
40
- // TODO: Also explicitly set scales should be validated
41
40
  throw new Error(
42
41
  channel +
43
42
  " does not support " +
@@ -96,6 +95,44 @@ export function getDefaultScaleType(channel, dataType) {
96
95
  return type;
97
96
  }
98
97
 
98
+ /**
99
+ * Validates explicit scale type choices against the resolved data type and
100
+ * channel constraints.
101
+ *
102
+ * @param {Channel} channel
103
+ * @param {import("../spec/channel.js").Type} dataType
104
+ * @param {import("../spec/scale.js").ScaleType | undefined} scaleType
105
+ * @param {string} source
106
+ */
107
+ export function validateScaleTypeCompatibility(
108
+ channel,
109
+ dataType,
110
+ scaleType,
111
+ source
112
+ ) {
113
+ if (!scaleType) {
114
+ return;
115
+ }
116
+
117
+ if (
118
+ [INDEX, LOCUS].includes(scaleType) &&
119
+ !isPrimaryPositionalChannel(channel)
120
+ ) {
121
+ throw new Error(
122
+ `Index and locus scales are only supported on positional channels (x/y). Channel "${channel}" resolves to scale type "${scaleType}".`
123
+ );
124
+ }
125
+
126
+ if (
127
+ ([INDEX, LOCUS].includes(dataType) && scaleType !== dataType) ||
128
+ (scaleType === LOCUS && scaleType !== dataType)
129
+ ) {
130
+ throw new Error(
131
+ `${source} "${scaleType}" is incompatible with "${dataType}" data.`
132
+ );
133
+ }
134
+ }
135
+
99
136
  /**
100
137
  * @param {import("../spec/scale.js").Scale} props
101
138
  * @param {Channel} channel
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @typedef {import("../spec/channel.js").ChannelWithScale} ChannelWithScale
3
+ * @typedef {import("../spec/scale.js").Scale} Scale
4
+ * @typedef {import("../view/view.js").default} View
5
+ * @typedef {import("./scaleResolution.js").default} ScaleResolution
6
+ *
7
+ * @typedef {object} ViewLevelScaleConfigMapping
8
+ * @prop {View} view
9
+ * @prop {ChannelWithScale} channel
10
+ * @prop {Scale} config
11
+ * @prop {ScaleResolution | undefined} resolution
12
+ */
13
+ /**
14
+ * Maps view-level scale configs to the unique scale resolution visible from
15
+ * each configured subtree. Configs with no matching resolution stay pending.
16
+ *
17
+ * @param {View} root
18
+ * @returns {ViewLevelScaleConfigMapping[]}
19
+ */
20
+ export function mapViewLevelScaleConfigs(root: View): ViewLevelScaleConfigMapping[];
21
+ /**
22
+ * Maps view-level scale configs and attaches non-pending configs to their
23
+ * target resolutions.
24
+ *
25
+ * @param {View} root
26
+ * @returns {ViewLevelScaleConfigMapping[]}
27
+ */
28
+ export function attachViewLevelScaleConfigs(root: View): ViewLevelScaleConfigMapping[];
29
+ /**
30
+ * Clears view-level scale configs owned by views in the subtree.
31
+ *
32
+ * @param {View} root
33
+ */
34
+ export function clearViewLevelScaleConfigs(root: View): void;
35
+ export type ChannelWithScale = import("../spec/channel.js").ChannelWithScale;
36
+ export type Scale = import("../spec/scale.js").Scale;
37
+ export type View = import("../view/view.js").default;
38
+ export type ScaleResolution = import("./scaleResolution.js").default;
39
+ export type ViewLevelScaleConfigMapping = {
40
+ view: View;
41
+ channel: ChannelWithScale;
42
+ config: Scale;
43
+ resolution: ScaleResolution | undefined;
44
+ };
45
+ //# sourceMappingURL=viewLevelScaleConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewLevelScaleConfig.d.ts","sourceRoot":"","sources":["../../../src/scales/viewLevelScaleConfig.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AACH,+CAHW,IAAI,GACF,2BAA2B,EAAE,CAwBzC;AAED;;;;;;GAMG;AACH,kDAHW,IAAI,GACF,2BAA2B,EAAE,CAczC;AAED;;;;GAIG;AACH,iDAFW,IAAI,QAYd;+BA/EY,OAAO,oBAAoB,EAAE,gBAAgB;oBAC7C,OAAO,kBAAkB,EAAE,KAAK;mBAChC,OAAO,iBAAiB,EAAE,OAAO;8BACjC,OAAO,sBAAsB,EAAE,OAAO;;UAGzC,IAAI;aACJ,gBAAgB;YAChB,KAAK;gBACL,eAAe,GAAG,SAAS"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @typedef {import("../spec/channel.js").ChannelWithScale} ChannelWithScale
3
+ * @typedef {import("../spec/scale.js").Scale} Scale
4
+ * @typedef {import("../view/view.js").default} View
5
+ * @typedef {import("./scaleResolution.js").default} ScaleResolution
6
+ *
7
+ * @typedef {object} ViewLevelScaleConfigMapping
8
+ * @prop {View} view
9
+ * @prop {ChannelWithScale} channel
10
+ * @prop {Scale} config
11
+ * @prop {ScaleResolution | undefined} resolution
12
+ */
13
+
14
+ /**
15
+ * Maps view-level scale configs to the unique scale resolution visible from
16
+ * each configured subtree. Configs with no matching resolution stay pending.
17
+ *
18
+ * @param {View} root
19
+ * @returns {ViewLevelScaleConfigMapping[]}
20
+ */
21
+ export function mapViewLevelScaleConfigs(root) {
22
+ /** @type {ViewLevelScaleConfigMapping[]} */
23
+ const mappings = [];
24
+
25
+ for (const view of root.getDescendants()) {
26
+ const scales = view.spec.scales;
27
+ if (!scales) {
28
+ continue;
29
+ }
30
+
31
+ for (const [channel, config] of Object.entries(scales)) {
32
+ mappings.push(
33
+ mapViewLevelScaleConfig(
34
+ view,
35
+ /** @type {ChannelWithScale} */ (channel),
36
+ config
37
+ )
38
+ );
39
+ }
40
+ }
41
+
42
+ return mappings;
43
+ }
44
+
45
+ /**
46
+ * Maps view-level scale configs and attaches non-pending configs to their
47
+ * target resolutions.
48
+ *
49
+ * @param {View} root
50
+ * @returns {ViewLevelScaleConfigMapping[]}
51
+ */
52
+ export function attachViewLevelScaleConfigs(root) {
53
+ clearViewLevelScaleConfigs(root);
54
+ const mappings = mapViewLevelScaleConfigs(root);
55
+ for (const mapping of mappings) {
56
+ if (mapping.resolution) {
57
+ mapping.resolution.attachViewLevelScaleConfig(
58
+ mapping.view,
59
+ mapping.config
60
+ );
61
+ }
62
+ }
63
+ return mappings;
64
+ }
65
+
66
+ /**
67
+ * Clears view-level scale configs owned by views in the subtree.
68
+ *
69
+ * @param {View} root
70
+ */
71
+ export function clearViewLevelScaleConfigs(root) {
72
+ const views = new Set(root.getDescendants());
73
+ const resolutions = collectAllScaleResolutions(root);
74
+
75
+ for (const resolution of resolutions) {
76
+ const config = resolution.getViewLevelScaleConfig();
77
+ if (config && views.has(config.view)) {
78
+ resolution.clearViewLevelScaleConfig(config.view);
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {View} view
85
+ * @param {ChannelWithScale} channel
86
+ * @param {Scale} config
87
+ * @returns {ViewLevelScaleConfigMapping}
88
+ */
89
+ function mapViewLevelScaleConfig(view, channel, config) {
90
+ const resolutions = collectVisibleScaleResolutions(view, channel);
91
+
92
+ if (resolutions.size > 1) {
93
+ throw new Error(
94
+ `View-level scales.${channel} maps to multiple scale resolutions. ` +
95
+ `Move scales.${channel} closer to the intended subtree or configure scale resolution explicitly.`
96
+ );
97
+ }
98
+
99
+ const resolution = resolutions.values().next().value;
100
+ return {
101
+ view,
102
+ channel,
103
+ config,
104
+ resolution,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * @param {View} view
110
+ * @param {ChannelWithScale} channel
111
+ * @returns {Set<ScaleResolution>}
112
+ */
113
+ function collectVisibleScaleResolutions(view, channel) {
114
+ /** @type {Set<ScaleResolution>} */
115
+ const resolutions = new Set();
116
+ for (const descendant of view.getDescendants()) {
117
+ const resolution = descendant.getScaleResolution(channel);
118
+ if (resolution) {
119
+ resolutions.add(resolution);
120
+ }
121
+ }
122
+ return resolutions;
123
+ }
124
+
125
+ /**
126
+ * @param {View} view
127
+ * @returns {Set<ScaleResolution>}
128
+ */
129
+ function collectAllScaleResolutions(view) {
130
+ /** @type {Set<ScaleResolution>} */
131
+ const resolutions = new Set();
132
+ for (const descendant of view.getDescendants()) {
133
+ for (const resolution of Object.values(descendant.resolutions.scale)) {
134
+ resolutions.add(resolution);
135
+ }
136
+ }
137
+ return resolutions;
138
+ }
@@ -67,8 +67,17 @@ export interface Scale {
67
67
  * For _temporal_ fields, `domain` can be a two-element array minimum and maximum values, in the form of either timestamps or the [DateTime definition objects](https://vega.github.io/vega-lite/docs/types.html#datetime).
68
68
  *
69
69
  * For _ordinal_ and _nominal_ fields, `domain` can be an array that lists valid input values.
70
+
71
+ * The domain can also be defined by an expression reference that evaluates to the domain array.
72
+ * Array elements may also be expression references.
73
+ *
70
74
  */
71
- domain?: ScalarDomain | ComplexDomain | SelectionDomainRef | ExprRef;
75
+ domain?:
76
+ | ScalarDomain
77
+ | ComplexDomain
78
+ | SelectionDomainRef
79
+ | ExprRef
80
+ | DomainValueArray;
72
81
 
73
82
  /**
74
83
  * Inserts a single mid-point value into a two-element domain. The mid-point value must lie between the domain minimum and maximum values. This property can be useful for setting a midpoint for [diverging color scales](https://vega.github.io/vega-lite/docs/scale.html#piecewise). The domainMid property is only intended for use with scales supporting continuous, piecewise domains.
@@ -97,10 +106,10 @@ export interface Scale {
97
106
  * transition.
98
107
  *
99
108
  * Set this to `false` to apply domain updates immediately. The default is
100
- * `true`, except for domains defined by an `ExprRef`, which default to
109
+ * `true`, except for domains that include `ExprRef`s, which default to
101
110
  * `false` unless overridden.
102
111
  *
103
- * __Default value:__ `true`, except `false` for ExprRef-driven domains.
112
+ * __Default value:__ `true`, except `false` for `ExprRef`-driven domains.
104
113
  */
105
114
  domainTransition?: boolean | Record<string, unknown>;
106
115
 
@@ -109,9 +118,9 @@ export interface Scale {
109
118
  *
110
119
  * - A string indicating a [pre-defined named scale range](https://vega.github.io/vega-lite/docs/scale.html#range-config) (e.g., example, `"symbol"`, or `"diverging"`).
111
120
  *
112
- * - For [continuous scales](https://vega.github.io/vega-lite/docs/scale.html#continuous), two-element array indicating minimum and maximum values, or an array with more than two entries for specifying a [piecewise scale](https://vega.github.io/vega-lite/docs/scale.html#piecewise).
121
+ * - For [continuous scales](https://vega.github.io/vega-lite/docs/scale.html#continuous), two-element array indicating minimum and maximum values, or an array with more than two entries for specifying a [piecewise scale](https://vega.github.io/vega-lite/docs/scale.html#piecewise). Array elements may also be expression references.
113
122
  *
114
- * - For [discrete](https://vega.github.io/vega-lite/docs/scale.html#discrete) and [discretizing](https://vega.github.io/vega-lite/docs/scale.html#discretizing) scales, an array of desired output values.
123
+ * - For [discrete](https://vega.github.io/vega-lite/docs/scale.html#discrete) and [discretizing](https://vega.github.io/vega-lite/docs/scale.html#discretizing) scales, an array of desired output values. Array elements may also be expression references.
115
124
  *
116
125
  * __Notes:__
117
126
  *
@@ -119,7 +128,7 @@ export interface Scale {
119
128
  *
120
129
  * 2) Any directly specified `range` for `x` and `y` channels will be ignored. Range can be customized via the view's corresponding [size](https://vega.github.io/vega-lite/docs/size.html) (`width` and `height`).
121
130
  */
122
- range?: number[] | string[] | string | ExprRef[];
131
+ range?: (number | string | ExprRef)[] | string;
123
132
 
124
133
  // ordinal
125
134
 
@@ -257,6 +266,10 @@ export interface Scale {
257
266
 
258
267
  export type InlineLocusAssembly = GenomeDefinition;
259
268
 
269
+ export type DomainValue = number | string | boolean | ExprRef;
270
+
271
+ export type DomainValueArray = DomainValue[];
272
+
260
273
  export interface SelectionDomainRef {
261
274
  /**
262
275
  * Name of an interval selection parameter that provides the domain.