@genome-spy/core 0.43.3 → 0.45.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 (124) hide show
  1. package/dist/bundle/index.es.js +5231 -4324
  2. package/dist/bundle/index.js +197 -85
  3. package/dist/schema.json +723 -104
  4. package/dist/src/data/collector.d.ts.map +1 -1
  5. package/dist/src/data/collector.js +4 -2
  6. package/dist/src/data/flowOptimizer.test.js +12 -3
  7. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  8. package/dist/src/data/sources/dataUtils.js +3 -1
  9. package/dist/src/data/sources/lazy/axisTickSource.d.ts +1 -1
  10. package/dist/src/data/sources/lazy/axisTickSource.d.ts.map +1 -1
  11. package/dist/src/data/sources/lazy/axisTickSource.js +2 -2
  12. package/dist/src/data/sources/lazy/bigBedSource.d.ts +1 -1
  13. package/dist/src/data/sources/lazy/bigBedSource.d.ts.map +1 -1
  14. package/dist/src/data/sources/lazy/bigBedSource.js +52 -20
  15. package/dist/src/data/sources/lazy/bigWigSource.d.ts +6 -1
  16. package/dist/src/data/sources/lazy/bigWigSource.d.ts.map +1 -1
  17. package/dist/src/data/sources/lazy/bigWigSource.js +33 -9
  18. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
  19. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  20. package/dist/src/data/sources/lazy/singleAxisLazySource.js +1 -3
  21. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts +13 -14
  22. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  23. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +70 -48
  24. package/dist/src/data/sources/sequenceSource.d.ts.map +1 -1
  25. package/dist/src/data/sources/sequenceSource.js +14 -5
  26. package/dist/src/data/sources/sequenceSource.test.js +23 -5
  27. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  28. package/dist/src/data/sources/urlSource.js +15 -2
  29. package/dist/src/data/transforms/aggregate.d.ts.map +1 -1
  30. package/dist/src/data/transforms/aggregate.js +5 -2
  31. package/dist/src/data/transforms/filterScoredLabels.js +1 -1
  32. package/dist/src/encoder/encoder.d.ts +2 -4
  33. package/dist/src/encoder/encoder.d.ts.map +1 -1
  34. package/dist/src/encoder/encoder.js +20 -10
  35. package/dist/src/encoder/encoder.test.js +3 -0
  36. package/dist/src/genomeSpy.d.ts +8 -5
  37. package/dist/src/genomeSpy.d.ts.map +1 -1
  38. package/dist/src/genomeSpy.js +121 -42
  39. package/dist/src/gl/glslScaleGenerator.d.ts +23 -3
  40. package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
  41. package/dist/src/gl/glslScaleGenerator.js +137 -42
  42. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  43. package/dist/src/gl/webGLHelper.js +5 -7
  44. package/dist/src/index.d.ts +1 -1
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/index.js +1 -1
  47. package/dist/src/marks/link.common.glsl.js +2 -0
  48. package/dist/src/marks/link.d.ts.map +1 -1
  49. package/dist/src/marks/link.js +19 -9
  50. package/dist/src/marks/link.vertex.glsl.js +1 -1
  51. package/dist/src/marks/mark.d.ts +25 -20
  52. package/dist/src/marks/mark.d.ts.map +1 -1
  53. package/dist/src/marks/mark.js +234 -129
  54. package/dist/src/marks/point.common.glsl.js +1 -1
  55. package/dist/src/marks/point.d.ts +1 -4
  56. package/dist/src/marks/point.d.ts.map +1 -1
  57. package/dist/src/marks/point.js +31 -23
  58. package/dist/src/marks/point.vertex.glsl.js +1 -1
  59. package/dist/src/marks/rect.common.glsl.js +2 -0
  60. package/dist/src/marks/rect.d.ts.map +1 -1
  61. package/dist/src/marks/rect.js +12 -12
  62. package/dist/src/marks/rect.vertex.glsl.js +1 -1
  63. package/dist/src/marks/rule.common.glsl.js +1 -1
  64. package/dist/src/marks/rule.js +2 -2
  65. package/dist/src/marks/text.common.glsl.js +1 -1
  66. package/dist/src/marks/text.d.ts.map +1 -1
  67. package/dist/src/marks/text.js +17 -9
  68. package/dist/src/spec/channel.d.ts +4 -3
  69. package/dist/src/spec/data.d.ts +11 -10
  70. package/dist/src/spec/mark.d.ts +28 -46
  71. package/dist/src/spec/parameter.d.ts +127 -0
  72. package/dist/src/spec/root.d.ts +1 -0
  73. package/dist/src/spec/scale.d.ts +2 -1
  74. package/dist/src/spec/title.d.ts +5 -4
  75. package/dist/src/spec/view.d.ts +20 -5
  76. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  77. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  78. package/dist/src/styles/genome-spy.css.js +52 -5
  79. package/dist/src/styles/genome-spy.scss +63 -10
  80. package/dist/src/styles/update.sh +6 -0
  81. package/dist/src/tooltip/dataTooltipHandler.js +1 -1
  82. package/dist/src/tooltip/refseqGeneTooltipHandler.js +1 -1
  83. package/dist/src/tooltip/tooltipHandler.d.ts +1 -1
  84. package/dist/src/tooltip/tooltipHandler.d.ts.map +1 -1
  85. package/dist/src/tooltip/tooltipHandler.ts +1 -1
  86. package/dist/src/types/embedApi.d.ts +6 -0
  87. package/dist/src/types/scaleResolutionApi.d.ts +7 -3
  88. package/dist/src/types/viewContext.d.ts +2 -3
  89. package/dist/src/utils/debounce.d.ts +2 -2
  90. package/dist/src/utils/debounce.d.ts.map +1 -1
  91. package/dist/src/utils/debounce.js +5 -2
  92. package/dist/src/utils/expression.d.ts +2 -2
  93. package/dist/src/utils/expression.d.ts.map +1 -1
  94. package/dist/src/utils/expression.js +3 -3
  95. package/dist/src/utils/formatObject.d.ts +2 -2
  96. package/dist/src/utils/formatObject.d.ts.map +1 -1
  97. package/dist/src/utils/formatObject.js +2 -2
  98. package/dist/src/utils/inputBinding.d.ts +5 -0
  99. package/dist/src/utils/inputBinding.d.ts.map +1 -0
  100. package/dist/src/utils/inputBinding.js +115 -0
  101. package/dist/src/utils/ui/tooltip.js +1 -1
  102. package/dist/src/view/axisView.js +3 -3
  103. package/dist/src/view/paramMediator.d.ts +108 -0
  104. package/dist/src/view/paramMediator.d.ts.map +1 -0
  105. package/dist/src/view/paramMediator.js +337 -0
  106. package/dist/src/view/paramMediator.test.js +211 -0
  107. package/dist/src/view/scaleResolution.d.ts +8 -18
  108. package/dist/src/view/scaleResolution.d.ts.map +1 -1
  109. package/dist/src/view/scaleResolution.js +225 -126
  110. package/dist/src/view/scaleResolution.test.js +7 -7
  111. package/dist/src/view/unitView.d.ts.map +1 -1
  112. package/dist/src/view/unitView.js +10 -3
  113. package/dist/src/view/view.d.ts +4 -1
  114. package/dist/src/view/view.d.ts.map +1 -1
  115. package/dist/src/view/view.js +21 -7
  116. package/dist/src/view/viewFactory.d.ts.map +1 -1
  117. package/dist/src/view/viewFactory.js +45 -0
  118. package/dist/src/view/viewUtils.d.ts +5 -1
  119. package/dist/src/view/viewUtils.d.ts.map +1 -1
  120. package/dist/src/view/viewUtils.js +9 -4
  121. package/package.json +16 -17
  122. package/dist/src/paramBroker.d.ts +0 -30
  123. package/dist/src/paramBroker.d.ts.map +0 -1
  124. package/dist/src/paramBroker.js +0 -102
@@ -45,15 +45,15 @@ export default class ScaleResolution implements ScaleResolutionApi {
45
45
  * e.g., zoomed. The call is synchronous and happens before the views
46
46
  * are rendered.
47
47
  *
48
- * @param {"domain"} type
48
+ * @param {ScaleResolutionEventType} type
49
49
  * @param {ScaleResolutionListener} listener function
50
50
  */
51
- addEventListener(type: "domain", listener: import("../types/scaleResolutionApi.js").ScaleResolutionListener): void;
51
+ addEventListener(type: import("../types/scaleResolutionApi.js").ScaleResolutionEventType, listener: import("../types/scaleResolutionApi.js").ScaleResolutionListener): void;
52
52
  /**
53
- * @param {"domain"} type
53
+ * @param {ScaleResolutionEventType} type
54
54
  * @param {ScaleResolutionListener} listener function
55
55
  */
56
- removeEventListener(type: "domain", listener: import("../types/scaleResolutionApi.js").ScaleResolutionListener): void;
56
+ removeEventListener(type: import("../types/scaleResolutionApi.js").ScaleResolutionEventType, listener: import("../types/scaleResolutionApi.js").ScaleResolutionListener): void;
57
57
  /**
58
58
  * Add a view to this resolution.
59
59
  * N.B. This is expected to be called in depth-first order
@@ -62,18 +62,6 @@ export default class ScaleResolution implements ScaleResolutionApi {
62
62
  * @param {ChannelWithScale} channel
63
63
  */
64
64
  pushUnitView(view: import("./unitView.js").default, channel: import("../spec/channel.js").ChannelWithScale): void;
65
- /**
66
- * Returns true if the domain has been defined explicitly, i.e. not extracted from the data.
67
- */
68
- isExplicitDomain(): boolean;
69
- isDomainInitialized(): boolean;
70
- /**
71
- * Returns the merged scale properties supplemented with inferred properties
72
- * and domain.
73
- *
74
- * @returns {import("../spec/scale.js").Scale}
75
- */
76
- getScaleProps(): import("../spec/scale.js").Scale;
77
65
  /**
78
66
  * Unions the configured domains of all participating views.
79
67
  *
@@ -91,9 +79,11 @@ export default class ScaleResolution implements ScaleResolutionApi {
91
79
  */
92
80
  reconfigure(): void;
93
81
  /**
94
- * @returns {VegaScale}
82
+ * @returns {ScaleWithProps}
95
83
  */
96
- getScale(): import("../types/encoder.js").VegaScale;
84
+ get scale(): import("../types/encoder.js").VegaScale & {
85
+ props: import("../spec/scale.js").Scale;
86
+ };
97
87
  getDomain(): any[];
98
88
  /**
99
89
  * @returns {NumericDomain | ComplexDomain}
@@ -1 +1 @@
1
- {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/view/scaleResolution.js"],"names":[],"mappings":"AA20BA;;;;;;;;;;GAUG;AACH,6CAFW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QA4BrE;AAl0BD,0CAA2C;AAC3C,gCAAiC;AACjC,gCAAiC;AACjC,4BAA6B;AAC7B,4BAA6B;AAE7B;;;;GAIG;AACH;;;;;;;GAOG;AACH;IAyBI;;OAEG;IACH,2DASC;IARG,8CAAsB;IACtB,oDAAoD;IACpD,SADW,gBAAgB,EAAE,CACZ;IACjB,+DAA+D;IAC/D,MADW,MAAM,CACD;IAEhB,iEAAiE;IACjE,MADW,MAAM,CACI;IAGzB;;;;;;;OAOG;IACH,uBAHW,QAAQ,oFAQlB;IAED;;;OAGG;IACH,0BAHW,QAAQ,oFAQlB;IAWD;;;;;;OAMG;IACH,kHA0BC;IAED;;OAEG;IACH,4BAEC;IAED,+BAiBC;IAuBD;;;;;OAKG;IACH,iBAFa,OAAO,kBAAkB,EAAE,KAAK,CA+D5C;IAYD;;;;OAIG;IACH,qEAMC;IAED;;;;OAIG;IACH,+DAQC;IAED;;OAEG;IACH,oBAkCC;IAED;;OAEG;IACH,oDAmBC;IAED,mBAEC;IAED;;OAEG;IACH,oBAFa,mFAA6B,CAOzC;IAED;;;;OAIG;IACH,oBAKC;IAED;;OAEG;IACH,sBAGC;IAUD;;;;;;;OAOG;IACH,kBALW,MAAM,eACN,MAAM,OACN,MAAM,GACJ,OAAO,CAwEnB;IAED;;;;;;OAMG;IACH,eAJW,mFAA6B,aAC7B,OAAO,GAAG,MAAM,iBAgD1B;IAED;;;;OAIG;IACH,qBAcC;IAED;;;;;OAKG;IACH,uBAOC;IA8DD;;;OAGG;IACH,aAFa,OAAO,qBAAqB,EAAE,OAAO,CAajD;IAID;;;;;OAKG;IACH,uBAFW,MAAM,yDAUhB;IAED;;OAEG;IACH,iBAFW,MAAM,yDAKhB;IAED;;;OAGG;IACH,6EAFa,MAAM,CAQlB;IAED;;;OAGG;IACH,mHAFa,MAAM,EAAE,CAOpB;;CA6BJ;wIA9rBY;IAAC,IAAI,EAAE,OAAO,eAAe,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAC"}
1
+ {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/view/scaleResolution.js"],"names":[],"mappings":"AA86BA;;;;;;;;;;GAUG;AACH,6CAFW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QA4BrE;AAr6BD,0CAA2C;AAC3C,gCAAiC;AACjC,gCAAiC;AACjC,4BAA6B;AAC7B,4BAA6B;AAE7B;;;;GAIG;AACH;;;;;;;GAOG;AACH;IAyCI;;OAEG;IACH,2DASC;IARG,8CAAsB;IACtB,oDAAoD;IACpD,SADW,gBAAgB,EAAE,CACZ;IACjB,+DAA+D;IAC/D,MADW,MAAM,CACD;IAEhB,iEAAiE;IACjE,MADW,MAAM,CACI;IAWzB;;;;;;;OAOG;IACH,4KAEC;IAED;;;OAGG;IACH,+KAEC;IAcD;;;;;;OAMG;IACH,kHA0BC;IAmLD;;;;OAIG;IACH,qEAMC;IAED;;;;OAIG;IACH,+DAQC;IAED;;OAEG;IACH,oBAyCC;IAED;;OAEG;IACH;eAhXkC,OAAO,kBAAkB,EAAE,KAAK;MAuZjE;IAED,mBAEC;IAED;;OAEG;IACH,oBAFa,mFAA6B,CAOzC;IAED;;;;OAIG;IACH,oBAKC;IAED;;OAEG;IACH,sBAGC;IAUD;;;;;;;OAOG;IACH,kBALW,MAAM,eACN,MAAM,OACN,MAAM,GACJ,OAAO,CAwEnB;IAED;;;;;;OAMG;IACH,eAJW,mFAA6B,aAC7B,OAAO,GAAG,MAAM,iBAgD1B;IAED;;;;OAIG;IACH,qBAcC;IAED;;;;;OAKG;IACH,uBAOC;IA8DD;;;OAGG;IACH,aAFa,OAAO,qBAAqB,EAAE,OAAO,CAajD;IAID;;;;;OAKG;IACH,uBAFW,MAAM,yDAUhB;IAED;;OAEG;IACH,iBAFW,MAAM,yDAKhB;IAED;;;OAGG;IACH,6EAFa,MAAM,CAQlB;IAED;;;OAGG;IACH,mHAFa,MAAM,EAAE,CAOpB;;CA6BJ;wIAjyBY;IAAC,IAAI,EAAE,OAAO,eAAe,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAC"}
@@ -20,7 +20,6 @@ import { scale as vegaScale, isDiscrete, isContinuous } from "vega-scale";
20
20
  import mergeObjects from "../utils/mergeObjects.js";
21
21
  import createScale, { configureScale } from "../scale/scale.js";
22
22
 
23
- import { invalidate, getCachedOrCall } from "../utils/propertyCacher.js";
24
23
  import {
25
24
  getChannelDefWithScale,
26
25
  isColorChannel,
@@ -37,6 +36,7 @@ import { NominalDomain } from "../utils/domainArray.js";
37
36
  import { easeCubicInOut } from "d3-ease";
38
37
  import { asArray, shallowArrayEquals } from "../utils/arrayUtils.js";
39
38
  import eerp from "../utils/eerp.js";
39
+ import { isExprRef } from "./paramMediator.js";
40
40
 
41
41
  // Register scaleLocus to Vega-Scale.
42
42
  // Loci are discrete but the scale's domain can be adjusted in a continuous manner.
@@ -66,6 +66,7 @@ export const INDEX = "index";
66
66
  export default class ScaleResolution {
67
67
  /**
68
68
  * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionApi} ScaleResolutionApi
69
+ * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionEventType} ScaleResolutionEventType
69
70
  * @typedef {import("../spec/channel.js").Channel} Channel
70
71
  * @typedef {import("../spec/channel.js").ChannelWithScale} ChannelWithScale
71
72
  * @typedef {import("../spec/scale.js").NumericDomain} NumericDomain
@@ -77,16 +78,31 @@ export default class ScaleResolution {
77
78
  * @typedef {import("../utils/domainArray.js").DomainArray} DomainArray
78
79
  * @typedef {import("../genome/genome.js").ChromosomalLocus} ChromosomalLocus
79
80
  * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionListener} ScaleResolutionListener
81
+ *
82
+ * @typedef {VegaScale & { props: import("../spec/scale.js").Scale }} ScaleWithProps
80
83
  */
81
84
 
82
85
  /** @type {number[]} */
83
- #zoomExtent = undefined;
86
+ #zoomExtent;
87
+
88
+ /**
89
+ * @type {Record<ScaleResolutionEventType, Set<ScaleResolutionListener>>}
90
+ */
91
+ #listeners = {
92
+ domain: new Set(),
93
+ range: new Set(),
94
+ };
84
95
 
85
- /** @type {Set<ScaleResolutionListener>} Observers that are called when the scale domain is changed */
86
- #domainListeners = new Set();
96
+ /** @type {ScaleWithProps} */
97
+ #scale;
87
98
 
88
- /** @type {VegaScale} */
89
- #scale = undefined;
99
+ /**
100
+ * Keeps track of the expression references in the range. If range is modified,
101
+ * new expressions are created and the old ones must be invalidated.
102
+ *
103
+ * @type {Set<import("./paramMediator.js").ExprRefFunction>}
104
+ */
105
+ #rangeExprRefListeners = new Set();
90
106
 
91
107
  /**
92
108
  * @param {Channel} channel
@@ -102,36 +118,41 @@ export default class ScaleResolution {
102
118
  this.name = undefined;
103
119
  }
104
120
 
121
+ get #firstMemberView() {
122
+ return this.members[0].view;
123
+ }
124
+
125
+ get #viewContext() {
126
+ return this.#firstMemberView.context;
127
+ }
128
+
105
129
  /**
106
130
  * Adds a listener that is called when the scale domain is changed,
107
131
  * e.g., zoomed. The call is synchronous and happens before the views
108
132
  * are rendered.
109
133
  *
110
- * @param {"domain"} type
134
+ * @param {ScaleResolutionEventType} type
111
135
  * @param {ScaleResolutionListener} listener function
112
136
  */
113
137
  addEventListener(type, listener) {
114
- if (type != "domain") {
115
- throw new Error("Unsupported event type: " + type);
116
- }
117
- this.#domainListeners.add(listener);
138
+ this.#listeners[type].add(listener);
118
139
  }
119
140
 
120
141
  /**
121
- * @param {"domain"} type
142
+ * @param {ScaleResolutionEventType} type
122
143
  * @param {ScaleResolutionListener} listener function
123
144
  */
124
145
  removeEventListener(type, listener) {
125
- if (type != "domain") {
126
- throw new Error("Unsupported event type: " + type);
127
- }
128
- this.#domainListeners.delete(listener);
146
+ this.#listeners[type].delete(listener);
129
147
  }
130
148
 
131
- #notifyDomainListeners() {
132
- for (const listener of this.#domainListeners.values()) {
149
+ /**
150
+ * @param {ScaleResolutionEventType} type
151
+ */
152
+ #notifyListeners(type) {
153
+ for (const listener of this.#listeners[type].values()) {
133
154
  listener({
134
- type: "domain",
155
+ type,
135
156
  scaleResolution: this,
136
157
  });
137
158
  }
@@ -175,11 +196,11 @@ export default class ScaleResolution {
175
196
  /**
176
197
  * Returns true if the domain has been defined explicitly, i.e. not extracted from the data.
177
198
  */
178
- isExplicitDomain() {
199
+ #isExplicitDomain() {
179
200
  return !!this.getConfiguredDomain();
180
201
  }
181
202
 
182
- isDomainInitialized() {
203
+ #isDomainInitialized() {
183
204
  const s = this.#scale;
184
205
  if (!s) {
185
206
  return false;
@@ -205,18 +226,15 @@ export default class ScaleResolution {
205
226
  * @returns {import("../spec/scale.js").Scale}
206
227
  */
207
228
  #getMergedScaleProps() {
208
- return getCachedOrCall(this, "mergedScaleProps", () => {
209
- const propArray = this.members
210
- .map(
211
- (member) =>
212
- getChannelDefWithScale(member.view, member.channel)
213
- .scale
214
- )
215
- .filter((props) => props !== undefined);
229
+ const propArray = this.members
230
+ .map(
231
+ (member) =>
232
+ getChannelDefWithScale(member.view, member.channel).scale
233
+ )
234
+ .filter((props) => props !== undefined);
216
235
 
217
- // TODO: Disabled scale: https://vega.github.io/vega-lite/docs/scale.html#disable
218
- return mergeObjects(propArray, "scale", ["domain"]);
219
- });
236
+ // TODO: Disabled scale: https://vega.github.io/vega-lite/docs/scale.html#disable
237
+ return mergeObjects(propArray, "scale", ["domain"]);
220
238
  }
221
239
 
222
240
  /**
@@ -225,67 +243,121 @@ export default class ScaleResolution {
225
243
  *
226
244
  * @returns {import("../spec/scale.js").Scale}
227
245
  */
228
- getScaleProps() {
229
- // eslint-disable-next-line complexity
230
- return getCachedOrCall(this, "scaleProps", () => {
231
- const mergedProps = this.#getMergedScaleProps();
232
- if (mergedProps === null || mergedProps.type == "null") {
233
- // No scale (pass-thru)
234
- // TODO: Check that the channel is compatible
235
- return { type: "null" };
236
- }
246
+ #getScaleProps() {
247
+ const mergedProps = this.#getMergedScaleProps();
248
+ if (mergedProps === null || mergedProps.type == "null") {
249
+ // No scale (pass-thru)
250
+ // TODO: Check that the channel is compatible
251
+ return { type: "null" };
252
+ }
237
253
 
238
- const props = {
239
- ...this.#getDefaultScaleProperties(this.type),
240
- ...mergedProps,
241
- };
254
+ const props = {
255
+ ...this.#getDefaultScaleProperties(this.type),
256
+ ...mergedProps,
257
+ };
242
258
 
243
- if (!props.type) {
244
- props.type = getDefaultScaleType(this.channel, this.type);
245
- }
259
+ if (!props.type) {
260
+ props.type = getDefaultScaleType(this.channel, this.type);
261
+ }
246
262
 
247
- const domain = this.#getInitialDomain();
263
+ const domain = this.#getInitialDomain();
248
264
 
249
- if (domain && domain.length > 0) {
250
- props.domain = domain;
251
- } else if (isDiscrete(props.type)) {
252
- props.domain = new NominalDomain();
253
- }
265
+ if (domain && domain.length > 0) {
266
+ props.domain = domain;
267
+ } else if (isDiscrete(props.type)) {
268
+ props.domain = new NominalDomain();
269
+ }
254
270
 
255
- if (!props.domain && props.domainMid !== undefined) {
256
- // Initialize with a bogus domain so that scale.js can inject the domainMid.
257
- // The number of domain elements must be know before the glsl scale is generated.
258
- props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
259
- }
271
+ if (!props.domain && props.domainMid !== undefined) {
272
+ // Initialize with a bogus domain so that scale.js can inject the domainMid.
273
+ // The number of domain elements must be know before the glsl scale is generated.
274
+ props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
275
+ }
260
276
 
261
- // Reverse discrete y axis
262
- if (
263
- this.channel == "y" &&
264
- isDiscrete(props.type) &&
265
- props.reverse == undefined
266
- ) {
267
- props.reverse = true;
268
- }
277
+ // Reverse discrete y axis
278
+ if (
279
+ this.channel == "y" &&
280
+ isDiscrete(props.type) &&
281
+ props.reverse == undefined
282
+ ) {
283
+ props.reverse = true;
284
+ }
269
285
 
270
- if (props.range && props.scheme) {
271
- delete props.scheme;
272
- // TODO: Props should be set more intelligently
273
- /*
286
+ if (props.range && props.scheme) {
287
+ delete props.scheme;
288
+ // TODO: Props should be set more intelligently
289
+ /*
274
290
  throw new Error(
275
291
  `Scale has both "range" and "scheme" defined! Views: ${this._getViewPaths()}`
276
292
  );
277
293
  */
278
- }
294
+ }
279
295
 
280
- // By default, index and locus scales are zoomable, others are not
281
- if (!("zoom" in props) && ["index", "locus"].includes(props.type)) {
282
- props.zoom = true;
283
- }
296
+ // By default, index and locus scales are zoomable, others are not
297
+ if (!("zoom" in props) && ["index", "locus"].includes(props.type)) {
298
+ props.zoom = true;
299
+ }
300
+
301
+ applyLockedProperties(props, this.channel);
302
+
303
+ return props;
304
+ }
305
+
306
+ /**
307
+ * Configures range. If range is an array of expressions, they are evaluated
308
+ * and the scale is updated when the expressions change.
309
+ */
310
+ #configureRange() {
311
+ const props = this.#scale.props;
312
+ const range = props.range;
313
+ this.#rangeExprRefListeners.forEach((fn) => fn.invalidate());
314
+
315
+ if (!range || !isArray(range)) {
316
+ // Named ranges?
317
+ return;
318
+ }
319
+
320
+ /**
321
+ * @param {T} array
322
+ * @param {boolean} reverse
323
+ * @returns {T}
324
+ * @template T
325
+ */
326
+ const flip = (array, reverse) =>
327
+ // @ts-ignore TODO: Fix the type (should be a generic union array type)
328
+ reverse ? array.slice().reverse() : array;
329
+
330
+ if (range.some(isExprRef)) {
331
+ /** @type {(() => void)[]} */
332
+ let expressions;
333
+
334
+ const evaluateAndSet = () => {
335
+ this.#scale.range(
336
+ flip(
337
+ expressions.map((expr) => expr()),
338
+ props.reverse
339
+ )
340
+ );
341
+ };
284
342
 
285
- applyLockedProperties(props, this.channel);
343
+ expressions = range.map((elem) => {
344
+ if (isExprRef(elem)) {
345
+ const fn =
346
+ this.#firstMemberView.paramMediator.createExpression(
347
+ elem.expr
348
+ );
349
+ fn.addListener(evaluateAndSet);
350
+ this.#rangeExprRefListeners.add(fn);
351
+ return () => fn(null);
352
+ } else {
353
+ return () => elem;
354
+ }
355
+ });
286
356
 
287
- return props;
288
- });
357
+ evaluateAndSet();
358
+ } else {
359
+ this.#scale.range(flip(range, props.reverse));
360
+ }
289
361
  }
290
362
 
291
363
  #getInitialDomain() {
@@ -330,53 +402,64 @@ export default class ScaleResolution {
330
402
  * Reconfigures the scale: updates domain and other settings
331
403
  */
332
404
  reconfigure() {
333
- if (this.#scale && this.#scale.type != "null") {
334
- const domainWasInitialized = this.isDomainInitialized();
405
+ const scale = this.#scale;
335
406
 
336
- const previousDomain = this.#scale.domain();
407
+ if (!scale || scale.type == "null") {
408
+ return;
409
+ }
337
410
 
338
- invalidate(this, "scaleProps");
339
- const props = this.getScaleProps();
340
- configureScale(props, this.#scale);
341
- if (isContinuous(this.#scale.type)) {
342
- this.#zoomExtent = this.#getZoomExtent();
343
- }
411
+ const domainWasInitialized = this.#isDomainInitialized();
412
+ const previousDomain = scale.domain();
344
413
 
345
- if (!domainWasInitialized) {
346
- this.#notifyDomainListeners();
347
- return;
348
- }
414
+ const props = this.#getScaleProps();
415
+ configureScale({ ...props, range: undefined }, scale);
349
416
 
350
- const newDomain = this.#scale.domain();
351
- if (!shallowArrayEquals(newDomain, previousDomain)) {
352
- if (this.isZoomable()) {
353
- // Don't mess with zoomed views, restore the previous domain
354
- this.#scale.domain(previousDomain);
355
- } else if (this.#isZoomingSupported()) {
356
- // It can be zoomed, so lets make a smooth transition.
357
- // Restore the previous domain and zoom smoothly to the new domain.
358
- this.#scale.domain(previousDomain);
359
- this.zoomTo(newDomain, 500); // TODO: Configurable duration
360
- } else {
361
- // Update immediately if the previous domain was the initial domain [0, 0]
362
- this.#notifyDomainListeners();
363
- }
417
+ // Annotate the scale with the new props
418
+ scale.props = props;
419
+ this.#configureRange();
420
+
421
+ if (isContinuous(scale.type)) {
422
+ this.#zoomExtent = this.#getZoomExtent();
423
+ }
424
+
425
+ if (!domainWasInitialized) {
426
+ this.#notifyListeners("domain");
427
+ return;
428
+ }
429
+
430
+ const newDomain = scale.domain();
431
+ if (!shallowArrayEquals(newDomain, previousDomain)) {
432
+ if (this.isZoomable()) {
433
+ // Don't mess with zoomed views, restore the previous domain
434
+ scale.domain(previousDomain);
435
+ } else if (this.#isZoomingSupported()) {
436
+ // It can be zoomed, so lets make a smooth transition.
437
+ // Restore the previous domain and zoom smoothly to the new domain.
438
+ scale.domain(previousDomain);
439
+ this.zoomTo(newDomain, 500); // TODO: Configurable duration
440
+ } else {
441
+ // Update immediately if the previous domain was the initial domain [0, 0]
442
+ this.#notifyListeners("domain");
364
443
  }
365
444
  }
366
445
  }
367
446
 
368
447
  /**
369
- * @returns {VegaScale}
448
+ * @returns {ScaleWithProps}
370
449
  */
371
- getScale() {
450
+ get scale() {
372
451
  if (this.#scale) {
373
452
  return this.#scale;
374
453
  }
375
454
 
376
- const props = this.getScaleProps();
455
+ const props = this.#getScaleProps();
456
+
457
+ const scale = createScale({ ...props, range: undefined });
458
+ // Annotate the scale with props
459
+ scale.props = props;
377
460
 
378
- const scale = createScale(props);
379
461
  this.#scale = scale;
462
+ this.#configureRange();
380
463
 
381
464
  if (isScaleLocus(scale)) {
382
465
  scale.genome(this.getGenome());
@@ -386,11 +469,27 @@ export default class ScaleResolution {
386
469
  this.#zoomExtent = this.#getZoomExtent();
387
470
  }
388
471
 
472
+ // Hijack the range method
473
+ const range = scale.range;
474
+ if (range) {
475
+ const notify = () => this.#notifyListeners("range");
476
+ scale.range = function (/** @type {any} */ _) {
477
+ if (arguments.length) {
478
+ range(_);
479
+ notify();
480
+ } else {
481
+ return range();
482
+ }
483
+ };
484
+ // The initial setting
485
+ notify();
486
+ }
487
+
389
488
  return scale;
390
489
  }
391
490
 
392
491
  getDomain() {
393
- return this.getScale().domain();
492
+ return this.scale.domain();
394
493
  }
395
494
 
396
495
  /**
@@ -420,14 +519,14 @@ export default class ScaleResolution {
420
519
  */
421
520
  isZoomable() {
422
521
  // Check explicit configuration
423
- return this.#isZoomingSupported() && !!this.getScaleProps().zoom;
522
+ return this.#isZoomingSupported() && !!this.scale.props.zoom;
424
523
  }
425
524
 
426
525
  /**
427
526
  * Returns true if zooming is supported but not necessarily allowed in view spec.
428
527
  */
429
528
  #isZoomingSupported() {
430
- const type = this.getScale().type;
529
+ const type = this.scale.type;
431
530
  return isContinuous(type);
432
531
  }
433
532
 
@@ -444,7 +543,7 @@ export default class ScaleResolution {
444
543
  return false;
445
544
  }
446
545
 
447
- const scale = this.getScale();
546
+ const scale = this.scale;
448
547
  const oldDomain = scale.domain();
449
548
  let newDomain = [...oldDomain];
450
549
 
@@ -452,7 +551,7 @@ export default class ScaleResolution {
452
551
  // @ts-ignore
453
552
  let anchor = scale.invert(scaleAnchor);
454
553
 
455
- if (this.getScaleProps().reverse) {
554
+ if (scale.props.reverse) {
456
555
  pan = -pan;
457
556
  }
458
557
 
@@ -504,7 +603,7 @@ export default class ScaleResolution {
504
603
 
505
604
  if ([0, 1].some((i) => newDomain[i] != oldDomain[i])) {
506
605
  scale.domain(newDomain);
507
- this.#notifyDomainListeners();
606
+ this.#notifyListeners("domain");
508
607
  return true;
509
608
  }
510
609
 
@@ -533,7 +632,7 @@ export default class ScaleResolution {
533
632
 
534
633
  const animator = this.members[0]?.view.context.animator;
535
634
 
536
- const scale = this.getScale();
635
+ const scale = this.scale;
537
636
  const from = /** @type {number[]} */ (scale.domain());
538
637
 
539
638
  if (duration > 0 && from.length == 2) {
@@ -552,16 +651,16 @@ export default class ScaleResolution {
552
651
  const wt = (fw - w) / (fw - tw);
553
652
  const c = wt * tc + (1 - wt) * fc;
554
653
  scale.domain([c - w / 2, c + w / 2]);
555
- this.#notifyDomainListeners();
654
+ this.#notifyListeners("domain");
556
655
  },
557
656
  });
558
657
 
559
658
  scale.domain(to);
560
- this.#notifyDomainListeners();
659
+ this.#notifyListeners("domain");
561
660
  } else {
562
661
  scale.domain(to);
563
662
  animator?.requestRender();
564
- this.#notifyDomainListeners();
663
+ this.#notifyListeners("domain");
565
664
  }
566
665
  }
567
666
 
@@ -580,7 +679,7 @@ export default class ScaleResolution {
580
679
 
581
680
  if ([0, 1].some((i) => newDomain[i] != oldDomain[i])) {
582
681
  this.#scale.domain(newDomain);
583
- this.#notifyDomainListeners();
682
+ this.#notifyListeners("domain");
584
683
  return true;
585
684
  }
586
685
  return false;
@@ -595,14 +694,14 @@ export default class ScaleResolution {
595
694
  getZoomLevel() {
596
695
  // Zoom level makes sense only for user-zoomable scales where zoom extent is defined
597
696
  if (this.isZoomable()) {
598
- return span(this.#zoomExtent) / span(this.getScale().domain());
697
+ return span(this.#zoomExtent) / span(this.scale.domain());
599
698
  }
600
699
 
601
700
  return 1.0;
602
701
  }
603
702
 
604
703
  #getZoomExtent() {
605
- const props = this.getScaleProps();
704
+ const props = this.scale.props;
606
705
  const zoom = props.zoom;
607
706
 
608
707
  if (isZoomParams(zoom)) {
@@ -632,12 +731,12 @@ export default class ScaleResolution {
632
731
  const channel = this.channel;
633
732
  const props = {};
634
733
 
635
- if (this.isExplicitDomain()) {
734
+ if (this.#isExplicitDomain()) {
636
735
  props.zero = false;
637
736
  }
638
737
 
639
738
  if (isPositionalChannel(channel)) {
640
- props.nice = !this.isExplicitDomain();
739
+ props.nice = !this.#isExplicitDomain();
641
740
  } else if (isColorChannel(channel)) {
642
741
  // TODO: Named ranges
643
742
  props.scheme =
@@ -687,7 +786,7 @@ export default class ScaleResolution {
687
786
  * @param {number} value
688
787
  */
689
788
  invertToComplex(value) {
690
- const scale = this.getScale();
789
+ const scale = this.scale;
691
790
  if ("invert" in scale) {
692
791
  const inverted = /** @type {number} */ (scale.invert(value));
693
792
  return this.toComplex(inverted);